Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RatakondalaArun/min-sdk-not-found-384 #392

Merged
merged 8 commits into from Aug 2, 2022
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