mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
Support running apk with more than one activity (#18716)
This commit is contained in:
parent
87a067704a
commit
def7634ba0
|
@ -60,8 +60,16 @@ class AndroidApk extends ApplicationPackage {
|
|||
return null;
|
||||
}
|
||||
|
||||
final List<String> aaptArgs = <String>[aaptPath, 'dump', 'badging', applicationBinary];
|
||||
final ApkManifestData data = ApkManifestData.parseFromAaptBadging(runCheckedSync(aaptArgs));
|
||||
final List<String> aaptArgs = <String>[
|
||||
aaptPath,
|
||||
'dump',
|
||||
'xmltree',
|
||||
applicationBinary,
|
||||
'AndroidManifest.xml',
|
||||
];
|
||||
|
||||
final ApkManifestData data = ApkManifestData
|
||||
.parseFromXmlDump(runCheckedSync(aaptArgs));
|
||||
|
||||
if (data == null) {
|
||||
printError('Unable to read manifest info from $applicationBinary.');
|
||||
|
@ -116,9 +124,12 @@ class AndroidApk extends ApplicationPackage {
|
|||
for (xml.XmlElement category in document.findAllElements('category')) {
|
||||
if (category.getAttribute('android:name') == 'android.intent.category.LAUNCHER') {
|
||||
final xml.XmlElement activity = category.parent.parent;
|
||||
final String activityName = activity.getAttribute('android:name');
|
||||
launchActivity = '$packageId/$activityName';
|
||||
break;
|
||||
final String enabled = activity.getAttribute('android:enabled');
|
||||
if (enabled == null || enabled == 'true') {
|
||||
final String activityName = activity.getAttribute('android:name');
|
||||
launchActivity = '$packageId/$activityName';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -349,34 +360,133 @@ class ApplicationPackageStore {
|
|||
}
|
||||
}
|
||||
|
||||
class _Entry {
|
||||
_Element parent;
|
||||
int level;
|
||||
}
|
||||
|
||||
class _Element extends _Entry {
|
||||
List<_Entry> children;
|
||||
String name;
|
||||
|
||||
_Element.fromLine(String line, _Element parent) {
|
||||
// E: application (line=29)
|
||||
final List<String> parts = line.trimLeft().split(' ');
|
||||
name = parts[1];
|
||||
level = line.length - line.trimLeft().length;
|
||||
this.parent = parent;
|
||||
children = <_Entry>[];
|
||||
}
|
||||
|
||||
void addChild(_Entry child) {
|
||||
children.add(child);
|
||||
}
|
||||
|
||||
_Attribute firstAttribute(String name) {
|
||||
return children.firstWhere(
|
||||
(_Entry e) => e is _Attribute && e.key.startsWith(name),
|
||||
orElse: () => null,
|
||||
);
|
||||
}
|
||||
|
||||
_Element firstElement(String name) {
|
||||
return children.firstWhere(
|
||||
(_Entry e) => e is _Element && e.name.startsWith(name),
|
||||
orElse: () => null,
|
||||
);
|
||||
}
|
||||
|
||||
Iterable<_Entry> allElements(String name) {
|
||||
return children.where(
|
||||
(_Entry e) => e is _Element && e.name.startsWith(name));
|
||||
}
|
||||
}
|
||||
|
||||
class _Attribute extends _Entry {
|
||||
String key;
|
||||
String value;
|
||||
|
||||
_Attribute.fromLine(String line, _Element parent) {
|
||||
// A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
|
||||
const String attributePrefix = 'A: ';
|
||||
final List<String> keyVal = line
|
||||
.substring(line.indexOf(attributePrefix) + attributePrefix.length)
|
||||
.split('=');
|
||||
key = keyVal[0];
|
||||
value = keyVal[1];
|
||||
level = line.length - line.trimLeft().length;
|
||||
this.parent = parent;
|
||||
}
|
||||
}
|
||||
|
||||
class ApkManifestData {
|
||||
ApkManifestData._(this._data);
|
||||
|
||||
static ApkManifestData parseFromAaptBadging(String data) {
|
||||
static ApkManifestData parseFromXmlDump(String data) {
|
||||
if (data == null || data.trim().isEmpty)
|
||||
return null;
|
||||
|
||||
// package: name='io.flutter.gallery' versionCode='1' versionName='0.0.1' platformBuildVersionName='NMR1'
|
||||
// launchable-activity: name='io.flutter.app.FlutterActivity' label='' icon=''
|
||||
final Map<String, Map<String, String>> map = <String, Map<String, String>>{};
|
||||
final List<String> lines = data.split('\n');
|
||||
assert(lines.length > 3);
|
||||
|
||||
final RegExp keyValueRegex = new RegExp(r"(\S+?)='(.*?)'");
|
||||
final _Element manifest = new _Element.fromLine(lines[1], null);
|
||||
_Element currentElement = manifest;
|
||||
|
||||
for (String line in data.split('\n')) {
|
||||
final int index = line.indexOf(':');
|
||||
if (index != -1) {
|
||||
final String name = line.substring(0, index);
|
||||
line = line.substring(index + 1).trim();
|
||||
for (String line in lines.skip(2)) {
|
||||
final String trimLine = line.trimLeft();
|
||||
final int level = line.length - trimLine.length;
|
||||
|
||||
final Map<String, String> entries = <String, String>{};
|
||||
map[name] = entries;
|
||||
// Handle level out
|
||||
while(level <= currentElement.level) {
|
||||
currentElement = currentElement.parent;
|
||||
}
|
||||
|
||||
for (Match m in keyValueRegex.allMatches(line)) {
|
||||
entries[m.group(1)] = m.group(2);
|
||||
if (level > currentElement.level) {
|
||||
switch (trimLine[0]) {
|
||||
case 'A':
|
||||
currentElement
|
||||
.addChild(new _Attribute.fromLine(line, currentElement));
|
||||
break;
|
||||
case 'E':
|
||||
final _Element element = new _Element.fromLine(line, currentElement);
|
||||
currentElement.addChild(element);
|
||||
currentElement = element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final _Element application = manifest.firstElement('application');
|
||||
assert(application != null);
|
||||
|
||||
final Iterable<_Entry> activities = application.allElements('activity');
|
||||
|
||||
_Element launchActivity;
|
||||
for (_Element activity in activities) {
|
||||
final _Attribute enabled = activity.firstAttribute('android:enabled');
|
||||
if (enabled == null || enabled.value.contains('0xffffffff')) {
|
||||
launchActivity = activity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final _Attribute package = manifest.firstAttribute('package');
|
||||
// "io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
|
||||
final String packageName = package.value.substring(1, package.value.indexOf('" '));
|
||||
|
||||
if (launchActivity == null) {
|
||||
printError('Error running $packageName. Default activity not found');
|
||||
return null;
|
||||
}
|
||||
|
||||
final _Attribute nameAttribute = launchActivity.firstAttribute('android:name');
|
||||
// "io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
|
||||
final String activityName = nameAttribute
|
||||
.value.substring(1, nameAttribute.value.indexOf('" '));
|
||||
|
||||
final Map<String, Map<String, String>> map = <String, Map<String, String>>{};
|
||||
map['package'] = <String, String>{'name': packageName};
|
||||
map['launchable-activity'] = <String, String>{'name': activityName};
|
||||
|
||||
return new ApkManifestData._(map);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,13 +18,24 @@ import 'src/context.dart';
|
|||
|
||||
void main() {
|
||||
group('ApkManifestData', () {
|
||||
testUsingContext('parse sdk', () {
|
||||
final ApkManifestData data =
|
||||
ApkManifestData.parseFromAaptBadging(_aaptData);
|
||||
test('Select explicity enabled activity', () {
|
||||
final ApkManifestData data = ApkManifestData.parseFromXmlDump(_aaptDataWithExplicitEnabledActivity);
|
||||
expect(data, isNotNull);
|
||||
expect(data.packageName, 'io.flutter.gallery');
|
||||
expect(data.launchableActivityName, 'io.flutter.app.FlutterActivity');
|
||||
expect(data.data['application']['label'], 'Flutter Gallery');
|
||||
expect(data.packageName, 'io.flutter.examples.hello_world');
|
||||
expect(data.launchableActivityName, 'io.flutter.examples.hello_world.MainActivity2');
|
||||
});
|
||||
test('Select default enabled activity', () {
|
||||
final ApkManifestData data = ApkManifestData.parseFromXmlDump(_aaptDataWithDefaultEnabledActivity);
|
||||
expect(data, isNotNull);
|
||||
expect(data.packageName, 'io.flutter.examples.hello_world');
|
||||
expect(data.launchableActivityName, 'io.flutter.examples.hello_world.MainActivity2');
|
||||
});
|
||||
testUsingContext('Error on no enabled activity', () {
|
||||
final ApkManifestData data = ApkManifestData.parseFromXmlDump(_aaptDataWithNoEnabledActivity);
|
||||
expect(data, isNull);
|
||||
final BufferLogger logger = context[Logger];
|
||||
expect(
|
||||
logger.errorText, 'Error running io.flutter.examples.hello_world. Default activity not found\n');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -152,33 +163,6 @@ void main() {
|
|||
});
|
||||
}
|
||||
|
||||
const String _aaptData = '''
|
||||
package: name='io.flutter.gallery' versionCode='1' versionName='0.0.1' platformBuildVersionName='NMR1'
|
||||
sdkVersion:'14'
|
||||
targetSdkVersion:'21'
|
||||
uses-permission: name='android.permission.INTERNET'
|
||||
application-label:'Flutter Gallery'
|
||||
application-icon-160:'res/mipmap-mdpi-v4/ic_launcher.png'
|
||||
application-icon-240:'res/mipmap-hdpi-v4/ic_launcher.png'
|
||||
application-icon-320:'res/mipmap-xhdpi-v4/ic_launcher.png'
|
||||
application-icon-480:'res/mipmap-xxhdpi-v4/ic_launcher.png'
|
||||
application-icon-640:'res/mipmap-xxxhdpi-v4/ic_launcher.png'
|
||||
application: label='Flutter Gallery' icon='res/mipmap-mdpi-v4/ic_launcher.png'
|
||||
application-debuggable
|
||||
launchable-activity: name='io.flutter.app.FlutterActivity' label='' icon=''
|
||||
feature-group: label=''
|
||||
uses-feature: name='android.hardware.screen.portrait'
|
||||
uses-implied-feature: name='android.hardware.screen.portrait' reason='one or more activities have specified a portrait orientation'
|
||||
uses-feature: name='android.hardware.touchscreen'
|
||||
uses-implied-feature: name='android.hardware.touchscreen' reason='default feature for all apps'
|
||||
main
|
||||
supports-screens: 'small' 'normal' 'large' 'xlarge'
|
||||
supports-any-density: 'true'
|
||||
locales: '--_--'
|
||||
densities: '160' '240' '320' '480' '640'
|
||||
native-code: 'armeabi-v7a'
|
||||
''';
|
||||
|
||||
final Map<String, String> _swiftBuildSettings = <String, String>{
|
||||
'ARCHS': 'arm64',
|
||||
'ASSETCATALOG_COMPILER_APPICON_NAME': 'AppIcon',
|
||||
|
@ -192,6 +176,118 @@ final Map<String, String> _swiftBuildSettings = <String, String>{
|
|||
'SWIFT_VERSION': '3.0',
|
||||
};
|
||||
|
||||
const String _aaptDataWithExplicitEnabledActivity =
|
||||
'''N: android=http://schemas.android.com/apk/res/android
|
||||
E: manifest (line=7)
|
||||
A: android:versionCode(0x0101021b)=(type 0x10)0x1
|
||||
A: android:versionName(0x0101021c)="0.0.1" (Raw: "0.0.1")
|
||||
A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
|
||||
E: uses-sdk (line=12)
|
||||
A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
|
||||
A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1b
|
||||
E: uses-permission (line=21)
|
||||
A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
|
||||
E: application (line=29)
|
||||
A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
|
||||
A: android:icon(0x01010002)=@0x7f010000
|
||||
A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
|
||||
A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
|
||||
E: activity (line=34)
|
||||
A: android:theme(0x01010000)=@0x1030009
|
||||
A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
|
||||
A: android:enabled(0x0101000e)=(type 0x12)0x0
|
||||
A: android:launchMode(0x0101001d)=(type 0x10)0x1
|
||||
A: android:configChanges(0x0101001f)=(type 0x11)0x400035b4
|
||||
A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
|
||||
A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
|
||||
E: intent-filter (line=42)
|
||||
E: action (line=43)
|
||||
A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
|
||||
E: category (line=45)
|
||||
A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
|
||||
E: activity (line=48)
|
||||
A: android:theme(0x01010000)=@0x1030009
|
||||
A: android:label(0x01010001)="app2" (Raw: "app2")
|
||||
A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity2" (Raw: "io.flutter.examples.hello_world.MainActivity2")
|
||||
A: android:enabled(0x0101000e)=(type 0x12)0xffffffff
|
||||
E: intent-filter (line=53)
|
||||
E: action (line=54)
|
||||
A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
|
||||
E: category (line=56)
|
||||
A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")''';
|
||||
|
||||
|
||||
const String _aaptDataWithDefaultEnabledActivity =
|
||||
'''N: android=http://schemas.android.com/apk/res/android
|
||||
E: manifest (line=7)
|
||||
A: android:versionCode(0x0101021b)=(type 0x10)0x1
|
||||
A: android:versionName(0x0101021c)="0.0.1" (Raw: "0.0.1")
|
||||
A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
|
||||
E: uses-sdk (line=12)
|
||||
A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
|
||||
A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1b
|
||||
E: uses-permission (line=21)
|
||||
A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
|
||||
E: application (line=29)
|
||||
A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
|
||||
A: android:icon(0x01010002)=@0x7f010000
|
||||
A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
|
||||
A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
|
||||
E: activity (line=34)
|
||||
A: android:theme(0x01010000)=@0x1030009
|
||||
A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
|
||||
A: android:enabled(0x0101000e)=(type 0x12)0x0
|
||||
A: android:launchMode(0x0101001d)=(type 0x10)0x1
|
||||
A: android:configChanges(0x0101001f)=(type 0x11)0x400035b4
|
||||
A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
|
||||
A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
|
||||
E: intent-filter (line=42)
|
||||
E: action (line=43)
|
||||
A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
|
||||
E: category (line=45)
|
||||
A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
|
||||
E: activity (line=48)
|
||||
A: android:theme(0x01010000)=@0x1030009
|
||||
A: android:label(0x01010001)="app2" (Raw: "app2")
|
||||
A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity2" (Raw: "io.flutter.examples.hello_world.MainActivity2")
|
||||
E: intent-filter (line=53)
|
||||
E: action (line=54)
|
||||
A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
|
||||
E: category (line=56)
|
||||
A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")''';
|
||||
|
||||
|
||||
const String _aaptDataWithNoEnabledActivity =
|
||||
'''N: android=http://schemas.android.com/apk/res/android
|
||||
E: manifest (line=7)
|
||||
A: android:versionCode(0x0101021b)=(type 0x10)0x1
|
||||
A: android:versionName(0x0101021c)="0.0.1" (Raw: "0.0.1")
|
||||
A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
|
||||
E: uses-sdk (line=12)
|
||||
A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
|
||||
A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1b
|
||||
E: uses-permission (line=21)
|
||||
A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
|
||||
E: application (line=29)
|
||||
A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
|
||||
A: android:icon(0x01010002)=@0x7f010000
|
||||
A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
|
||||
A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
|
||||
E: activity (line=34)
|
||||
A: android:theme(0x01010000)=@0x1030009
|
||||
A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
|
||||
A: android:enabled(0x0101000e)=(type 0x12)0x0
|
||||
A: android:launchMode(0x0101001d)=(type 0x10)0x1
|
||||
A: android:configChanges(0x0101001f)=(type 0x11)0x400035b4
|
||||
A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
|
||||
A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
|
||||
E: intent-filter (line=42)
|
||||
E: action (line=43)
|
||||
A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
|
||||
E: category (line=45)
|
||||
A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")''';
|
||||
|
||||
|
||||
class MockIosWorkFlow extends Mock implements IOSWorkflow {
|
||||
@override
|
||||
String getPlistValueFromFile(String path, String key) {
|
||||
|
|
Loading…
Reference in a new issue