diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index ec6f6166b7..2a13c4c398 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart @@ -559,7 +559,32 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit } String _createAndroidIdentifier(String organization, String name) { - return '$organization.$name'.replaceAll('_', ''); + // Android application ID is specified in: https://developer.android.com/studio/build/application-id + // All characters must be alphanumeric or an underscore [a-zA-Z0-9_]. + String tmpIdentifier = '$organization.$name'; + final RegExp disallowed = RegExp(r'[^\w\.]'); + tmpIdentifier = tmpIdentifier.replaceAll(disallowed, ''); + + // It must have at least two segments (one or more dots). + final List segments = tmpIdentifier + .split('.') + .where((String segment) => segment.isNotEmpty) + .toList(); + while (segments.length < 2) { + segments.add('untitled'); + } + + // Each segment must start with a letter. + final RegExp segmentPatternRegex = RegExp(r'^[a-zA-Z][\w]*$'); + final List prefixedSegments = segments + .map((String segment) { + if (!segmentPatternRegex.hasMatch(segment)) { + return 'u'+segment; + } + return segment; + }) + .toList(); + return prefixedSegments.join('.'); } String _createPluginClassName(String name) { @@ -569,10 +594,21 @@ String _createPluginClassName(String name) { String _createUTIIdentifier(String organization, String name) { // Create a UTI (https://en.wikipedia.org/wiki/Uniform_Type_Identifier) from a base name + name = camelCase(name); + String tmpIdentifier = '$organization.$name'; final RegExp disallowed = RegExp(r'[^a-zA-Z0-9\-\.\u0080-\uffff]+'); - name = camelCase(name).replaceAll(disallowed, ''); - name = name.isEmpty ? 'untitled' : name; - return '$organization.$name'; + tmpIdentifier = tmpIdentifier.replaceAll(disallowed, ''); + + // It must have at least two segments (one or more dots). + final List segments = tmpIdentifier + .split('.') + .where((String segment) => segment.isNotEmpty) + .toList(); + while (segments.length < 2) { + segments.add('untitled'); + } + + return segments.join('.'); } final Set _packageDependencies = Set.from([ diff --git a/packages/flutter_tools/test/commands/create_test.dart b/packages/flutter_tools/test/commands/create_test.dart index bdb26cd20b..70ad7467f8 100644 --- a/packages/flutter_tools/test/commands/create_test.dart +++ b/packages/flutter_tools/test/commands/create_test.dart @@ -56,7 +56,7 @@ void main() { projectDir, [], [ - 'android/app/src/main/java/com/example/flutterproject/MainActivity.java', + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', 'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', 'flutter_project.iml', 'ios/Flutter/AppFrameworkInfo.plist', @@ -74,7 +74,7 @@ void main() { projectDir, [], [ - 'android/app/src/main/java/com/example/flutterproject/MainActivity.java', + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', 'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', 'flutter_project.iml', 'ios/Flutter/AppFrameworkInfo.plist', @@ -120,7 +120,7 @@ void main() { await projectDir.absolute.childDirectory('blag').create(recursive: true); await projectDir.absolute.childDirectory('.idea').create(recursive: true); await _createAndAnalyzeProject(projectDir, [], [ - 'android/app/src/main/java/com/example/flutterproject/MainActivity.java', + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', 'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', 'flutter_project.iml', 'ios/Flutter/AppFrameworkInfo.plist', @@ -136,7 +136,7 @@ void main() { await projectDir.absolute.childDirectory('lib').create(recursive: true); await projectDir.absolute.childDirectory('ios').create(recursive: true); await _createAndAnalyzeProject(projectDir, [], [ - 'android/app/src/main/java/com/example/flutterproject/MainActivity.java', + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', 'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', 'flutter_project.iml', 'ios/Flutter/AppFrameworkInfo.plist', @@ -155,8 +155,8 @@ void main() { projectDir, [], [ - 'android/src/main/java/com/example/flutterproject/FlutterProjectPlugin.java', - 'example/android/app/src/main/java/com/example/flutterprojectexample/MainActivity.java', + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', 'example/ios/Runner/AppDelegate.h', 'example/ios/Runner/AppDelegate.m', 'example/ios/Runner/main.m', @@ -180,9 +180,9 @@ void main() { 'test/flutter_project_test.dart', ], unexpectedPaths: [ - 'android/app/src/main/java/com/example/flutterproject/MainActivity.java', - 'android/src/main/java/com/example/flutterproject/FlutterProjectPlugin.java', - 'example/android/app/src/main/java/com/example/flutterprojectexample/MainActivity.java', + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', 'example/ios/Runner/AppDelegate.h', 'example/ios/Runner/AppDelegate.m', 'example/ios/Runner/main.m', @@ -203,14 +203,14 @@ void main() { projectDir, ['--no-pub', '--template=app', '--android-language=kotlin', '--ios-language=swift'], [ - 'android/app/src/main/kotlin/com/example/flutterproject/MainActivity.kt', + 'android/app/src/main/kotlin/com/example/flutter_project/MainActivity.kt', 'ios/Runner/AppDelegate.swift', 'ios/Runner/Runner-Bridging-Header.h', 'lib/main.dart', '.idea/libraries/KotlinJavaRuntime.xml', ], unexpectedPaths: [ - 'android/app/src/main/java/com/example/flutterproject/MainActivity.java', + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', 'ios/Runner/AppDelegate.h', 'ios/Runner/AppDelegate.m', 'ios/Runner/main.m', @@ -227,9 +227,9 @@ void main() { 'test/flutter_project_test.dart', ], unexpectedPaths: [ - 'android/app/src/main/java/com/example/flutterproject/MainActivity.java', - 'android/src/main/java/com/example/flutterproject/FlutterProjectPlugin.java', - 'example/android/app/src/main/java/com/example/flutterprojectexample/MainActivity.java', + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', 'example/ios/Runner/AppDelegate.h', 'example/ios/Runner/AppDelegate.m', 'example/ios/Runner/main.m', @@ -251,8 +251,8 @@ void main() { projectDir, ['--template=plugin'], [ - 'android/src/main/java/com/example/flutterproject/FlutterProjectPlugin.java', - 'example/android/app/src/main/java/com/example/flutterprojectexample/MainActivity.java', + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', 'example/ios/Runner/AppDelegate.h', 'example/ios/Runner/AppDelegate.m', 'example/ios/Runner/main.m', @@ -271,8 +271,8 @@ void main() { projectDir, ['--no-pub', '--template=plugin', '-a', 'kotlin', '--ios-language', 'swift'], [ - 'android/src/main/kotlin/com/example/flutterproject/FlutterProjectPlugin.kt', - 'example/android/app/src/main/kotlin/com/example/flutterprojectexample/MainActivity.kt', + 'android/src/main/kotlin/com/example/flutter_project/FlutterProjectPlugin.kt', + 'example/android/app/src/main/kotlin/com/example/flutter_project_example/MainActivity.kt', 'example/ios/Runner/AppDelegate.swift', 'example/ios/Runner/Runner-Bridging-Header.h', 'example/lib/main.dart', @@ -282,8 +282,8 @@ void main() { 'lib/flutter_project.dart', ], unexpectedPaths: [ - 'android/src/main/java/com/example/flutterproject/FlutterProjectPlugin.java', - 'example/android/app/src/main/java/com/example/flutterprojectexample/MainActivity.java', + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', 'example/ios/Runner/AppDelegate.h', 'example/ios/Runner/AppDelegate.m', 'example/ios/Runner/main.m', @@ -296,12 +296,12 @@ void main() { projectDir, ['--no-pub', '--template=plugin', '--org', 'com.bar.foo'], [ - 'android/src/main/java/com/bar/foo/flutterproject/FlutterProjectPlugin.java', - 'example/android/app/src/main/java/com/bar/foo/flutterprojectexample/MainActivity.java', + 'android/src/main/java/com/bar/foo/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/bar/foo/flutter_project_example/MainActivity.java', ], unexpectedPaths: [ - 'android/src/main/java/com/example/flutterproject/FlutterProjectPlugin.java', - 'example/android/app/src/main/java/com/example/flutterprojectexample/MainActivity.java', + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', ], ); }, timeout: allowForCreateFlutterProject); @@ -312,11 +312,11 @@ void main() { ['--no-pub', '--template=plugin', '--project-name', 'xyz'], [ 'android/src/main/java/com/example/xyz/XyzPlugin.java', - 'example/android/app/src/main/java/com/example/xyzexample/MainActivity.java', + 'example/android/app/src/main/java/com/example/xyz_example/MainActivity.java', ], unexpectedPaths: [ - 'android/src/main/java/com/example/flutterproject/FlutterProjectPlugin.java', - 'example/android/app/src/main/java/com/example/flutterprojectexample/MainActivity.java', + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', ], ); }, timeout: allowForCreateFlutterProject); @@ -518,6 +518,54 @@ void main() { Platform: _kNoColorTerminalPlatform, }, timeout: allowForCreateFlutterProject); + testUsingContext('has correct application id for android and bundle id for ios', () async { + Cache.flutterRoot = '../..'; + when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision); + when(mockFlutterVersion.channel).thenReturn(frameworkChannel); + + final CreateCommand command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); + + String tmpProjectDir = fs.path.join(tempDir.path, 'hello_flutter'); + await runner.run(['create', '--template=app', '--no-pub', '--org', 'com.example', tmpProjectDir]); + FlutterProject project = await FlutterProject.fromDirectory(fs.directory(tmpProjectDir)); + expect( + project.ios.productBundleIdentifier, + 'com.example.helloFlutter' + ); + expect( + project.android.applicationId, + 'com.example.hello_flutter' + ); + + tmpProjectDir = fs.path.join(tempDir.path, 'test_abc'); + await runner.run(['create', '--template=app', '--no-pub', '--org', 'abc^*.1#@', tmpProjectDir]); + project = await FlutterProject.fromDirectory(fs.directory(tmpProjectDir)); + expect( + project.ios.productBundleIdentifier, + 'abc.1.testAbc' + ); + expect( + project.android.applicationId, + 'abc.u1.test_abc' + ); + + tmpProjectDir = fs.path.join(tempDir.path, 'flutter_project'); + await runner.run(['create', '--template=app', '--no-pub', '--org', '#+^%', tmpProjectDir]); + project = await FlutterProject.fromDirectory(fs.directory(tmpProjectDir)); + expect( + project.ios.productBundleIdentifier, + 'flutterProject.untitled' + ); + expect( + project.android.applicationId, + 'flutter_project.untitled' + ); + }, overrides: { + FlutterVersion: () => mockFlutterVersion, + Platform: _kNoColorTerminalPlatform, + }, timeout: allowForCreateFlutterProject); + testUsingContext('can re-gen default template over existing project', () async { Cache.flutterRoot = '../..'; @@ -616,7 +664,7 @@ void main() { projectDir, [], [ - '.android/app/src/main/java/com/bar/foo/flutterproject/host/MainActivity.java', + '.android/app/src/main/java/com/bar/foo/flutter_project/host/MainActivity.java', ], ); }, timeout: allowForRemotePubInvocation); @@ -647,10 +695,10 @@ void main() { projectDir, ['--no-pub'], [ - 'android/app/src/main/java/com/bar/foo/flutterproject/MainActivity.java', + 'android/app/src/main/java/com/bar/foo/flutter_project/MainActivity.java', ], unexpectedPaths: [ - 'android/app/src/main/java/com/example/flutterproject/MainActivity.java', + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', ], ); }, timeout: allowForCreateFlutterProject); @@ -682,12 +730,12 @@ void main() { projectDir, ['--no-pub', '--template=plugin'], [ - 'example/android/app/src/main/java/com/bar/foo/flutterprojectexample/MainActivity.java', + 'example/android/app/src/main/java/com/bar/foo/flutter_project_example/MainActivity.java', 'ios/Classes/FlutterProjectPlugin.h', ], unexpectedPaths: [ - 'example/android/app/src/main/java/com/example/flutterprojectexample/MainActivity.java', - 'android/src/main/java/com/example/flutterproject/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', ], ); final FlutterProject project = await FlutterProject.fromDirectory(projectDir);