-
Notifications
You must be signed in to change notification settings - Fork 404
/
Copy pathandroid.dart
476 lines (434 loc) · 15.5 KB
/
android.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
// ignore_for_file: public_member_api_docs
import 'dart:io';
import 'package:flutter_launcher_icons/config/config.dart';
import 'package:flutter_launcher_icons/constants.dart';
import 'package:flutter_launcher_icons/constants.dart' as constants;
import 'package:flutter_launcher_icons/custom_exceptions.dart';
import 'package:flutter_launcher_icons/utils.dart' as utils;
import 'package:flutter_launcher_icons/xml_templates.dart' as xml_template;
import 'package:image/image.dart';
import 'package:path/path.dart' as path;
class AndroidIconTemplate {
AndroidIconTemplate({required this.size, required this.directoryName});
final String directoryName;
final int size;
}
final List<AndroidIconTemplate> adaptiveForegroundIcons = <AndroidIconTemplate>[
AndroidIconTemplate(directoryName: 'drawable-mdpi', size: 108),
AndroidIconTemplate(directoryName: 'drawable-hdpi', size: 162),
AndroidIconTemplate(directoryName: 'drawable-xhdpi', size: 216),
AndroidIconTemplate(directoryName: 'drawable-xxhdpi', size: 324),
AndroidIconTemplate(directoryName: 'drawable-xxxhdpi', size: 432),
];
List<AndroidIconTemplate> androidIcons = <AndroidIconTemplate>[
AndroidIconTemplate(directoryName: 'mipmap-mdpi', size: 48),
AndroidIconTemplate(directoryName: 'mipmap-hdpi', size: 72),
AndroidIconTemplate(directoryName: 'mipmap-xhdpi', size: 96),
AndroidIconTemplate(directoryName: 'mipmap-xxhdpi', size: 144),
AndroidIconTemplate(directoryName: 'mipmap-xxxhdpi', size: 192),
];
void createDefaultIcons(
Config config,
String? flavor,
) {
utils.printStatus('Creating default icons Android');
// TODO(p-mazhnik): support prefixPath
final String? filePath = config.getImagePathAndroid();
if (filePath == null) {
throw const InvalidConfigException(errorMissingImagePath);
}
final Image? image = utils.decodeImageFile(filePath);
if (image == null) {
return;
}
final File androidManifestFile = File(constants.androidManifestFile);
if (config.isCustomAndroidFile) {
utils.printStatus('Adding a new Android launcher icon');
final String iconName = config.android;
isAndroidIconNameCorrectFormat(iconName);
final String iconPath = '$iconName.png';
for (AndroidIconTemplate template in androidIcons) {
_saveNewImages(template, image, iconPath, flavor);
}
overwriteAndroidManifestWithNewLauncherIcon(iconName, androidManifestFile);
} else {
utils.printStatus(
'Overwriting the default Android launcher icon with a new icon',
);
for (AndroidIconTemplate template in androidIcons) {
overwriteExistingIcons(
template,
image,
constants.androidFileName,
flavor,
);
}
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,
);
}
return true;
}
void createAdaptiveIcons(
Config config,
String? flavor,
) {
utils.printStatus('Creating adaptive icons Android');
// Retrieve the necessary Flutter Launcher Icons configuration from the pubspec.yaml file
final String? backgroundConfig = config.adaptiveIconBackground;
final String? foregroundImagePath = config.adaptiveIconForeground;
if (backgroundConfig == null || foregroundImagePath == null) {
throw const InvalidConfigException(errorMissingImagePath);
}
final Image? foregroundImage = utils.decodeImageFile(foregroundImagePath);
if (foregroundImage == null) {
return;
}
// Create adaptive icon foreground images
for (AndroidIconTemplate androidIcon in adaptiveForegroundIcons) {
overwriteExistingIcons(
androidIcon,
foregroundImage,
constants.androidAdaptiveForegroundFileName,
flavor,
);
}
// Create adaptive icon background
if (isAdaptiveIconConfigPngFile(backgroundConfig)) {
_createAdaptiveBackgrounds(
config,
backgroundConfig,
flavor,
);
} else {
updateColorsXmlFile(backgroundConfig, flavor);
}
}
void createAdaptiveMonochromeIcons(
Config config,
String? flavor,
) {
utils.printStatus('Creating adaptive monochrome icons Android');
// Retrieve the necessary Flutter Launcher Icons configuration from the pubspec.yaml file
final String? monochromeImagePath = config.adaptiveIconMonochrome;
if (monochromeImagePath == null) {
throw const InvalidConfigException(errorMissingImagePath);
}
final Image? monochromeImage = utils.decodeImageFile(monochromeImagePath);
if (monochromeImage == null) {
return;
}
// Create adaptive icon monochrome images
for (AndroidIconTemplate androidIcon in adaptiveForegroundIcons) {
overwriteExistingIcons(
androidIcon,
monochromeImage,
constants.androidAdaptiveMonochromeFileName,
flavor,
);
}
}
void createMipmapXmlFile(
Config config,
String? flavor,
) {
utils.printStatus('Creating mipmap xml file Android');
String xmlContent = '';
if (config.hasAndroidAdaptiveConfig) {
if (isAdaptiveIconConfigPngFile(config.adaptiveIconBackground!)) {
xmlContent +=
' <background android:drawable="@drawable/ic_launcher_background"/>\n';
} else {
xmlContent +=
' <background android:drawable="@color/ic_launcher_background"/>\n';
}
xmlContent += '''
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="${config.adaptiveIconForegroundInset}%" />
</foreground>
''';
}
if (config.hasAndroidAdaptiveMonochromeConfig) {
xmlContent += '''
<monochrome>
<inset
android:drawable="@drawable/ic_launcher_monochrome"
android:inset="${config.adaptiveIconForegroundInset}%" />
</monochrome>
''';
}
late File mipmapXmlFile;
if (config.isCustomAndroidFile) {
mipmapXmlFile = File(
constants.androidAdaptiveXmlFolder(flavor) + config.android + '.xml',
);
} else {
mipmapXmlFile = File(
constants.androidAdaptiveXmlFolder(flavor) +
constants.androidDefaultIconName +
'.xml',
);
}
mipmapXmlFile.create(recursive: true).then((File adaptiveIconFile) {
adaptiveIconFile.writeAsString(
xml_template.mipmapXmlFile.replaceAll('{{CONTENT}}', xmlContent),
);
});
}
/// Retrieves the colors.xml file for the project.
///
/// If the colors.xml file is found, it is updated with a new color item for the
/// adaptive icon background.
///
/// If not, the colors.xml file is created and a color item for the adaptive icon
/// background is included in the new colors.xml file.
void updateColorsXmlFile(String backgroundConfig, String? flavor) {
final File colorsXml = File(constants.androidColorsFile(flavor));
if (colorsXml.existsSync()) {
utils.printStatus(
'Updating colors.xml with color for adaptive icon background',
);
updateColorsFile(colorsXml, backgroundConfig);
} else {
utils.printStatus('No colors.xml file found in your Android project');
utils.printStatus(
'Creating colors.xml file and adding it to your Android project',
);
createNewColorsFile(backgroundConfig, flavor);
}
}
/// creates adaptive background using png image
void _createAdaptiveBackgrounds(
Config config,
String adaptiveIconBackgroundImagePath,
String? flavor,
) {
final String filePath = adaptiveIconBackgroundImagePath;
final Image? image = utils.decodeImageFile(filePath);
if (image == null) {
return;
}
// 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,
);
}
}
/// 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) {
colorsFile.writeAsString(xml_template.colorsXml).then((File file) {
updateColorsFile(colorsFile, backgroundColor);
});
});
}
/// Updates the colors.xml with the new adaptive launcher icon color
void updateColorsFile(File colorsFile, String backgroundColor) {
// Write foreground color
final List<String> lines = colorsFile.readAsLinesSync();
bool foundExisting = false;
for (int x = 0; x < lines.length; x++) {
String line = lines[x];
if (line.contains('name="ic_launcher_background"')) {
foundExisting = true;
// replace anything between tags which does not contain another tag
line = line.replaceAll(RegExp(r'>([^><]*)<'), '>$backgroundColor<');
lines[x] = line;
break;
}
}
// 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>',
);
}
colorsFile.writeAsStringSync(lines.join('\n'));
}
/// Overrides the existing launcher icons in the project
/// Note: Do not change interpolation unless you end up with better results (see issue for result when using cubic
/// interpolation)
/// https://github.com/fluttercommunity/flutter_launcher_icons/issues/101#issuecomment-495528733
void overwriteExistingIcons(
AndroidIconTemplate template,
Image image,
String filename,
String? flavor,
) {
final Image newFile = utils.createResizedImage(template.size, image);
File(
constants.androidResFolder(flavor) +
template.directoryName +
'/' +
filename,
).create(recursive: true).then((File file) {
file.writeAsBytesSync(encodePng(newFile));
});
}
/// 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,
) {
final Image newFile = utils.createResizedImage(template.size, image);
File(
constants.androidResFolder(flavor) +
template.directoryName +
'/' +
iconFilePath,
).create(recursive: true).then((File file) {
file.writeAsBytesSync(encodePng(newFile));
});
}
/// Updates the line which specifies the launcher icon within the AndroidManifest.xml
/// with the new icon name (only if it has changed)
///
/// Note: default iconName = "ic_launcher"
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);
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,
) {
return oldManifestLines.map((String line) {
if (line.contains('android:icon')) {
// Using RegExp replace the value of android:icon to point to the new icon
// anything but a quote of any length: [^"]*
// an escaped quote: \\" (escape slash, because it exists regex)
// quote, no quote / quote with things behind : \"[^"]*
// 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"',
);
} else {
return line;
}
}).toList();
}
/// 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 [constants.androidDefaultAndroidMinSDK]
int minSdk() {
final androidGradleFile = File(constants.androidGradleFile);
final androidLocalPropertiesFile = File(constants.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) {
final List<String> lines = file.readAsLinesSync();
for (String line in lines) {
if (line.contains('minSdkVersion')) {
if (line.contains('//') &&
line.indexOf('//') < line.indexOf('minSdkVersion')) {
// This line is commented
continue;
}
// 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);
}
}
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 null;
}
/// Returns true if the adaptive icon configuration is a PNG image
bool isAdaptiveIconConfigPngFile(String backgroundFile) {
return backgroundFile.endsWith('.png');
}
/// (NOTE THIS IS JUST USED FOR UNIT TEST)
/// Ensures the correct path is used for generating adaptive icons
/// "Next you must create alternative drawable resources in your app for use with
/// Android 8.0 (API level 26) in res/mipmap-anydpi/ic_launcher.xml"
/// Source: https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive
bool isCorrectMipmapDirectoryForAdaptiveIcon(String path) {
return path == 'android/app/src/main/res/mipmap-anydpi-v26/';
}