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
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
164 changes: 87 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 @@ -47,51 +47,42 @@ void createDefaultIcons(
}
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,8 @@ void createAdaptiveIconMipmapXmlFile(
}

/// creates adaptive background using png image
void createAdaptiveBackgrounds(Map<String, dynamic> yamlConfig,
String adaptiveIconBackgroundImagePath, String? flavor) {
void createAdaptiveBackgrounds(
RatakondalaArun marked this conversation as resolved.
Show resolved Hide resolved
Map<String, dynamic> yamlConfig, String adaptiveIconBackgroundImagePath, String? flavor) {
final String filePath = adaptiveIconBackgroundImagePath;
final Image? image = decodeImageFile(filePath);
if (image == null) {
Expand All @@ -153,24 +138,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 +160,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 +185,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 +215,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 +225,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) {
RatakondalaArun marked this conversation as resolved.
Show resolved Hide resolved
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 +238,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) {
RatakondalaArun marked this conversation as resolved.
Show resolved Hide resolved
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 +256,31 @@ 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
int minSdk() {
/// 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.
return getMinSdkFlutterGradle(androidLocalPropertiesFile) ??
getMinSdkFromFile(androidGradleFile) ??
getMinSdkFromFile(androidLocalPropertiesFile);
}

/// Retrieves the minSdk value from [File]
int getMinSdkFromFile(File file) {
int? getMinSdkFromFile(File file) {
RatakondalaArun marked this conversation as resolved.
Show resolved Hide resolved
final List<String> lines = file.readAsLinesSync();
for (String line in lines) {
if (line.contains('minSdkVersion')) {
Expand All @@ -324,10 +291,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
6 changes: 5 additions & 1 deletion lib/constants.dart
Expand Up @@ -9,6 +9,10 @@ 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';

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 +66,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
3 changes: 1 addition & 2 deletions lib/main.dart
Expand Up @@ -13,7 +13,6 @@ import 'package:flutter_launcher_icons/pubspec_parser.dart';
import 'package:flutter_launcher_icons/web/web_icon_generator.dart';
import 'package:flutter_launcher_icons/windows/windows_icon_generator.dart';
import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';

const String fileOption = 'file';
const String helpFlag = 'help';
Expand Down Expand Up @@ -126,7 +125,7 @@ Future<void> createIconsFromConfig(
}

if (isNeedingNewAndroidIcon(config) || hasAndroidAdaptiveConfig(config)) {
final int minSdk = android_launcher_icons.minSdk();
final int minSdk = config['min_sdk_android'] ?? android_launcher_icons.minSdk() ?? 0;
RatakondalaArun marked this conversation as resolved.
Show resolved Hide resolved
if (minSdk == 0) {
throw const InvalidConfigException(errorMissingMinSdk);
}
Expand Down