diff --git a/.github/workflows/cronet.yml b/.github/workflows/cronet.yml index 7bc1a4f089..0976a756ff 100644 --- a/.github/workflows/cronet.yml +++ b/.github/workflows/cronet.yml @@ -6,10 +6,12 @@ on: - main - master paths: + - '.github/workflows/cronet.yaml' - 'pkgs/cronet_http/**' - 'pkgs/http_client_conformance_tests/**' pull_request: paths: + - '.github/workflows/cronet.yaml' - 'pkgs/cronet_http/**' - 'pkgs/http_client_conformance_tests/**' schedule: @@ -19,48 +21,48 @@ env: PUB_ENVIRONMENT: bot.github jobs: - analyze: - name: Lint and static analysis - runs-on: ubuntu-latest - defaults: - run: - working-directory: pkgs/cronet_http + verify: + name: Format & Analyze & Test + runs-on: macos-latest + strategy: + matrix: + package: ['cronet_http', 'cronet_http_embedded'] steps: - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' - uses: subosito/flutter-action@v2 with: - # TODO: Change to 'stable' when a release version of flutter - # pins version 1.21.1 or later of 'package:test' - channel: 'master' + channel: 'stable' + - name: Make cronet_http_embedded copy + if: ${{ matrix.package == 'cronet_http_embedded' }} + run: | + cp -r pkgs/cronet_http pkgs/cronet_http_embedded + cd pkgs/cronet_http_embedded + flutter pub get && dart tool/prepare_for_embedded.dart - id: install name: Install dependencies + working-directory: 'pkgs/${{ matrix.package }}' run: flutter pub get - name: Check formatting + working-directory: 'pkgs/${{ matrix.package }}' run: dart format --output=none --set-exit-if-changed . if: always() && steps.install.outcome == 'success' - name: Analyze code + working-directory: 'pkgs/${{ matrix.package }}' run: flutter analyze --fatal-infos if: always() && steps.install.outcome == 'success' - - test: - # Test package:cupertino_http use flutter integration tests. - needs: analyze - name: "Build and test" - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - name: Run tests uses: reactivecircus/android-emulator-runner@v2 + if: always() && steps.install.outcome == 'success' with: + # api-level/minSdkVersion should be help in sync in: + # - .github/workflows/cronet.yml + # - pkgs/cronet_http/android/build.gradle + # - pkgs/cronet_http/example/android/app/build.gradle api-level: 28 - target: playstore - arch: x86_64 + target: ${{ matrix.package == 'cronet_http_embedded' && 'google_apis' || 'playstore' }} profile: pixel - script: cd ./pkgs/cronet_http/example && flutter test --timeout=1200s integration_test/ + script: cd 'pkgs/${{ matrix.package }}/example' && flutter test --timeout=1200s integration_test/ diff --git a/pkgs/cronet_http/CHANGELOG.md b/pkgs/cronet_http/CHANGELOG.md index 59ef74fe62..cf1fcbc208 100644 --- a/pkgs/cronet_http/CHANGELOG.md +++ b/pkgs/cronet_http/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.0 + +* No functional changes. + ## 0.4.2 * Require `package:jni >= 0.7.2` to remove a potential buffer overflow. diff --git a/pkgs/cronet_http/README.md b/pkgs/cronet_http/README.md index 0238eb6d63..49acf3390d 100644 --- a/pkgs/cronet_http/README.md +++ b/pkgs/cronet_http/README.md @@ -1,31 +1,61 @@ +[![pub package](https://img.shields.io/pub/v/cronet_http.svg)](https://pub.dev/packages/cronet_http) +[![package publisher](https://img.shields.io/pub/publisher/cronet_http.svg)](https://pub.dev/packages/cronet_http/publisher) + An Android Flutter plugin that provides access to the -[Cronet](https://developer.android.com/guide/topics/connectivity/cronet/reference/org/chromium/net/package-summary) +[Cronet][] HTTP client. Cronet is available as part of -[Google Play Services](https://developers.google.com/android/guides/overview). +[Google Play Services][]. -This package depends on -[Google Play Services](https://developers.google.com/android/guides/overview) -for its Cronet implementation. +This package depends on [Google Play Services][] for its [Cronet][] +implementation. [`package:cronet_http_embedded`](https://pub.dev/packages/cronet_http_embedded) -is functionally identical to this package but embeds Cronet directly instead -of relying on -[Google Play Services](https://developers.google.com/android/guides/overview). - -## Status: Experimental - -**NOTE**: This package is currently experimental and published under the -[labs.dart.dev](https://dart.dev/dart-team-packages) pub publisher in order to -solicit feedback. - -For packages in the labs.dart.dev publisher we generally plan to either graduate -the package into a supported publisher (dart.dev, tools.dart.dev) after a period -of feedback and iteration, or discontinue the package. These packages have a -much higher expected rate of API and breaking changes. - -Your feedback is valuable and will help us evolve this package. -For general feedback and suggestions please comment in the -[feedback issue](https://github.com/dart-lang/http/issues/764). -For bugs, please file an issue in the -[bug tracker](https://github.com/dart-lang/http/issues). +is functionally identical to this package but embeds [Cronet][] directly +instead of relying on [Google Play Services][]. + +## Motivation + +Using [Cronet][], rather than the socket-based [dart:io HttpClient][] +implemententation, has several advantages: + +1. It automatically supports Android platform features such as HTTP proxies. +2. It supports configurable caching. +3. It supports more HTTP features such as HTTP/3. + +## Using + +The easiest way to use this library is via the the high-level interface +defined by [package:http Client][]. + +This approach allows the same HTTP code to be used on all platforms, while +still allowing platform-specific setup. + +```dart +import 'package:cronet_http/cronet_http.dart'; +import 'package:http/http.dart'; +import 'package:http/io_client.dart'; + +void main() async { + late Client httpClient; + if (Platform.isAndroid) { + final engine = CronetEngine.build( + cacheMode: CacheMode.memory, + cacheMaxSize: 2 * 1024 * 1024, + userAgent: 'Book Agent'); + httpClient = CronetClient.fromCronetEngine(engine); + } else { + httpClient = IOClient(HttpClient()..userAgent = 'Book Agent'); + } + + final response = await client.get(Uri.https( + 'www.googleapis.com', + '/books/v1/volumes', + {'q': 'HTTP', 'maxResults': '40', 'printType': 'books'})); +} +``` + +[Cronet]: https://developer.android.com/guide/topics/connectivity/cronet/reference/org/chromium/net/package-summary +[dart:io HttpClient]: https://api.dart.dev/stable/dart-io/HttpClient-class.html +[Google Play Services]: https://developers.google.com/android/guides/overview +[package:http Client]: https://pub.dev/documentation/http/latest/http/Client-class.html diff --git a/pkgs/cronet_http/android/build.gradle b/pkgs/cronet_http/android/build.gradle index 3a91d8a295..96bb197c73 100644 --- a/pkgs/cronet_http/android/build.gradle +++ b/pkgs/cronet_http/android/build.gradle @@ -2,14 +2,14 @@ group 'io.flutter.plugins.cronet_http' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.7.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,6 +25,11 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.cronet_http' + } + compileSdkVersion 31 compileOptions { @@ -41,7 +46,11 @@ android { } defaultConfig { - minSdkVersion 16 + // api-level/minSdkVersion should be help in sync in: + // - .github/workflows/cronet.yml + // - pkgs/cronet_http/android/build.gradle + // - pkgs/cronet_http/example/android/app/build.gradle + minSdkVersion 28 } defaultConfig { diff --git a/pkgs/cronet_http/example/android/app/build.gradle b/pkgs/cronet_http/example/android/app/build.gradle index 88b33564c3..1f7cd94749 100644 --- a/pkgs/cronet_http/example/android/app/build.gradle +++ b/pkgs/cronet_http/example/android/app/build.gradle @@ -46,7 +46,11 @@ android { applicationId "io.flutter.cronet_http_example" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion flutter.minSdkVersion + // api-level/minSdkVersion should be help in sync in: + // - .github/workflows/cronet.yml + // - pkgs/cronet_http/android/build.gradle + // - pkgs/cronet_http/example/android/app/build.gradle + minSdkVersion 28 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/pkgs/cronet_http/example/android/app/src/debug/AndroidManifest.xml b/pkgs/cronet_http/example/android/app/src/debug/AndroidManifest.xml index b17be9f3cb..6170951031 100644 --- a/pkgs/cronet_http/example/android/app/src/debug/AndroidManifest.xml +++ b/pkgs/cronet_http/example/android/app/src/debug/AndroidManifest.xml @@ -1,4 +1,4 @@ + package="io.flutter.cronet_http_example"> diff --git a/pkgs/cronet_http/example/android/app/src/main/AndroidManifest.xml b/pkgs/cronet_http/example/android/app/src/main/AndroidManifest.xml index e8d5f3f208..254760d3ed 100644 --- a/pkgs/cronet_http/example/android/app/src/main/AndroidManifest.xml +++ b/pkgs/cronet_http/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="io.flutter.cronet_http_example"> + package="io.flutter.cronet_http_example"> diff --git a/pkgs/cronet_http/example/android/build.gradle b/pkgs/cronet_http/example/android/build.gradle index 83ae220041..954fa1cd5c 100644 --- a/pkgs/cronet_http/example/android/build.gradle +++ b/pkgs/cronet_http/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.7.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/pkgs/cronet_http/example/android/gradle/wrapper/gradle-wrapper.properties b/pkgs/cronet_http/example/android/gradle/wrapper/gradle-wrapper.properties index cc5527d781..cfe88f6904 100644 --- a/pkgs/cronet_http/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/pkgs/cronet_http/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/pkgs/cronet_http/pubspec.yaml b/pkgs/cronet_http/pubspec.yaml index 602a1bbfd5..5f4229ebf8 100644 --- a/pkgs/cronet_http/pubspec.yaml +++ b/pkgs/cronet_http/pubspec.yaml @@ -1,5 +1,5 @@ name: cronet_http -version: 0.4.2 +version: 1.0.0 description: >- An Android Flutter plugin that provides access to the Cronet HTTP client. repository: https://github.com/dart-lang/http/tree/master/pkgs/cronet_http diff --git a/pkgs/cronet_http/tool/prepare_for_embedded.dart b/pkgs/cronet_http/tool/prepare_for_embedded.dart index e0f99d7a23..3442bc6843 100644 --- a/pkgs/cronet_http/tool/prepare_for_embedded.dart +++ b/pkgs/cronet_http/tool/prepare_for_embedded.dart @@ -13,6 +13,9 @@ /// 1. Modifying the Gradle build file to reference the embedded Cronet. /// 2. Modifying the *name* and *description* in `pubspec.yaml`. /// 3. Replacing `README.md` with `README_EMBEDDED.md`. +/// 4. Change the name of `cronet_http.dart` to `cronet_http_embedded.dart`. +/// 5. Update all the imports from `package:cronet_http/cronet_http.dart` to +/// `package:cronet_http_embedded/cronet_http_embedded.dart` /// /// After running this script, `flutter pub publish` /// can be run to update package:cronet_http_embedded. @@ -40,7 +43,7 @@ final _cronetVersionUri = Uri.https( 'android/maven2/org/chromium/net/group-index.xml', ); -void main() async { +void main(List args) async { if (Directory.current.path.endsWith('tool')) { _packageDirectory = Directory.current.parent; } else { @@ -51,6 +54,8 @@ void main() async { updateCronetDependency(latestVersion); updatePubSpec(); updateReadme(); + updateLibraryName(); + updateImports(); } Future _getLatestCronetVersion() async { @@ -69,7 +74,7 @@ Future _getLatestCronetVersion() async { return versions.last; } -/// Update android/build.gradle +/// Update android/build.gradle. void updateCronetDependency(String latestVersion) { final fBuildGradle = File('${_packageDirectory.path}/android/build.gradle'); final gradleContent = fBuildGradle.readAsStringSync(); @@ -88,18 +93,48 @@ void updateCronetDependency(String latestVersion) { fBuildGradle.writeAsStringSync(newGradleContent); } -/// Update pubspec.yaml +/// Update pubspec.yaml and example/pubspec.yaml. void updatePubSpec() { + print('Updating pubspec.yaml'); final fPubspec = File('${_packageDirectory.path}/pubspec.yaml'); final yamlEditor = YamlEditor(fPubspec.readAsStringSync()) ..update(['name'], _packageName) ..update(['description'], _packageDescription); fPubspec.writeAsStringSync(yamlEditor.toString()); + print('Updating example/pubspec.yaml'); + final examplePubspec = File('${_packageDirectory.path}/example/pubspec.yaml'); + final replaced = examplePubspec + .readAsStringSync() + .replaceAll('cronet_http:', 'cronet_http_embedded:'); + examplePubspec.writeAsStringSync(replaced); } -/// Move README_EMBEDDED.md to replace README.md +/// Move README_EMBEDDED.md to replace README.md. void updateReadme() { + print('Updating README.md from README_EMBEDDED.md'); File('${_packageDirectory.path}/README.md').deleteSync(); File('${_packageDirectory.path}/README_EMBEDDED.md') .renameSync('${_packageDirectory.path}/README.md'); } + +void updateImports() { + print('Updating imports in Dart files'); + for (final file in _packageDirectory.listSync(recursive: true)) { + if (file is File && file.path.endsWith('.dart')) { + final updatedSource = file.readAsStringSync().replaceAll( + 'package:cronet_http/cronet_http.dart', + 'package:cronet_http_embedded/cronet_http_embedded.dart', + ); + file.writeAsStringSync(updatedSource); + } + } +} + +void updateLibraryName() { + print('Renaming cronet_http.dart to cronet_http_embedded.dart'); + File( + '${_packageDirectory.path}/lib/cronet_http.dart', + ).renameSync( + '${_packageDirectory.path}/lib/cronet_http_embedded.dart', + ); +} diff --git a/pkgs/cupertino_http/CHANGELOG.md b/pkgs/cupertino_http/CHANGELOG.md index 47e25cb590..d768f7a5c7 100644 --- a/pkgs/cupertino_http/CHANGELOG.md +++ b/pkgs/cupertino_http/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.2.0-wip +## 1.2.0 * Add support for setting additional http headers in `URLSessionConfiguration`. diff --git a/pkgs/cupertino_http/pubspec.yaml b/pkgs/cupertino_http/pubspec.yaml index 81905aa87e..0a97bdd25d 100644 --- a/pkgs/cupertino_http/pubspec.yaml +++ b/pkgs/cupertino_http/pubspec.yaml @@ -1,5 +1,5 @@ name: cupertino_http -version: 1.2.0-wip +version: 1.2.0 description: >- A macOS/iOS Flutter plugin that provides access to the Foundation URL Loading System. diff --git a/pkgs/http/CHANGELOG.md b/pkgs/http/CHANGELOG.md index 9bbbb05a2d..7b8dec4fdd 100644 --- a/pkgs/http/CHANGELOG.md +++ b/pkgs/http/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.3-wip + +* Add `MockClient.pngResponse`, which makes it easier to fake image responses. + ## 1.1.2 * Allow `web: '>=0.3.0 <0.5.0'`. diff --git a/pkgs/http/lib/src/mock_client.dart b/pkgs/http/lib/src/mock_client.dart index bf2df40ee7..52f108a577 100644 --- a/pkgs/http/lib/src/mock_client.dart +++ b/pkgs/http/lib/src/mock_client.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:convert'; + import 'base_client.dart'; import 'base_request.dart'; import 'byte_stream.dart'; @@ -10,6 +12,11 @@ import 'response.dart'; import 'streamed_request.dart'; import 'streamed_response.dart'; +final _pngImageData = base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDw' + 'AEhQGAhKmMIQAAAABJRU5ErkJggg==', +); + // TODO(nweiz): once Dart has some sort of Rack- or WSGI-like standard for // server APIs, MockClient should conform to it. @@ -69,6 +76,17 @@ class MockClient extends BaseClient { var bodyStream = request.finalize(); return await _handler(request, bodyStream); } + + /// Return a response containing a PNG image. + static Response pngResponse({BaseRequest? request}) { + final headers = { + 'content-type': 'image/png', + 'content-length': '${_pngImageData.length}' + }; + + return Response.bytes(_pngImageData, 200, + request: request, headers: headers, reasonPhrase: 'OK'); + } } /// A handler function that receives [StreamedRequest]s and sends diff --git a/pkgs/http/pubspec.yaml b/pkgs/http/pubspec.yaml index 118292fa13..1645f96048 100644 --- a/pkgs/http/pubspec.yaml +++ b/pkgs/http/pubspec.yaml @@ -1,5 +1,5 @@ name: http -version: 1.1.2 +version: 1.1.3-wip description: A composable, multi-platform, Future-based API for HTTP requests. repository: https://github.com/dart-lang/http/tree/master/pkgs/http diff --git a/pkgs/http/test/mock_client_test.dart b/pkgs/http/test/mock_client_test.dart index db561c51d6..625285cb33 100644 --- a/pkgs/http/test/mock_client_test.dart +++ b/pkgs/http/test/mock_client_test.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:http/src/request.dart'; import 'package:http/testing.dart'; import 'package:test/test.dart'; @@ -43,4 +44,25 @@ void main() { expect(await client.read(Uri.http('example.com', '/foo')), equals('you did it')); }); + + test('pngResponse with default options', () { + final response = MockClient.pngResponse(); + expect(response.statusCode, 200); + expect(response.bodyBytes.take(8), + [137, 80, 78, 71, 13, 10, 26, 10] // PNG header + ); + expect(response.request, null); + expect(response.headers, containsPair('content-type', 'image/png')); + }); + + test('pngResponse with request', () { + final request = Request('GET', Uri.https('example.com')); + final response = MockClient.pngResponse(request: request); + expect(response.statusCode, 200); + expect(response.bodyBytes.take(8), + [137, 80, 78, 71, 13, 10, 26, 10] // PNG header + ); + expect(response.request, request); + expect(response.headers, containsPair('content-type', 'image/png')); + }); }