Skip to content

Commit

Permalink
Merge pull request fluttercommunity#392 from fluttercommunity/Ratakon…
Browse files Browse the repository at this point in the history
…dalaArun/min-sdk-not-found-384

RatakondalaArun/min-sdk-not-found-384
  • Loading branch information
RatakondalaArun committed Aug 2, 2022
2 parents f3a811e + f58fdce commit 4ffd0f5
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 80 deletions.
5 changes: 5 additions & 0 deletions README.md
Expand Up @@ -21,6 +21,7 @@ flutter_icons:
android: "launcher_icon"
ios: true
image_path: "assets/icon/icon.png"
min_sdk_android: 21 # android min sdk min:16, default 21
web:
generate: true
image_path: "path/to/image.png"
Expand Down Expand Up @@ -71,6 +72,10 @@ Shown below is the full list of attributes which you can specify within your Flu

- `image_path_ios`: The location of the icon image file specific for iOS platform (optional - if not defined then the image_path is used)

- `min_sdk_android`: Specify android min sdk value

- `remove_alpha_ios`: Removes alpha channel for IOS icons

- `web`: Add web related configs
- `generate`: Specifies weather to generate icons for this platform or not
- `image_path`: Path to web icon.png
Expand Down
170 changes: 93 additions & 77 deletions lib/android.dart
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter_launcher_icons/xml_templates.dart' as xml_template;
import 'package:image/image.dart';
import 'package:flutter_launcher_icons/custom_exceptions.dart';
import 'package:flutter_launcher_icons/constants.dart' as constants;
import 'package:path/path.dart' as path;

class AndroidIconTemplate {
AndroidIconTemplate({required this.size, required this.directoryName});
Expand All @@ -28,8 +29,7 @@ List<AndroidIconTemplate> androidIcons = <AndroidIconTemplate>[
AndroidIconTemplate(directoryName: 'mipmap-xxxhdpi', size: 192),
];

void createDefaultIcons(
Map<String, dynamic> flutterLauncherIconsConfig, String? flavor) {
void createDefaultIcons(Map<String, dynamic> flutterLauncherIconsConfig, String? flavor) {
printStatus('Creating default icons Android');
final String filePath = getAndroidIconPath(flutterLauncherIconsConfig);
final Image? image = decodeImageFile(filePath);
Expand All @@ -43,55 +43,46 @@ void createDefaultIcons(
isAndroidIconNameCorrectFormat(iconName);
final String iconPath = '$iconName.png';
for (AndroidIconTemplate template in androidIcons) {
saveNewImages(template, image, iconPath, flavor);
_saveNewImages(template, image, iconPath, flavor);
}
overwriteAndroidManifestWithNewLauncherIcon(iconName, androidManifestFile);
} else {
printStatus(
'Overwriting the default Android launcher icon with a new icon');
printStatus('Overwriting the default Android launcher icon with a new icon');
for (AndroidIconTemplate template in androidIcons) {
overwriteExistingIcons(
template, image, constants.androidFileName, flavor);
overwriteExistingIcons(template, image, constants.androidFileName, flavor);
}
overwriteAndroidManifestWithNewLauncherIcon(
constants.androidDefaultIconName, androidManifestFile);
overwriteAndroidManifestWithNewLauncherIcon(constants.androidDefaultIconName, androidManifestFile);
}
}

/// Ensures that the Android icon name is in the correct format
bool isAndroidIconNameCorrectFormat(String iconName) {
// assure the icon only consists of lowercase letters, numbers and underscore
if (!RegExp(r'^[a-z0-9_]+$').hasMatch(iconName)) {
throw const InvalidAndroidIconNameException(
constants.errorIncorrectIconName);
throw const InvalidAndroidIconNameException(constants.errorIncorrectIconName);
}
return true;
}

void createAdaptiveIcons(
Map<String, dynamic> flutterLauncherIconsConfig, String? flavor) {
void createAdaptiveIcons(Map<String, dynamic> flutterLauncherIconsConfig, String? flavor) {
printStatus('Creating adaptive icons Android');

// Retrieve the necessary Flutter Launcher Icons configuration from the pubspec.yaml file
final String backgroundConfig =
flutterLauncherIconsConfig['adaptive_icon_background'];
final String foregroundImagePath =
flutterLauncherIconsConfig['adaptive_icon_foreground'];
final String backgroundConfig = flutterLauncherIconsConfig['adaptive_icon_background'];
final String foregroundImagePath = flutterLauncherIconsConfig['adaptive_icon_foreground'];
final Image? foregroundImage = decodeImageFile(foregroundImagePath);
if (foregroundImage == null) {
return;
}

// Create adaptive icon foreground images
for (AndroidIconTemplate androidIcon in adaptiveForegroundIcons) {
overwriteExistingIcons(androidIcon, foregroundImage,
constants.androidAdaptiveForegroundFileName, flavor);
overwriteExistingIcons(androidIcon, foregroundImage, constants.androidAdaptiveForegroundFileName, flavor);
}

// Create adaptive icon background
if (isAdaptiveIconConfigPngFile(backgroundConfig)) {
createAdaptiveBackgrounds(
flutterLauncherIconsConfig, backgroundConfig, flavor);
_createAdaptiveBackgrounds(flutterLauncherIconsConfig, backgroundConfig, flavor);
} else {
createAdaptiveIconMipmapXmlFile(flutterLauncherIconsConfig, flavor);
updateColorsXmlFile(backgroundConfig, flavor);
Expand All @@ -112,28 +103,22 @@ void updateColorsXmlFile(String backgroundConfig, String? flavor) {
updateColorsFile(colorsXml, backgroundConfig);
} else {
printStatus('No colors.xml file found in your Android project');
printStatus(
'Creating colors.xml file and adding it to your Android project');
printStatus('Creating colors.xml file and adding it to your Android project');
createNewColorsFile(backgroundConfig, flavor);
}
}

/// Creates the xml file required for the adaptive launcher icon
/// FILE LOCATED HERE: res/mipmap-anydpi/{icon-name-from-yaml-config}.xml
void createAdaptiveIconMipmapXmlFile(
Map<String, dynamic> flutterLauncherIconsConfig, String? flavor) {
void createAdaptiveIconMipmapXmlFile(Map<String, dynamic> flutterLauncherIconsConfig, String? flavor) {
if (isCustomAndroidFile(flutterLauncherIconsConfig)) {
File(constants.androidAdaptiveXmlFolder(flavor) +
getNewIconName(flutterLauncherIconsConfig) +
'.xml')
File(constants.androidAdaptiveXmlFolder(flavor) + getNewIconName(flutterLauncherIconsConfig) + '.xml')
.create(recursive: true)
.then((File adaptiveIcon) {
adaptiveIcon.writeAsString(xml_template.icLauncherXml);
});
} else {
File(constants.androidAdaptiveXmlFolder(flavor) +
constants.androidDefaultIconName +
'.xml')
File(constants.androidAdaptiveXmlFolder(flavor) + constants.androidDefaultIconName + '.xml')
.create(recursive: true)
.then((File adaptiveIcon) {
adaptiveIcon.writeAsString(xml_template.icLauncherXml);
Expand All @@ -142,8 +127,11 @@ void createAdaptiveIconMipmapXmlFile(
}

/// creates adaptive background using png image
void createAdaptiveBackgrounds(Map<String, dynamic> yamlConfig,
String adaptiveIconBackgroundImagePath, String? flavor) {
void _createAdaptiveBackgrounds(
Map<String, dynamic> yamlConfig,
String adaptiveIconBackgroundImagePath,
String? flavor,
) {
final String filePath = adaptiveIconBackgroundImagePath;
final Image? image = decodeImageFile(filePath);
if (image == null) {
Expand All @@ -153,24 +141,19 @@ void createAdaptiveBackgrounds(Map<String, dynamic> yamlConfig,
// creates a png image (ic_adaptive_background.png) for the adaptive icon background in each of the locations
// it is required
for (AndroidIconTemplate androidIcon in adaptiveForegroundIcons) {
saveNewImages(androidIcon, image,
constants.androidAdaptiveBackgroundFileName, flavor);
_saveNewImages(androidIcon, image, constants.androidAdaptiveBackgroundFileName, flavor);
}

// Creates the xml file required for the adaptive launcher icon
// FILE LOCATED HERE: res/mipmap-anydpi/{icon-name-from-yaml-config}.xml
if (isCustomAndroidFile(yamlConfig)) {
File(constants.androidAdaptiveXmlFolder(flavor) +
getNewIconName(yamlConfig) +
'.xml')
File(constants.androidAdaptiveXmlFolder(flavor) + getNewIconName(yamlConfig) + '.xml')
.create(recursive: true)
.then((File adaptiveIcon) {
adaptiveIcon.writeAsString(xml_template.icLauncherDrawableBackgroundXml);
});
} else {
File(constants.androidAdaptiveXmlFolder(flavor) +
constants.androidDefaultIconName +
'.xml')
File(constants.androidAdaptiveXmlFolder(flavor) + constants.androidDefaultIconName + '.xml')
.create(recursive: true)
.then((File adaptiveIcon) {
adaptiveIcon.writeAsString(xml_template.icLauncherDrawableBackgroundXml);
Expand All @@ -180,9 +163,7 @@ void createAdaptiveBackgrounds(Map<String, dynamic> yamlConfig,

/// Creates a colors.xml file if it was missing from android/app/src/main/res/values/colors.xml
void createNewColorsFile(String backgroundColor, String? flavor) {
File(constants.androidColorsFile(flavor))
.create(recursive: true)
.then((File colorsFile) {
File(constants.androidColorsFile(flavor)).create(recursive: true).then((File colorsFile) {
colorsFile.writeAsString(xml_template.colorsXml).then((File file) {
updateColorsFile(colorsFile, backgroundColor);
});
Expand All @@ -207,8 +188,7 @@ void updateColorsFile(File colorsFile, String backgroundColor) {

// Add new line if we didn't find an existing value
if (!foundExisting) {
lines.insert(lines.length - 1,
'\t<color name="ic_launcher_background">$backgroundColor</color>');
lines.insert(lines.length - 1, '\t<color name="ic_launcher_background">$backgroundColor</color>');
}

colorsFile.writeAsStringSync(lines.join('\n'));
Expand Down Expand Up @@ -238,10 +218,7 @@ void overwriteExistingIcons(
String? flavor,
) {
final Image newFile = createResizedImage(template.size, image);
File(constants.androidResFolder(flavor) +
template.directoryName +
'/' +
filename)
File(constants.androidResFolder(flavor) + template.directoryName + '/' + filename)
.create(recursive: true)
.then((File file) {
file.writeAsBytesSync(encodePng(newFile));
Expand All @@ -251,13 +228,9 @@ void overwriteExistingIcons(
/// Saves new launcher icons to the project, keeping the old launcher icons.
/// Note: Do not change interpolation unless you end up with better results
/// https://github.com/fluttercommunity/flutter_launcher_icons/issues/101#issuecomment-495528733
void saveNewImages(AndroidIconTemplate template, Image image,
String iconFilePath, String? flavor) {
void _saveNewImages(AndroidIconTemplate template, Image image, String iconFilePath, String? flavor) {
final Image newFile = createResizedImage(template.size, image);
File(constants.androidResFolder(flavor) +
template.directoryName +
'/' +
iconFilePath)
File(constants.androidResFolder(flavor) + template.directoryName + '/' + iconFilePath)
.create(recursive: true)
.then((File file) {
file.writeAsBytesSync(encodePng(newFile));
Expand All @@ -268,19 +241,15 @@ void saveNewImages(AndroidIconTemplate template, Image image,
/// with the new icon name (only if it has changed)
///
/// Note: default iconName = "ic_launcher"
Future<void> overwriteAndroidManifestWithNewLauncherIcon(
String iconName, File androidManifestFile) async {
Future<void> overwriteAndroidManifestWithNewLauncherIcon(String iconName, File androidManifestFile) async {
// we do not use `file.readAsLinesSync()` here because that always gets rid of the last empty newline
final List<String> oldManifestLines =
(await androidManifestFile.readAsString()).split('\n');
final List<String> transformedLines =
transformAndroidManifestWithNewLauncherIcon(oldManifestLines, iconName);
final List<String> oldManifestLines = (await androidManifestFile.readAsString()).split('\n');
final List<String> transformedLines = _transformAndroidManifestWithNewLauncherIcon(oldManifestLines, iconName);
await androidManifestFile.writeAsString(transformedLines.join('\n'));
}

/// Updates only the line containing android:icon with the specified iconName
List<String> transformAndroidManifestWithNewLauncherIcon(
List<String> oldManifestLines, String iconName) {
List<String> _transformAndroidManifestWithNewLauncherIcon(List<String> oldManifestLines, String iconName) {
return oldManifestLines.map((String line) {
if (line.contains('android:icon')) {
// Using RegExp replace the value of android:icon to point to the new icon
Expand All @@ -290,30 +259,34 @@ List<String> transformAndroidManifestWithNewLauncherIcon(
// repeat as often as wanted with no quote at start: [^"]*(\"[^"]*)*
// escaping the slash to place in string: [^"]*(\\"[^"]*)*"
// result: any string which does only include escaped quotes
return line.replaceAll(RegExp(r'android:icon="[^"]*(\\"[^"]*)*"'),
'android:icon="@mipmap/$iconName"');
return line.replaceAll(RegExp(r'android:icon="[^"]*(\\"[^"]*)*"'), 'android:icon="@mipmap/$iconName"');
} else {
return line;
}
}).toList();
}

/// Retrieves the minSdk value from the Android build.gradle file or local.properties file
/// Retrieves the minSdk value from the
/// - flutter.gradle: `'$FLUTTER_ROOT/packages/flutter_tools/gradle/flutter.gradle'`
/// - build.gradle: `'android/app/build.gradle'`
/// - local.properties: `'android/local.properties'`
///
/// If found none returns 0
int minSdk() {
final androidGradleFile = File(constants.androidGradleFile);
final androidLocalPropertiesFile = File(constants.androidLocalPropertiesFile);

// look in build.gradle first
final minSdkValue = getMinSdkFromFile(androidGradleFile);

// look in local.properties. Didn't find minSdk, assume the worst
return minSdkValue != 0
? minSdkValue
: getMinSdkFromFile(androidLocalPropertiesFile);
// looks for minSdk value in build.gradle, flutter.gradle & local.properties.
// this should always be order
// first check build.gradle, then local.properties, then flutter.gradle
return _getMinSdkFromFile(androidGradleFile) ??
_getMinSdkFromFile(androidLocalPropertiesFile) ??
_getMinSdkFlutterGradle(androidLocalPropertiesFile) ??
constants.androidDefaultAndroidMinSDK;
}

/// Retrieves the minSdk value from [File]
int getMinSdkFromFile(File file) {
int? _getMinSdkFromFile(File file) {
final List<String> lines = file.readAsLinesSync();
for (String line in lines) {
if (line.contains('minSdkVersion')) {
Expand All @@ -324,10 +297,53 @@ int getMinSdkFromFile(File file) {
// remove anything from the line that is not a digit
final String minSdk = line.replaceAll(RegExp(r'[^\d]'), '');
// when minSdkVersion value not found
return int.tryParse(minSdk) ?? 0;
return int.tryParse(minSdk);
}
}
return null; // Didn't find minSdk, assume the worst
}

/// A helper function to [_getMinSdkFlutterGradle]
/// which retrives value of `flutter.sdk` from `local.properties` file
String? _getFlutterSdkPathFromLocalProperties(File file) {
final List<String> lines = file.readAsLinesSync();
for (String line in lines) {
if (!line.contains('flutter.sdk=')) {
continue;
}
if (line.contains('#') && line.indexOf('#') < line.indexOf('flutter.sdk=')) {
continue;
}
final flutterSdkPath = line.split('=').last.trim();
if (flutterSdkPath.isEmpty) {
return null;
}
return flutterSdkPath;
}
return null;
}

/// Retrives value of `minSdkVersion` from `flutter.gradle`
int? _getMinSdkFlutterGradle(File localPropertiesFile) {
final flutterRoot = _getFlutterSdkPathFromLocalProperties(localPropertiesFile);
if (flutterRoot == null) {
return null;
}

final flutterGradleFile = File(path.join(flutterRoot, constants.androidFlutterGardlePath));

final List<String> lines = flutterGradleFile.readAsLinesSync();
for (String line in lines) {
if (!line.contains('static int minSdkVersion =')) {
continue;
}
if (line.contains('//') && line.indexOf('//') < line.indexOf('static int minSdkVersion =')) {
continue;
}
final minSdk = line.split('=').last.trim();
return int.tryParse(minSdk);
}
return 0; // Didn't find minSdk, assume the worst
return null;
}

/// Method for the retrieval of the Android icon path
Expand Down
9 changes: 8 additions & 1 deletion lib/constants.dart
Expand Up @@ -9,6 +9,13 @@ String androidColorsFile(String? flavor) => "android/app/src/${flavor ?? 'main'}
const String androidManifestFile = 'android/app/src/main/AndroidManifest.xml';
const String androidGradleFile = 'android/app/build.gradle';
const String androidLocalPropertiesFile = 'android/local.properties';

/// Relative path to flutter.gradle from flutter sdk path
const String androidFlutterGardlePath = 'packages/flutter_tools/gradle/flutter.gradle';

/// Default min_sdk value for android
/// https://github.com/flutter/flutter/blob/master/packages/flutter_tools/gradle/flutter.gradle#L35-L37
const int androidDefaultAndroidMinSDK = 21;
const String androidFileName = 'ic_launcher.png';
const String androidAdaptiveForegroundFileName = 'ic_launcher_foreground.png';
const String androidAdaptiveBackgroundFileName = 'ic_launcher_background.png';
Expand Down Expand Up @@ -62,7 +69,7 @@ const String errorMissingPlatform = 'No platform specified within config to gene
const String errorMissingRegularAndroid = 'Adaptive icon config found but no regular Android config. '
'Below API 26 the regular Android config is required';
const String errorMissingMinSdk = 'Cannot not find minSdk from android/app/build.gradle or android/local.properties'
'Specify minSdk in either android/app/build.gradle or android/local.properties';
' Specify minSdk in your flutter_launcher_config.yaml with "min_sdk_android"';
const String errorIncorrectIconName = 'The icon name must contain only lowercase a-z, 0-9, or underscore: '
'E.g. "ic_my_new_icon"';

Expand Down
10 changes: 10 additions & 0 deletions lib/flutter_launcher_icons_config.dart
Expand Up @@ -42,6 +42,14 @@ class FlutterLauncherIconsConfig {
@JsonKey(name: 'adaptive_icon_background')
final String? adaptiveIconBackground;

/// Android min_sdk_android
@JsonKey(name: 'min_sdk_android', defaultValue: constants.androidDefaultAndroidMinSDK)
final int minSdkAndroid;

/// IOS remove_alpha_ios
@JsonKey(name: 'remove_alpha_ios', defaultValue: true)
final bool removeAlphaIOS;

/// Web platform config
@JsonKey(name: 'web')
final WebConfig? webConfig;
Expand All @@ -59,6 +67,8 @@ class FlutterLauncherIconsConfig {
this.imagePathIOS,
this.adaptiveIconForeground,
this.adaptiveIconBackground,
this.minSdkAndroid = constants.androidDefaultAndroidMinSDK,
this.removeAlphaIOS = true,
this.webConfig,
this.windowsConfig,
});
Expand Down

0 comments on commit 4ffd0f5

Please sign in to comment.