Android min sdk 24 part 1 (#170882)

- **bump template version, update migration code to handle api 21-23 and
expand it to cover multiple min api level definitions**
- **bump add to app project to minimum api level 24**
- **PathUtilsTest converted to using config, removed test code that
branched on versions older than 24**
- **Update additional build.gradle files to have minsdk 24 and
documenation**

Related to #170807

G3 cls 
- espresso https://critique.corp.google.com/cl/776592881
- integration tests cl/778034198
- removal of pre api 23 samples tests cl/777652802


## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

---------

Co-authored-by: Gray Mackall <34871572+gmackall@users.noreply.github.com>
This commit is contained in:
Reid Baker 2025-07-02 14:30:22 +00:00 committed by GitHub
parent 4937e469d6
commit 1dfb59d281
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 118 additions and 210 deletions

View File

@ -14,13 +14,12 @@ Future<void> main() async {
try {
await runProjectTest((FlutterProject flutterProject) async {
section('APK contains plugin classes');
await flutterProject.setMinSdkVersion(21);
await flutterProject.addPlugin('google_maps_flutter:^2.2.1');
await flutterProject.addPlugin('google_maps_flutter:^2.12.1');
await inDirectory(flutterProject.rootPath, () async {
await flutter(
'build',
options: <String>['apk', '--debug', '--target-platform=android-arm'],
options: <String>['apk', '--debug', '--target-platform=android-arm64'],
);
final File apk = File(
'${flutterProject.rootPath}/build/app/outputs/flutter-apk/app-debug.apk',
@ -30,8 +29,8 @@ Future<void> main() async {
}
// https://github.com/flutter/flutter/issues/72185
await checkApkContainsMethods(apk, <String>[
'io.flutter.plugins.googlemaps.GoogleMapController void onFlutterViewAttached(android.view.View)',
'io.flutter.plugins.googlemaps.GoogleMapController void onFlutterViewDetached()',
'io.flutter.plugins.googlemaps.GoogleMapController android.view.View getView()',
'io.flutter.plugins.googlemaps.GoogleMapController void dispose()',
]);
});
});

View File

@ -300,18 +300,6 @@ android {
});
}
Future<void> setMinSdkVersion(int sdkVersion) async {
final File buildScript = appBuildFile;
buildScript.openWrite(mode: FileMode.append).write('''
android {
defaultConfig {
minSdk = $sdkVersion
}
}
''');
}
Future<void> getPackages() async {
await inDirectory(Directory(rootPath), () async {
await flutter('pub', options: <String>['get']);

View File

@ -43,7 +43,7 @@ android {
}
defaultConfig {
minSdk = 21
minSdk = 24
targetSdk = 35
versionCode = flutterVersionCode.toInteger()
versionName = flutterVersionName

View File

@ -24,7 +24,7 @@ android {
defaultConfig {
applicationId = "io.flutter.addtoapp"
minSdk = 21
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"

View File

@ -24,7 +24,7 @@ android {
defaultConfig {
applicationId "io.flutter.add2app"
minSdk = 21
minSdk = 24
targetSdk = 35
versionCode 1
versionName "1.0"

View File

@ -24,7 +24,7 @@ android {
defaultConfig {
applicationId "io.flutter.add2app"
minSdk = 21
minSdk = 24
targetSdk = 35
versionCode 1
versionName "1.0"

View File

@ -21,10 +21,10 @@ apply plugin: "com.android.library"
android {
namespace "io.flutter.embedding"
compileSdk 36
compileSdk = 36
defaultConfig {
minSdkVersion 21
minSdk = 24
}
sourceSets {

View File

@ -10,7 +10,7 @@ import androidx.annotation.VisibleForTesting;
public class Build {
/** For use in place of the Android Build.VERSION_CODES class. */
public static class API_LEVELS {
@VisibleForTesting public static final int FLUTTER_MIN = 21;
@VisibleForTesting public static final int FLUTTER_MIN = 24;
/** Android 5.0 (Lollipop) */
public static final int API_21 = 21;
/** Android 5.1 (Lollipop MR1) */

View File

@ -783,7 +783,8 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
result.setParent(rootAccessibilityView);
}
if (semanticsNode.previousNodeId != -1 && Build.VERSION.SDK_INT >= API_LEVELS.API_22) {
if (semanticsNode.previousNodeId != -1) {
// Requires at least android api 22.
result.setTraversalAfter(rootAccessibilityView, semanticsNode.previousNodeId);
}

View File

@ -38,6 +38,7 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.test.core.app.ActivityScenario;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.Build.API_LEVELS;
import io.flutter.FlutterInjector;
import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode;
import io.flutter.embedding.engine.FlutterEngine;
@ -601,7 +602,7 @@ public class FlutterActivityTest {
}
@Test
@Config(minSdk = API_LEVELS.API_21, maxSdk = API_LEVELS.API_28)
@Config(minSdk = API_LEVELS.FLUTTER_MIN, maxSdk = API_LEVELS.API_28)
public void fullyDrawn_beforeAndroidQ() {
Intent intent = FlutterActivityWithReportFullyDrawn.createDefaultIntent(ctx);
ActivityController<FlutterActivityWithReportFullyDrawn> activityController =

View File

@ -306,7 +306,7 @@ public class FlutterViewTest {
// set to -1 values, so it is clear if the wrong algorithm is used.
@Test
@TargetApi(30)
@Config(sdk = 30)
@Config(sdk = API_LEVELS.API_30)
public void reportSystemInsetWhenNotFullscreen() {
// Without custom shadows, the default system ui visibility flags is 0.
FlutterView flutterView = new FlutterView(ctx);
@ -348,7 +348,7 @@ public class FlutterViewTest {
// set to -1 values, so it is clear if the wrong algorithm is used.
@Test
@TargetApi(34)
@Config(minSdk = 34)
@Config(minSdk = API_LEVELS.API_34)
public void reportSystemInsetWhenNotFullscreenForSystemBar() {
// Without custom shadows, the default system ui visibility flags is 0.
FlutterView flutterView = new FlutterView(ctx);
@ -426,7 +426,7 @@ public class FlutterViewTest {
// This test uses the pre-API 30 Algorithm for window insets.
@Test
@TargetApi(28)
@Config(sdk = 28)
@Config(sdk = API_LEVELS.API_28)
public void reportSystemInsetWhenNotFullscreenLegacy() {
// Without custom shadows, the default system ui visibility flags is 0.
FlutterView flutterView = new FlutterView(ctx);
@ -459,7 +459,7 @@ public class FlutterViewTest {
@SuppressWarnings("deprecation")
// getSystemUiVisibility, getWindowSystemUiVisibility, required to test pre api 30 behavior.
@Test
@Config(minSdk = 23, maxSdk = 29, qualifiers = "land")
@Config(minSdk = API_LEVELS.FLUTTER_MIN, maxSdk = API_LEVELS.API_29, qualifiers = "land")
public void systemInsetHandlesFullscreenNavbarRight() {
FlutterView flutterView = spy(new FlutterView(ctx));
setExpectedDisplayRotation(Surface.ROTATION_90);
@ -521,7 +521,7 @@ public class FlutterViewTest {
}
@Test
@Config(minSdk = API_LEVELS.API_23, maxSdk = API_LEVELS.API_29, qualifiers = "land")
@Config(minSdk = API_LEVELS.FLUTTER_MIN, maxSdk = API_LEVELS.API_29, qualifiers = "land")
public void calculateShouldZeroSidesInLandscapeRotation270API23Plus() {
FlutterView flutterView = spy(new FlutterView(ctx));
setExpectedDisplayRotation(Surface.ROTATION_270);
@ -531,7 +531,7 @@ public class FlutterViewTest {
@SuppressWarnings("deprecation")
// getSystemUiVisibility, getWindowSystemUiVisibility required to test pre api 30 behavior.
@Test
@Config(minSdk = 23, maxSdk = 29, qualifiers = "land")
@Config(minSdk = API_LEVELS.FLUTTER_MIN, maxSdk = API_LEVELS.API_29, qualifiers = "land")
public void systemInsetHandlesFullscreenNavbarLeft() {
FlutterView flutterView = spy(new FlutterView(ctx));
setExpectedDisplayRotation(Surface.ROTATION_270);
@ -573,7 +573,7 @@ public class FlutterViewTest {
// set to -1 values, so it is clear if the wrong algorithm is used.
@Test
@TargetApi(30)
@Config(sdk = 30, qualifiers = "land")
@Config(sdk = API_LEVELS.API_30, qualifiers = "land")
public void systemInsetGetInsetsFullscreen() {
FlutterView flutterView = spy(new FlutterView(ctx));
setExpectedDisplayRotation(Surface.ROTATION_270);
@ -611,7 +611,7 @@ public class FlutterViewTest {
// This test uses the pre-API 30 Algorithm for window insets.
@Test
@TargetApi(28)
@Config(sdk = 28, qualifiers = "land")
@Config(sdk = API_LEVELS.API_28, qualifiers = "land")
public void systemInsetGetInsetsFullscreenLegacy() {
FlutterView flutterView = spy(new FlutterView(ctx));
setExpectedDisplayRotation(Surface.ROTATION_270);
@ -652,7 +652,7 @@ public class FlutterViewTest {
// set to -1 values, so it is clear if the wrong algorithm is used.
@Test
@TargetApi(30)
@Config(sdk = 30, qualifiers = "land")
@Config(sdk = API_LEVELS.API_30, qualifiers = "land")
public void systemInsetDisplayCutoutSimple() {
FlutterView flutterView = spy(new FlutterView(ctx));
assertEquals(0, flutterView.getSystemUiVisibility());
@ -700,7 +700,7 @@ public class FlutterViewTest {
@SuppressWarnings("deprecation")
@Test
@Config(minSdk = 28)
@Config(minSdk = API_LEVELS.API_28)
public void onApplyWindowInsetsSetsDisplayCutouts() {
// Use an Activity context so that FlutterView.onAttachedToWindow completes.
try (ActivityScenario<Activity> scenario = ActivityScenario.launch(Activity.class)) {
@ -1030,7 +1030,7 @@ public class FlutterViewTest {
@Test
@SuppressLint("PrivateApi")
@Config(sdk = Build.VERSION_CODES.P)
@Config(sdk = API_LEVELS.API_28)
public void findViewByAccessibilityIdTraversal_returnsRootViewOnAndroid28() throws Exception {
FlutterView flutterView = new FlutterView(ctx);
@ -1041,7 +1041,7 @@ public class FlutterViewTest {
}
@Test
@Config(sdk = Build.VERSION_CODES.P)
@Config(sdk = API_LEVELS.API_28)
@SuppressLint("PrivateApi")
public void findViewByAccessibilityIdTraversal_returnsChildViewOnAndroid28() throws Exception {
FlutterView flutterView = new FlutterView(ctx);
@ -1058,7 +1058,7 @@ public class FlutterViewTest {
}
@Test
@Config(sdk = Build.VERSION_CODES.Q)
@Config(sdk = API_LEVELS.API_29)
@SuppressLint("PrivateApi")
public void findViewByAccessibilityIdTraversal_returnsRootViewOnAndroid29() throws Exception {
FlutterView flutterView = new FlutterView(ctx);
@ -1126,7 +1126,7 @@ public class FlutterViewTest {
// set to -1 values, so it is clear if the wrong algorithm is used.
@Test
@TargetApi(30)
@Config(sdk = 30)
@Config(sdk = API_LEVELS.API_30)
public void setPaddingTopToZeroForFullscreenMode() {
FlutterView flutterView = new FlutterView(ctx);
FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
@ -1163,7 +1163,7 @@ public class FlutterViewTest {
@Test
@TargetApi(28)
@Config(
sdk = 28,
sdk = API_LEVELS.API_28,
shadows = {
FlutterViewTest.ShadowFullscreenView.class,
})

View File

@ -5,7 +5,6 @@
package io.flutter.embedding.engine.loader;
import static android.os.Looper.getMainLooper;
import static io.flutter.Build.API_LEVELS;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertNotNull;
@ -16,13 +15,10 @@ import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context;
import android.os.Bundle;
@ -38,7 +34,6 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.robolectric.annotation.Config;
@RunWith(AndroidJUnit4.class)
public class FlutterLoaderTest {
@ -404,18 +399,4 @@ public class FlutterLoaderTest {
List<String> arguments = Arrays.asList(shellArgsCaptor.getValue());
assertTrue(arguments.contains(shaderModeArg));
}
@Test
@TargetApi(API_LEVELS.API_23)
@Config(sdk = API_LEVELS.API_23)
public void itReportsFpsToVsyncWaiterAndroidM() {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
FlutterLoader flutterLoader = new FlutterLoader(mockFlutterJNI);
Context appContextSpy = spy(ctx);
assertFalse(flutterLoader.initialized());
flutterLoader.startInitialization(appContextSpy);
verify(appContextSpy, never()).getSystemService(anyString());
}
}

View File

@ -247,109 +247,6 @@ public class LocalizationPluginTest {
assertEquals(result[2], "");
}
// Tests the legacy pre API 24 algorithm.
@Test
@Config(minSdk = API_LEVELS.API_21, maxSdk = API_LEVELS.API_23, qualifiers = "es-rMX")
public void computePlatformResolvedLocale_emptySupportedLocales_beforeAndroidN() {
FlutterJNI flutterJNI = new FlutterJNI();
DartExecutor dartExecutor = mock(DartExecutor.class);
flutterJNI.setLocalizationPlugin(
new LocalizationPlugin(ctx, new LocalizationChannel(dartExecutor)));
String[] supportedLocales = new String[] {};
String[] result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
assertEquals(result.length, 0);
}
@Test
@Config(minSdk = API_LEVELS.API_21, maxSdk = API_LEVELS.API_23, qualifiers = "")
public void computePlatformResolvedLocale_selectFirstLocaleWhenNoUserSetting_beforeAndroidN() {
FlutterJNI flutterJNI = new FlutterJNI();
DartExecutor dartExecutor = mock(DartExecutor.class);
flutterJNI.setLocalizationPlugin(
new LocalizationPlugin(ctx, new LocalizationChannel(dartExecutor)));
String[] supportedLocales =
new String[] {
"fr", "FR", "",
"zh", "", "",
"en", "CA", ""
};
String[] result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
assertEquals(result.length, 3);
assertEquals(result[0], "fr");
assertEquals(result[1], "FR");
assertEquals(result[2], "");
}
@Test
@Config(minSdk = API_LEVELS.API_21, maxSdk = API_LEVELS.API_23, qualifiers = "fr-rCH")
public void computePlatformResolvedLocale_selectFirstLocaleWhenNoExactMatch_beforeAndroidN() {
FlutterJNI flutterJNI = new FlutterJNI();
DartExecutor dartExecutor = mock(DartExecutor.class);
flutterJNI.setLocalizationPlugin(
new LocalizationPlugin(ctx, new LocalizationChannel(dartExecutor)));
// Example from https://developer.android.com/guide/topics/resources/multilingual-support#postN
String[] supportedLocales =
new String[] {
"en", "", "",
"de", "DE", "",
"es", "ES", "",
"fr", "FR", "",
"it", "IT", ""
};
String[] result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
assertEquals(result.length, 3);
assertEquals(result[0], "en");
assertEquals(result[1], "");
assertEquals(result[2], "");
}
@Test
@Config(minSdk = API_LEVELS.API_21, maxSdk = API_LEVELS.API_23, qualifiers = "it-rIT")
public void computePlatformResolvedLocale_selectExactMatchLocale_beforeAndroidN() {
FlutterJNI flutterJNI = new FlutterJNI();
DartExecutor dartExecutor = mock(DartExecutor.class);
flutterJNI.setLocalizationPlugin(
new LocalizationPlugin(ctx, new LocalizationChannel(dartExecutor)));
String[] supportedLocales =
new String[] {
"en", "", "",
"de", "DE", "",
"es", "ES", "",
"fr", "FR", "",
"it", "IT", ""
};
String[] result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
assertEquals(result.length, 3);
assertEquals(result[0], "it");
assertEquals(result[1], "IT");
assertEquals(result[2], "");
}
@Test
@Config(minSdk = API_LEVELS.API_21, maxSdk = API_LEVELS.API_23, qualifiers = "fr-rCH")
public void computePlatformResolvedLocale_selectOnlyLanguageLocale_beforeAndroidN() {
FlutterJNI flutterJNI = new FlutterJNI();
DartExecutor dartExecutor = mock(DartExecutor.class);
flutterJNI.setLocalizationPlugin(
new LocalizationPlugin(ctx, new LocalizationChannel(dartExecutor)));
String[] supportedLocales =
new String[] {
"en", "", "",
"de", "DE", "",
"es", "ES", "",
"fr", "FR", "",
"fr", "", "",
"it", "IT", ""
};
String[] result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
assertEquals(result.length, 3);
assertEquals(result[0], "fr");
assertEquals(result[1], "");
assertEquals(result[2], "");
}
@Test
public void localeFromString_languageOnly() {
Locale locale = LocalizationPlugin.localeFromString("en");

View File

@ -17,6 +17,7 @@ import android.view.Display;
import android.view.inputmethod.InputMethodManager;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.Build.API_LEVELS;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
@ -25,7 +26,7 @@ import org.robolectric.annotation.Config;
@TargetApi(API_LEVELS.API_28)
public class SingleViewPresentationTest {
@Test
@Config(minSdk = API_LEVELS.API_21, maxSdk = API_LEVELS.API_30)
@Config(minSdk = API_LEVELS.FLUTTER_MIN, maxSdk = API_LEVELS.API_30)
public void returnsOuterContextInputMethodManager() {
// There's a bug in Android Q caused by the IMM being instanced per display.
// https://github.com/flutter/flutter/issues/38375. We need the context returned by
@ -56,7 +57,7 @@ public class SingleViewPresentationTest {
}
@Test
@Config(minSdk = API_LEVELS.API_21, maxSdk = API_LEVELS.API_30)
@Config(minSdk = API_LEVELS.FLUTTER_MIN, maxSdk = API_LEVELS.API_30)
public void returnsOuterContextInputMethodManager_createDisplayContext() {
// The IMM should also persist across display contexts created from the base context.

View File

@ -65,9 +65,7 @@ public class PathUtilsTest {
public void canGetCacheDir() {
Context context = mock(Context.class);
when(context.getCacheDir()).thenReturn(new File(APP_DATA_PATH + "/cache"));
if (Build.VERSION.SDK_INT >= API_LEVELS.API_21) {
when(context.getCodeCacheDir()).thenReturn(new File(APP_DATA_PATH + "/code_cache"));
}
when(context.getCodeCacheDir()).thenReturn(new File(APP_DATA_PATH + "/code_cache"));
assertTrue(PathUtils.getCacheDirectory(context).startsWith(APP_DATA_PATH));
}
@ -75,14 +73,10 @@ public class PathUtilsTest {
public void canOnlyGetCachePathWhenDiskFullAndCacheDirNotCreated() {
Context context = mock(Context.class);
when(context.getCacheDir()).thenReturn(null);
if (Build.VERSION.SDK_INT >= API_LEVELS.API_21) {
when(context.getCodeCacheDir()).thenReturn(null);
}
if (Build.VERSION.SDK_INT >= API_LEVELS.API_24) {
when(context.getDataDir()).thenReturn(new File(APP_DATA_PATH));
} else {
when(context.getApplicationInfo().dataDir).thenReturn(APP_DATA_PATH);
}
// Requires at least api 21.
when(context.getCodeCacheDir()).thenReturn(null);
// Requires at least api 24.
when(context.getDataDir()).thenReturn(new File(APP_DATA_PATH));
assertEquals(PathUtils.getCacheDirectory(context), APP_DATA_PATH + "/cache");
}
}

View File

@ -697,8 +697,8 @@ public class AccessibilityBridgeTest {
verify(mockRootView, times(1)).setAccessibilityPaneTitle(eq("new_node2"));
}
@Config(sdk = API_LEVELS.API_21)
@Test
@Config(minSdk = API_LEVELS.FLUTTER_MIN)
public void itCanPerformSetText() {
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
@ -736,8 +736,8 @@ public class AccessibilityBridgeTest {
.dispatchSemanticsAction(1, AccessibilityBridge.Action.SET_TEXT, expectedText);
}
@Config(sdk = API_LEVELS.API_21)
@Test
@Config(minSdk = API_LEVELS.FLUTTER_MIN)
public void itCanPredictSetText() {
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
@ -775,8 +775,8 @@ public class AccessibilityBridgeTest {
assertEquals(nodeInfo.getText().toString(), expectedText);
}
@Config(sdk = API_LEVELS.API_21)
@Test
@Config(minSdk = API_LEVELS.FLUTTER_MIN)
public void itBuildsAttributedString() {
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
@ -841,8 +841,8 @@ public class AccessibilityBridgeTest {
assertEquals(actual.getSpanEnd(spellOutSpan), 9);
}
@Config(sdk = API_LEVELS.API_21)
@Test
@Config(minSdk = API_LEVELS.FLUTTER_MIN)
public void itSetsTextCorrectly() {
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
@ -976,8 +976,8 @@ public class AccessibilityBridgeTest {
assertEquals(actual.toString(), root.identifier);
}
@Config(sdk = API_LEVELS.API_21)
@Test
@Config(minSdk = API_LEVELS.FLUTTER_MIN)
public void itCanCreateAccessibilityNodeInfoWithSetText() {
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
@ -1605,7 +1605,7 @@ public class AccessibilityBridgeTest {
accessibilityBridge.performAction(
1, AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, bundle);
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(1);
// The seletction should be at the beginning of the third line.
// The selection should be at the beginning of the third line.
assertEquals(nodeInfo.getTextSelectionStart(), 21);
assertEquals(nodeInfo.getTextSelectionEnd(), 21);
@ -1617,7 +1617,7 @@ public class AccessibilityBridgeTest {
accessibilityBridge.performAction(
1, AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, bundle);
nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(1);
// The seletction should be at the beginning of the second line.
// The selection should be at the beginning of the second line.
assertEquals(nodeInfo.getTextSelectionStart(), 11);
assertEquals(nodeInfo.getTextSelectionEnd(), 11);
}

View File

@ -33,10 +33,10 @@ println "=========================================="
android {
namespace 'io.flutter.app.test'
compileSdk 36
compileSdk = 36
defaultConfig {
minSdkVersion 21
minSdk = 24
}
compileOptions {

View File

@ -4,3 +4,6 @@
!ide_templates/intellij/.idea
!templates/**/.idea
# Do not need gradle cache of flutter gradle plugin code checked in.
gradle/bin/**

View File

@ -27,9 +27,15 @@ Tests can be run in Android Studio, or directly with Gradle: `./gradlew test`
the engines `third_party` directory at
`<flutter_root>/engine/src/flutter/third_party/gradle/bin/gradle`).
Alternatively, you can run all the tests in one file by passing in the fully qualified class name,
e.g. `./gradlew test --tests com.flutter.gradle.BaseApplicationNameHandlerTest`, or one test in
one file by passing in the fully qualified class name followed by the method name,
If you can not run the test task try running `./gradlew tasks`. If that does not work then there is
a configuration error. The most common one is using the wrong version of java. Java can be
overridden by setting the `JAVA_HOME` environment variable.
This example sets the java version to 17 downloaded with brew and then runs the tests:
`JAVA_HOME=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/ ./gradlew test`
You can run all the tests in one file by passing in the fully qualified class name,
e.g. `./gradlew test --tests com.flutter.gradle.BaseApplicationNameHandlerTest`, or one test in
one file by passing in the fully qualified class name followed by the method name,
e.g `./gradlew test --tests "com.flutter.gradle.BaseApplicationNameHandlerTest.setBaseName respects Flutter tool property"`.
Sometimes changing a test name and then running it will cause an IDE error. To get Android Studio back

View File

@ -105,11 +105,12 @@ object DependencyVersionChecker {
@VisibleForTesting internal val errorKGPVersion: Version = Version(1, 7, 0)
// If this value is changed, then make sure to change the documentation on https://docs.flutter.dev/reference/supported-platforms
// Non inclusive.
@VisibleForTesting
internal val warnMinSdkVersion: Int = 21
internal val warnMinSdkVersion: Int = 24
@VisibleForTesting
internal val errorMinSdkVersion: Int = 1
internal val errorMinSdkVersion: Int = 23
/**
* Checks if the project's Android build time dependencies are each within the respective

View File

@ -23,7 +23,7 @@ open class FlutterExtension {
val compileSdkVersion: Int = 36
/** Sets the minSdkVersion used by default in Flutter app projects. */
val minSdkVersion: Int = 21
val minSdkVersion: Int = 24
/**
* Sets the targetSdkVersion used by default in Flutter app projects.

View File

@ -228,7 +228,7 @@ class DependencyVersionCheckerTest {
@Test
fun `min SDK version in warn range results in warning logs`() {
val exampleWarnSDKVersion = 19
val exampleWarnSDKVersion = 23
val flavorName1 = "flavor1"
val flavorName2 = "flavor2"
val mockProject =
@ -340,8 +340,8 @@ class DependencyVersionCheckerTest {
assertEquals(
dependencyValidationException.message,
"Error: Your project's minimum Android SDK (flavor='flavor') version (0) is lower than " +
"Flutter's minimum supported version of 1. Please upgrade your minimum Android SDK " +
"Error: Your project's minimum Android SDK (flavor='flavor') version ($version) is lower than " +
"Flutter's minimum supported version of $errorMinSdkVersion. Please upgrade your minimum Android SDK " +
"(flavor='flavor') version. \n" +
"Alternatively, use the flag \"--android-skip-build-dependency-validation\" to " +
"bypass this check.\n" +
@ -358,7 +358,7 @@ class DependencyVersionCheckerTest {
val mockExtraPropertiesExtension = mockk<ExtraPropertiesExtension>()
val projectDir = "projectDir"
val flavor = "flavor"
val version = 20
val version = 23
every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit
every { mockLogger.error(any()) } returns Unit
@ -377,8 +377,8 @@ class DependencyVersionCheckerTest {
assertEquals(
warningMessageSlot.captured,
"Warning: Flutter support for your project's minimum Android SDK (flavor='flavor') " +
"version (20) will soon be dropped. Please upgrade your minimum Android SDK " +
"(flavor='flavor') version to a version of at least 21 soon.\n" +
"version ($version) will soon be dropped. Please upgrade your minimum Android SDK " +
"(flavor='flavor') version to a version of at least $warnMinSdkVersion soon.\n" +
"Alternatively, use the flag \"--android-skip-build-dependency-validation\" to " +
"bypass this check.\n" +
"\n" +
@ -475,6 +475,9 @@ private object MockProjectFactory {
val variant = mockk<Variant>()
every { variant.name } returns it.flavor
every { variant.minSdk } returns mockk { every { apiLevel } returns it.version }
// Gradle prints a warning about this api being removed in gradle 9 but there is
// no way to suppress individual warnings. We can remove this line when our tests
// use gradle 9 or higher.
every { variant.minSdkVersion } returns mockk { every { apiLevel } returns it.version }
onVariantsFnSlot.captured.invoke(variant)
}

View File

@ -19,7 +19,7 @@ All of the Android SDK/NDK versions noted in `gradle_utils.dart`
(`compileSdkVersion`, `minSdkVersion`, `targetSdkVersion`, `ndkVersion`)
versions should match the values in Flutter Gradle Plugin (`FlutterExtension`),
so updating any of these versions also requires an update in
[flutter.groovy](../../../gradle/src/main/groovy/flutter.groovy).
[FlutterExtension](../../../gradle/src/main/kotlin/FlutterExtention.kt).
When updating the Android `compileSdkVersion`, `minSdkVersion`, or
`targetSdkVersion`, make sure that:
@ -30,9 +30,11 @@ SDK versions are updated (you should see these fail if you do not fix them
preemptively).
Also, make sure to also update to the same version in the following places:
- The version in the buildscript block in `packages/flutter_tools/gradle/src/main/groovy/flutter.groovy`.
- The version in the buildscript block in `packages/flutter_tools/gradle/src/main/kotlin/dependency_version_checker.gradle.kts`.
- The versions for error/warn in `packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt`.
- The version in the dependencies block in `packages/flutter_tools/gradle/build.gradle.kts`.
- The flutter min version in `engine/src/flutter/shell/platform/android/io/flutter/Build.java`
- The versions used when engine testing in `engine/src/flutter/shell/platform/android/test_runner/build.gradle`
- The versions used when working in engine/shell in `engine/src/flutter/shell/platform/android/build.gradle`
#### Gradle
When updating the Gradle version used in project templates

View File

@ -51,7 +51,7 @@ const String templateKotlinGradlePluginVersion = '2.1.0';
//
// Please see the README before changing any of these values.
const String compileSdkVersion = '36';
const String minSdkVersion = '21';
const String minSdkVersion = '24';
const String targetSdkVersion = '36';
const String ndkVersion = '27.0.12077973';
@ -158,9 +158,10 @@ final RegExp gradleOrgVersionMatch = RegExp(
);
// This matches uncommented minSdkVersion lines in the module-level build.gradle
// file which have minSdkVersion 16,17, 18, 19, or 20.
// file which have minSdkVersion 16, 17, 18, 19, 20, 21, 22, 23 set with space sytax,
// equals syntax and when using minSdk or minSdkVersion.
final RegExp tooOldMinSdkVersionMatch = RegExp(
r'(?<=^\s*)minSdkVersion (1[6789]|20)(?=\s*(?://|$))',
r'(?<=^\s*)minSdk(Version)?\s=?\s?(1[6789]|2[0123])(?=\s*(?://|$))',
multiLine: true,
);

View File

@ -349,7 +349,7 @@ tasks.register("clean", Delete) {
});
});
group('migrate min sdk versions less than 21 to flutter.minSdkVersion '
group('migrate min sdk versions less than 24 to flutter.minSdkVersion '
'when in a FlutterProject that is an app', () {
late MemoryFileSystem memoryFileSystem;
late BufferLogger bufferLogger;
@ -392,13 +392,43 @@ tasks.register("clean", Delete) {
);
});
testWithoutContext('do nothing when >=api 21', () async {
testWithoutContext('replace when api 21', () async {
const String minSdkVersion21 = 'minSdkVersion 21';
project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion21));
await migration.migrate();
expect(
project.appGradleFile.readAsStringSync(),
sampleModuleGradleBuildFile(minSdkVersion21),
sampleModuleGradleBuildFile(replacementMinSdkText),
);
});
testWithoutContext('replace when api 22', () async {
const String minSdkVersion20 = 'minSdkVersion = 22';
project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion20));
await migration.migrate();
expect(
project.appGradleFile.readAsStringSync(),
sampleModuleGradleBuildFile(replacementMinSdkText),
);
});
testWithoutContext('replace when api 23', () async {
const String minSdkVersion20 = 'minSdk = 23';
project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion20));
await migration.migrate();
expect(
project.appGradleFile.readAsStringSync(),
sampleModuleGradleBuildFile(replacementMinSdkText),
);
});
testWithoutContext('do nothing when >=api 24', () async {
const String minSdkVersion24 = 'minSdkVersion 24';
project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion24));
await migration.migrate();
expect(
project.appGradleFile.readAsStringSync(),
sampleModuleGradleBuildFile(minSdkVersion24),
);
});
@ -415,7 +445,7 @@ tasks.register("clean", Delete) {
testWithoutContext('avoid rewriting comments', () async {
const String code =
'// minSdkVersion 19 // old default\n'
' minSdkVersion 23 // new version';
' minSdkVersion 24 // new version';
project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(code));
await migration.migrate();
expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(code));
@ -447,7 +477,7 @@ tasks.register("clean", Delete) {
);
});
testWithoutContext('do nothing when minSdkVersion is set '
testWithoutContext('migrate when minSdkVersion is set '
'using = syntax', () async {
const String equalsSyntaxMinSdkVersion19 = 'minSdkVersion = 19';
project.appGradleFile.writeAsStringSync(
@ -456,7 +486,7 @@ tasks.register("clean", Delete) {
await migration.migrate();
expect(
project.appGradleFile.readAsStringSync(),
sampleModuleGradleBuildFile(equalsSyntaxMinSdkVersion19),
sampleModuleGradleBuildFile(replacementMinSdkText),
);
});
});

View File

@ -138,7 +138,7 @@ class DeferredComponentModule {
}
defaultConfig {
minSdkVersion 21
minSdkVersion 24
targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName