From 993027f4ced011af4079c8ab6673e2bae0a71660 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:32:17 +0200 Subject: [PATCH 1/8] Dart sources debuggable as Dart; Flutter support in the Gradle plugin The Dart transpiler marks every statement with its Dart line and stores a line table in each generated class; DartLineMapper (build-engine) rewrites the compiled classes after javac so their line numbers, source file and a JSR-45 source map point at the .dart file. A Java debugger steps through the Dart source and stack traces name main.dart:12 on every target. The transcode-flutter goal's logic moves into FlutterProjectBuilder in build-engine, which the Maven goal and a new Gradle transcodeFlutter task both run. convert-to-gradle leaves the runtime to the plugin. gradle-flutter-test.sh builds one Flutter app both ways and compares them. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/gradle-smoke.yml | 17 + maven/build-engine/pom.xml | 6 + .../com/codename1/maven/DartLineMapper.java | 330 +++++++++++++ .../maven/FlutterProjectBuilder.java | 461 ++++++++++++++++++ .../com/codename1/maven/GradleConversion.java | 6 +- .../codename1/maven/DartLineMapperTest.java | 164 +++++++ .../java/com/codename1/gradle/AppSupport.java | 68 ++- .../gradle/tasks/RemapCompatAction.java | 19 + .../gradle/tasks/TranscodeFlutterTask.java | 124 +++++ .../com/codename1/maven/RemapCompatMojo.java | 17 + .../codename1/maven/TranscodeFlutterMojo.java | 381 +-------------- .../transpiler/codegen/DartLineTable.java | 148 ++++++ .../dart/transpiler/codegen/JavaEmitter.java | 47 +- .../transpiler/codegen/DartLineTableTest.java | 60 +++ .../golden/counter/expected/MainLib.java | 2 + .../golden/counter/expected/MyApp.java | 2 + .../golden/counter/expected/MyHomePage.java | 2 + .../counter/expected/_MyHomePageState.java | 4 + .../golden/m2demo/expected/DemoApp.java | 2 + .../golden/m2demo/expected/DemoPage.java | 2 + .../golden/m2demo/expected/MainLib.java | 2 + .../m2demo/expected/_DemoPageState.java | 10 + .../integration-tests/gradle-flutter-test.sh | 102 ++++ 23 files changed, 1606 insertions(+), 370 deletions(-) create mode 100644 maven/build-engine/src/main/java/com/codename1/maven/DartLineMapper.java create mode 100644 maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectBuilder.java create mode 100644 maven/build-engine/src/test/java/com/codename1/maven/DartLineMapperTest.java create mode 100644 maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/TranscodeFlutterTask.java create mode 100644 maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/DartLineTable.java create mode 100644 maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/codegen/DartLineTableTest.java create mode 100755 maven/integration-tests/gradle-flutter-test.sh diff --git a/.github/workflows/gradle-smoke.yml b/.github/workflows/gradle-smoke.yml index 86a3d139c46..bc573e98c36 100644 --- a/.github/workflows/gradle-smoke.yml +++ b/.github/workflows/gradle-smoke.yml @@ -12,6 +12,8 @@ name: Gradle build smoke # the Gradle plugin reuses the Maven engine # gradle-cn1lib-test.sh a Gradle-built cn1lib consumed by a Gradle app # AND a Maven app +# gradle-flutter-test.sh src/main/flutter: same classes and assets both ways, +# mapped to the Dart source; removed sources leave nothing # gradle-migrate-test.sh Ant and Maven to Gradle; legacy .cn1lib refused # gradle-tools-test.sh Settings opens, and renders, from cn1:settings and # from ./gradlew settings @@ -34,6 +36,9 @@ on: - 'maven/css-compiler/**' - 'maven/css-cli/**' - 'maven/build-engine/**' + - 'maven/dart-transpiler/**' + - 'maven/dart-runtime/**' + - 'maven/flutter-runtime/**' - 'maven/project-model/**' - 'maven/cn1app-archetype/**' - 'maven/backend/**' @@ -58,6 +63,9 @@ on: - 'maven/css-compiler/**' - 'maven/css-cli/**' - 'maven/build-engine/**' + - 'maven/dart-transpiler/**' + - 'maven/dart-runtime/**' + - 'maven/flutter-runtime/**' - 'maven/project-model/**' - 'maven/cn1app-archetype/**' - 'maven/backend/**' @@ -135,12 +143,21 @@ jobs: cd maven mvn archetype:update-local-catalog -Plocal-dev-javase mvn archetype:crawl -Plocal-dev-javase + # The Flutter runtime compiles at release 17, so the JDK 8 reactor + # above leaves it out (the flutter-modules profile is JDK activated). + - name: Build the Flutter runtime + run: | + cd maven + JAVA_HOME="$GRADLE_JAVA_HOME" mvn -B -ntp install -pl dart-runtime,flutter-runtime -DskipTests -Dmaven.javadoc.skip=true - name: Gradle application working-directory: maven/integration-tests run: xvfb-run -a bash gradle-app-test.sh - name: Maven and Gradle upload parity working-directory: maven/integration-tests run: bash gradle-maven-parity-test.sh + - name: Flutter sources, Maven and Gradle + working-directory: maven/integration-tests + run: bash gradle-flutter-test.sh - name: Gradle cn1lib, consumed by Gradle and Maven working-directory: maven/integration-tests # DISPLAY and WAYLAND_DISPLAY are cleared on purpose, and this must diff --git a/maven/build-engine/pom.xml b/maven/build-engine/pom.xml index 77025a84f47..f37bec0d69b 100644 --- a/maven/build-engine/pom.xml +++ b/maven/build-engine/pom.xml @@ -177,6 +177,12 @@ org.apache.httpcomponents httpclient + + + ${project.groupId} + codenameone-dart-transpiler + ${project.version} + org.ow2.asm asm diff --git a/maven/build-engine/src/main/java/com/codename1/maven/DartLineMapper.java b/maven/build-engine/src/main/java/com/codename1/maven/DartLineMapper.java new file mode 100644 index 00000000000..7f99f24d93e --- /dev/null +++ b/maven/build-engine/src/main/java/com/codename1/maven/DartLineMapper.java @@ -0,0 +1,330 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +/// Points the classes transpiled from Dart at the Dart source. +/// +/// The transpiler writes, into each class it generates, a constant naming +/// the `.dart` files and pairing Java lines with the Dart lines they came +/// from (`DartLineTable` in dart-transpiler). This rewrites every line +/// number of the class and its nested classes to the Dart line, sets the +/// source file to the `.dart` file, adds a JSR-45 source map carrying the +/// paths, and removes the constant. A Java debugger then steps through, and +/// sets breakpoints in, the Dart source, and a stack trace names +/// `main.dart:12` -- on every target, since the device translators read the +/// same line tables. +/// +/// The output package is a build setting (`cn1.flutter.package`), so the +/// whole classes tree is scanned; a class is parsed only when its bytes +/// hold the constant's name. Runs after javac; a class without the +/// constant, or already rewritten, is left alone. +public final class DartLineMapper { + /// Must match `DartLineTable.FIELD`; a long table continues in + /// `FIELD1`, `FIELD2`... + static final String FIELD = "$cn1DartLines"; + private static final byte[] FIELD_BYTES = FIELD.getBytes(StandardCharsets.US_ASCII); + + private DartLineMapper() { + } + + /// Rewrites the classes under `classesDir`; returns how many. + public static int apply(File classesDir) throws IOException { + return applyDir(classesDir); + } + + private static int applyDir(File dir) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + return 0; + } + int count = 0; + Map tables = new HashMap(); + for (File f : files) { + String n = f.getName(); + if (f.isDirectory()) { + count += applyDir(f); + } else if (n.endsWith(".class")) { + byte[] bytes = Files.readAllBytes(f.toPath()); + if (contains(bytes, FIELD_BYTES)) { + Table t = read(bytes); + if (t != null) { + tables.put(n.substring(0, n.length() - ".class".length()), t); + } + } + } + } + if (tables.isEmpty()) { + return count; + } + for (File f : files) { + String n = f.getName(); + if (!n.endsWith(".class") || f.isDirectory()) { + continue; + } + // The class's own table, else -- for a class javac split out of + // another (anonymous, local, nested) -- its host's. The host comes + // from the class file itself, never from the name: a Dart + // identifier may hold a '$', and `Foo$Bar` is then a class of + // its own, maybe of another file, not part of `Foo`. + String stem = n.substring(0, n.length() - ".class".length()); + Table t = tables.get(stem); + byte[] bytes = null; + if (t == null && stem.indexOf('$') > 0) { + bytes = Files.readAllBytes(f.toPath()); + String host = host(bytes); + if (host != null) { + t = tables.get(host.substring(host.lastIndexOf('/') + 1)); + } + } + if (t != null) { + Files.write(f.toPath(), rewrite(bytes != null ? bytes : Files.readAllBytes(f.toPath()), t)); + count++; + } + } + return count; + } + + /// The class a javac-split class belongs to: its nest host, else the + /// class enclosing it (anonymous and local classes), else the outer + /// class its own InnerClasses entry names; null for a top-level class. + static String host(byte[] bytes) { + final String[] found = new String[3]; + final String[] self = new String[1]; + new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public void visit(int version, int access, String name, String signature, String superName, + String[] interfaces) { + self[0] = name; + } + + @Override + public void visitNestHost(String nestHost) { + found[0] = nestHost; + } + + @Override + public void visitOuterClass(String owner, String name, String descriptor) { + found[1] = owner; + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + if (name.equals(self[0]) && outerName != null) { + found[2] = outerName; + } + } + }, ClassReader.SKIP_CODE); + for (String h : found) { + if (h != null) { + return h; + } + } + return null; + } + + private static boolean contains(byte[] hay, byte[] needle) { + outer: + for (int i = 0, last = hay.length - needle.length; i <= last; i++) { + for (int j = 0; j < needle.length; j++) { + if (hay[i + j] != needle[j]) { + continue outer; + } + } + return true; + } + return false; + } + + /// A class's line table. + /// + /// The class's own Dart file keeps its line numbers. Lines of any other + /// file (a `part` file's function in its library's class) are moved past + /// the previous file's last line, because a line table carries no file: + /// the source map gives each file its own range, so a debugger still + /// shows the right file and line, while a stack trace names the class's + /// own file with the shifted number. + static final class Table { + final List paths = new ArrayList(); + final TreeMap lines = new TreeMap(); + final int[] max; + final int[] offset; + static final int MAX_LINE = 0xffff; + + Table(String value) { + String[] rows = value.split("\n"); + for (int i = 0; i < rows.length - 1; i++) { + paths.add(rows[i]); + } + max = new int[paths.size()]; + offset = new int[paths.size()]; + List parsed = new ArrayList(); + for (String pair : rows[rows.length - 1].split(",")) { + String[] p = pair.split(":"); + int file = p.length > 2 ? Integer.parseInt(p[2]) : 0; + int line = Integer.parseInt(p[1]); + parsed.add(new int[] {Integer.parseInt(p[0]), line, file}); + max[file] = Math.max(max[file], line); + } + for (int i = 1; i < offset.length; i++) { + offset[i] = offset[i - 1] + max[i - 1]; + // A line number is a u2. Files that no longer fit keep no + // range of their own: their statements map to the line before + // them, rather than wrap into another file's range. + if (offset[i] + max[i] > MAX_LINE) { + max[i] = 0; + } + } + for (int[] p : parsed) { + if (p[2] == 0 || max[p[2]] > 0) { + lines.put(p[0], p[1] + offset[p[2]]); + } + } + if (lines.isEmpty()) { + lines.put(1, 1); + } + } + + /// Java lines before the first statement -- field initializers, a + /// static initializer, a constructor's header -- take the first + /// statement's line: a line table must name some line, and the class's + /// own file is the right one even where the line is approximate. + int map(int javaLine) { + Map.Entry e = lines.floorEntry(javaLine); + return e != null ? e.getValue() : lines.firstEntry().getValue(); + } + + static String fileName(String path) { + return path.substring(path.lastIndexOf('/') + 1); + } + + String fileName() { + return fileName(paths.get(0)); + } + + /// The Dart stratum, the default: every file's lines mapped onto + /// its range of the rewritten line table. + String smap(String javaFile) { + StringBuilder sb = new StringBuilder("SMAP\n").append(javaFile).append("\nDart\n*S Dart\n*F\n"); + for (int i = 0; i < paths.size(); i++) { + sb.append("+ ").append(i + 1).append(' ').append(fileName(paths.get(i))).append('\n') + .append(paths.get(i)).append('\n'); + } + sb.append("*L\n"); + for (int i = 0; i < paths.size(); i++) { + if (max[i] > 0) { + sb.append("1#").append(i + 1).append(',').append(max[i]).append(':') + .append(offset[i] + 1).append('\n'); + } + } + return sb.append("*E\n").toString(); + } + } + + static Table read(byte[] bytes) { + final TreeMap parts = new TreeMap(); + new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, Object v) { + int index = index(name); + if (index >= 0 && v instanceof String) { + parts.put(index, (String) v); + } + return null; + } + }, ClassReader.SKIP_CODE); + if (parts.isEmpty()) { + return null; + } + StringBuilder value = new StringBuilder(); + for (String p : parts.values()) { + value.append(p); + } + return new Table(value.toString()); + } + + /// The chunk number of a table constant, or -1 for any other field. + static int index(String name) { + if (!name.startsWith(FIELD)) { + return -1; + } + String rest = name.substring(FIELD.length()); + if (rest.isEmpty()) { + return 0; + } + for (int i = 0; i < rest.length(); i++) { + if (rest.charAt(i) < '0' || rest.charAt(i) > '9') { + return -1; + } + } + return Integer.parseInt(rest); + } + + static byte[] rewrite(byte[] bytes, final Table t) { + ClassReader r = new ClassReader(bytes); + // Not ClassWriter(r, 0): that copies the reader's constant pool whole, + // so the table's strings would stay in every class after its field + // is gone. + ClassWriter w = new ClassWriter(0); + r.accept(new ClassVisitor(Opcodes.ASM9, w) { + @Override + public void visitSource(String source, String debug) { + super.visitSource(t.fileName(), t.smap(source == null ? "" : source)); + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, Object v) { + return index(name) >= 0 ? null : super.visitField(access, name, desc, signature, v); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, + String[] exceptions) { + return new MethodVisitor(Opcodes.ASM9, super.visitMethod(access, name, desc, signature, exceptions)) { + @Override + public void visitLineNumber(int line, Label start) { + super.visitLineNumber(t.map(line), start); + } + }; + } + }, 0); + return w.toByteArray(); + } +} diff --git a/maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectBuilder.java b/maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectBuilder.java new file mode 100644 index 00000000000..95cceb6c980 --- /dev/null +++ b/maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectBuilder.java @@ -0,0 +1,461 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.build.Log; +import com.codename1.builders.BuildException; +import com.codename1.dart.transpiler.api.DartTranspiler; +import com.codename1.dart.transpiler.api.Diagnostic; +import com.codename1.dart.transpiler.api.TranspileRequest; +import com.codename1.dart.transpiler.api.TranspileResult; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/// Builds `src/main/flutter` -- Flutter/Dart sources and their assets -- into +/// Java source on the Codename One Flutter runtime +/// (`codenameone-flutter-runtime`) and flat resources. The Maven plugin's +/// `transcode-flutter` goal and the Gradle plugin's `transcodeFlutter` task +/// both run it, so the two builds cannot drift. +/// +/// - `src/main/flutter/**.dart` are transpiled as one program, with a +/// digest-based fast skip; unchanged outputs are not rewritten, keeping +/// incremental javac warm. The generated code is Java 17 source. +/// - `src/main/flutter/assets/**` and `src/main/flutter/packages/**`, and any +/// other pubspec `assets:`/`fonts:` key `cn1:import-flutter-project` +/// recorded (`FlutterProjectImporter.ASSET_KEYS_RECORD`), are flattened into +/// the assets directory, because Codename One resources are flat on every +/// port. +/// +/// Without the source directory, [#build()] removes whatever an earlier build +/// of one generated. +public final class FlutterProjectBuilder { + /// The runtime the generated code runs on. + public static final String RUNTIME_ARTIFACT = "codenameone-flutter-runtime"; + /// The package generated code goes into unless `cn1.flutter.package` says otherwise. + public static final String DEFAULT_PACKAGE = "com.codename1.generated.flutter"; + + private final File sourceDir; + private final File outputDir; + private final File assetsDir; + private final File assetManifest; + private final File stateFile; + private final String packageName; + private final Log log; + private final List stubClasspath = new ArrayList(); + private final List javaSourceRoots = new ArrayList(); + private File classesDir; + + /// @param sourceDir `src/main/flutter` + /// @param outputDir where the generated Java goes; owned by this builder + /// @param assetsDir where flattened assets go -- a resources root of the build + /// @param assetManifest the record of the asset names written, so the next + /// build can remove the ones it no longer writes; outside `outputDir` + /// @param stateFile the transpiler's incremental state + public FlutterProjectBuilder(File sourceDir, File outputDir, File assetsDir, File assetManifest, + File stateFile, String packageName, Log log) { + this.sourceDir = sourceDir; + this.outputDir = outputDir; + this.assetsDir = assetsDir; + this.assetManifest = assetManifest; + this.stateFile = stateFile; + this.packageName = packageName == null || packageName.isEmpty() ? DEFAULT_PACKAGE : packageName; + this.log = log; + } + + /// True when `dir` is a Flutter source tree to build. + public static boolean isFlutterProject(File dir) { + return dir != null && dir.isDirectory(); + } + + /// A jar or directory whose `META-INF/dart/*.dart` stubs give the Dart + /// API the program compiles against: the compile class path. + public FlutterProjectBuilder stubClasspath(Iterable entries) { + for (File f : entries) { + if (f != null) { + stubClasspath.add(f); + } + } + return this; + } + + /// The application's own Java roots. Its Java can name a plugin pack too + /// (the import-generated entry point installs the firebase_messaging and + /// app_links hooks): the transpiler copies exactly the packs that are + /// named, so it must see that Java as well. + public FlutterProjectBuilder javaSourceRoots(Iterable roots) { + for (File dir : roots) { + if (dir != null && dir.isDirectory() && !dir.getAbsoluteFile().equals(outputDir.getAbsoluteFile())) { + javaSourceRoots.add(dir); + } + } + return this; + } + + /// The compiled classes, when the build compiles into a directory it + /// does not empty (Maven's `target/classes`). The transpiler deletes the + /// source of a plugin pack the app stopped naming, but javac does not + /// reliably delete the class compiled from it on an earlier build. Left + /// there, those classes would still ship -- and still imply the + /// permissions (location, push, ...) the builders derive from the classes + /// they find. + public FlutterProjectBuilder classesDir(File dir) { + this.classesDir = dir; + return this; + } + + /// Transpiles and copies the assets, or cleans up when there is nothing to build. + public void build() throws BuildException { + if (!isFlutterProject(sourceDir)) { + deleteRecursive(outputDir); + removeDeselectedPackClasses(); + // Flutter was removed from the project: its assets go with it. + removeStaleAssets(new HashSet()); + return; + } + checkJdk(); + TranspileRequest req = new TranspileRequest() + .sourceRoot(sourceDir) + .outputDir(outputDir) + .packageName(packageName) + .stateFile(stateFile); + for (File f : stubClasspath) { + req.stubClasspathEntry(f); + } + for (File dir : javaSourceRoots) { + req.javaSourceRoot(dir); + } + TranspileResult result = new DartTranspiler().transpile(req); + for (Diagnostic d : result.getDiagnostics()) { + String msg = "src/main/flutter/" + d.file + ":[" + d.line + "," + d.col + "] " + + d.message + " (dart2java:" + d.code + ")"; + if (d.severity == Diagnostic.Severity.ERROR) { + log.error(msg); + } else if (d.severity == Diagnostic.Severity.WARNING) { + log.warn(msg); + } else { + log.info(msg); + } + } + if (result.hasErrors()) { + throw new BuildException("Flutter transpilation failed with " + + result.errors().size() + " error(s); see log above. " + + "Confirm the Dart files pass `dart analyze` -- constructs outside the " + + "currently supported subset are reported with a milestone code."); + } + if (result.isUpToDate()) { + log.info("Flutter sources are up to date"); + } else { + log.info("Transpiled Flutter sources to " + outputDir); + } + removeDeselectedPackClasses(); + copyAssets(); + } + + private void removeDeselectedPackClasses() { + if (classesDir == null) { + return; + } + String packs = "com/codename1/flutter/plugins".replace('/', File.separatorChar); + File[] compiled = new File(classesDir, packs).listFiles(); + if (compiled == null) { + return; + } + File selected = new File(outputDir, packs); + for (File pack : compiled) { + if (pack.isDirectory() && !new File(selected, pack.getName()).isDirectory()) { + deleteRecursive(pack); + log.debug("Removed classes of unused Flutter plugin pack " + pack.getName()); + } + } + } + + private static void checkJdk() throws BuildException { + String spec = System.getProperty("java.specification.version", "1.8"); + int major; + try { + major = Integer.parseInt(spec.startsWith("1.") ? spec.substring(2) : spec); + } catch (NumberFormatException e) { + major = 8; + } + if (major < 17) { + throw new BuildException("Flutter support generates Java 17 source, but this build " + + "is running on JDK " + spec + ". Run the build with JDK 17 or newer " + + "(e.g. from https://adoptium.net) to use src/main/flutter."); + } + } + + /** + * Copies {@code src/main/flutter/assets} into the build output, flattened. + * + *

Codename One resources are flat on every port -- {@code getResourceAsStream} + * rejects a name containing a {@code '/'} past the leading one -- so the asset + * tree cannot be mirrored. Each asset is written to the output root under the + * name produced by {@link #flatAssetName}, which the Flutter runtime's + * {@code FlutterAssets} recomputes when resolving {@code Image.asset(...)}.

+ */ + private void copyAssets() throws BuildException { + File outDir = assetsDir; + Set written = new HashSet(); + List keys = new ArrayList(); + int count = 0; + try { + // Both roots a pubspec asset key can start with. "assets/" is an app's own + // bundle; "packages/" is one it pulls from a dependency, which is how the + // gallery ships its artwork (packages/flutter_gallery_assets/assets/...). + // Only "assets/" was copied before, so every packaged asset key resolved to + // nothing on every port - the images were simply absent. + for (String root : ASSET_ROOTS) { + File dir = new File(sourceDir, root); + if (!dir.isDirectory()) { + continue; + } + Files.createDirectories(outDir.toPath()); + // the root stays in the Flutter asset key, matching pubspec paths + count += flattenInto(dir, root, outDir, written, keys); + } + // Flutter allows an asset or font declared anywhere project-relative -- + // fonts/MyFont.ttf, images/logo.png -- not only under assets/ or + // packages/, so the scan above missed every one of those: the key + // resolved to nothing on every port. cn1:import-flutter-project records + // exactly which keys it copied, in FlutterProjectImporter.ASSET_KEYS_RECORD; + // a hand-wired project (no import, so no such record) is unaffected -- + // it keeps relying on the scan above alone, as before. + for (String key : readDeclaredAssetKeys()) { + if (ASSET_ROOTS_SET.contains(topSegment(key))) { + continue; // already copied by the scan above + } + Files.createDirectories(outDir.toPath()); + count += flattenDeclaredAsset(key, outDir, written, keys); + } + if (count > 0) { + writeAssetManifest(outDir, keys, written); + log.info("Flattened " + count + " Flutter asset(s) into the build output"); + } + } catch (IOException e) { + throw new BuildException("Failed copying Flutter assets", e); + } + removeStaleAssets(written); + } + + /// The `FlutterProjectImporter.ASSET_KEYS_RECORD` keys a previous + /// `cn1:import-flutter-project` wrote beside `src/main/flutter`, or + /// empty when the project was hand-wired (never imported) or predates + /// the record. The record's file name is the importer's own constant. + private List readDeclaredAssetKeys() throws IOException { + File f = new File(sourceDir, FlutterProjectImporter.ASSET_KEYS_RECORD); + if (!f.isFile()) { + return java.util.Collections.emptyList(); + } + List out = new ArrayList(); + for (String line : Files.readAllLines(f.toPath(), java.nio.charset.StandardCharsets.UTF_8)) { + line = line.trim(); + if (line.length() > 0) { + out.add(line); + } + } + return out; + } + + private static String topSegment(String key) { + int slash = key.indexOf('/'); + return slash < 0 ? key : key.substring(0, slash); + } + + /// Flattens one declared asset key outside {@link #ASSET_ROOTS}: a file, + /// or a directory (pubspec allows declaring a whole directory), rooted + /// at {@code sourceDir}. Missing on disk is not an error here -- + /// {@code FlutterProjectImporter} already warned about that key when it + /// could not resolve it. + private int flattenDeclaredAsset(String key, File outDir, Set written, List keys) + throws IOException { + File node = new File(sourceDir, key.replace('/', File.separatorChar)); + if (node.isDirectory()) { + return flattenInto(node, key, outDir, written, keys); + } + if (!node.isFile()) { + return 0; + } + String flat = flatAssetName(key); + Files.copy(node.toPath(), new File(outDir, flat).toPath(), StandardCopyOption.REPLACE_EXISTING); + written.add(flat); + keys.add(key); + return 1; + } + + /** + * Deletes the flattened assets an earlier build wrote that this one did not, + * then records what this build wrote. + * + *

Flattening only ever ADDS to {@code target/classes}. An asset renamed or + * deleted in {@code src/main/flutter} -- or the whole Flutter tree removed -- + * therefore kept being packaged by every incremental build until a clean one: + * a bigger application, and a stale {@code Image.asset} lookup that still + * succeeded against a file the project no longer has.

+ * + *

Only names from this plugin's own record are ever deleted. Sweeping every + * {@code cn1f_*} file would be simpler, but the output directory is shared with + * the application's own resources, and a name this plugin did not write is not + * one it should remove.

+ */ + private void removeStaleAssets(Set written) throws BuildException { + File outDir = assetsDir; + File manifest = assetManifest; + try { + if (manifest.isFile()) { + for (String name : Files.readAllLines(manifest.toPath(), java.nio.charset.StandardCharsets.UTF_8)) { + name = name.trim(); + // Defensive: a record entry must name a flat asset in the output + // root, never a path, whatever ended up in the file. + if (name.startsWith("cn1f_") && name.indexOf('/') < 0 && name.indexOf('\\') < 0 + && !written.contains(name)) { + Files.deleteIfExists(new File(outDir, name).toPath()); + } + } + } + if (written.isEmpty()) { + Files.deleteIfExists(manifest.toPath()); + } else { + List sorted = new ArrayList(written); + java.util.Collections.sort(sorted); + Files.createDirectories(manifest.getParentFile().toPath()); + Files.write(manifest.toPath(), sorted, java.nio.charset.StandardCharsets.UTF_8); + } + } catch (IOException e) { + throw new BuildException("Failed removing stale Flutter assets", e); + } + } + + /** The directory names under {@code src/main/flutter} that hold bundled assets. */ + private static final String[] ASSET_ROOTS = {"assets", "packages"}; + private static final Set ASSET_ROOTS_SET = + new HashSet(java.util.Arrays.asList(ASSET_ROOTS)); + + /** + * The bundled list of asset keys, one per line, which the runtime's + * {@code FlutterAssets} reads to learn which resolution variants exist. Flutter + * builds the same thing (AssetManifest) for the same reason: a variant can sit + * in any positive scale directory -- {@code 2.5x/}, or {@code 2x/} as well as + * {@code 2.0x/} -- so no fixed probe list finds them all, and one outside it was + * bundled and never used. + * + *

The name is outside the namespace {@link #flatAssetName} can produce: the + * encoder writes every {@code '_'} of a key as {@code "__"} and every {@code '/'} + * as {@code "_s"}, so after the {@code cn1f_} prefix a single underscore is always + * followed by another underscore or an {@code s}. {@code "cn1f__A..."} -- an + * underscore followed by {@code A} -- is therefore no asset's flat name. The + * plain {@code cn1f_AssetManifest.txt} used to be, once {@link #readDeclaredAssetKeys} + * began flattening project-relative keys outside {@code assets/} and + * {@code packages/}: a declared {@code AssetManifest.txt} flattened to it and + * {@link #writeAssetManifest} overwrote the developer's asset. The runtime reads + * the same literal ({@code FlutterAssets.MANIFEST}); the two must change + * together.

+ */ + static final String ASSET_MANIFEST = "cn1f__AssetManifest.txt"; + + + private void writeAssetManifest(File outDir, List keys, Set written) throws IOException { + List sorted = new ArrayList(keys); + java.util.Collections.sort(sorted); + Files.write(new File(outDir, ASSET_MANIFEST).toPath(), sorted, java.nio.charset.StandardCharsets.UTF_8); + written.add(ASSET_MANIFEST); + } + + private int flattenInto(File dir, String assetPrefix, File outDir, Set written, + List keys) throws IOException { + File[] children = dir.listFiles(); + if (children == null) { + return 0; + } + int count = 0; + for (File child : children) { + String key = assetPrefix + "/" + child.getName(); + if (child.isDirectory()) { + count += flattenInto(child, key, outDir, written, keys); + } else { + String flat = flatAssetName(key); + Files.copy(child.toPath(), new File(outDir, flat).toPath(), + StandardCopyOption.REPLACE_EXISTING); + written.add(flat); + keys.add(key); + count++; + } + } + return count; + } + + /** + * The flat resource name for a Flutter asset key. {@code '_'} is an escape + * always followed by one character -- {@code "__"} for an underscore, + * {@code "_s"} for the separator -- which is prefix-free, so no two keys + * share a name. Doubling underscores and using a single one as the + * separator was not: {@code a_/b} and {@code a/_b} both became + * {@code a___b}, and one asset was silently written over the other. + * Extensions survive for native bundlers. + * + *

Keep in sync with {@code com.codename1.flutter.FlutterAssets} in + * the Flutter runtime -- deliberately duplicated rather than shared, because + * this plugin must not depend on the runtime it builds against.

+ */ + static String flatAssetName(String assetKey) { + String p = assetKey; + while (p.startsWith("/")) { + p = p.substring(1); + } + StringBuilder sb = new StringBuilder("cn1f_"); + for (int i = 0; i < p.length(); i++) { + char c = p.charAt(i); + if (c == '_') { + sb.append("__"); + } else if (c == '/') { + sb.append("_s"); + } else { + sb.append(c); + } + } + return sb.toString(); + } + + private static void deleteRecursive(File dir) { + File[] children = dir == null ? null : dir.listFiles(); + if (children != null) { + for (File child : children) { + if (child.isDirectory()) { + deleteRecursive(child); + } else { + child.delete(); + } + } + } + if (dir != null) { + dir.delete(); + } + } +} diff --git a/maven/build-engine/src/main/java/com/codename1/maven/GradleConversion.java b/maven/build-engine/src/main/java/com/codename1/maven/GradleConversion.java index 1a31f14427d..0ee61921705 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/GradleConversion.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/GradleConversion.java @@ -1103,8 +1103,10 @@ private static void copyPlatformResources(ProjectLayout from, ProjectLayout to) // version, together with the translator of that version: a // declaration carried over from the pom could name another one. "codenameone-unity-compat", - // Likewise for a project with src/main/kmp. - "codenameone-compose-compat")); + // Likewise for a project with src/main/kmp, + "codenameone-compose-compat", + // and for one with src/main/flutter. + "codenameone-flutter-runtime")); /// `build.gradle.kts` lines for the dependencies a module's pom declares /// itself, leaving out the framework, which the plugin adds. A dependency of diff --git a/maven/build-engine/src/test/java/com/codename1/maven/DartLineMapperTest.java b/maven/build-engine/src/test/java/com/codename1/maven/DartLineMapperTest.java new file mode 100644 index 00000000000..a7215aa196e --- /dev/null +++ b/maven/build-engine/src/test/java/com/codename1/maven/DartLineMapperTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.TreeSet; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +/// A class transpiled from Dart reports the Dart files and lines. +public class DartLineMapperTest { + /// The shape DartLineTable writes: a mark before each statement and the + /// table, split over two constants, before the closing brace. Line 7 is + /// a `part` file's statement (file index 1). + private static final String SOURCE = "package my.app;\n" + + "public class MainLib {\n" + + " static int f(int a) {\n" + + " //@dart:3 lib/main.dart\n" + + " int b = a + 1;\n" + + " //@dart:4 lib/part.dart\n" + + " int c = b * 2;\n" + + " //@dart:9 lib/main.dart\n" + + " return new Runnable() { public void run() { } }.hashCode() + c;\n" + + " }\n" + + " static final String $cn1DartLines = \"lib/main.dart\\nlib/part.dart\\n5:3,7:\";\n" + + " static final String $cn1DartLines1 = \"4:1,9:9\";\n" + + "}\n"; + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + @Test + public void rewritesLinesSourceAndDropsTheTable() throws Exception { + File classes = tmp.newFolder("classes"); + CompatFixtures.compile(tmp.newFolder(), classes, "my/app/MainLib.java", SOURCE); + assertEquals(2, DartLineMapper.apply(classes)); + + Seen outer = scan(new File(classes, "my/app/MainLib.class")); + assertEquals("main.dart", outer.source); + assertFalse("the table is removed", outer.hasTable); + // main.dart keeps its lines; part.dart's line 4 sits past main.dart's + // last line (9), at 13. + assertEquals("[3, 9, 13]", new TreeSet(outer.lines).toString()); + assertEquals("SMAP\nMainLib.java\nDart\n*S Dart\n*F\n" + + "+ 1 main.dart\nlib/main.dart\n+ 2 part.dart\nlib/part.dart\n" + + "*L\n1#1,9:1\n1#2,4:10\n*E\n", outer.debug); + + // The anonymous class has no table of its own and takes its outer one. + Seen inner = scan(new File(classes, "my/app/MainLib$1.class")); + assertEquals("main.dart", inner.source); + assertEquals("[9]", new TreeSet(inner.lines).toString()); + + // Gone from the constant pool too, not only as a field. + String raw = new String(Files.readAllBytes(new File(classes, "my/app/MainLib.class").toPath()), + java.nio.charset.StandardCharsets.ISO_8859_1); + assertFalse("the table's strings are left in the class", raw.contains("cn1DartLines")); + assertFalse("the table's strings are left in the class", raw.contains("5:3,7:")); + + // Running it again changes nothing: the table is gone. + assertEquals(0, DartLineMapper.apply(classes)); + } + + /// A Dart identifier may hold a '$': `MainLib$Other` is a class of its + /// own, not part of `MainLib`, and must not take its table. + @Test + public void aDollarInATopLevelNameIsNotNesting() throws Exception { + File classes = tmp.newFolder("classes"); + CompatFixtures.compile(tmp.newFolder(), classes, "my/app/MainLib.java", SOURCE, + "my/app/MainLib$Other.java", "package my.app;\npublic class MainLib$Other { int f() { return 1; } }\n"); + assertEquals(2, DartLineMapper.apply(classes)); + assertEquals("MainLib$Other.java", scan(new File(classes, "my/app/MainLib$Other.class")).source); + } + + /// Lines past what a u2 line number holds are not wrapped into another range. + @Test + public void filesPastTheLineLimitKeepNoRange() { + DartLineMapper.Table t = new DartLineMapper.Table("a.dart\nb.dart\n1:60000,5:9000:1"); + assertEquals(60000, t.map(1)); + assertEquals(60000, t.map(5)); + assertFalse(t.smap("A.java"), t.smap("A.java").contains("1#2")); + } + + @Test + public void otherClassesAreLeftAlone() throws Exception { + File classes = tmp.newFolder("classes"); + CompatFixtures.compile(tmp.newFolder(), classes, "my/app/Plain.java", + "package my.app;\npublic final class Plain { static final String $cn1DartLinesX = \"\"; }\n"); + File f = new File(classes, "my/app/Plain.class"); + byte[] before = Files.readAllBytes(f.toPath()); + assertEquals(0, DartLineMapper.apply(classes)); + assertTrue(java.util.Arrays.equals(before, Files.readAllBytes(f.toPath()))); + assertEquals(0, DartLineMapper.apply(tmp.newFolder("empty"))); + } + + private static final class Seen { + String source; + String debug; + boolean hasTable; + final List lines = new ArrayList(); + } + + private static Seen scan(File f) throws Exception { + final Seen s = new Seen(); + new ClassReader(Files.readAllBytes(f.toPath())).accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public void visitSource(String source, String debug) { + s.source = source; + s.debug = debug; + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, String sig, Object v) { + s.hasTable |= DartLineMapper.index(name) >= 0; + return null; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String sig, String[] ex) { + return new MethodVisitor(Opcodes.ASM9) { + @Override + public void visitLineNumber(int line, Label start) { + s.lines.add(line); + } + }; + } + }, 0); + return s; + } +} diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/AppSupport.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/AppSupport.java index 01d917bdf26..d4e01664397 100644 --- a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/AppSupport.java +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/AppSupport.java @@ -130,6 +130,7 @@ static void apply(final Project project, final ProjectLayout layout, final Coden // classpath as it is at that moment. final TaskProvider unity = registerUnity(project, layout, ext, userProperties, main); + registerFlutter(project, layout, ext, userProperties, main); // The simulator's native-interface implementations: a real source set, so // src/javase/java compiles against the application and the JavaSE port and @@ -357,7 +358,8 @@ static void apply(final Project project, final ProjectLayout layout, final Coden // The FXML documents are compiled by this action, first: // after javac, so that a document can name a class of // the application, and before the relocation. - .withFxmlSourceDirectory(new File(layout.buildDir(), "generated/cn1-fxml"))); + .withFxmlSourceDirectory(new File(layout.buildDir(), "generated/cn1-fxml")) + .withFlutterSourceDirectory(flutterSourceDir(layout))); compile.doLast("cn1Compliance", new com.codename1.gradle.tasks.ComplianceAction(layout.rootDir(), layout.projectDir(), compile.getDestinationDirectory().getAsFile(), project.getName(), main.getCompileClasspath(), compileArtifacts, complianceProperties) @@ -900,6 +902,70 @@ static TaskProvider registerUnity(f return unity; } + /// `src/main/flutter`. + static File flutterSourceDir(ProjectLayout layout) { + return new File(layout.projectDir(), "src" + File.separator + "main" + File.separator + "flutter"); + } + + /// `transcodeFlutter`, for an application with `src/main/flutter`: the + /// Dart sources become a main source root and their flattened assets a + /// directory of the main output. The runtime is an `implementation` + /// dependency, added here as it is for Unity, at the plugin's own version. + /// The generated package is the `cn1.flutter.package` Gradle property (or + /// `-D` system property), as the Maven goal reads it. + static void registerFlutter(final Project project, final ProjectLayout layout, final CodenameOneExtension ext, + final Provider> userProperties, final SourceSet main) { + final File flutterDir = flutterSourceDir(layout); + final File flutterOut = new File(layout.buildDir(), "generated" + File.separator + "sources" + + File.separator + "cn1-flutter"); + if (com.codename1.maven.FlutterProjectBuilder.isFlutterProject(flutterDir)) { + addFramework(project, "implementation", ext.getVersion(), + com.codename1.maven.FlutterProjectBuilder.RUNTIME_ARTIFACT); + } + // The configuration, not the source set's classpath, which is about to + // include this task's own output. + final org.gradle.api.file.FileCollection stubs = project.getConfigurations() + .getByName(main.getCompileClasspathConfigurationName()); + TaskProvider flutter = project.getTasks().register( + "transcodeFlutter", com.codename1.gradle.tasks.TranscodeFlutterTask.class, t -> { + common(t, project, layout, ext, userProperties); + t.setDescription("Transpiles the Flutter/Dart sources in src/main/flutter to Java"); + t.getFlutterDirectory().set(flutterDir); + t.getStubClasspath().from(stubs); + // The application's own Java roots: every root of the main + // source set outside the build directory, and Kotlin's. + // Roots other tasks generate are left out -- reading them + // would need a dependency on each of those tasks, this + // one's own output among them, and generated SVG, GUI or + // Unity code never names a Flutter plugin pack. + t.getJavaSourceRoots().from(project.provider(() -> { + List roots = new ArrayList(); + // Canonical: a build directory reached through a symlink + // (/tmp and /private/tmp on macOS) is still the same one. + String build = layout.buildDir().getCanonicalPath() + File.separator; + for (File dir : main.getJava().getSrcDirs()) { + if (!dir.getCanonicalPath().startsWith(build)) { + roots.add(dir); + } + } + roots.add(new File(layout.projectDir(), "src" + File.separator + "main" + + File.separator + "kotlin")); + return roots; + })); + t.getPackageName().set(project.getProviders().gradleProperty("cn1.flutter.package") + .orElse(project.getProviders().systemProperty("cn1.flutter.package")) + .orElse(com.codename1.maven.FlutterProjectBuilder.DEFAULT_PACKAGE)); + t.getOutputDirectory().set(flutterOut); + t.getAssetsDirectory().set(new File(layout.buildDir(), "cn1-flutter/resources")); + t.getStateDirectory().set(new File(layout.buildDir(), "cn1-flutter/state")); + // The task itself is asked, through the argument, so the + // spec holds no reference to it for the configuration cache. + t.onlyIf(x -> ((com.codename1.gradle.tasks.TranscodeFlutterTask) x).hasWork()); + }); + main.getJava().srcDir(flutter.flatMap(com.codename1.gradle.tasks.TranscodeFlutterTask::getOutputDirectory)); + main.getOutput().dir(flutter.flatMap(com.codename1.gradle.tasks.TranscodeFlutterTask::getAssetsDirectory)); + } + /// `generateGuiSources`, whose generated views are a main source root. An /// application's and a cn1lib's alike: a library's own views extend the /// `Abstract*` classes it generates, exactly as an application's do. diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/RemapCompatAction.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/RemapCompatAction.java index 7dac1a03921..83e73e62bd8 100644 --- a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/RemapCompatAction.java +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/RemapCompatAction.java @@ -55,6 +55,7 @@ public class RemapCompatAction implements Action { private File desktopDir; private FileCollection shipWhenEmpty; private File fxmlSources; + private File flutterSourceDir; public RemapCompatAction(File classesDir, FileCollection compileClasspath, File onClickNames, boolean relocateOnly, FileCollection handlerDirs) { @@ -90,6 +91,13 @@ public RemapCompatAction withFxmlSourceDirectory(File dir) { /// pass ships the runtimes and generates what has to be generated -- but /// `compileJava` does nothing at all for a module with no Java source, /// and then this directory is the only one there is to ship them in. + /// `src/main/flutter`: with it, the classes transpiled from Dart are + /// mapped to the Dart source (`DartLineMapper`). + public RemapCompatAction withFlutterSourceDirectory(File dir) { + this.flutterSourceDir = dir; + return this; + } + public RemapCompatAction shippingWhenEmpty(FileCollection javaSources) { this.shipWhenEmpty = javaSources; return this; @@ -97,6 +105,17 @@ public RemapCompatAction shippingWhenEmpty(FileCollection javaSources) { @Override public void execute(Task task) { + // Classes transpiled from Dart get the Dart file and lines, so a + // debugger and a stack trace show the Dart source. Before the early + // return: a Flutter app needs no compatibility layer. Only for a + // project with Dart sources, since it reads every class. + if (com.codename1.maven.FlutterProjectBuilder.isFlutterProject(flutterSourceDir)) { + try { + com.codename1.maven.DartLineMapper.apply(classesDir); + } catch (java.io.IOException ex) { + throw new GradleException("Could not map Dart line numbers in " + classesDir, ex); + } + } Set classpath = compileClasspath.getFiles(); // By the jars alone, as the first test: with none there is nothing to // read the classes for. diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/TranscodeFlutterTask.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/TranscodeFlutterTask.java new file mode 100644 index 00000000000..13385922850 --- /dev/null +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/TranscodeFlutterTask.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.gradle.tasks; + +import com.codename1.builders.BuildException; +import com.codename1.maven.FlutterProjectBuilder; +import org.gradle.api.GradleException; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.LocalState; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskAction; +import org.gradle.work.DisableCachingByDefault; + +import java.io.File; + +/// Transpiles `src/main/flutter` -- Flutter/Dart sources and their assets -- +/// into Java on the Codename One Flutter runtime. The same +/// [FlutterProjectBuilder] as the Maven plugin's `transcode-flutter` goal. +/// +/// The flattened assets go to a directory of this task's own, an output +/// directory of the main source set, never into `compileJava`'s destination: +/// javac empties that on a full recompile. For the same reason no plugin-pack +/// class sweep is needed here -- Gradle's incremental compile removes the +/// class of a source the transpiler deleted. +/// +/// Without a Flutter project the task is skipped, unless an earlier build of +/// one left output behind: then it runs once more to remove it. +/// +/// Not cacheable: the transpiler keeps its own digest of everything it read, +/// and skips the work itself when nothing changed. +@DisableCachingByDefault(because = "The transpiler keeps its own incremental state") +public abstract class TranscodeFlutterTask extends Cn1Task { + + /// `src/main/flutter`. + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract DirectoryProperty getFlutterDirectory(); + + /// The compile class path: its `META-INF/dart/*.dart` stubs are the Dart + /// API the program compiles against. + @Classpath + public abstract ConfigurableFileCollection getStubClasspath(); + + /// The application's own Java, which can name a plugin pack. + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract ConfigurableFileCollection getJavaSourceRoots(); + + /// `cn1.flutter.package`. + @Input + public abstract Property getPackageName(); + + /// The generated Java source. + @OutputDirectory + public abstract DirectoryProperty getOutputDirectory(); + + /// The flattened assets, a resources root of the main source set. + @OutputDirectory + public abstract DirectoryProperty getAssetsDirectory(); + + /// The transpiler's state and the record of the assets written. + @LocalState + public abstract DirectoryProperty getStateDirectory(); + + @TaskAction + public void transcode() { + File state = getStateDirectory().get().getAsFile(); + try { + new FlutterProjectBuilder(getFlutterDirectory().get().getAsFile(), + getOutputDirectory().get().getAsFile(), getAssetsDirectory().get().getAsFile(), + new File(state, "assets.lst"), new File(state, "state.txt"), + getPackageName().get(), log()) + .stubClasspath(getStubClasspath().getFiles()) + .javaSourceRoots(getJavaSourceRoots().getFiles()) + .build(); + } catch (BuildException ex) { + throw new GradleException(ex.getMessage(), ex.getCause() == null ? ex : ex.getCause()); + } + } + + /// Whether there is a Flutter project to build, or the output of one that + /// is no longer there to remove: the generated sources are a source root + /// and the assets part of the main output, so what a deleted project left + /// in them would ship until `clean`. + public boolean hasWork() { + if (FlutterProjectBuilder.isFlutterProject(getFlutterDirectory().get().getAsFile())) { + return true; + } + for (DirectoryProperty d : new DirectoryProperty[] {getOutputDirectory(), getAssetsDirectory()}) { + String[] children = d.get().getAsFile().list(); + if (children != null && children.length > 0) { + return true; + } + } + return false; + } +} diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapCompatMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapCompatMojo.java index 65a0c409e96..0ad35d32854 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapCompatMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapCompatMojo.java @@ -59,6 +59,11 @@ public class RemapCompatMojo extends AbstractCN1Mojo { defaultValue = "${project.build.directory}/generated-resources/desktop") private File desktopResourcesOutputDir; + /// `transcode-flutter`'s source directory, under the same property: the + /// classes transpiled from it are mapped to the Dart source. + @Parameter(property = "cn1.flutter.sourceDir", defaultValue = "${project.basedir}/src/main/flutter") + private File flutterSourceDir; + @Override protected void executeImpl() throws MojoExecutionException, MojoFailureException { List classpath = new ArrayList(); @@ -67,6 +72,18 @@ protected void executeImpl() throws MojoExecutionException, MojoFailureException classpath.add(artifact.getFile()); } } + // Classes transpiled from Dart get the Dart file and lines, so a + // debugger and a stack trace show the Dart source. Before the + // early return: a Flutter app needs no compatibility layer. Only + // for a project with Dart sources, since it reads every class. + File compiled = new File(project.getBuild().getOutputDirectory()); + if (FlutterProjectBuilder.isFlutterProject(flutterSourceDir)) { + try { + DartLineMapper.apply(compiled); + } catch (java.io.IOException ex) { + throw new MojoExecutionException("Could not map Dart line numbers in " + compiled, ex); + } + } // By the jars alone: whether a desktop layer is also used can only be // read from the classes, and Kotlin's are not all in place yet. if (CompatLayers.active(classpath).isEmpty()) { diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/TranscodeFlutterMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/TranscodeFlutterMojo.java index c72370e40d3..83577f162b9 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/TranscodeFlutterMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/TranscodeFlutterMojo.java @@ -23,10 +23,6 @@ */ package com.codename1.maven; -import com.codename1.dart.transpiler.api.DartTranspiler; -import com.codename1.dart.transpiler.api.Diagnostic; -import com.codename1.dart.transpiler.api.TranspileRequest; -import com.codename1.dart.transpiler.api.TranspileResult; import org.apache.maven.model.Dependency; import org.apache.maven.plugin.MojoExecutionException; @@ -37,17 +33,11 @@ import org.apache.maven.plugins.annotations.ResolutionScope; import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; /** - * Transpiles Flutter/Dart sources under {@code src/main/flutter} into Java + * Runs {@link FlutterProjectBuilder}: transpiles Flutter/Dart sources under {@code src/main/flutter} into Java * source targeting the Codename One Flutter runtime * ({@code codenameone-flutter-runtime}), so Flutter UI code runs as plain * Codename One components at native speed -- no Dart VM or Flutter engine. @@ -79,7 +69,7 @@ requiresDependencyResolution = ResolutionScope.COMPILE) public class TranscodeFlutterMojo extends AbstractCN1Mojo { - private static final String RUNTIME_ARTIFACT = "codenameone-flutter-runtime"; + private static final String RUNTIME_ARTIFACT = FlutterProjectBuilder.RUNTIME_ARTIFACT; @Parameter(property = "cn1.flutter.sourceDir", defaultValue = "${project.basedir}/src/main/flutter") private File flutterSourceDir; @@ -87,7 +77,7 @@ public class TranscodeFlutterMojo extends AbstractCN1Mojo { @Parameter(property = "cn1.flutter.outputDir", defaultValue = "${project.build.directory}/generated-sources/flutter") private File flutterOutputDir; - @Parameter(property = "cn1.flutter.package", defaultValue = "com.codename1.generated.flutter") + @Parameter(property = "cn1.flutter.package", defaultValue = FlutterProjectBuilder.DEFAULT_PACKAGE) private String flutterPackage; @Parameter(property = "cn1.flutter.stateFile", defaultValue = "${project.build.directory}/flutter-transpiler/state.txt") @@ -95,98 +85,34 @@ public class TranscodeFlutterMojo extends AbstractCN1Mojo { @Override protected void executeImpl() throws MojoExecutionException, MojoFailureException { - if (flutterSourceDir == null || !flutterSourceDir.isDirectory()) { - sweepStaleOutput(); - removeDeselectedPackClasses(); - // Flutter was removed from the project: its assets go with it. - removeStaleAssets(new HashSet()); - return; + boolean flutter = FlutterProjectBuilder.isFlutterProject(flutterSourceDir); + if (flutter) { + checkRuntimeDependency(); } - checkJdk(); - checkRuntimeDependency(); - - TranspileRequest req = new TranspileRequest() - .sourceRoot(flutterSourceDir) - .outputDir(flutterOutputDir) - .packageName(flutterPackage) - .stateFile(stateFile); + List classpath = new ArrayList(); // runtime dependencies contribute API stubs via META-INF/dart/*.dart for (Object o : project.getArtifacts()) { org.apache.maven.artifact.Artifact a = (org.apache.maven.artifact.Artifact) o; - if (a.getFile() != null) { - req.stubClasspathEntry(a.getFile()); - } + classpath.add(a.getFile()); } - // The app's own Java can name a plugin pack too (the import-generated entry point - // installs the firebase_messaging and app_links hooks): the transpiler copies - // exactly the packs that are named, so it must see that Java as well. + List javaRoots = new ArrayList(); for (Object root : project.getCompileSourceRoots()) { - File dir = new File(String.valueOf(root)); - if (dir.isDirectory() && !dir.getAbsoluteFile().equals(flutterOutputDir.getAbsoluteFile())) { - req.javaSourceRoot(dir); - } - } - TranspileResult result = new DartTranspiler().transpile(req); - for (Diagnostic d : result.getDiagnostics()) { - String msg = "src/main/flutter/" + d.file + ":[" + d.line + "," + d.col + "] " - + d.message + " (dart2java:" + d.code + ")"; - if (d.severity == Diagnostic.Severity.ERROR) { - getLog().error(msg); - } else if (d.severity == Diagnostic.Severity.WARNING) { - getLog().warn(msg); - } else { - getLog().info(msg); - } - } - if (result.hasErrors()) { - throw new MojoFailureException("Flutter transpilation failed with " - + result.errors().size() + " error(s); see log above. " - + "Confirm the Dart files pass `dart analyze` -- constructs outside the " - + "currently supported subset are reported with a milestone code."); - } - if (result.isUpToDate()) { - getLog().info("Flutter sources are up to date"); - } else { - getLog().info("Transpiled Flutter sources to " + flutterOutputDir); - } - removeDeselectedPackClasses(); - copyAssets(); - registerSourceRoot(); - } - - /** - * The transpiler deletes the source of a plugin pack the app stopped naming, but javac - * does not reliably delete the class compiled from it on an earlier build. Left in - * target/classes, those classes would still ship -- and still imply the permissions - * (location, push, ...) the builders derive from the classes they find. - */ - private void removeDeselectedPackClasses() { - String packs = "com/codename1/flutter/plugins".replace('/', File.separatorChar); - File[] compiled = new File(project.getBuild().getOutputDirectory(), packs).listFiles(); - if (compiled == null) { - return; - } - File selected = new File(flutterOutputDir, packs); - for (File pack : compiled) { - if (pack.isDirectory() && !new File(selected, pack.getName()).isDirectory()) { - deleteRecursive(pack); - getLog().debug("Removed classes of unused Flutter plugin pack " + pack.getName()); - } + javaRoots.add(new File(String.valueOf(root))); } - } - - private void checkJdk() throws MojoFailureException { - String spec = System.getProperty("java.specification.version", "1.8"); - int major; + File classes = new File(project.getBuild().getOutputDirectory()); try { - major = Integer.parseInt(spec.startsWith("1.") ? spec.substring(2) : spec); - } catch (NumberFormatException e) { - major = 8; + new FlutterProjectBuilder(flutterSourceDir, flutterOutputDir, classes, + new File(project.getBuild().getDirectory(), "cn1-flutter-assets.lst"), stateFile, + flutterPackage, MavenLog.of(getLog())) + .stubClasspath(classpath) + .javaSourceRoots(javaRoots) + .classesDir(classes) + .build(); + } catch (com.codename1.builders.BuildException e) { + throw new MojoFailureException(e.getMessage(), e); } - if (major < 17) { - throw new MojoFailureException("Flutter support generates Java 17 source, but this build " - + "is running on JDK " + spec + ". Run Maven with JDK 17 or newer " - + "(e.g. from https://adoptium.net) to use src/main/flutter."); + if (flutter) { + registerSourceRoot(); } } @@ -206,269 +132,6 @@ private void checkRuntimeDependency() throws MojoFailureException { + "
\n"); } - /** - * Copies {@code src/main/flutter/assets} into the build output, flattened. - * - *

Codename One resources are flat on every port -- {@code getResourceAsStream} - * rejects a name containing a {@code '/'} past the leading one -- so the asset - * tree cannot be mirrored. Each asset is written to the output root under the - * name produced by {@link #flatAssetName}, which the Flutter runtime's - * {@code FlutterAssets} recomputes when resolving {@code Image.asset(...)}.

- */ - void copyAssets() throws MojoExecutionException { - File outDir = new File(project.getBuild().getOutputDirectory()); - Set written = new HashSet(); - List keys = new ArrayList(); - int count = 0; - try { - // Both roots a pubspec asset key can start with. "assets/" is an app's own - // bundle; "packages/" is one it pulls from a dependency, which is how the - // gallery ships its artwork (packages/flutter_gallery_assets/assets/...). - // Only "assets/" was copied before, so every packaged asset key resolved to - // nothing on every port - the images were simply absent. - for (String root : ASSET_ROOTS) { - File dir = new File(flutterSourceDir, root); - if (!dir.isDirectory()) { - continue; - } - Files.createDirectories(outDir.toPath()); - // the root stays in the Flutter asset key, matching pubspec paths - count += flattenInto(dir, root, outDir, written, keys); - } - // Flutter allows an asset or font declared anywhere project-relative -- - // fonts/MyFont.ttf, images/logo.png -- not only under assets/ or - // packages/, so the scan above missed every one of those: the key - // resolved to nothing on every port. cn1:import-flutter-project records - // exactly which keys it copied, in FlutterProjectImporter.ASSET_KEYS_RECORD; - // a hand-wired project (no import, so no such record) is unaffected -- - // it keeps relying on the scan above alone, as before. - for (String key : readDeclaredAssetKeys()) { - if (ASSET_ROOTS_SET.contains(topSegment(key))) { - continue; // already copied by the scan above - } - Files.createDirectories(outDir.toPath()); - count += flattenDeclaredAsset(key, outDir, written, keys); - } - if (count > 0) { - writeAssetManifest(outDir, keys, written); - getLog().info("Flattened " + count + " Flutter asset(s) into the build output"); - } - } catch (IOException e) { - throw new MojoExecutionException("Failed copying Flutter assets", e); - } - removeStaleAssets(written); - } - - /// The `FlutterProjectImporter.ASSET_KEYS_RECORD` keys a previous - /// `cn1:import-flutter-project` wrote beside `src/main/flutter`, or - /// empty when the project was hand-wired (never imported) or predates - /// the record. `FlutterProjectImporter` is package-private but lives in - /// `build-engine`, which this plugin already depends on and already - /// invokes directly (`ImportFlutterProjectMojo`) -- so its record - /// filename is read from that one constant rather than duplicated here. - private List readDeclaredAssetKeys() throws IOException { - File f = new File(flutterSourceDir, FlutterProjectImporter.ASSET_KEYS_RECORD); - if (!f.isFile()) { - return java.util.Collections.emptyList(); - } - List out = new ArrayList(); - for (String line : Files.readAllLines(f.toPath(), java.nio.charset.StandardCharsets.UTF_8)) { - line = line.trim(); - if (line.length() > 0) { - out.add(line); - } - } - return out; - } - - private static String topSegment(String key) { - int slash = key.indexOf('/'); - return slash < 0 ? key : key.substring(0, slash); - } - - /// Flattens one declared asset key outside {@link #ASSET_ROOTS}: a file, - /// or a directory (pubspec allows declaring a whole directory), rooted - /// at {@code flutterSourceDir}. Missing on disk is not an error here -- - /// {@code FlutterProjectImporter} already warned about that key when it - /// could not resolve it. - private int flattenDeclaredAsset(String key, File outDir, Set written, List keys) - throws IOException { - File node = new File(flutterSourceDir, key.replace('/', File.separatorChar)); - if (node.isDirectory()) { - return flattenInto(node, key, outDir, written, keys); - } - if (!node.isFile()) { - return 0; - } - String flat = flatAssetName(key); - Files.copy(node.toPath(), new File(outDir, flat).toPath(), StandardCopyOption.REPLACE_EXISTING); - written.add(flat); - keys.add(key); - return 1; - } - - /** - * Deletes the flattened assets an earlier build wrote that this one did not, - * then records what this build wrote. - * - *

Flattening only ever ADDS to {@code target/classes}. An asset renamed or - * deleted in {@code src/main/flutter} -- or the whole Flutter tree removed -- - * therefore kept being packaged by every incremental build until a clean one: - * a bigger application, and a stale {@code Image.asset} lookup that still - * succeeded against a file the project no longer has.

- * - *

Only names from this plugin's own record are ever deleted. Sweeping every - * {@code cn1f_*} file would be simpler, but the output directory is shared with - * the application's own resources, and a name this plugin did not write is not - * one it should remove.

- */ - void removeStaleAssets(Set written) throws MojoExecutionException { - File outDir = new File(project.getBuild().getOutputDirectory()); - File manifest = assetManifest(); - try { - if (manifest.isFile()) { - for (String name : Files.readAllLines(manifest.toPath(), java.nio.charset.StandardCharsets.UTF_8)) { - name = name.trim(); - // Defensive: a record entry must name a flat asset in the output - // root, never a path, whatever ended up in the file. - if (name.startsWith("cn1f_") && name.indexOf('/') < 0 && name.indexOf('\\') < 0 - && !written.contains(name)) { - Files.deleteIfExists(new File(outDir, name).toPath()); - } - } - } - if (written.isEmpty()) { - Files.deleteIfExists(manifest.toPath()); - } else { - List sorted = new ArrayList(written); - java.util.Collections.sort(sorted); - Files.createDirectories(manifest.getParentFile().toPath()); - Files.write(manifest.toPath(), sorted, java.nio.charset.StandardCharsets.UTF_8); - } - } catch (IOException e) { - throw new MojoExecutionException("Failed removing stale Flutter assets", e); - } - } - - /** - * The record of flattened asset names. Beside the output rather than inside - * the generated-sources directory, which {@link #sweepStaleOutput} deletes. - */ - private File assetManifest() { - return new File(project.getBuild().getDirectory(), "cn1-flutter-assets.lst"); - } - - /** The directory names under {@code src/main/flutter} that hold bundled assets. */ - private static final String[] ASSET_ROOTS = {"assets", "packages"}; - private static final Set ASSET_ROOTS_SET = - new HashSet(java.util.Arrays.asList(ASSET_ROOTS)); - - /** - * The bundled list of asset keys, one per line, which the runtime's - * {@code FlutterAssets} reads to learn which resolution variants exist. Flutter - * builds the same thing (AssetManifest) for the same reason: a variant can sit - * in any positive scale directory -- {@code 2.5x/}, or {@code 2x/} as well as - * {@code 2.0x/} -- so no fixed probe list finds them all, and one outside it was - * bundled and never used. - * - *

The name is outside the namespace {@link #flatAssetName} can produce: the - * encoder writes every {@code '_'} of a key as {@code "__"} and every {@code '/'} - * as {@code "_s"}, so after the {@code cn1f_} prefix a single underscore is always - * followed by another underscore or an {@code s}. {@code "cn1f__A..."} -- an - * underscore followed by {@code A} -- is therefore no asset's flat name. The - * plain {@code cn1f_AssetManifest.txt} used to be, once {@link #readDeclaredAssetKeys} - * began flattening project-relative keys outside {@code assets/} and - * {@code packages/}: a declared {@code AssetManifest.txt} flattened to it and - * {@link #writeAssetManifest} overwrote the developer's asset. The runtime reads - * the same literal ({@code FlutterAssets.MANIFEST}); the two must change - * together.

- */ - static final String ASSET_MANIFEST = "cn1f__AssetManifest.txt"; - - - private void writeAssetManifest(File outDir, List keys, Set written) throws IOException { - List sorted = new ArrayList(keys); - java.util.Collections.sort(sorted); - Files.write(new File(outDir, ASSET_MANIFEST).toPath(), sorted, java.nio.charset.StandardCharsets.UTF_8); - written.add(ASSET_MANIFEST); - } - - private int flattenInto(File dir, String assetPrefix, File outDir, Set written, - List keys) throws IOException { - File[] children = dir.listFiles(); - if (children == null) { - return 0; - } - int count = 0; - for (File child : children) { - String key = assetPrefix + "/" + child.getName(); - if (child.isDirectory()) { - count += flattenInto(child, key, outDir, written, keys); - } else { - String flat = flatAssetName(key); - Files.copy(child.toPath(), new File(outDir, flat).toPath(), - StandardCopyOption.REPLACE_EXISTING); - written.add(flat); - keys.add(key); - count++; - } - } - return count; - } - - /** - * The flat resource name for a Flutter asset key. {@code '_'} is an escape - * always followed by one character -- {@code "__"} for an underscore, - * {@code "_s"} for the separator -- which is prefix-free, so no two keys - * share a name. Doubling underscores and using a single one as the - * separator was not: {@code a_/b} and {@code a/_b} both became - * {@code a___b}, and one asset was silently written over the other. - * Extensions survive for native bundlers. - * - *

Keep in sync with {@code com.codename1.flutter.FlutterAssets} in - * the Flutter runtime -- deliberately duplicated rather than shared, because - * this plugin must not depend on the runtime it builds against.

- */ - static String flatAssetName(String assetKey) { - String p = assetKey; - while (p.startsWith("/")) { - p = p.substring(1); - } - StringBuilder sb = new StringBuilder("cn1f_"); - for (int i = 0; i < p.length(); i++) { - char c = p.charAt(i); - if (c == '_') { - sb.append("__"); - } else if (c == '/') { - sb.append("_s"); - } else { - sb.append(c); - } - } - return sb.toString(); - } - - private void sweepStaleOutput() { - if (flutterOutputDir != null && flutterOutputDir.isDirectory()) { - deleteRecursive(flutterOutputDir); - getLog().debug("Removed stale Flutter generated sources"); - } - } - - private void deleteRecursive(File dir) { - File[] children = dir.listFiles(); - if (children != null) { - for (File child : children) { - if (child.isDirectory()) { - deleteRecursive(child); - } else { - child.delete(); - } - } - } - dir.delete(); - } - private void registerSourceRoot() { String path = flutterOutputDir.getAbsolutePath(); if (!project.getCompileSourceRoots().contains(path)) { diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/DartLineTable.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/DartLineTable.java new file mode 100644 index 00000000000..80e8bda3495 --- /dev/null +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/DartLineTable.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.codegen; + +import java.util.ArrayList; +import java.util.List; + +/// The Dart line table of a generated Java file. +/// +/// The emitter writes a [#MARK] comment line in front of every Dart +/// statement it lowers. Once the file is final (after the dynamic-case +/// pruning, which drops lines), [#apply] reads the marks into +/// `javaLine:dartLine` pairs and stores them in a constant, [#FIELD], added +/// just before the type's closing brace so that no line above it moves. +/// `DartLineMapper` in build-engine reads the constant from the compiled +/// class, rewrites the class's line numbers and source file to the Dart +/// ones, adds a JSR-45 source map and removes the constant -- so a Java +/// debugger steps through the Dart source and a stack trace names +/// `main.dart:12`. +/// +/// The constant's value: the Dart files, one per line, the class's own +/// first; then one line of comma separated pairs, `javaLine:dartLine` for +/// the first file and `javaLine:dartLine:fileIndex` for any other (a +/// `part` file's function lands in its owning library's class). +public final class DartLineTable { + /// Must match `DartLineMapper.FIELD` in build-engine. A long table is + /// split over `FIELD`, `FIELD1`, `FIELD2`...: a class file constant + /// cannot exceed 65535 bytes, and javac rejects a longer literal. + public static final String FIELD = "$cn1DartLines"; + /// Opens a mark line: `//@dart: `. + static final String MARK = "//@dart:"; + /// Characters per constant, well under the class file limit even if + /// every character took three bytes. + private static final int CHUNK = 20000; + + private DartLineTable() { + } + + /// The mark line for a statement at `line` of `file`, or null when the + /// node carries no position. + static String mark(String file, int line) { + if (file == null || line <= 0) { + return null; + } + return MARK + line + " " + file.replace('\\', '/'); + } + + /// True for a line (indented or not) that is a mark. + static boolean isMark(String line) { + return line.trim().startsWith(MARK); + } + + /// `java` with its line table added, `ownFile` first among the files; + /// unchanged when it holds no mark. + static String apply(String java, String ownFile) { + List files = new ArrayList(); + files.add(ownFile.replace('\\', '/')); + StringBuilder pairs = new StringBuilder(); + int line = 0; + int dart = 0; + int fileIndex = 0; + String last = null; + int start = 0; + while (start < java.length()) { + int end = java.indexOf('\n', start); + if (end < 0) { + end = java.length(); + } + line++; + int i = start; + while (i < end && java.charAt(i) == ' ') { + i++; + } + if (java.startsWith(MARK, i)) { + int space = java.indexOf(' ', i + MARK.length()); + dart = Integer.parseInt(java.substring(i + MARK.length(), space)); + String f = java.substring(space + 1, end); + fileIndex = files.indexOf(f); + if (fileIndex < 0) { + fileIndex = files.size(); + files.add(f); + } + } else if (dart > 0 && i < end) { + String pair = fileIndex == 0 ? dart + "" : dart + ":" + fileIndex; + if (!pair.equals(last)) { + pairs.append(pairs.length() == 0 ? "" : ",").append(line).append(':').append(pair); + last = pair; + } + } + start = end + 1; + } + int close = java.lastIndexOf('}'); + if (pairs.length() == 0 || close < 0) { + return java; + } + StringBuilder value = new StringBuilder(); + for (String f : files) { + value.append(f).append('\n'); + } + value.append(pairs); + StringBuilder fields = new StringBuilder(); + for (int at = 0, n = 0; at < value.length(); at += CHUNK, n++) { + // Interfaces (mixins) take no `private`; a bare static final + // compiles to the same ConstantValue in every kind of type. + fields.append(" static final String ").append(FIELD).append(n == 0 ? "" : String.valueOf(n)) + .append(" = ").append(literal(value.substring(at, Math.min(value.length(), at + CHUNK)))) + .append(";\n"); + } + return java.substring(0, close) + fields + java.substring(close); + } + + private static String literal(String value) { + StringBuilder lit = new StringBuilder("\""); + for (int k = 0; k < value.length(); k++) { + char c = value.charAt(k); + if (c == '\n') { + lit.append("\\n"); + } else if (c == '"' || c == '\\') { + lit.append('\\').append(c); + } else if (c < 0x20 || c > 0x7e) { + lit.append(String.format("\\u%04x", (int) c)); + } else { + lit.append(c); + } + } + return lit.append('"').toString(); + } +} diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java index abc52194ca3..178975ce1a1 100644 --- a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java @@ -134,8 +134,34 @@ private List pruneDynamicCases(List files) { return pruned; } + /// The Dart file each generated Java file was emitted from, by its relative path. + private final Map dartFileOf = new java.util.HashMap(); + public List emit() { - return pruneDynamicCases(emitAll()); + List pruned = pruneDynamicCases(emitAll()); + // After the pruning, which drops lines: the table pairs final Java lines. + List mapped = new ArrayList(pruned.size()); + for (GeneratedFile g : pruned) { + String dart = g == null ? null : dartFileOf.get(g.relativePath); + mapped.add(dart == null ? g : new GeneratedFile(g.relativePath, DartLineTable.apply(g.content, dart))); + } + return mapped; + } + + /// Adds `g` to `out`, remembering the Dart file it came from for its line table. + private void addMapped(List out, GeneratedFile g, String dartFile) { + out.add(g); + if (g != null && dartFile != null) { + dartFileOf.put(g.relativePath, dartFile); + } + } + + /// Writes the line mark ahead of what `n` lowers to (see DartLineTable). + private void lineMark(Node n, Ctx ctx) { + String mark = n == null ? null : DartLineTable.mark(n.file, n.line); + if (mark != null) { + ctx.writer().line(mark); + } } private List emitAll() { @@ -151,12 +177,13 @@ private List emitAll() { for (Library lib : program.libraries) { for (ClassDecl c : lib.classes) { try { + String dart = c.file != null ? c.file : lib.fileName; if (c.extensionOn != null) { - out.add(emitExtension(c)); + addMapped(out, emitExtension(c), dart); } else if (c.isMixin) { - out.add(emitMixin(c)); + addMapped(out, emitMixin(c), dart); } else { - out.add(emitClass(c)); + addMapped(out, emitClass(c), dart); } } catch (RuntimeException | StackOverflowError ex) { emitCrash(lib, "class " + c.name, ex); @@ -164,14 +191,14 @@ private List emitAll() { } for (EnumDecl e : lib.enums) { try { - out.add(emitEnum(e)); + addMapped(out, emitEnum(e), e.file != null ? e.file : lib.fileName); } catch (RuntimeException | StackOverflowError ex) { emitCrash(lib, "enum " + e.name, ex); } } if (!lib.functions.isEmpty() || !lib.topLevelVars.isEmpty()) { try { - out.add(emitLibClass(lib)); + addMapped(out, emitLibClass(lib), lib.fileName); } catch (RuntimeException | StackOverflowError ex) { emitCrash(lib, "library " + lib.fileName, ex); } @@ -2421,6 +2448,7 @@ private String emitMethodLike(Method m, Ctx ctx, boolean classIsAbstract) { ctx.writer().line("return Future.value(null);"); } } else if (m.exprBody != null) { + lineMark(m.exprBody, ctx); if (asyncFuture) { Out o = emitExpr(m.exprBody, null, ctx); ctx.importClass("dart.async.Future"); @@ -2517,6 +2545,9 @@ private void emitStatements(Block b, Ctx ctx) { } private void emitStatement(Stmt s, Ctx ctx) { + if (s != null && !(s instanceof Block)) { + lineMark(s, ctx); + } if (s != null && s.labels != null && !isLoop(s)) { // A labeled non-loop statement is a labeled Java block, which `break label;` // leaves. A loop carries its label on its own header (loopLabel), where @@ -7153,7 +7184,7 @@ private Lazy emitLazyOperand(Expr e, TypeRef expected, Ctx ctx, int depth) { // break-loop for no change in behaviour. Ctx.Writer w = ctx.writer(); for (String line : lifted.split("\n")) { - if (!line.trim().isEmpty()) { + if (!line.trim().isEmpty() && !DartLineTable.isMark(line)) { w.line(line.trim()); } } @@ -7170,7 +7201,7 @@ private Lazy emitLazyOperand(Expr e, TypeRef expected, Ctx ctx, int depth) { private static boolean onlyTempDeclarations(String lifted) { for (String line : lifted.split("\n")) { String t = line.trim(); - if (!t.isEmpty() && !TEMP_DECLARATION.matcher(t).matches()) { + if (!t.isEmpty() && !DartLineTable.isMark(t) && !TEMP_DECLARATION.matcher(t).matches()) { return false; } } diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/codegen/DartLineTableTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/codegen/DartLineTableTest.java new file mode 100644 index 00000000000..579d555d054 --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/codegen/DartLineTableTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.codegen; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class DartLineTableTest { + @Test + void pairsJavaLinesWithDartLinesAndAddsNoLineAbove() { + String java = "class A {\n" + + " void f() {\n" + + " //@dart:3 lib/a.dart\n" + + " x();\n" + + " y();\n" + + " //@dart:7 lib/p.dart\n" + + " z();\n" + + " }\n" + + "}\n"; + String out = DartLineTable.apply(java, "lib/a.dart"); + assertEquals(java.substring(0, java.lastIndexOf('}')), + out.substring(0, java.lastIndexOf('}')), "no line above the table moves"); + assertTrue(out.contains("static final String $cn1DartLines = \"lib/a.dart\\nlib/p.dart\\n4:3,7:7:1\";"), out); + } + + @Test + void longTableIsSplitAndUnmarkedFileIsUnchanged() { + StringBuilder java = new StringBuilder("class A {\n"); + for (int i = 1; i < 6000; i++) { + java.append("//@dart:").append(i).append(" a.dart\nx();\n"); + } + java.append("}\n"); + String out = DartLineTable.apply(java.toString(), "a.dart"); + assertTrue(out.contains("$cn1DartLines1 = "), "split past one constant"); + String plain = "class B {\n}\n"; + assertEquals(plain, DartLineTable.apply(plain, "b.dart")); + } +} diff --git a/maven/dart-transpiler/src/test/resources/golden/counter/expected/MainLib.java b/maven/dart-transpiler/src/test/resources/golden/counter/expected/MainLib.java index c86e4031b6c..03473980bfb 100644 --- a/maven/dart-transpiler/src/test/resources/golden/counter/expected/MainLib.java +++ b/maven/dart-transpiler/src/test/resources/golden/counter/expected/MainLib.java @@ -9,7 +9,9 @@ private MainLib() { } public static void main$() { + //@dart:4 main.dart FlutterUI.runApp(new MyApp(null)); } + static final String $cn1DartLines = "main.dart\n13:4"; } diff --git a/maven/dart-transpiler/src/test/resources/golden/counter/expected/MyApp.java b/maven/dart-transpiler/src/test/resources/golden/counter/expected/MyApp.java index 1498ece897a..e17233a789d 100644 --- a/maven/dart-transpiler/src/test/resources/golden/counter/expected/MyApp.java +++ b/maven/dart-transpiler/src/test/resources/golden/counter/expected/MyApp.java @@ -18,6 +18,7 @@ public MyApp(Key key) { @Override public Widget build(BuildContext context) { + //@dart:12 main.dart var $t0 = new MaterialApp(); $t0.title("Flutter Demo"); var $t1 = new ThemeData(); @@ -28,4 +29,5 @@ public Widget build(BuildContext context) { return $t0; } + static final String $cn1DartLines = "main.dart\n22:12"; } diff --git a/maven/dart-transpiler/src/test/resources/golden/counter/expected/MyHomePage.java b/maven/dart-transpiler/src/test/resources/golden/counter/expected/MyHomePage.java index 2066115485b..c3bb5c0a592 100644 --- a/maven/dart-transpiler/src/test/resources/golden/counter/expected/MyHomePage.java +++ b/maven/dart-transpiler/src/test/resources/golden/counter/expected/MyHomePage.java @@ -19,7 +19,9 @@ public MyHomePage(Key key, String title) { @Override public State createState() { + //@dart:29 main.dart return new _MyHomePageState(); } + static final String $cn1DartLines = "main.dart\n23:29"; } diff --git a/maven/dart-transpiler/src/test/resources/golden/counter/expected/_MyHomePageState.java b/maven/dart-transpiler/src/test/resources/golden/counter/expected/_MyHomePageState.java index afa7f8e1c1d..bda6f8f0f41 100644 --- a/maven/dart-transpiler/src/test/resources/golden/counter/expected/_MyHomePageState.java +++ b/maven/dart-transpiler/src/test/resources/golden/counter/expected/_MyHomePageState.java @@ -28,13 +28,16 @@ public class _MyHomePageState extends State { } void _incrementCounter() { + //@dart:36 main.dart this.setState(() -> { + //@dart:37 main.dart this._counter++; }); } @Override public Widget build(BuildContext context) { + //@dart:43 main.dart var $t0 = new Scaffold(); var $t1 = new AppBar(); $t1.backgroundColor(Theme.of(context).colorScheme().inversePrimary()); @@ -56,4 +59,5 @@ public Widget build(BuildContext context) { return $t0; } + static final String $cn1DartLines = "main.dart\n32:36,34:37,41:43"; } diff --git a/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoApp.java b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoApp.java index ebf6428bac8..5d6ecfcd154 100644 --- a/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoApp.java +++ b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoApp.java @@ -18,6 +18,7 @@ public DemoApp(Key key) { @Override public Widget build(BuildContext context) { + //@dart:12 main.dart var $t0 = new MaterialApp(); $t0.title("M2 Demo"); var $t1 = new ThemeData(); @@ -28,4 +29,5 @@ public Widget build(BuildContext context) { return $t0; } + static final String $cn1DartLines = "main.dart\n22:12"; } diff --git a/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoPage.java b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoPage.java index a0155c32fa5..e28c2b79361 100644 --- a/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoPage.java +++ b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoPage.java @@ -13,7 +13,9 @@ public DemoPage(Key key) { @Override public State createState() { + //@dart:27 main.dart return new _DemoPageState(); } + static final String $cn1DartLines = "main.dart\n17:27"; } diff --git a/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/MainLib.java b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/MainLib.java index 95e44e85523..2f7a3186811 100644 --- a/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/MainLib.java +++ b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/MainLib.java @@ -9,7 +9,9 @@ private MainLib() { } public static void main$() { + //@dart:4 main.dart FlutterUI.runApp(new DemoApp(null)); } + static final String $cn1DartLines = "main.dart\n13:4"; } diff --git a/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/_DemoPageState.java b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/_DemoPageState.java index e191fc8b8f5..865c258c0e7 100644 --- a/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/_DemoPageState.java +++ b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/_DemoPageState.java @@ -42,6 +42,7 @@ public class _DemoPageState extends State { @Override public Widget build(BuildContext context) { + //@dart:36 main.dart var $t0 = new Scaffold(); var $t1 = new AppBar(); $t1.title(new Text("M2 Widgets")); @@ -59,7 +60,9 @@ public Widget build(BuildContext context) { $t6.child(new Text("Clear")); var $t7 = new TextButton(); $t7.onPressed(() -> { + //@dart:55 main.dart this.setState(() -> { + //@dart:56 main.dart this._taps++; }); }); @@ -68,6 +71,7 @@ public Widget build(BuildContext context) { $t3.child($t4); var $t8 = new Expanded(); $t8.child(ListView.builder(null, this._items.length(), (context$0, index) -> { + //@dart:69 main.dart var $t9 = new Card(); var $t10 = new Padding(); $t10.padding(EdgeInsets.all(12.0)); @@ -87,16 +91,22 @@ public Widget build(BuildContext context) { } void _addItem() { + //@dart:91 main.dart this.setState(() -> { + //@dart:92 main.dart this._items.add("Item " + DartRuntime.str(this._items.length() + 1L)); }); } void _clear() { + //@dart:97 main.dart this.setState(() -> { + //@dart:98 main.dart this._items.clear(); + //@dart:99 main.dart this._taps = 0L; }); } + static final String $cn1DartLines = "main.dart\n46:36,64:55,66:56,75:69,95:91,97:92,103:97,105:98,107:99"; } diff --git a/maven/integration-tests/gradle-flutter-test.sh b/maven/integration-tests/gradle-flutter-test.sh new file mode 100755 index 00000000000..da20daf1f98 --- /dev/null +++ b/maven/integration-tests/gradle-flutter-test.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# Flutter sources (src/main/flutter) in a Gradle application. An archetype app +# gets a Dart counter and an asset, is built with Maven, converted to Gradle +# and built again; both builds must produce the same transpiled classes and +# flattened assets, and both must point those classes at the Dart source +# (SourceFile main.dart, a JSR-45 map) for the debugger. Then the Flutter +# sources are deleted and the Gradle build must drop everything they produced. +# +# Needs the reactor installed (mvn install) with codenameone-flutter-runtime, +# which builds only on JDK 17+, and a JDK 17+; see inc/gradle.sh. +SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" +set -e +source "$SCRIPTPATH/inc/env.sh" +source "$SCRIPTPATH/inc/gradle.sh" + +WORKDIR="$SCRIPTPATH/build/gradle-flutter" +rm -rf "$WORKDIR" +mkdir -p "$WORKDIR" +cd "$WORKDIR" + +# The transpiler refuses to run below JDK 17, and so does the Maven build of it. +mvn17() { + JAVA_HOME="$GRADLE_JDK" mvn_local "$@" +} + +PKG=com.acme.flutterapp +mvn17 archetype:generate -DarchetypeArtifactId=cn1app-archetype -DarchetypeGroupId=com.codenameone \ + -DarchetypeVersion="$CN1_VERSION" -DartifactId=flutterapp -DgroupId=$PKG -Dpackage=$PKG \ + -Dversion=1.0-SNAPSHOT -DmainName=FlutterApp -DjavaVersion=17 -DinteractiveMode=false > "$WORKDIR/archetype.log" 2>&1 \ + || { tail -40 "$WORKDIR/archetype.log"; fail "archetype:generate"; } +APP="$WORKDIR/flutterapp" + +# The runtime dependency the Maven goal asks for; the Gradle plugin adds its own. +python3 - "$APP/common/pom.xml" <<'EOF' +import sys +path = sys.argv[1] +text = open(path).read() +dep = """ + + com.codenameone + codenameone-flutter-runtime + ${cn1.version} + """ +assert "" in text +open(path, "w").write(text.replace(" ", dep, 1)) +EOF +FL="$APP/common/src/main/flutter" +mkdir -p "$FL/assets/img" +cp "$SCRIPTPATH/../dart-transpiler/src/test/resources/golden/counter/main.dart" "$FL/main.dart" +printf 'not really a png' > "$FL/assets/img/logo_1.png" + +# process-classes: remap-compat, which maps the classes to the Dart source, runs there. +mvn17 -f "$APP/pom.xml" -pl common -am process-classes > "$WORKDIR/maven.log" 2>&1 \ + || { tail -60 "$WORKDIR/maven.log"; fail "the Maven build of the Flutter app"; } +MCLASSES="$APP/common/target/classes" +FPKG=com/codename1/generated/flutter +[ -f "$MCLASSES/$FPKG/MyApp.class" ] || fail "Maven transpiled no Flutter classes" + +mvn17 "com.codenameone:codenameone-maven-plugin:$CN1_VERSION:convert-to-gradle" \ + -Dcn1.sourceProject="$APP" -Dcn1.outputDir="$WORKDIR/gradle" > "$WORKDIR/convert.log" 2>&1 \ + || { cat "$WORKDIR/convert.log"; fail "converting the Flutter app"; } +G="$WORKDIR/gradle" +use_local_plugin "$G" +[ -f "$G/src/main/flutter/main.dart" ] || fail "the conversion did not carry src/main/flutter" +grep -q 'codenameone-flutter-runtime' "$G/build.gradle.kts" \ + && fail "the conversion declared the runtime the plugin adds itself" + +run_gradle "$G" classes > "$WORKDIR/gradle.log" 2>&1 || { tail -60 "$WORKDIR/gradle.log"; fail "the Gradle build of the Flutter app"; } +GCLASSES="$G/build/classes/java/main" + +# Same transpiled classes both ways. +(cd "$MCLASSES" && find $FPKG -name '*.class' | sort) > "$WORKDIR/maven-classes.txt" +(cd "$GCLASSES" && find $FPKG -name '*.class' | sort) > "$WORKDIR/gradle-classes.txt" +diff "$WORKDIR/maven-classes.txt" "$WORKDIR/gradle-classes.txt" \ + || fail "Maven and Gradle transpiled different Flutter classes" + +# Same flattened assets, and the manifest the runtime reads. +ASSET=cn1f_assets_simg_slogo__1.png +[ -f "$MCLASSES/$ASSET" ] || fail "Maven did not flatten the asset" +[ -f "$G/build/cn1-flutter/resources/$ASSET" ] || fail "Gradle did not flatten the asset" +[ -f "$G/build/cn1-flutter/resources/cn1f__AssetManifest.txt" ] || fail "Gradle wrote no asset manifest" + +# Both builds point the classes at the Dart source. +for c in "$MCLASSES" "$GCLASSES"; do + OUT=$("$GRADLE_JDK/bin/javap" -v -cp "$c" com.codename1.generated.flutter._MyHomePageState) + echo "$OUT" | grep -q 'SourceFile: "main.dart"' || fail "$c: the class does not name main.dart" + echo "$OUT" | grep -q 'SourceDebugExtension' || fail "$c: the class carries no source map" + echo "$OUT" | grep -q 'cn1DartLines' && fail "$c: the line table was left in the class" +done + +# The jar holds the assets: they are part of the main output. +run_gradle "$G" jar > "$WORKDIR/jar.log" 2>&1 || { tail -40 "$WORKDIR/jar.log"; fail "gradle jar"; } +assert_zip_has "$(ls "$G"/build/libs/*.jar | head -1)" "^$ASSET\$" + +# Removing the Flutter sources removes what they produced. +rm -r "$G/src/main/flutter" +run_gradle "$G" classes > "$WORKDIR/gradle-removed.log" 2>&1 \ + || { tail -60 "$WORKDIR/gradle-removed.log"; fail "the Gradle build after removing the Flutter sources"; } +[ -f "$GCLASSES/$FPKG/MyApp.class" ] && fail "the transpiled classes outlived their Dart sources" +[ -f "$G/build/cn1-flutter/resources/$ASSET" ] && fail "the flattened asset outlived its source" + +echo "PASS: Flutter sources build the same with Gradle as with Maven" From 2cc2a118d02fd7da2c8091f97b0a81bf0c9b4d2d Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:36:21 +0200 Subject: [PATCH 2/8] Flutter interop: dart:developer, RandomAccessFile, import report, 17 plugin packs, Firebase federated sign-in Library-scoped stubs (dartlib_*.dart) so dart:developer's log does not shadow dart:math's; RandomAccessFile; the importer skips web-only branches, transpiles desktop-only plugins and writes a JSON report with the transitive cause of every unsupported package. Plugin packs for timezone, app_settings, install referrer, geocoding, file_picker, audioplayers, mixpanel, web auth, image compress, image_picker platform, webview, video player/thumbnail/compress, camera, emoji picker and svg. firebase_auth exchanges Google/Apple/OAuth credentials through signInWithIdp; google_sign_in over GoogleConnect. Co-Authored-By: Claude Opus 5.5 --- .../src/com/codename1/social/AppleSignIn.java | 1 + .../codename1/social/AppleSignInResult.java | 10 + .../maven/FlutterProjectImporter.java | 213 +++++- .../maven/FlutterProjectImporterTest.java | 105 +++ .../maven/ImportFlutterProjectMojo.java | 10 + .../main/java/dart/developer/Developer.java | 88 +++ .../main/java/dart/developer/Timeline.java | 49 ++ .../java/dart/developer/TimelineTask.java | 49 ++ .../src/main/java/dart/io/File.java | 15 + .../src/main/java/dart/io/FileLock.java | 29 + .../src/main/java/dart/io/FileMode.java | 23 +- .../main/java/dart/io/RandomAccessFile.java | 453 +++++++++++++ .../main/java/dart/typed_data/Uint8List.java | 13 + .../dart/transpiler/analyze/StubRegistry.java | 31 +- .../codename1/dart/transpiler/ast/Ast.java | 2 + .../dart/transpiler/codegen/JavaEmitter.java | 54 +- .../dart/transpiler/parser/AstBuilder.java | 8 +- .../dart_developer_library/expect.txt | 7 + .../behavior/dart_developer_library/main.dart | 19 + .../behavior/lib_io_random_access/expect.txt | 10 + .../behavior/lib_io_random_access/main.dart | 53 ++ .../plugin_firebase_credentials/expect.txt | 13 + .../plugin_firebase_credentials/main.dart | 46 ++ .../behavior/plugin_media_packs/expect.txt | 24 + .../behavior/plugin_media_packs/main.dart | 95 +++ .../behavior/plugin_util_packs/expect.txt | 20 + .../behavior/plugin_util_packs/main.dart | 103 +++ .../plugins/appsettings/AppSettings.java | 124 ++++ .../appsettings/AppSettingsPanelType.java | 29 + .../plugins/appsettings/AppSettingsType.java | 32 + .../plugins/audioplayers/AssetSource.java | 69 ++ .../plugins/audioplayers/AudioPlayer.java | 323 +++++++++ .../plugins/audioplayers/BytesSource.java | 56 ++ .../audioplayers/DeviceFileSource.java | 52 ++ .../plugins/audioplayers/PlayerMode.java | 29 + .../plugins/audioplayers/PlayerState.java | 29 + .../plugins/audioplayers/ReleaseMode.java | 29 + .../flutter/plugins/audioplayers/Source.java | 71 ++ .../plugins/audioplayers/UrlSource.java | 52 ++ .../plugins/camera/CameraController.java | 250 +++++++ .../plugins/camera/CameraDescription.java | 67 ++ .../plugins/camera/CameraException.java | 49 ++ .../plugins/camera/CameraLensDirection.java | 29 + .../flutter/plugins/camera/CameraPreview.java | 50 ++ .../camera/CameraPreviewRenderElement.java | 54 ++ .../flutter/plugins/camera/CameraValue.java | 88 +++ .../flutter/plugins/camera/Cameras.java | 56 ++ .../flutter/plugins/camera/FlashMode.java | 29 + .../plugins/camera/ImageFormatGroup.java | 29 + .../plugins/camera/ResolutionPreset.java | 29 + .../flutter/plugins/emojipicker/Category.java | 29 + .../plugins/emojipicker/CategoryEmoji.java | 45 ++ .../flutter/plugins/emojipicker/Config.java | 74 ++ .../flutter/plugins/emojipicker/Emoji.java | 57 ++ .../plugins/emojipicker/EmojiData.java | 637 ++++++++++++++++++ .../plugins/emojipicker/EmojiGrid.java | 83 +++ .../plugins/emojipicker/EmojiPicker.java | 109 +++ .../emojipicker/EmojiPickerRenderElement.java | 53 ++ .../plugins/emojipicker/EmojiPickerUtils.java | 64 ++ .../plugins/filepicker/FilePicker.java | 222 ++++++ .../plugins/filepicker/FilePickerResult.java | 63 ++ .../flutter/plugins/filepicker/FileType.java | 29 + .../plugins/filepicker/PlatformFile.java | 101 +++ .../plugins/firebase/AuthCredential.java | 64 ++ .../plugins/firebase/EmailAuthProvider.java | 40 ++ .../plugins/firebase/FederatedSignIn.java | 38 ++ .../plugins/firebase/FirebaseAuth.java | 62 +- .../plugins/firebase/OAuthCredential.java | 85 +++ .../plugins/firebase/OAuthProvider.java | 71 ++ .../firebaseapple/AppleAuthProvider.java | 110 +++ .../firebasegoogle/GoogleAuthProvider.java | 124 ++++ .../flutter/plugins/geocoding/Geocoding.java | 228 +++++++ .../flutter/plugins/geocoding/Location.java | 71 ++ .../geocoding/NoResultFoundException.java | 41 ++ .../flutter/plugins/geocoding/Placemark.java | 156 +++++ .../plugins/googlesignin/GoogleSignIn.java | 195 ++++++ .../googlesignin/GoogleSignInAccount.java | 106 +++ .../GoogleSignInAuthentication.java | 47 ++ .../plugins/imagecompress/CompressFormat.java | 29 + .../imagecompress/FlutterImageCompress.java | 203 ++++++ .../imagepicker/ImagePickerAndroid.java | 51 ++ .../imagepicker/ImagePickerPlatform.java | 51 ++ .../AndroidPlayInstallReferrer.java | 42 ++ .../installreferrer/ReferrerDetails.java | 75 +++ .../flutter/plugins/mixpanel/Json.java | 116 ++++ .../flutter/plugins/mixpanel/Mixpanel.java | 321 +++++++++ .../flutter/plugins/mixpanel/People.java | 104 +++ .../flutter/plugins/svg/ColorFilter.java | 42 ++ .../flutter/plugins/svg/SvgPicture.java | 171 +++++ .../flutter/plugins/svg/SvgRenderElement.java | 64 ++ .../plugins/timezone/FlutterTimezone.java | 69 ++ .../plugins/videocompress/MediaInfo.java | 78 +++ .../videocompress/ObservableBuilder.java | 56 ++ .../plugins/videocompress/Subscription.java | 39 ++ .../plugins/videocompress/VideoCompress.java | 118 ++++ .../plugins/videocompress/VideoQuality.java | 29 + .../plugins/videoplayer/VideoPlayer.java | 50 ++ .../videoplayer/VideoPlayerController.java | 305 +++++++++ .../videoplayer/VideoPlayerOptions.java | 36 + .../videoplayer/VideoPlayerRenderElement.java | 54 ++ .../plugins/videoplayer/VideoPlayerValue.java | 126 ++++ .../videoplayer/VideoProgressColors.java | 44 ++ .../videoplayer/VideoProgressIndicator.java | 63 ++ .../VideoProgressRenderElement.java | 109 +++ .../plugins/videothumbnail/ImageFormat.java | 29 + .../videothumbnail/VideoThumbnail.java | 53 ++ .../plugins/webauth/FlutterWebAuth2.java | 137 ++++ .../webauth/FlutterWebAuth2Options.java | 98 +++ .../plugins/webview/JavaScriptMessage.java | 44 ++ .../plugins/webview/JavaScriptMode.java | 29 + .../plugins/webview/LoadRequestMethod.java | 29 + .../plugins/webview/NavigationDecision.java | 29 + .../plugins/webview/NavigationDelegate.java | 69 ++ .../plugins/webview/NavigationRequest.java | 46 ++ .../plugins/webview/WebResourceError.java | 53 ++ .../plugins/webview/WebViewController.java | 417 ++++++++++++ .../plugins/webview/WebViewRenderElement.java | 50 ++ .../plugins/webview/WebViewWidget.java | 48 ++ .../main/resources/META-INF/dart/dart_io.dart | 40 ++ .../META-INF/dart/dartlib_developer.dart | 40 ++ .../plugin_android_play_install_referrer.dart | 18 + .../META-INF/dart/plugin_app_settings.dart | 17 + .../META-INF/dart/plugin_audioplayers.dart | 75 +++ .../META-INF/dart/plugin_camera.dart | 70 ++ .../dart/plugin_emoji_picker_flutter.dart | 44 ++ .../META-INF/dart/plugin_file_picker.dart | 42 ++ .../META-INF/dart/plugin_firebase.dart | 62 +- .../dart/plugin_flutter_image_compress.dart | 20 + .../META-INF/dart/plugin_flutter_svg.dart | 33 + .../dart/plugin_flutter_timezone.dart | 7 + .../dart/plugin_flutter_web_auth_2.dart | 19 + .../META-INF/dart/plugin_geocoding.dart | 46 ++ .../META-INF/dart/plugin_google_sign_in.dart | 37 + .../dart/plugin_image_picker_platform.dart | 19 + .../dart/plugin_mixpanel_flutter.dart | 45 ++ .../META-INF/dart/plugin_video_compress.dart | 43 ++ .../META-INF/dart/plugin_video_player.dart | 66 ++ .../META-INF/dart/plugin_video_thumbnail.dart | 13 + .../META-INF/dart/plugin_webview_flutter.dart | 74 ++ 139 files changed, 10544 insertions(+), 34 deletions(-) create mode 100644 maven/dart-runtime/src/main/java/dart/developer/Developer.java create mode 100644 maven/dart-runtime/src/main/java/dart/developer/Timeline.java create mode 100644 maven/dart-runtime/src/main/java/dart/developer/TimelineTask.java create mode 100644 maven/dart-runtime/src/main/java/dart/io/FileLock.java create mode 100644 maven/dart-runtime/src/main/java/dart/io/RandomAccessFile.java create mode 100644 maven/dart-transpiler/src/test/resources/behavior/dart_developer_library/expect.txt create mode 100644 maven/dart-transpiler/src/test/resources/behavior/dart_developer_library/main.dart create mode 100644 maven/dart-transpiler/src/test/resources/behavior/lib_io_random_access/expect.txt create mode 100644 maven/dart-transpiler/src/test/resources/behavior/lib_io_random_access/main.dart create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_credentials/expect.txt create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_credentials/main.dart create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_media_packs/expect.txt create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_media_packs/main.dart create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_util_packs/expect.txt create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_util_packs/main.dart create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/appsettings/AppSettings.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/appsettings/AppSettingsPanelType.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/appsettings/AppSettingsType.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/AssetSource.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/AudioPlayer.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/BytesSource.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/DeviceFileSource.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/PlayerMode.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/PlayerState.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/ReleaseMode.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/Source.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/UrlSource.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraController.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraDescription.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraException.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraLensDirection.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraPreview.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraPreviewRenderElement.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraValue.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/Cameras.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/FlashMode.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/ImageFormatGroup.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/ResolutionPreset.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/Category.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/CategoryEmoji.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/Config.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/Emoji.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiData.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiGrid.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiPicker.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiPickerRenderElement.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiPickerUtils.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/FilePicker.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/FilePickerResult.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/FileType.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/PlatformFile.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/AuthCredential.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/EmailAuthProvider.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FederatedSignIn.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/OAuthCredential.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/OAuthProvider.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseapple/AppleAuthProvider.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebasegoogle/GoogleAuthProvider.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/Geocoding.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/Location.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/NoResultFoundException.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/Placemark.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlesignin/GoogleSignIn.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlesignin/GoogleSignInAccount.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlesignin/GoogleSignInAuthentication.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagecompress/CompressFormat.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagecompress/FlutterImageCompress.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagepicker/ImagePickerAndroid.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagepicker/ImagePickerPlatform.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/installreferrer/AndroidPlayInstallReferrer.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/installreferrer/ReferrerDetails.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/mixpanel/Json.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/mixpanel/Mixpanel.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/mixpanel/People.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/svg/ColorFilter.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/svg/SvgPicture.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/svg/SvgRenderElement.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/timezone/FlutterTimezone.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/MediaInfo.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/ObservableBuilder.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/Subscription.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/VideoCompress.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/VideoQuality.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayer.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerController.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerOptions.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerRenderElement.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerValue.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoProgressColors.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoProgressIndicator.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoProgressRenderElement.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videothumbnail/ImageFormat.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videothumbnail/VideoThumbnail.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webauth/FlutterWebAuth2.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webauth/FlutterWebAuth2Options.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/JavaScriptMessage.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/JavaScriptMode.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/LoadRequestMethod.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/NavigationDecision.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/NavigationDelegate.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/NavigationRequest.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebResourceError.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebViewController.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebViewRenderElement.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebViewWidget.java create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/dartlib_developer.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_android_play_install_referrer.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_app_settings.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_audioplayers.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_camera.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_emoji_picker_flutter.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_file_picker.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_image_compress.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_svg.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_timezone.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_web_auth_2.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_geocoding.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_google_sign_in.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_image_picker_platform.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_mixpanel_flutter.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_compress.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_player.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_thumbnail.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_webview_flutter.dart diff --git a/CodenameOne/src/com/codename1/social/AppleSignIn.java b/CodenameOne/src/com/codename1/social/AppleSignIn.java index d8c3b430eae..d572976244a 100644 --- a/CodenameOne/src/com/codename1/social/AppleSignIn.java +++ b/CodenameOne/src/com/codename1/social/AppleSignIn.java @@ -208,6 +208,7 @@ public void run() { return; } AppleSignInResult result = parsePackedResult(packed); + result.rawNonce = plainNonce; if (result.getIdentityToken() == null) { callback.onError("Apple Sign-In returned no identity token"); return; diff --git a/CodenameOne/src/com/codename1/social/AppleSignInResult.java b/CodenameOne/src/com/codename1/social/AppleSignInResult.java index 790c89d6151..c47498a5cc7 100644 --- a/CodenameOne/src/com/codename1/social/AppleSignInResult.java +++ b/CodenameOne/src/com/codename1/social/AppleSignInResult.java @@ -38,6 +38,7 @@ public final class AppleSignInResult { String userId; String email; String fullName; + String rawNonce; AppleSignInResult() {} @@ -74,4 +75,13 @@ public String getEmail() { public String getFullName() { return fullName; } + + /// The nonce this sign-in sent, before hashing: the native sheet sends Apple its + /// SHA-256, so the identity token's `nonce` claim is that hash, and a service that + /// checks the token against the request -- Firebase Authentication's + /// `signInWithIdp` -- needs this value to recompute it. `null` for the web flow, + /// whose request carries an unhashed nonce of its own. + public String getRawNonce() { + return rawNonce; + } } diff --git a/maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectImporter.java b/maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectImporter.java index 11a65b9b31b..1c156ef2fa6 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectImporter.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectImporter.java @@ -172,10 +172,89 @@ public static final class Result { /// the reason: a platform plugin, or a library that needs dart:ffi, dart:html /// or dart:mirrors. These are what an import of this app cannot provide. public final Map unsupportedPackages = new TreeMap(); + /// A plugin with no Android or iOS implementation, transpiled as Dart (see + /// [#isForeignPlatformPlugin]), with the platforms it does implement. + public final Map foreignPlugins = new TreeMap(); + /// For an unsupported package, the packages its verdict came through, ending with + /// the one that is unsupported itself: `[firebase_ui_oauth, flutter_svg, + /// vector_graphics_compiler]` when vector_graphics_compiler needs dart:ffi. + public final Map> unsupportedChains = new TreeMap>(); + /// Every package the import reached, with the app's own `lib/` files that reach + /// it, directly or through other packages. + public final Map> reachedFrom = new TreeMap>(); public String appName; public String appVersion; public int copiedFiles; public int copiedAssets; + + /// The report as JSON, for a tool or a CI step to read: every package with its + /// status (`covered`, `unsupported`), the note or reason, the chain an + /// unsupported verdict came through, and the app files that reach it. + public String toJson() { + StringBuilder b = new StringBuilder("{\n"); + b.append(" \"app\": ").append(jsonString(appName)).append(",\n"); + b.append(" \"version\": ").append(jsonString(appVersion)).append(",\n"); + b.append(" \"packages\": ["); + boolean first = true; + for (int pass = 0; pass < 2; pass++) { + for (String entry : pass == 0 ? covered : uncovered) { + int paren = entry.indexOf(" ("); + String name = paren < 0 ? entry : entry.substring(0, paren); + String note = paren < 0 ? null : entry.substring(paren + 2, entry.length() - 1); + b.append(first ? "\n" : ",\n"); + first = false; + b.append(" {\"package\": ").append(jsonString(name)); + b.append(", \"status\": \"").append(pass == 0 ? "covered" : "unsupported").append('"'); + if (note != null) { + b.append(", \"note\": ").append(jsonString(note)); + } + List chain = unsupportedChains.get(name); + if (pass == 1 && chain != null) { + b.append(", \"cause\": ").append(jsonString(unsupportedPackages.get(chain.get(chain.size() - 1)))); + b.append(", \"chain\": ").append(jsonArray(chain)); + } + Set from = reachedFrom.get(name); + if (from != null) { + b.append(", \"reachedFrom\": ").append(jsonArray(from)); + } + b.append('}'); + } + } + b.append("\n ],\n"); + b.append(" \"missingAssets\": ").append(jsonArray(missingAssets)).append(",\n"); + b.append(" \"unresolvedPackages\": ").append(jsonArray(unresolvedPackages)).append(",\n"); + b.append(" \"assetCollisions\": ").append(jsonArray(assetCollisions)).append("\n"); + return b.append("}\n").toString(); + } + + private static String jsonArray(java.util.Collection items) { + StringBuilder b = new StringBuilder("["); + for (String i : items) { + if (b.length() > 1) { + b.append(", "); + } + b.append(jsonString(i)); + } + return b.append(']').toString(); + } + + private static String jsonString(String v) { + if (v == null) { + return "null"; + } + StringBuilder b = new StringBuilder("\""); + for (int i = 0; i < v.length(); i++) { + char c = v.charAt(i); + if (c == '"' || c == '\\') { + b.append('\\').append(c); + } else if (c < 0x20) { + b.append(String.format("\\u%04x", (int) c)); + } else { + b.append(c); + } + } + return b.append('"').toString(); + } } private final Log log; @@ -389,6 +468,10 @@ public Result importProject(File source, File commonDir, String mainPackage, Str if (note == null) { note = assetOnlyCovered.get(dep); } + if (note == null && r.foreignPlugins.containsKey(dep) && r.transpiledPackages.containsKey(dep)) { + note = "a " + r.foreignPlugins.get(dep) + "-only plugin, transpiled as Dart; its platform " + + "calls throw MissingPluginException"; + } if (note == null && r.transpiledPackages.containsKey(dep)) { note = "transpiled from its sources, " + r.transpiledPackages.get(dep) + " file(s)"; } @@ -424,7 +507,7 @@ public Result importProject(File source, File commonDir, String mainPackage, Str /// test answers true for exactly these, and the walk below follows the same /// alternative it will pick. static final Set SUPPORTED_DART_LIBRARIES = new HashSet(java.util.Arrays.asList( - "core", "async", "collection", "convert", "io", "math", "typed_data", "ui")); + "core", "async", "collection", "convert", "developer", "io", "math", "typed_data", "ui")); private static final Pattern DIRECTIVE = Pattern.compile( "(?m)^\\s*(import|export|part)\\s+(?!of\\b)(['\"])([^'\"]+)\\2([^;]*);"); @@ -465,7 +548,8 @@ private int importPackageSources(File libDir, String ownName, Map Map libDirs = new HashMap(); Map counts = new TreeMap(); Map> importers = new HashMap>(); - Map pluginCache = new HashMap(); + // package -> the app's own lib/ files that import it directly + Map> ownImporters = new HashMap>(); int copied = 0; while (!work.isEmpty()) { String[] item = work.poll(); @@ -521,6 +605,14 @@ private int importPackageSources(File libDir, String ownName, Map } if (nextPkg != null) { lastReachedPackages.add(nextPkg); + if (pkg == null) { + Set files = ownImporters.get(nextPkg); + if (files == null) { + files = new TreeSet(); + ownImporters.put(nextPkg, files); + } + files.add(rel); + } if (pkg != null && !pkg.equals(nextPkg)) { Set by = importers.get(nextPkg); if (by == null) { @@ -545,12 +637,11 @@ private int importPackageSources(File libDir, String ownName, Map r.unsupportedPackages.put(nextPkg, "sources not found; run `flutter pub get`"); continue; } - Boolean plugin = pluginCache.get(nextPkg); - if (plugin == null) { - plugin = Boolean.valueOf(isPlatformPlugin(dir)); - pluginCache.put(nextPkg, plugin); - } - if (plugin.booleanValue()) { + Set platforms = pluginPlatforms(dir); + if (isForeignPlatformPlugin(platforms)) { + r.foreignPlugins.put(nextPkg, platforms.isEmpty() ? "no platform" + : String.join("/", platforms)); + } else if (platforms != null) { r.unsupportedPackages.put(nextPkg, "a platform plugin with no Codename One stub"); continue; } @@ -575,10 +666,43 @@ private int importPackageSources(File libDir, String ownName, Map for (String p : by) { if (!r.unsupportedPackages.containsKey(p)) { r.unsupportedPackages.put(p, "depends on " + dep + ", which is unsupported"); + List chain = new ArrayList(); + chain.add(p); + List rest = r.unsupportedChains.get(dep); + chain.addAll(rest == null ? java.util.Collections.singletonList(dep) : rest); + r.unsupportedChains.put(p, chain); verdicts.add(p); } } } + for (String u : r.unsupportedPackages.keySet()) { + if (!r.unsupportedChains.containsKey(u)) { + r.unsupportedChains.put(u, java.util.Collections.singletonList(u)); + } + } + // The app files behind each package: its own importers, and those of every + // package that imports it, transitively. + for (String reached : lastReachedPackages) { + Set files = new TreeSet(); + Set visited = new HashSet(); + java.util.ArrayDeque up = new java.util.ArrayDeque(); + up.add(reached); + while (!up.isEmpty()) { + String q = up.poll(); + if (!visited.add(q)) { + continue; + } + Set direct = ownImporters.get(q); + if (direct != null) { + files.addAll(direct); + } + Set by = importers.get(q); + if (by != null) { + up.addAll(by); + } + } + r.reachedFrom.put(reached, files); + } // A package found unsupported after some of its files were copied is not // transpiled: drop what was copied, so the build does not compile half of it. for (String bad : r.unsupportedPackages.keySet()) { @@ -622,28 +746,87 @@ static String selectUri(String defaultUri, String conditions) { /// Whether a package's pubspec declares `flutter: plugin:` -- a federated or /// classic platform plugin, whose Dart side calls into native code. static boolean isPlatformPlugin(File packageLibDir) { + return pluginPlatforms(packageLibDir) != null; + } + + /// The platforms a plugin's pubspec lists under `flutter: plugin: platforms:`, + /// null for a package that is not a plugin. A plugin in the format that predates + /// `platforms:` (`androidPackage`/`pluginClass` directly under `plugin:`) served + /// Android and iOS, and answers those two. + static Set pluginPlatforms(File packageLibDir) { File pubspec = new File(packageLibDir.getParentFile(), "pubspec.yaml"); if (!pubspec.isFile()) { - return false; + return null; } try { boolean inFlutter = false; + int pluginIndent = -1; + int platformsIndent = -1; + int keyIndent = -1; + Set platforms = null; for (String line : Files.readAllLines(pubspec.toPath(), Charset.forName("UTF-8"))) { - if (line.trim().isEmpty() || line.trim().startsWith("#")) { + String t = line.trim(); + if (t.isEmpty() || t.startsWith("#")) { continue; } - if (!line.startsWith(" ")) { + int indent = line.length() - line.replaceAll("^\\s+", "").length(); + if (indent == 0) { + if (pluginIndent >= 0) { + break; + } inFlutter = line.startsWith("flutter:"); continue; } - if (inFlutter && line.trim().startsWith("plugin:")) { - return true; + if (!inFlutter) { + continue; + } + if (pluginIndent < 0) { + if (t.startsWith("plugin:")) { + pluginIndent = indent; + } + continue; + } + if (indent <= pluginIndent) { + break; + } + if (platformsIndent < 0) { + if (t.startsWith("platforms:")) { + platformsIndent = indent; + platforms = new TreeSet(); + } + continue; + } + if (indent <= platformsIndent) { + break; + } + if (keyIndent < 0) { + keyIndent = indent; + } + int colon = t.indexOf(':'); + if (indent == keyIndent && colon > 0) { + platforms.add(t.substring(0, colon).trim()); } } + if (pluginIndent < 0) { + return null; + } + if (platforms == null) { + return new TreeSet(java.util.Arrays.asList("android", "ios")); + } + return platforms; } catch (IOException e) { - return false; + return null; } - return false; + } + + /// A plugin with no Android or iOS implementation: what it does natively happens only + /// on a desktop or the web, platforms Codename One does not build for. A mobile Flutter + /// app reaches one only behind a platform check (`firebase_ui_oauth` depends on + /// `desktop_webview_auth` for its desktop sign-in), so the plugin's Dart is + /// transpiled like any pure-Dart package and its method-channel calls throw + /// MissingPluginException, as they do on a platform the plugin does not implement. + static boolean isForeignPlatformPlugin(Set platforms) { + return platforms != null && !platforms.contains("android") && !platforms.contains("ios"); } /// Removes `//` and `/* */` comments, keeping string literals intact enough for diff --git a/maven/build-engine/src/test/java/com/codename1/maven/FlutterProjectImporterTest.java b/maven/build-engine/src/test/java/com/codename1/maven/FlutterProjectImporterTest.java index e254ac47223..c4a7e4ad886 100644 --- a/maven/build-engine/src/test/java/com/codename1/maven/FlutterProjectImporterTest.java +++ b/maven/build-engine/src/test/java/com/codename1/maven/FlutterProjectImporterTest.java @@ -716,6 +716,111 @@ public void entryPointForwardsPluginCallbacksOnlyWhenUsed() { assertTrue(all.contains("com.codename1.push.Push.subscribeToTopic(topic);")); } + /// package:http's shape: a conditional import whose io branch is taken, and a + /// `web` dependency that only the js_interop branch imports. Neither the browser + /// file nor package:web is reached, so http transpiles and web is never blamed. + @Test + public void aWebOnlyBranchAndItsDependencyAreNotFollowed() throws Exception { + File root = Files.createTempDirectory("flutter-conditional").toFile(); + File app = new File(root, "demo"); + write(new File(app, "pubspec.yaml"), "name: demo\nversion: 1.0.0\n" + + "dependencies:\n flutter:\n sdk: flutter\n http: 1.0.0\n"); + write(new File(app, "lib/main.dart"), "import 'package:http/http.dart';\nvoid main() {}\n"); + File http = new File(root, "cache/http-1.0.0"); + write(new File(http, "pubspec.yaml"), "name: http\ndependencies:\n web: 1.0.0\n"); + write(new File(http, "lib/http.dart"), "import 'src/client_stub.dart'\n" + + " if (dart.library.js_interop) 'src/browser_client.dart'\n" + + " if (dart.library.io) 'src/io_client.dart';\nint h() => 1;\n"); + write(new File(http, "lib/src/io_client.dart"), "import 'dart:io';\nint io() => 1;\n"); + write(new File(http, "lib/src/browser_client.dart"), + "import 'package:web/web.dart';\nimport 'dart:js_interop';\nint b() => 1;\n"); + write(new File(http, "lib/src/client_stub.dart"), "int s() => 1;\n"); + File web = new File(root, "cache/web-1.0.0"); + write(new File(web, "lib/web.dart"), "import 'dart:js_interop';\n"); + packageConfig(app, "http", http, "web", web); + + File common = new File(root, "common"); + FlutterProjectImporter.Result r = new FlutterProjectImporter(new com.codename1.build.SystemStreamLog()) + .importProject(app, common, null, null); + + assertTrue(r.unsupportedPackages.toString(), r.unsupportedPackages.isEmpty()); + assertEquals(Integer.valueOf(2), r.transpiledPackages.get("http")); + assertTrue(new File(common, "src/main/flutter/packages/http/src/io_client.dart").exists()); + assertFalse(new File(common, "src/main/flutter/packages/http/src/browser_client.dart").exists()); + assertFalse(r.reachedFrom.toString(), r.reachedFrom.containsKey("web")); + } + + /// A plugin with only desktop platforms is transpiled as Dart, not reported as an + /// unsupported plugin that takes every package above it down with it. + @Test + public void aDesktopOnlyPluginIsTranspiledNotBlamed() throws Exception { + File root = Files.createTempDirectory("flutter-desktop-plugin").toFile(); + File app = new File(root, "demo"); + write(new File(app, "pubspec.yaml"), "name: demo\nversion: 1.0.0\n" + + "dependencies:\n flutter:\n sdk: flutter\n ui_oauth: 1.0.0\n"); + write(new File(app, "lib/main.dart"), "import 'package:ui_oauth/ui_oauth.dart';\nvoid main() {}\n"); + File ui = new File(root, "cache/ui_oauth-1.0.0"); + write(new File(ui, "lib/ui_oauth.dart"), "import 'package:desk_auth/desk_auth.dart';\nint u() => 1;\n"); + File desk = new File(root, "cache/desk_auth-1.0.0"); + write(new File(desk, "pubspec.yaml"), "name: desk_auth\nflutter:\n plugin:\n platforms:\n" + + " macos:\n pluginClass: DeskAuthPlugin\n windows:\n" + + " pluginClass: DeskAuthPlugin\n"); + write(new File(desk, "lib/desk_auth.dart"), "import 'dart:developer';\nint d() => 1;\n"); + File mobile = new File(root, "cache/mobile-1.0.0"); + write(new File(mobile, "pubspec.yaml"), "name: mobile\nflutter:\n plugin:\n platforms:\n" + + " android:\n package: a.b\n pluginClass: P\n web:\n" + + " pluginClass: W\n"); + packageConfig(app, "ui_oauth", ui, "desk_auth", desk); + + File common = new File(root, "common"); + FlutterProjectImporter.Result r = new FlutterProjectImporter(new com.codename1.build.SystemStreamLog()) + .importProject(app, common, null, null); + + assertTrue(r.unsupportedPackages.toString(), r.unsupportedPackages.isEmpty()); + assertEquals("macos/windows", r.foreignPlugins.get("desk_auth")); + assertTrue(new File(common, "src/main/flutter/packages/desk_auth/desk_auth.dart").exists()); + assertEquals("[android, web]", String.valueOf(FlutterProjectImporter.pluginPlatforms(new File(mobile, "lib")))); + assertFalse(FlutterProjectImporter.isForeignPlatformPlugin( + FlutterProjectImporter.pluginPlatforms(new File(mobile, "lib")))); + } + + /// The JSON report names the chain behind a verdict and the app file behind it. + @Test + public void theReportCarriesTheTransitiveCause() throws Exception { + File root = Files.createTempDirectory("flutter-report").toFile(); + File app = new File(root, "demo"); + write(new File(app, "pubspec.yaml"), "name: demo\nversion: 1.0.0\n" + + "dependencies:\n flutter:\n sdk: flutter\n outer: 1.0.0\n"); + write(new File(app, "lib/screens/login.dart"), "import 'package:outer/outer.dart';\nint l() => 1;\n"); + write(new File(app, "lib/main.dart"), "void main() {}\n"); + File outer = new File(root, "cache/outer-1.0.0"); + write(new File(outer, "lib/outer.dart"), "import 'package:inner/inner.dart';\nint o() => 1;\n"); + File inner = new File(root, "cache/inner-1.0.0"); + write(new File(inner, "lib/inner.dart"), "import 'dart:ffi';\nint i() => 1;\n"); + packageConfig(app, "outer", outer, "inner", inner); + + FlutterProjectImporter.Result r = new FlutterProjectImporter(new com.codename1.build.SystemStreamLog()) + .importProject(app, new File(root, "common"), null, null); + + assertEquals("[outer, inner]", r.unsupportedChains.get("outer").toString()); + assertEquals("[screens/login.dart]", r.reachedFrom.get("inner").toString()); + String json = r.toJson(); + assertTrue(json, json.contains("{\"package\": \"outer\", \"status\": \"unsupported\", " + + "\"note\": \"depends on inner, which is unsupported\", " + + "\"cause\": \"needs dart:ffi, which has no Codename One counterpart (inner.dart)\", " + + "\"chain\": [\"outer\", \"inner\"], \"reachedFrom\": [\"screens/login.dart\"]}")); + } + + private static void packageConfig(File app, String n1, File d1, String n2, File d2) throws Exception { + write(new File(app, ".dart_tool/package_config.json"), "{\n \"configVersion\": 2,\n" + + " \"packages\": [\n" + + " {\n \"name\": \"" + n1 + "\",\n \"rootUri\": \"" + d1.toURI() + + "\",\n \"packageUri\": \"lib/\"\n },\n" + + " {\n \"name\": \"" + n2 + "\",\n \"rootUri\": \"" + d2.toURI() + + "\",\n \"packageUri\": \"lib/\"\n }\n" + + " ]\n}\n"); + } + @Test public void pluginPacksAreCovered() { for (String p : new String[] {"share_plus", "geolocator", "image_picker", "firebase_auth", diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/ImportFlutterProjectMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/ImportFlutterProjectMojo.java index a872b1623cb..dc4a408fefe 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/ImportFlutterProjectMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/ImportFlutterProjectMojo.java @@ -90,6 +90,16 @@ protected void executeImpl() throws MojoExecutionException, MojoFailureException getLog().warn(" asset not imported, its path is taken by a lib/ source file: " + a + " (rename one of them)"); } + // The same report, machine-readable: each package's status, why an unsupported + // one is (through which dependencies), and the app files that reach it. + File report = new File(common, "target/flutter-import-report.json"); + try { + report.getParentFile().mkdirs(); + java.nio.file.Files.write(report.toPath(), r.toJson().getBytes("UTF-8")); + getLog().info("Import report: " + report); + } catch (java.io.IOException e) { + getLog().warn("Could not write " + report + ": " + e.getMessage()); + } if (!r.unresolvedPackages.isEmpty()) { // Each one was already warned individually, with the remedy, as it was hit. getLog().warn(" " + r.unresolvedPackages.size() + " pub package(s) could not be resolved: " diff --git a/maven/dart-runtime/src/main/java/dart/developer/Developer.java b/maven/dart-runtime/src/main/java/dart/developer/Developer.java new file mode 100644 index 00000000000..c81fffa4286 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/developer/Developer.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.developer; + +import com.codename1.io.Log; +import com.codename1.ui.Display; + +import dart.runtime.DartRuntime; + +/** + * dart:developer's top-level functions. {@code log} goes to Codename One's + * {@link Log}, where a device log, the simulator console and a crash report all read + * it -- the closest thing to the DevTools logging view a running app has (stderr before + * Codename One starts). The debugger + * and service-protocol calls have no VM service to talk to and do nothing. + */ +public final class Developer { + /// `Level.SEVERE` in package:logging, the level dart:developer's own docs use. + private static final long SEVERE = 1000; + + private Developer() { + } + + /** + * {@code log(message, {time, sequenceNumber, level, name, zone, error, stackTrace})}. + * Written as {@code [name] message}, with the error and stack trace after it. + */ + public static void log(String message, Object time, Long sequenceNumber, long level, String name, + Object zone, Object error, Object stackTrace) { + StringBuilder sb = new StringBuilder(); + if (name != null && name.length() > 0) { + sb.append('[').append(name).append("] "); + } + sb.append(message); + if (error != null) { + sb.append('\n').append(DartRuntime.str(error)); + } + if (stackTrace != null) { + sb.append('\n').append(DartRuntime.str(stackTrace)); + } + if (Display.isInitialized()) { + Log.p(sb.toString(), level >= SEVERE ? Log.ERROR : Log.INFO); + } else { + // Log writes through the implementation, which does not exist yet; stderr, + // not stdout, which is where print goes. + System.err.println(sb); + } + } + + /** {@code debugger({when, message})}: there is no attached Dart debugger to stop in. */ + public static boolean debugger(boolean when, String message) { + return when; + } + + /** {@code inspect(object)}: answers its argument, as Dart does without a service. */ + public static Object inspect(Object object) { + return object; + } + + /** {@code postEvent(eventKind, eventData, {stream})}: no service listens. */ + public static void postEvent(String eventKind, Object eventData, String stream) { + } + + /** {@code registerExtension(method, handler)}: no service calls it. */ + public static void registerExtension(String method, Object handler) { + } +} diff --git a/maven/dart-runtime/src/main/java/dart/developer/Timeline.java b/maven/dart-runtime/src/main/java/dart/developer/Timeline.java new file mode 100644 index 00000000000..77d13d3f0c1 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/developer/Timeline.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.developer; + +import dart.runtime.Funcs; + +/** + * dart:developer's {@code Timeline}. There is no timeline recorder to write to, so the + * events are dropped; {@code timeSync} still runs its function and answers its result, + * which is the part a program depends on. + */ +public final class Timeline { + private Timeline() { + } + + public static void startSync(String name, Object arguments, Object flow) { + } + + public static void finishSync() { + } + + public static void instantSync(String name, Object arguments) { + } + + public static T timeSync(String name, Funcs.Func0 function, Object arguments, Object flow) { + return function.call(); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/developer/TimelineTask.java b/maven/dart-runtime/src/main/java/dart/developer/TimelineTask.java new file mode 100644 index 00000000000..d2650d2ff28 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/developer/TimelineTask.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.developer; + +/** dart:developer's {@code TimelineTask}; the events are dropped, see {@link Timeline}. */ +public class TimelineTask { + public TimelineTask() { + } + + public void parent(TimelineTask v) { + } + + public void filterKey(String v) { + } + + public void start(String name, Object arguments) { + } + + public void instant(String name, Object arguments) { + } + + public void finish(Object arguments) { + } + + public TimelineTask pass() { + return this; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/io/File.java b/maven/dart-runtime/src/main/java/dart/io/File.java index 8b41e01d363..ed23a2332e2 100644 --- a/maven/dart-runtime/src/main/java/dart/io/File.java +++ b/maven/dart-runtime/src/main/java/dart/io/File.java @@ -139,6 +139,21 @@ private static boolean appends(Object mode) { return mode instanceof FileMode && ((FileMode) mode).append_; } + // ----------------------------------------------------------- random access + + /** Dart's {@code openSync({mode})}; see {@link RandomAccessFile}. */ + public RandomAccessFile openSync(Object mode) { + return new RandomAccessFile(path, mode instanceof FileMode ? (FileMode) mode : FileMode.read); + } + + public Future open(Object mode) { + try { + return Future.value(openSync(mode)); + } catch (RuntimeException e) { + return Future.error(e); + } + } + public void writeAsBytesSync(Object bytes) { writeAsBytesSync(bytes, null, null); } diff --git a/maven/dart-runtime/src/main/java/dart/io/FileLock.java b/maven/dart-runtime/src/main/java/dart/io/FileLock.java new file mode 100644 index 00000000000..91910048c23 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/io/FileLock.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.io; + +/** Dart's {@code FileLock}; see {@link RandomAccessFile} for why a lock excludes nothing. */ +public enum FileLock { + shared, exclusive, blockingShared, blockingExclusive +} diff --git a/maven/dart-runtime/src/main/java/dart/io/FileMode.java b/maven/dart-runtime/src/main/java/dart/io/FileMode.java index 7896634dc10..cda9b0b097b 100644 --- a/maven/dart-runtime/src/main/java/dart/io/FileMode.java +++ b/maven/dart-runtime/src/main/java/dart/io/FileMode.java @@ -23,17 +23,26 @@ */ package dart.io; -/** Dart's {@code FileMode}; only whether a write appends matters here. */ +/** + * Dart's {@code FileMode}: whether a write appends, and for {@link File#open} whether the + * file can be read and whether opening it empties it. + */ public final class FileMode { - public static final FileMode read = new FileMode(false); - public static final FileMode write = new FileMode(false); - public static final FileMode append = new FileMode(true); - public static final FileMode writeOnly = new FileMode(false); - public static final FileMode writeOnlyAppend = new FileMode(true); + public static final FileMode read = new FileMode(false, true, false, false); + public static final FileMode write = new FileMode(false, true, true, true); + public static final FileMode append = new FileMode(true, true, true, false); + public static final FileMode writeOnly = new FileMode(false, false, true, true); + public static final FileMode writeOnlyAppend = new FileMode(true, false, true, false); final boolean append_; + final boolean readable; + final boolean writable; + final boolean truncates; - private FileMode(boolean append) { + private FileMode(boolean append, boolean readable, boolean writable, boolean truncates) { this.append_ = append; + this.readable = readable; + this.writable = writable; + this.truncates = truncates; } } diff --git a/maven/dart-runtime/src/main/java/dart/io/RandomAccessFile.java b/maven/dart-runtime/src/main/java/dart/io/RandomAccessFile.java new file mode 100644 index 00000000000..71d08d6d3f4 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/io/RandomAccessFile.java @@ -0,0 +1,453 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.io; + +import java.util.List; + +import dart.async.Future; +import dart.convert.ByteCodec; +import dart.typed_data.Uint8List; + +/** + * Dart's {@code RandomAccessFile}, what {@link File#open} answers. + * + *

Codename One's file system reads and writes whole streams, not positions, so the + * file is held in memory while it is open: {@code open} reads it, every read and write + * works on that copy, and {@code flush} and {@code close} write it back. When everything + * changed since the last write-back lies past the end of what is stored -- the way an + * append-only log such as Hive's box file grows -- only the new tail is appended; + * otherwise the file is rewritten. Memory therefore grows with the file, which suits the + * small key-value stores that use this, not a multi-megabyte archive.

+ * + *

The asynchronous methods do their work before returning a completed future, like + * {@link File}'s. The file is not shared with another process, so {@code lock} and + * {@code unlock} have nothing to exclude and succeed.

+ */ +public class RandomAccessFile { + private final String path; + private final FileMode mode; + private byte[] data; + private int length; + private long position; + /** Bytes stored on disk; everything at or past {@link #dirtyFrom} differs from them. */ + private int stored; + private int dirtyFrom = Integer.MAX_VALUE; + private boolean closed; + + RandomAccessFile(String path, FileMode mode) { + this.path = path; + this.mode = mode == null ? FileMode.read : mode; + Fs fs = Fs.get(); + boolean exists = fs.exists(path) && !fs.isDirectory(path); + if (!exists && !this.mode.writable) { + throw new FileSystemException("Cannot open file, path = '" + path + "' (No such file or directory)", + path); + } + if (fs.isDirectory(path)) { + throw new FileSystemException("Cannot open file, path = '" + path + "' (Is a directory)", path); + } + if (!exists || this.mode.truncates) { + fs.write(path, new byte[0], false); + data = new byte[0]; + } else { + data = fs.read(path); + } + length = data.length; + stored = length; + position = this.mode.append_ ? length : 0; + } + + /** The file's path, as {@code File(path).open()} was given it. */ + public String path() { + return path; + } + + // ------------------------------------------------------------- position + + public long positionSync() { + checkOpen(); + return position; + } + + public Future position() { + return attempt(new Op() { + @Override + public Long run() { + return Long.valueOf(positionSync()); + } + }); + } + + public void setPositionSync(long p) { + checkOpen(); + if (p < 0) { + throw new FileSystemException("Cannot set position in file, path = '" + path + "' (Invalid argument)", + path); + } + position = p; + } + + public Future setPosition(final long p) { + return attempt(new Op() { + @Override + public RandomAccessFile run() { + setPositionSync(p); + return RandomAccessFile.this; + } + }); + } + + public long lengthSync() { + checkOpen(); + return length; + } + + public Future length() { + return attempt(new Op() { + @Override + public Long run() { + return Long.valueOf(lengthSync()); + } + }); + } + + // ----------------------------------------------------------------- read + + /** The next byte, or -1 at the end of the file. */ + public long readByteSync() { + checkReadable(); + if (position >= length) { + return -1; + } + return data[(int) position++] & 0xff; + } + + public Future readByte() { + return attempt(new Op() { + @Override + public Long run() { + return Long.valueOf(readByteSync()); + } + }); + } + + /** Up to {@code count} bytes from the position; fewer at the end of the file. */ + public Uint8List readSync(long count) { + checkReadable(); + if (count < 0) { + throw new dart.core.ArgumentError("count must not be negative: " + count); + } + int from = (int) Math.min(position, length); + int n = (int) Math.min(count, length - from); + byte[] out = new byte[n]; + System.arraycopy(data, from, out, 0, n); + position = from + n; + return Uint8List.fromBytes(out); + } + + public Future read(final long count) { + return attempt(new Op() { + @Override + public Uint8List run() { + return readSync(count); + } + }); + } + + /** + * Reads into {@code buffer[start, end)} and answers how many bytes it read, fewer at + * the end of the file. + */ + public long readIntoSync(Object buffer, long start, Long end) { + checkReadable(); + int size = sizeOf(buffer); + int to = end == null ? size : (int) end.longValue(); + checkRange(start, to, size); + int from = (int) Math.min(position, length); + int n = (int) Math.min(to - start, length - from); + for (int i = 0; i < n; i++) { + store(buffer, (int) start + i, data[from + i] & 0xff); + } + position = from + n; + return n; + } + + public Future readInto(final Object buffer, final long start, final Long end) { + return attempt(new Op() { + @Override + public Long run() { + return Long.valueOf(readIntoSync(buffer, start, end)); + } + }); + } + + // ---------------------------------------------------------------- write + + public RandomAccessFile writeByteSync(long value) { + put(new byte[] {(byte) value}, 0, 1); + return this; + } + + public Future writeByte(final long value) { + return attempt(new Op() { + @Override + public RandomAccessFile run() { + return writeByteSync(value); + } + }); + } + + /** Writes {@code buffer[start, end)} at the position, growing the file as needed. */ + public RandomAccessFile writeFromSync(Object buffer, long start, Long end) { + byte[] bytes = ByteCodec.bytesOf(buffer); + int to = end == null ? bytes.length : (int) end.longValue(); + checkRange(start, to, bytes.length); + put(bytes, (int) start, to - (int) start); + return this; + } + + public Future writeFrom(final Object buffer, final long start, final Long end) { + return attempt(new Op() { + @Override + public RandomAccessFile run() { + return writeFromSync(buffer, start, end); + } + }); + } + + public RandomAccessFile writeStringSync(String string, Object encoding) { + ByteCodec codec = encoding instanceof ByteCodec ? (ByteCodec) encoding : dart.convert.Convert.utf8; + return writeFromSync(ByteCodec.bytesOf(codec.encode(string)), 0, null); + } + + public Future writeString(final String string, final Object encoding) { + return attempt(new Op() { + @Override + public RandomAccessFile run() { + return writeStringSync(string, encoding); + } + }); + } + + /** Cuts the file to {@code newLength} bytes, or pads it with zeros up to it. */ + public RandomAccessFile truncateSync(long newLength) { + checkWritable(); + if (newLength < 0) { + throw new FileSystemException("Cannot truncate file, path = '" + path + "' (Invalid argument)", path); + } + int n = (int) newLength; + ensure(n); + if (n > length) { + java.util.Arrays.fill(data, length, n, (byte) 0); + } + length = n; + // a shorter file is never an append; a longer one is, from the old end + dirtyFrom = Math.min(dirtyFrom, n < stored ? 0 : stored); + return this; + } + + public Future truncate(final long newLength) { + return attempt(new Op() { + @Override + public RandomAccessFile run() { + return truncateSync(newLength); + } + }); + } + + // -------------------------------------------------------------- persist + + /** Writes what changed back to the file system. */ + public RandomAccessFile flushSync() { + checkOpen(); + if (dirtyFrom == Integer.MAX_VALUE) { + return this; + } + byte[] copy; + if (dirtyFrom >= stored) { + copy = new byte[length - stored]; + System.arraycopy(data, stored, copy, 0, copy.length); + Fs.get().write(path, copy, true); + } else { + copy = new byte[length]; + System.arraycopy(data, 0, copy, 0, length); + Fs.get().write(path, copy, false); + } + stored = length; + dirtyFrom = Integer.MAX_VALUE; + return this; + } + + public Future flush() { + return attempt(new Op() { + @Override + public RandomAccessFile run() { + return flushSync(); + } + }); + } + + public void closeSync() { + if (closed) { + return; + } + flushSync(); + closed = true; + data = null; + } + + public Future close() { + return attempt(new Op() { + @Override + public Object run() { + closeSync(); + return null; + } + }); + } + + /** {@code lockSync([mode, start, end])}; see the class comment. */ + public RandomAccessFile lockSync(Object mode, long start, long end) { + checkOpen(); + return this; + } + + public Future lock(final Object mode, final long start, final long end) { + return attempt(new Op() { + @Override + public RandomAccessFile run() { + return lockSync(mode, start, end); + } + }); + } + + public RandomAccessFile unlockSync(long start, long end) { + checkOpen(); + return this; + } + + public Future unlock(final long start, final long end) { + return attempt(new Op() { + @Override + public RandomAccessFile run() { + return unlockSync(start, end); + } + }); + } + + @Override + public String toString() { + return "RandomAccessFile: '" + path + "'"; + } + + // -------------------------------------------------------------- helpers + + private void put(byte[] bytes, int offset, int count) { + checkWritable(); + if (mode.append_) { + // Dart: a write on a file opened to append always goes to the end + position = length; + } + int at = (int) position; + int end = at + count; + ensure(end); + // a write past the end leaves zeros between, which are as new as the write + dirtyFrom = Math.min(dirtyFrom, Math.min(at, length)); + if (at > length) { + java.util.Arrays.fill(data, length, at, (byte) 0); + } + System.arraycopy(bytes, offset, data, at, count); + if (end > length) { + length = end; + } + position = end; + } + + private void ensure(int size) { + if (size > data.length) { + byte[] grown = new byte[Math.max(size, data.length * 2)]; + System.arraycopy(data, 0, grown, 0, length); + data = grown; + } + } + + private void checkOpen() { + if (closed) { + throw new FileSystemException("File closed", path); + } + } + + private void checkReadable() { + checkOpen(); + if (!mode.readable) { + throw new FileSystemException("Cannot read from file, path = '" + path + "' (Bad file descriptor)", + path); + } + } + + private void checkWritable() { + checkOpen(); + if (!mode.writable) { + throw new FileSystemException("Cannot write to file, path = '" + path + "' (Bad file descriptor)", + path); + } + } + + private static void checkRange(long start, long end, int size) { + if (start < 0 || start > end || end > size) { + throw new dart.core.RangeError("Invalid range " + start + ".." + end + " of " + size); + } + } + + private static int sizeOf(Object buffer) { + if (buffer instanceof Uint8List) { + return (int) ((Uint8List) buffer).length(); + } + if (buffer instanceof List) { + return ((List) buffer).size(); + } + throw new dart.core.TypeError("Expected a List"); + } + + @SuppressWarnings("unchecked") + private static void store(Object buffer, int i, int value) { + if (buffer instanceof Uint8List) { + ((Uint8List) buffer).$indexSet(i, value); + } else { + ((List) buffer).set(i, Long.valueOf(value)); + } + } + + private interface Op { + T run(); + } + + private static Future attempt(Op op) { + try { + return Future.value(op.run()); + } catch (RuntimeException e) { + return Future.error(e); + } + } +} diff --git a/maven/dart-runtime/src/main/java/dart/typed_data/Uint8List.java b/maven/dart-runtime/src/main/java/dart/typed_data/Uint8List.java index 38ffc756cf1..106c1e0ac54 100644 --- a/maven/dart-runtime/src/main/java/dart/typed_data/Uint8List.java +++ b/maven/dart-runtime/src/main/java/dart/typed_data/Uint8List.java @@ -130,4 +130,17 @@ public boolean isNotEmpty() { public byte[] toBytes() { return bytes; } + + /// `[1, 2, 3]`, as Dart prints any list; it printed the Java object id. + @Override + public String toString() { + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < bytes.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(bytes[i] & 0xff); + } + return sb.append(']').toString(); + } } diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/StubRegistry.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/StubRegistry.java index cc22686fd1c..88eda6badef 100644 --- a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/StubRegistry.java +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/StubRegistry.java @@ -48,6 +48,15 @@ public final class StubRegistry { public final Map enums = new LinkedHashMap(); public final Map functions = new LinkedHashMap(); public final Map topLevelVars = new LinkedHashMap(); + /** + * Top-level functions of a {@code dart:} library whose names collide with another + * library's: {@code dart:developer}'s {@code log} is not {@code dart:math}'s. They are + * declared in a stub file named {@code dartlib_.dart}, kept out of + * {@link #functions}, and resolved only through an import of {@code dart:}. + * Its classes and enums are global like any other stub's. + */ + public final Map> libraryFunctions = + new LinkedHashMap>(); /** Stub extension declarations (`extension X on T { ... }`), keyed via their {@code on} type. */ public final List extensions = new ArrayList(); @@ -157,8 +166,18 @@ public void load(String name, String source, Diagnostics diags) { for (Ast.EnumDecl e : lib.enums) { enums.put(e.name, e); } + String scoped = scopedLibrary(name); for (Ast.FunctionDecl f : lib.functions) { - functions.put(f.name, f); + if (scoped == null) { + functions.put(f.name, f); + } else { + Map m = libraryFunctions.get(scoped); + if (m == null) { + m = new LinkedHashMap(); + libraryFunctions.put(scoped, m); + } + m.put(f.name, f); + } } for (Ast.FieldDecl v : lib.topLevelVars) { if (v.javaName != null) { @@ -167,6 +186,16 @@ public void load(String name, String source, Diagnostics diags) { } } + /** {@code developer} for {@code .../dartlib_developer.dart}, else null. */ + static String scopedLibrary(String stubName) { + String base = stubName.replace('\\', '/'); + base = base.substring(base.lastIndexOf('/') + 1); + if (base.startsWith("dartlib_") && base.endsWith(".dart")) { + return base.substring("dartlib_".length(), base.length() - ".dart".length()); + } + return null; + } + public boolean isStubClass(String dartName) { return classes.containsKey(dartName); } diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/ast/Ast.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/ast/Ast.java index de6a6f2b339..76a5faafa61 100644 --- a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/ast/Ast.java +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/ast/Ast.java @@ -160,6 +160,8 @@ public static class Directive extends Node { /** Names a `show` lists; null when there is no show combinator. */ public List show; public List hide = new ArrayList(); + /** The {@code as} name of an import, else null. */ + public String prefix; /** Whether a declaration named {@code name} passes this directive's combinators. */ public boolean admits(String name) { diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java index 178975ce1a1..ba4a6463ac3 100644 --- a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java @@ -8369,7 +8369,8 @@ private Out emitCall(Call c, TypeRef expected, Ctx ctx) { // is globally addressable, so dispatch it as a bare (unqualified) call. if (c.name != null && isImportPrefix(c.target, ctx) && (program.functions.containsKey(c.name) || program.classes.containsKey(c.name) - || stubs.isStubClass(c.name) || stubs.functions.containsKey(c.name))) { + || stubs.isStubClass(c.name) || stubs.functions.containsKey(c.name) + || prefixedLibraryFunction(((Ident) c.target).name, c.name, ctx))) { Call bare = new Call(); bare.file = c.file; bare.line = c.line; @@ -8641,8 +8642,13 @@ private Out emitBareCall(Call c, Ctx ctx) { // Object parameters: a primitive long/double/bool argument autoboxes. return new Out("DartRuntime.identical(" + a.code + ", " + b.code + ")", TypeRef.BOOL); } - // stub top-level function (e.g. runApp) - Ast.FunctionDecl sf = stubs.functions.get(n); + // stub top-level function (e.g. runApp); a dart: library's own, when its name + // collides with another's (dart:developer's log is not dart:math's), only + // through that library's import + Ast.FunctionDecl sf = libraryStubFunction(n, ctx); + if (sf == null) { + sf = stubs.functions.get(n); + } if (sf != null && sf.javaName != null) { int dot = sf.javaName.lastIndexOf('.'); String cls = sf.javaName.substring(0, dot); @@ -8677,6 +8683,48 @@ private Out emitBareCall(Call c, Ctx ctx) { return new Out("null", TypeRef.DYNAMIC, true); } + /** + * The {@code dartlib_.dart} stub function {@code n} this call reaches: through + * its prefix when the call is {@code prefix.n(...)} and the prefix imports + * {@code dart:}, else through an unprefixed import of {@code dart:} that + * admits {@code n}. Null when the call reaches no such function. + */ + private Ast.FunctionDecl libraryStubFunction(String n, Ctx ctx) { + if (stubs.libraryFunctions.isEmpty()) { + return null; + } + Ast.Library lib = ctx.library(); + if (lib == null) { + return null; + } + String prefix = ctx.callPrefix; + if (prefix != null) { + String uri = lib.prefixImports.get(prefix); + return uri != null && uri.startsWith("dart:") ? libraryFunction(uri.substring(5), n) : null; + } + for (Ast.Directive d : lib.importDirectives) { + if (d.uri == null || !d.uri.startsWith("dart:") || !d.admits(n) || d.prefix != null) { + continue; + } + Ast.FunctionDecl f = libraryFunction(d.uri.substring(5), n); + if (f != null) { + return f; + } + } + return null; + } + + private boolean prefixedLibraryFunction(String prefix, String n, Ctx ctx) { + Ast.Library lib = ctx.library(); + String uri = lib == null ? null : lib.prefixImports.get(prefix); + return uri != null && uri.startsWith("dart:") && libraryFunction(uri.substring(5), n) != null; + } + + private Ast.FunctionDecl libraryFunction(String library, String n) { + Map m = stubs.libraryFunctions.get(library); + return m == null ? null : m.get(n); + } + private Out emitMethodCallOn(Out target, Call c, Ctx ctx) { TypeRef tt = target.type; // A receiver typed with an import prefix (`intl.DateFormat`, `ui.Size`) keeps the diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/parser/AstBuilder.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/parser/AstBuilder.java index f9d9dcb1ee4..d2eb054dbed 100644 --- a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/parser/AstBuilder.java +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/parser/AstBuilder.java @@ -154,7 +154,9 @@ private void buildLibrary(Dart2Parser.LibraryDeclarationContext ctx, Library lib lib.importPrefixes.add(spec.identifier().getText()); lib.prefixImports.put(spec.identifier().getText(), stripQuotes(uri)); } - lib.importDirectives.add(directive(stripQuotes(uri), spec.combinator())); + Ast.Directive d = directive(stripQuotes(uri), spec.combinator()); + d.prefix = spec.identifier() == null ? null : spec.identifier().getText(); + lib.importDirectives.add(d); } else if (ie.libraryExport() != null) { Dart2Parser.LibraryExportContext ex = ie.libraryExport(); lib.exportDirectives.add(directive(stripQuotes(selectUri(ex.configurableUri())), ex.combinator())); @@ -171,13 +173,13 @@ private void buildLibrary(Dart2Parser.LibraryDeclarationContext ctx, Library lib /** * The {@code dart:} libraries this runtime implements, i.e. the {@code dart.library.X} - * conditions that are true here. {@code dart:io}, {@code dart:html}, {@code dart:ffi} + * conditions that are true here. {@code dart:html}, {@code dart:ffi} * and the JS interop libraries are not provided, so a conditional import keyed on one * of them takes its default (usually the portable stub), exactly as Dart picks it on * a platform without that library. */ private static final java.util.Set SUPPORTED_DART_LIBRARIES = new java.util.HashSet( - java.util.Arrays.asList("core", "async", "collection", "convert", "io", "math", "typed_data", "ui")); + java.util.Arrays.asList("core", "async", "collection", "convert", "developer", "io", "math", "typed_data", "ui")); /** * Picks the uri a configurable import/export names on this platform: the first diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_developer_library/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_developer_library/expect.txt new file mode 100644 index 00000000000..0c0440a08f9 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_developer_library/expect.txt @@ -0,0 +1,7 @@ +[app] hello +[app] bad +Bad state: x +0 +42 +seen +false diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_developer_library/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_developer_library/main.dart new file mode 100644 index 00000000000..c11d3aad3ec --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_developer_library/main.dart @@ -0,0 +1,19 @@ +// dart:developer: log is its own function, not dart:math's, whichever way the +// library is imported; the timeline runs what it times. +import 'dart:developer' as developer; +import 'dart:developer' show Timeline, TimelineTask; +import 'dart:math'; + +void main() { + developer.log('hello', name: 'app'); + developer.log('bad', name: 'app', level: 1000, error: StateError('x')); + print(log(1.0).round()); + final r = Timeline.timeSync('t', () => 41 + 1); + print(r); + Timeline.startSync('frame', arguments: {'n': 1}); + Timeline.finishSync(); + final task = TimelineTask()..start('load'); + task.finish(); + print(developer.inspect('seen')); + print(developer.debugger(when: false)); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/lib_io_random_access/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/lib_io_random_access/expect.txt new file mode 100644 index 00000000000..ae35c5ac547 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/lib_io_random_access/expect.txt @@ -0,0 +1,10 @@ +after flush 7 7 +stored [1, 2, 3, 4, 8, 7, 6, 42] +read [3, 4, 8] +into 3 [0, 7, 6, 42] +eof -1 len 8 +read only: true +emptied 0 +compacted [5, 0, 5] +missing: true +closed: File closed diff --git a/maven/dart-transpiler/src/test/resources/behavior/lib_io_random_access/main.dart b/maven/dart-transpiler/src/test/resources/behavior/lib_io_random_access/main.dart new file mode 100644 index 00000000000..7553826b75a --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/lib_io_random_access/main.dart @@ -0,0 +1,53 @@ +// RandomAccessFile the way Hive's storage backend drives it: an append-only +// frame log read back by offset, a compaction that truncates, and locks. +import 'dart:io'; +import 'dart:typed_data'; + +Future main() async { + final dir = await Directory.systemTemp.createTemp('raf'); + final file = File('${dir.path}/box.hive'); + + final w = await file.open(mode: FileMode.append); + await w.lock(); + await w.writeFrom([1, 2, 3, 4]); + await w.writeFrom(Uint8List.fromList([9, 8, 7, 6, 5]), 1, 4); + await w.flush(); + print('after flush ${await file.length()} ${await w.position()}'); + await w.writeByte(42); + await w.unlock(); + await w.close(); + print('stored ${await file.readAsBytes()}'); + + final r = await file.open(); + await r.setPosition(2); + print('read ${await r.read(3)}'); + final buf = List.filled(4, 0); + print('into ${await r.readInto(buf, 1)} $buf'); + print('eof ${await r.readByte()} len ${await r.length()}'); + try { + await r.writeFrom([1]); + } on FileSystemException catch (e) { + print('read only: ${e.message.startsWith('Cannot write')}'); + } + await r.close(); + + final c = file.openSync(mode: FileMode.write); + print('emptied ${c.lengthSync()}'); + c.writeFromSync([5, 5, 5, 5, 5]); + c.setPositionSync(1); + c.writeByteSync(0); + c.truncateSync(3); + c.closeSync(); + print('compacted ${file.readAsBytesSync()}'); + + try { + await File('${dir.path}/missing').open(); + } on FileSystemException catch (e) { + print('missing: ${e.path!.endsWith('missing')}'); + } + try { + r.lengthSync(); + } on FileSystemException catch (e) { + print('closed: ${e.message}'); + } +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_credentials/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_credentials/expect.txt new file mode 100644 index 00000000000..c7f474101d1 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_credentials/expect.txt @@ -0,0 +1,13 @@ +password password +google.com gid gat null +apple.com aid raw null +apple.com n +google.com apple.com password +google.com [drive] +oauth operation-not-allowed +apple operation-not-allowed +empty invalid-credential +[openid, email, profile, drive] +null false null +google sign_in_failed +out null diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_credentials/main.dart b/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_credentials/main.dart new file mode 100644 index 00000000000..f27d9ca23cd --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_credentials/main.dart @@ -0,0 +1,46 @@ +// firebase_auth credentials and providers, and google_sign_in, headless: the +// credentials carry what they were built from, and every interactive sign-in fails +// with the plugin's own error rather than hanging. +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +Future main() async { + final email = EmailAuthProvider.credential(email: 'a@b.c', password: 'pw'); + print('${email.providerId} ${email.signInMethod}'); + final google = GoogleAuthProvider.credential(idToken: 'gid', accessToken: 'gat'); + print('${google.providerId} ${google.idToken} ${google.accessToken} ${google.rawNonce}'); + final apple = OAuthProvider('apple.com').credential(idToken: 'aid', rawNonce: 'raw'); + print('${apple.providerId} ${apple.idToken} ${apple.rawNonce} ${apple.accessToken}'); + final viaApple = AppleAuthProvider.credentialWithIDToken('tok', 'n', null); + print('${viaApple.signInMethod} ${viaApple.rawNonce}'); + print(GoogleAuthProvider.PROVIDER_ID + ' ' + AppleAuthProvider.PROVIDER_ID + ' ' + EmailAuthProvider.PROVIDER_ID); + final provider = GoogleAuthProvider()..addScope('drive'); + print('${provider.providerId} ${provider.scopes}'); + + try { + await FirebaseAuth.instance.signInWithProvider(OAuthProvider('microsoft.com')); + } on FirebaseAuthException catch (e) { + print('oauth ${e.code}'); + } + try { + await FirebaseAuth.instance.signInWithProvider(AppleAuthProvider()); + } on FirebaseAuthException catch (e) { + print('apple ${e.code}'); + } + try { + await FirebaseAuth.instance.signInWithCredential(OAuthProvider('x.com').credential()); + } on FirebaseAuthException catch (e) { + print('empty ${e.code}'); + } + + final signIn = GoogleSignIn(scopes: ['drive'], clientId: '123-abc.apps.googleusercontent.com'); + print(signIn.scopes); + print('${signIn.currentUser} ${await signIn.isSignedIn()} ${await signIn.signInSilently()}'); + try { + await signIn.signIn(); + } on PlatformException catch (e) { + print('google ${e.code}'); + } + await signIn.signOut(); + print('out ${signIn.currentUser}'); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_media_packs/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/plugin_media_packs/expect.txt new file mode 100644 index 00000000000..c3a6419cb4e --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_media_packs/expect.txt @@ -0,0 +1,24 @@ +progress 100 +finished https://example.com/a +prevent https://blocked.example/b +progress 100 +finished https://example.com/c +current https://example.com/c +progress 100 +finished https://example.com/a +back https://example.com/a false +js ran +js result headless +video false false 0 1.0 1.0 +looping true notified 1 +init headless true +thumbnail unsupported +compress /tmp/v.mp4 [100.0] +cameras 0 +camera false CameraLensDirection.back +camera cameraNotAvailable +emoji 8 Category.SMILEYS Grinning Face +search 1 Slice Of Pizza +picker true +svg 32.0 16.0 +widgets 4 diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_media_packs/main.dart b/maven/dart-transpiler/src/test/resources/behavior/plugin_media_packs/main.dart new file mode 100644 index 00000000000..31e8d3ffb5e --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_media_packs/main.dart @@ -0,0 +1,95 @@ +// The media plugin packs, headless: the web view controller's own state and +// navigation decisions, player and camera defaults, the emoji set, the honest +// failures of the frame and transcode gaps, and widgets that build. +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:video_player/video_player.dart'; +import 'package:video_thumbnail/video_thumbnail.dart'; +import 'package:video_compress/video_compress.dart'; +import 'package:camera/camera.dart'; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +Future main() async { + try { await run(); } catch (e) { print('ERR $e'); } +} + +Future run() async { + final web = WebViewController(); + await web.setJavaScriptMode(JavaScriptMode.unrestricted); + await web.setNavigationDelegate(NavigationDelegate( + onPageFinished: (url) => print('finished $url'), + onProgress: (p) => print('progress $p'), + onNavigationRequest: (request) { + if (request.url.contains('blocked')) { + print('prevent ${request.url}'); + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + )); + await web.addJavaScriptChannel('Bridge', onMessageReceived: (m) => print('msg ${m.message}')); + await web.loadRequest(Uri.parse('https://example.com/a')); + await web.loadRequest(Uri.parse('https://blocked.example/b')); + await web.loadRequest(Uri.parse('https://example.com/c')); + print('current ${await web.currentUrl()}'); + await web.goBack(); + print('back ${await web.currentUrl()} ${await web.canGoBack()}'); + await web.runJavaScript('1 + 1'); + print('js ran'); + try { + await web.runJavaScriptReturningResult('1 + 1'); + } catch (e) { + print('js result headless'); + } + + final video = VideoPlayerController.networkUrl(Uri.parse('https://example.com/v.mp4')); + final v = video.value; + print('video ${v.isInitialized} ${v.isPlaying} ${v.position.inMilliseconds} ${v.aspectRatio} ${v.volume}'); + var notified = 0; + video.addListener(() => notified++); + await video.setLooping(true); + print('looping ${video.value.isLooping} notified $notified'); + try { + await video.initialize(); + } catch (e) { + print('init headless ${video.value.hasError}'); + } + + try { + await VideoThumbnail.thumbnailData(video: '/tmp/v.mp4', imageFormat: ImageFormat.JPEG, maxWidth: 128); + } catch (e) { + print('thumbnail unsupported'); + } + final progress = []; + final sub = VideoCompress.compressProgress$.subscribe((p) => progress.add(p)); + final info = await VideoCompress.compressVideo('/tmp/v.mp4', quality: VideoQuality.MediumQuality); + sub.unsubscribe(); + print('compress ${info?.path} $progress'); + + final cams = await availableCameras(); + print('cameras ${cams.length}'); + final cam = CameraController( + CameraDescription(name: '0', lensDirection: CameraLensDirection.back, sensorOrientation: 90), + ResolutionPreset.high, enableAudio: false); + print('camera ${cam.value.isInitialized} ${cam.description.lensDirection}'); + try { + await cam.initialize(); + } on CameraException catch (e) { + print('camera ${e.code}'); + } + + final sets = EmojiPicker.defaultEmojiSet; + print('emoji ${sets.length} ${sets.first.category} ${sets.first.emoji.first.name}'); + final found = await EmojiPickerUtils().searchEmoji('pizza', sets); + print('search ${found.length} ${found.first.name}'); + final controller = TextEditingController(); + final picker = EmojiPicker(textEditingController: controller, config: Config(height: 200)); + print('picker ${picker != null}'); + + final svg = SvgPicture.string('', width: 32, height: 16, + colorFilter: ColorFilter.mode(Colors.red, BlendMode.srcIn)); + print('svg ${svg.width} ${svg.height}'); + final widgets = [WebViewWidget(controller: web), VideoPlayer(video), CameraPreview(cam), svg]; + print('widgets ${widgets.length}'); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_util_packs/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/plugin_util_packs/expect.txt new file mode 100644 index 00000000000..ed5c7f62832 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_util_packs/expect.txt @@ -0,0 +1,20 @@ +timezone true +zones has local true +settings opened +referrer unsupported +no placemark +geocoder false +picked true +custom needs extensions +dir null +player p1 PlayerState.stopped +after play PlayerState.playing ReleaseMode.loop +after pause PlayerState.paused +after dispose PlayerState.disposed +mixpanel id user-1 queued 4 +mixpanel after headless flush 4 +mixpanel opted out true 0 +auth UNAVAILABLE +compress unsupported +photo picker true +image true diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_util_packs/main.dart b/maven/dart-transpiler/src/test/resources/behavior/plugin_util_packs/main.dart new file mode 100644 index 00000000000..ccad2d14783 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_util_packs/main.dart @@ -0,0 +1,103 @@ +// The utility plugin packs, headless: what each answers with no platform to ask. +import 'dart:typed_data'; + +import 'package:android_play_install_referrer/android_play_install_referrer.dart'; +import 'package:app_settings/app_settings.dart'; +import 'package:audioplayers/audioplayers.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:flutter_timezone/flutter_timezone.dart'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; +import 'package:flutter/services.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:image_picker_android/image_picker_android.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mixpanel_flutter/mixpanel_flutter.dart'; + +Future main() async { + try { await run(); } catch (e) { print('ERR $e'); } +} + +Future run() async { + final tz = await FlutterTimezone.getLocalTimezone(); + print('timezone ${tz.isNotEmpty}'); + final zones = await FlutterTimezone.getAvailableTimezones(); + print('zones has local ${zones.contains(tz)}'); + + await AppSettings.openAppSettings(type: AppSettingsType.location); + print('settings opened'); + + try { + await AndroidPlayInstallReferrer.installReferrer; + print('referrer?'); + } on UnsupportedError catch (_) { + print('referrer unsupported'); + } + + try { + await placemarkFromCoordinates(52.52, 13.40); + print('placemark?'); + } on NoResultFoundException catch (_) { + print('no placemark'); + } + await setLocaleIdentifier('en_US'); + print('geocoder ${await isPresent()}'); + + final picked = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: true); + print('picked ${picked == null}'); + try { + await FilePicker.platform.pickFiles(type: FileType.custom); + } on ArgumentError catch (_) { + print('custom needs extensions'); + } + print('dir ${await FilePicker.platform.getDirectoryPath()}'); + + final player = AudioPlayer(playerId: 'p1'); + print('player ${player.playerId} ${player.state}'); + await player.setReleaseMode(ReleaseMode.loop); + await player.play(AssetSource('sounds/ding.mp3')); + print('after play ${player.state} ${player.releaseMode}'); + await player.pause(); + print('after pause ${player.state}'); + await player.stop(); + await player.dispose(); + print('after dispose ${player.state}'); + + final mp = await Mixpanel.init('tok', trackAutomaticEvents: false, superProperties: {'app': 'pets'}); + mp.setServerURL('https://api-eu.mixpanel.com'); + await mp.track('Opened', properties: {'screen': 'home'}); + await mp.identify('user-1'); + mp.getPeople().set('plan', 'pro'); + mp.getPeople().increment('visits', 1); + print('mixpanel id ${await mp.getDistinctId()} queued ${mp.cn1PendingCount}'); + await mp.flush(); + print('mixpanel after headless flush ${mp.cn1PendingCount}'); + mp.optOutTracking(); + await mp.track('Hidden'); + print('mixpanel opted out ${await mp.hasOptedOutTracking()} ${mp.cn1PendingCount}'); + + try { + await FlutterWebAuth2.authenticate(url: 'https://example.com/auth', callbackUrlScheme: 'pets'); + print('auth?'); + } on PlatformException catch (e) { + print('auth ${e.code}'); + } + + try { + await FlutterImageCompress.compressWithList(Uint8List.fromList([1, 2, 3]), quality: 80, + format: CompressFormat.jpeg); + print('compressed?'); + } on UnsupportedError catch (_) { + print('compress unsupported'); + } + + ImagePickerAndroid.registerWith(); + final android = ImagePickerPlatform.instance; + if (android is ImagePickerAndroid) { + android.useAndroidPhotoPicker = true; + print('photo picker ${android.useAndroidPhotoPicker}'); + } + final img = await ImagePickerPlatform.instance.getImageFromSource(source: ImageSource.gallery); + print('image ${img == null}'); +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/appsettings/AppSettings.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/appsettings/AppSettings.java new file mode 100644 index 00000000000..dae21ead60e --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/appsettings/AppSettings.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.appsettings; + +import com.codename1.io.Log; +import com.codename1.ui.Display; + +import dart.async.Future; + +/** + * app_settings' {@code AppSettings}, over {@code Display.execute}. On iOS every type opens + * the application's own page ({@code app-settings:}), as the plugin does there. On Android + * a type with a system settings action opens it through an {@code intent:} URL, and every + * other type opens the application's details page. {@code asAnotherTask} is accepted and + * not applied: the port starts the activity itself. Elsewhere, and with no display, there + * is no settings page and the call does nothing. + */ +public final class AppSettings { + + private AppSettings() { + } + + public static Future openAppSettings(AppSettingsType type, Boolean asAnotherTask) { + if (!Display.isInitialized()) { + return Future.value(null); + } + Display d = Display.getInstance(); + String platform = d.getPlatformName(); + if ("ios".equals(platform)) { + d.execute("app-settings:"); + } else if ("and".equals(platform)) { + String action = androidAction(type == null ? AppSettingsType.settings : type); + if (action == null) { + String pkg = d.getProperty("package_name", null); + if (pkg == null) { + action = "android.settings.SETTINGS"; + } else { + d.execute("intent:" + pkg + + "#Intent;scheme=package;action=android.settings.APPLICATION_DETAILS_SETTINGS;end"); + return Future.value(null); + } + } + d.execute("intent:#Intent;action=" + action + ";end"); + } else { + Log.p("app_settings: no settings page on " + platform); + } + return Future.value(null); + } + + public static Future openAppSettingsPanel(AppSettingsPanelType type) { + if (!Display.isInitialized()) { + return Future.value(null); + } + if (!"and".equals(Display.getInstance().getPlatformName())) { + return openAppSettings(AppSettingsType.settings, Boolean.FALSE); + } + String action; + if (type == AppSettingsPanelType.nfc) { + action = "android.settings.panel.action.NFC"; + } else if (type == AppSettingsPanelType.volume) { + action = "android.settings.panel.action.VOLUME"; + } else if (type == AppSettingsPanelType.wifi) { + action = "android.settings.panel.action.WIFI"; + } else { + action = "android.settings.panel.action.INTERNET_CONNECTIVITY"; + } + Display.getInstance().execute("intent:#Intent;action=" + action + ";end"); + return Future.value(null); + } + + /** The system action for a type, or null for the application's details page. */ + static String androidAction(AppSettingsType type) { + switch (type) { + case accessibility: return "android.settings.ACCESSIBILITY_SETTINGS"; + case alarm: return "android.settings.REQUEST_SCHEDULE_EXACT_ALARM"; + case apn: return "android.settings.APN_SETTINGS"; + case appLocale: return "android.settings.LOCALE_SETTINGS"; + case batteryOptimization: return "android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS"; + case bluetooth: return "android.settings.BLUETOOTH_SETTINGS"; + case cellular: return "android.settings.DATA_ROAMING_SETTINGS"; + case dataRoaming: return "android.settings.DATA_ROAMING_SETTINGS"; + case date: return "android.settings.DATE_SETTINGS"; + case developer: return "android.settings.APPLICATION_DEVELOPMENT_SETTINGS"; + case device: return "android.settings.DEVICE_INFO_SETTINGS"; + case display: return "android.settings.DISPLAY_SETTINGS"; + case generalSettings: return "android.settings.SETTINGS"; + case hotspot: return "android.settings.TETHER_SETTINGS"; + case internalStorage: return "android.settings.INTERNAL_STORAGE_SETTINGS"; + case location: return "android.settings.LOCATION_SOURCE_SETTINGS"; + case lockAndPassword: return "android.app.action.SET_NEW_PASSWORD"; + case manageUnknownApps: return "android.settings.MANAGE_UNKNOWN_APP_SOURCES"; + case nfc: return "android.settings.NFC_SETTINGS"; + case notification: return "android.settings.APP_NOTIFICATION_SETTINGS"; + case security: return "android.settings.SECURITY_SETTINGS"; + case sound: return "android.settings.SOUND_SETTINGS"; + case subscriptions: return "android.settings.SYNC_SETTINGS"; + case vpn: return "android.settings.VPN_SETTINGS"; + case wifi: return "android.settings.WIFI_SETTINGS"; + case wireless: return "android.settings.WIRELESS_SETTINGS"; + default: return null; + } + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/appsettings/AppSettingsPanelType.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/appsettings/AppSettingsPanelType.java new file mode 100644 index 00000000000..425c85057c3 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/appsettings/AppSettingsPanelType.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.appsettings; + +/** app_settings' {@code AppSettingsPanelType}: an Android settings panel. */ +public enum AppSettingsPanelType { + internetConnectivity, nfc, volume, wifi +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/appsettings/AppSettingsType.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/appsettings/AppSettingsType.java new file mode 100644 index 00000000000..acb59125bee --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/appsettings/AppSettingsType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.appsettings; + +/** app_settings' {@code AppSettingsType}: the settings page to open. */ +public enum AppSettingsType { + accessibility, alarm, apn, appLocale, batteryOptimization, bluetooth, cellular, dataRoaming, + date, developer, device, display, generalSettings, hotspot, internalStorage, location, + lockAndPassword, manageUnknownApps, nfc, notification, security, settings, sound, + subscriptions, vpn, wifi, wireless +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/AssetSource.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/AssetSource.java new file mode 100644 index 00000000000..c918b0f4947 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/AssetSource.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.audioplayers; + +import com.codename1.flutter.FlutterAssets; +import com.codename1.media.Media; +import com.codename1.media.MediaManager; +import com.codename1.ui.Display; + +import java.io.IOException; +import java.io.InputStream; + +/** + * audioplayers' {@code AssetSource}. The path is relative to the AudioCache prefix, + * {@code assets/}, as in the plugin; the asset is the flattened resource the build wrote + * for that key (see {@link FlutterAssets#resourceName}). + */ +public class AssetSource extends Source { + static final String PREFIX = "assets/"; + private final String path; + + public AssetSource(String path) { + this(path, null); + } + + public AssetSource(String path, String mimeType) { + super(mimeType); + this.path = path; + } + + public String path() { + return path; + } + + String assetKey() { + return path.startsWith(PREFIX) ? path : PREFIX + path; + } + + @Override + Media open(Runnable onComplete) throws IOException { + InputStream in = Display.getInstance().getResourceAsStream(AssetSource.class, + FlutterAssets.resourceName(assetKey())); + if (in == null) { + throw new IOException("audioplayers: no asset " + assetKey()); + } + return MediaManager.createMedia(in, guessMime(path, mimeType()), onComplete); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/AudioPlayer.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/AudioPlayer.java new file mode 100644 index 00000000000..4b42cf720eb --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/AudioPlayer.java @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.audioplayers; + +import com.codename1.media.Media; +import com.codename1.ui.Display; + +import java.util.Timer; +import java.util.TimerTask; + +import dart.async.Future; +import dart.async.Stream; +import dart.async.StreamController; +import dart.core.Duration; +import dart.typed_data.Uint8List; + +/** + * audioplayers' {@code AudioPlayer}, over one Codename One Media per source + * ({@code MediaManager.createMedia}). Position is polled every 200 ms while playing; + * {@code ReleaseMode.loop} restarts the sound when it ends. Balance, playback rate, the + * audio context and the player mode are accepted and not applied. With no display there + * is no player: the state still follows the calls, so an app's UI logic runs, but nothing + * sounds and no position is reported. + */ +public class AudioPlayer { + private static int ids; + private String playerId; + private Media media; + private Source source; + private PlayerState state = PlayerState.stopped; + private ReleaseMode releaseMode = ReleaseMode.release; + private double volume = 1.0; + private Timer ticker; + private final StreamController complete = StreamController.broadcast(); + private final StreamController positions = StreamController.broadcast(); + private final StreamController durations = StreamController.broadcast(); + private final StreamController states = StreamController.broadcast(); + + public AudioPlayer() { + synchronized (AudioPlayer.class) { + ids++; + this.playerId = "cn1-player-" + ids; + } + } + + /** The {@code playerId} constructor argument. */ + public void playerId(String id) { + if (id != null) { + playerId = id; + } + } + + public String playerId() { + return playerId; + } + + public PlayerState state() { + return state; + } + + public Source source() { + return source; + } + + public double volume() { + return volume; + } + + public ReleaseMode releaseMode() { + return releaseMode; + } + + public Stream onPlayerComplete() { + return complete.stream(); + } + + public Stream onPositionChanged() { + return positions.stream(); + } + + public Stream onDurationChanged() { + return durations.stream(); + } + + public Stream onPlayerStateChanged() { + return states.stream(); + } + + private void state(PlayerState s) { + if (state != s) { + state = s; + states.add(s); + } + } + + private static Duration ms(long millis) { + return Duration.ofMicroseconds(millis * 1000L); + } + + public Future setSource(Source s) { + if (state == PlayerState.disposed) { + return Future.error(new dart.core.StateError("AudioPlayer has been disposed")); + } + releaseMedia(); + source = s; + if (s == null || !Display.isInitialized()) { + return Future.value(null); + } + try { + media = s.open(new Runnable() { + @Override + public void run() { + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + ended(); + } + }); + } + }); + media.setVolume((int) Math.round(volume * 100)); + int d = media.getDuration(); + if (d > 0) { + durations.add(ms(d)); + } + } catch (java.io.IOException e) { + media = null; + return Future.error(e); + } + return Future.value(null); + } + + public Future setSourceUrl(String url, String mimeType) { + return setSource(new UrlSource(url, mimeType)); + } + + public Future setSourceDeviceFile(String path, String mimeType) { + return setSource(new DeviceFileSource(path, mimeType)); + } + + public Future setSourceAsset(String path, String mimeType) { + return setSource(new AssetSource(path, mimeType)); + } + + public Future setSourceBytes(Uint8List bytes, String mimeType) { + return setSource(new BytesSource(bytes, mimeType)); + } + + public Future play(Source s, Double volume, Double balance, Object ctx, Duration position, + Object mode) { + if (volume != null) { + setVolume(volume.doubleValue()); + } + Future set = setSource(s); + if (set.completedError() != null) { + return set; + } + if (position != null) { + seek(position); + } + return resume(); + } + + public Future resume() { + if (media != null) { + media.play(); + startTicker(); + } + state(PlayerState.playing); + return Future.value(null); + } + + public Future pause() { + if (media != null) { + media.pause(); + } + stopTicker(); + state(PlayerState.paused); + return Future.value(null); + } + + public Future stop() { + if (media != null) { + media.pause(); + media.setTime(0); + } + stopTicker(); + state(PlayerState.stopped); + return Future.value(null); + } + + public Future release() { + stopTicker(); + releaseMedia(); + state(PlayerState.stopped); + return Future.value(null); + } + + public Future seek(Duration position) { + if (media != null && position != null) { + media.setTime((int) position.inMilliseconds()); + positions.add(position); + } + return Future.value(null); + } + + public Future setVolume(double v) { + volume = Math.max(0, Math.min(1, v)); + if (media != null) { + media.setVolume((int) Math.round(volume * 100)); + } + return Future.value(null); + } + + public Future setBalance(double balance) { + return Future.value(null); + } + + public Future setPlaybackRate(double rate) { + return Future.value(null); + } + + public Future setReleaseMode(ReleaseMode mode) { + releaseMode = mode == null ? ReleaseMode.release : mode; + return Future.value(null); + } + + public Future setPlayerMode(PlayerMode mode) { + return Future.value(null); + } + + public Future getDuration() { + if (media == null) { + return Future.value(null); + } + int d = media.getDuration(); + return Future.value(d < 0 ? null : ms(d)); + } + + public Future getCurrentPosition() { + return Future.value(media == null ? null : ms(Math.max(0, media.getTime()))); + } + + public Future dispose() { + release(); + state(PlayerState.disposed); + complete.close(); + positions.close(); + durations.close(); + states.close(); + return Future.value(null); + } + + private void ended() { + if (state == PlayerState.disposed) { + return; + } + if (releaseMode == ReleaseMode.loop && media != null) { + media.setTime(0); + media.play(); + return; + } + stopTicker(); + if (releaseMode == ReleaseMode.release) { + releaseMedia(); + } + state(PlayerState.completed); + complete.add(null); + } + + private void releaseMedia() { + if (media != null) { + media.cleanup(); + media = null; + } + } + + private void startTicker() { + stopTicker(); + ticker = new Timer(); + ticker.schedule(new TimerTask() { + @Override + public void run() { + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + if (media != null && state == PlayerState.playing) { + positions.add(ms(Math.max(0, media.getTime()))); + } + } + }); + } + }, 200, 200); + } + + private void stopTicker() { + if (ticker != null) { + ticker.cancel(); + ticker = null; + } + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/BytesSource.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/BytesSource.java new file mode 100644 index 00000000000..193abe0676a --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/BytesSource.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.audioplayers; + +import com.codename1.media.Media; +import com.codename1.media.MediaManager; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import dart.typed_data.Uint8List; + +/** audioplayers' {@code BytesSource}: a sound held in memory. */ +public class BytesSource extends Source { + private final Uint8List bytes; + + public BytesSource(Uint8List bytes) { + this(bytes, null); + } + + public BytesSource(Uint8List bytes, String mimeType) { + super(mimeType); + this.bytes = bytes; + } + + public Uint8List bytes() { + return bytes; + } + + @Override + Media open(Runnable onComplete) throws IOException { + return MediaManager.createMedia(new ByteArrayInputStream(bytes.toBytes()), guessMime(null, mimeType()), + onComplete); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/DeviceFileSource.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/DeviceFileSource.java new file mode 100644 index 00000000000..ddea04769af --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/DeviceFileSource.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.audioplayers; + +import com.codename1.media.Media; +import com.codename1.media.MediaManager; + +import java.io.IOException; + +/** audioplayers' {@code DeviceFileSource}: a FileSystemStorage path. */ +public class DeviceFileSource extends Source { + private final String path; + + public DeviceFileSource(String path) { + this(path, null); + } + + public DeviceFileSource(String path, String mimeType) { + super(mimeType); + this.path = path; + } + + public String path() { + return path; + } + + @Override + Media open(Runnable onComplete) throws IOException { + return MediaManager.createMedia(path, false, onComplete); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/PlayerMode.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/PlayerMode.java new file mode 100644 index 00000000000..a623a05cc75 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/PlayerMode.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.audioplayers; + +/** audioplayers' {@code PlayerMode}; accepted, every sound plays through one Media. */ +public enum PlayerMode { + mediaPlayer, lowLatency +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/PlayerState.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/PlayerState.java new file mode 100644 index 00000000000..bde724d0e2f --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/PlayerState.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.audioplayers; + +/** audioplayers' {@code PlayerState}. */ +public enum PlayerState { + stopped, playing, paused, completed, disposed +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/ReleaseMode.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/ReleaseMode.java new file mode 100644 index 00000000000..8cb771bfcc8 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/ReleaseMode.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.audioplayers; + +/** audioplayers' {@code ReleaseMode}: what happens when a sound ends. */ +public enum ReleaseMode { + release, loop, stop +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/Source.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/Source.java new file mode 100644 index 00000000000..e50fb8fd235 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/Source.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.audioplayers; + +import com.codename1.media.Media; + +import java.io.IOException; + +/** audioplayers' {@code Source}: what a player opens. */ +public abstract class Source { + private String mimeType; + + protected Source(String mimeType) { + this.mimeType = mimeType; + } + + /** The {@code mimeType} constructor argument. */ + public void mimeType(String v) { + mimeType = v; + } + + public String mimeType() { + return mimeType; + } + + /** Opens the sound as a Codename One Media; {@code onComplete} runs when it ends. */ + abstract Media open(Runnable onComplete) throws IOException; + + static String guessMime(String name, String given) { + if (given != null) { + return given; + } + String n = name == null ? "" : name; + if (endsWith(n, ".wav")) { + return "audio/wav"; + } + if (endsWith(n, ".ogg")) { + return "audio/ogg"; + } + if (endsWith(n, ".m4a") || endsWith(n, ".aac") || endsWith(n, ".mp4")) { + return "audio/mp4"; + } + return "audio/mpeg"; + } + + private static boolean endsWith(String s, String suffix) { + return s.length() >= suffix.length() + && s.regionMatches(true, s.length() - suffix.length(), suffix, 0, suffix.length()); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/UrlSource.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/UrlSource.java new file mode 100644 index 00000000000..98718854161 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/UrlSource.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.audioplayers; + +import com.codename1.media.Media; +import com.codename1.media.MediaManager; + +import java.io.IOException; + +/** audioplayers' {@code UrlSource}: a remote sound, streamed by the platform player. */ +public class UrlSource extends Source { + private final String url; + + public UrlSource(String url) { + this(url, null); + } + + public UrlSource(String url, String mimeType) { + super(mimeType); + this.url = url; + } + + public String url() { + return url; + } + + @Override + Media open(Runnable onComplete) throws IOException { + return MediaManager.createMedia(url, false, onComplete); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraController.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraController.java new file mode 100644 index 00000000000..302e56404fb --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraController.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.camera; + +import com.codename1.camera.Camera; +import com.codename1.camera.CameraFacing; +import com.codename1.camera.CameraInfo; +import com.codename1.camera.CameraSession; +import com.codename1.camera.CameraSessionOptions; +import com.codename1.camera.CapturedPhoto; +import com.codename1.camera.VideoRecording; +import com.codename1.flutter.foundation.ValueNotifier; +import com.codename1.flutter.plugins.crossfile.XFile; +import com.codename1.io.FileSystemStorage; +import com.codename1.ui.Display; +import com.codename1.util.SuccessCallback; + +import dart.async.Completer; +import dart.async.Future; + +/** + * camera's CameraController over a Codename One {@link CameraSession}: the preview + * is the session's CameraView, takePicture its takePhoto and video its + * startVideoRecording. The resolution preset picks a preview size; imageFormatGroup + * is accepted and photos are always JPEG. Without a display, or where the platform + * has no camera API, initialize fails with CameraException("cameraNotAvailable"). + */ +public class CameraController extends ValueNotifier { + private final CameraDescription description; + private final ResolutionPreset preset; + private boolean enableAudio = true; + private CameraSession session; + private VideoRecording recording; + + public CameraController(CameraDescription description, ResolutionPreset preset) { + super(new CameraValue()); + this.description = description; + this.preset = preset; + } + + public void enableAudio(Boolean v) { + enableAudio = v == null || v.booleanValue(); + } + + public void imageFormatGroup(ImageFormatGroup v) { + } + + public CameraDescription description() { + return description; + } + + CameraSession session() { + return session; + } + + public Future initialize() { + if (!Display.isInitialized() || !Camera.isSupported()) { + return error("cameraNotAvailable", "no camera on this platform"); + } + CameraInfo info = description == null ? null : description.info; + if (info == null) { + info = Camera.getDefault(description != null && description.lensDirection == CameraLensDirection.front + ? CameraFacing.FRONT : CameraFacing.BACK); + } + if (info == null) { + return error("cameraNotAvailable", "no camera matches " + description); + } + int[] size = size(preset); + session = Camera.open(info, new CameraSessionOptions().previewSize(size[0], size[1]).captureAudio(enableAudio)); + if (session == null) { + return error("CameraAccessDenied", "the camera could not be opened"); + } + CameraValue v = value().copy(); + v.initialized = true; + v.previewWidth = size[0]; + v.previewHeight = size[1]; + value(v); + return Future.value(null); + } + + private static int[] size(ResolutionPreset p) { + if (p == ResolutionPreset.low) { + return new int[] {320, 240}; + } + if (p == ResolutionPreset.medium) { + return new int[] {720, 480}; + } + if (p == ResolutionPreset.high) { + return new int[] {1280, 720}; + } + if (p == ResolutionPreset.veryHigh) { + return new int[] {1920, 1080}; + } + return new int[] {3840, 2160}; + } + + private Future error(String code, String why) { + CameraValue v = value().copy(); + v.errorDescription = why; + value(v); + return Future.error(new CameraException(code, why)); + } + + public Future takePicture() { + if (session == null) { + return error("Uninitialized CameraController", "takePicture() was called on an uninitialized CameraController."); + } + final Completer done = new Completer(); + CameraValue v = value().copy(); + v.takingPicture = true; + value(v); + session.takePhoto().ready(new SuccessCallback() { + @Override + public void onSucess(CapturedPhoto p) { + CameraValue v = value().copy(); + v.takingPicture = false; + value(v); + String path = p.getFilePath(); + if (path == null) { + path = FileSystemStorage.getInstance().getAppHomePath() + "cn1_camera_" + System.currentTimeMillis() + ".jpg"; + try { + java.io.OutputStream out = FileSystemStorage.getInstance().openOutputStream(path); + out.write(p.getJpegBytes()); + out.close(); + } catch (java.io.IOException e) { + done.completeError(new CameraException("captureFailure", e.getMessage())); + return; + } + } + done.complete(new XFile(path)); + } + }).except(new SuccessCallback() { + @Override + public void onSucess(Throwable t) { + CameraValue v = value().copy(); + v.takingPicture = false; + value(v); + done.completeError(new CameraException("captureFailure", String.valueOf(t.getMessage()))); + } + }); + return done.future(); + } + + public Future startVideoRecording(Object onAvailable) { + if (session == null) { + return error("Uninitialized CameraController", "startVideoRecording() was called on an uninitialized CameraController."); + } + String path = FileSystemStorage.getInstance().getAppHomePath() + "cn1_video_" + System.currentTimeMillis() + ".mp4"; + recording = session.startVideoRecording(path); + CameraValue v = value().copy(); + v.recording = true; + value(v); + return Future.value(null); + } + + public Future stopVideoRecording() { + if (recording == null) { + return error("No video is recording", "stopVideoRecording() was called with no recording."); + } + final Completer done = new Completer(); + final VideoRecording r = recording; + recording = null; + r.stopAndAwait().ready(new SuccessCallback() { + @Override + public void onSucess(String path) { + CameraValue v = value().copy(); + v.recording = false; + value(v); + done.complete(new XFile(path == null ? r.getRequestedPath() : path)); + } + }).except(new SuccessCallback() { + @Override + public void onSucess(Throwable t) { + CameraValue v = value().copy(); + v.recording = false; + value(v); + done.completeError(new CameraException("videoRecordingFailed", String.valueOf(t.getMessage()))); + } + }); + return done.future(); + } + + public Future setFlashMode(FlashMode mode) { + CameraValue v = value().copy(); + v.flashMode = mode; + value(v); + if (session != null) { + session.setFlashMode(mode == FlashMode.always ? com.codename1.camera.FlashMode.ON + : mode == FlashMode.torch ? com.codename1.camera.FlashMode.TORCH + : mode == FlashMode.off ? com.codename1.camera.FlashMode.OFF + : com.codename1.camera.FlashMode.AUTO); + } + return Future.value(null); + } + + public Future setZoomLevel(double zoom) { + if (session != null) { + session.setZoom((float) zoom); + } + return Future.value(null); + } + + public Future pausePreview() { + if (session != null) { + session.pause(); + } + return Future.value(null); + } + + public Future resumePreview() { + if (session != null) { + session.resume(); + } + return Future.value(null); + } + + @Override + public void dispose() { + if (recording != null) { + recording.stop(); + recording = null; + } + if (session != null) { + session.close(); + session = null; + } + super.dispose(); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraDescription.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraDescription.java new file mode 100644 index 00000000000..a8da5ef8787 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraDescription.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.camera; + +import com.codename1.camera.CameraInfo; + +/** camera's CameraDescription; a description from availableCameras carries its Codename One CameraInfo. */ +public class CameraDescription { + String name = ""; + CameraLensDirection lensDirection = CameraLensDirection.back; + long sensorOrientation; + CameraInfo info; + + public CameraDescription() { + } + + public void name(String v) { + name = v; + } + + public void lensDirection(CameraLensDirection v) { + lensDirection = v; + } + + public void sensorOrientation(Long v) { + sensorOrientation = v == null ? 0 : v.longValue(); + } + + public String name() { + return name; + } + + public CameraLensDirection lensDirection() { + return lensDirection; + } + + /** Codename One rotates the preview itself, so this is 90 for every camera, as most phones report. */ + public long sensorOrientation() { + return sensorOrientation; + } + + @Override + public String toString() { + return "CameraDescription(" + name + ", " + lensDirection + ", " + sensorOrientation + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraException.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraException.java new file mode 100644 index 00000000000..4f3c95da7e7 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.camera; + +/** camera's CameraException. */ +public class CameraException extends RuntimeException { + private final String code; + private final String description; + + public CameraException(String code, String description) { + super(description); + this.code = code; + this.description = description; + } + + public String code() { + return code; + } + + public String description() { + return description; + } + + @Override + public String toString() { + return "CameraException(" + code + ", " + description + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraLensDirection.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraLensDirection.java new file mode 100644 index 00000000000..0116b210d61 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraLensDirection.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.camera; + +/** camera's CameraLensDirection. */ +public enum CameraLensDirection { + front, back, external +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraPreview.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraPreview.java new file mode 100644 index 00000000000..70e5c9d3cee --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraPreview.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.camera; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.Size; + +/** camera's CameraPreview: hosts the session's CameraView. A child overlay is accepted and not drawn. */ +public class CameraPreview extends Widget { + final CameraController controller; + + public CameraPreview(CameraController controller) { + this.controller = controller; + } + + public void child(Widget v) { + } + + Size preferredLayoutSize() { + double ar = controller == null ? 1 : controller.value().aspectRatio(); + return new Size(300, 300 * ar); + } + + @Override + public Element createElement() { + return new CameraPreviewRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraPreviewRenderElement.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraPreviewRenderElement.java new file mode 100644 index 00000000000..093f2142aff --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraPreviewRenderElement.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.camera; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; + +/** Hosts the CameraView of a {@link CameraPreview}'s session; none before initialize or headless. */ +public class CameraPreviewRenderElement extends RenderElement { + public CameraPreviewRenderElement(CameraPreview widget) { + super(widget); + } + + @Override + protected Component createComponent() { + CameraController c = ((CameraPreview) widget()).controller; + if (c == null || c.session() == null) { + return null; + } + return c.session().createView(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + Size want = ((CameraPreview) widget()).preferredLayoutSize(); + double w = constraints.hasBoundedWidth() ? constraints.maxWidth() : Dp.px(want.width()); + double h = constraints.hasBoundedHeight() ? constraints.maxHeight() : Dp.px(want.height()); + return constraints.constrain(new Size(w, h)); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraValue.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraValue.java new file mode 100644 index 00000000000..6f422ba6e39 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/CameraValue.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.camera; + +import com.codename1.flutter.rendering.Size; + +/** camera's CameraValue: an immutable snapshot, copied on every change. */ +public class CameraValue { + boolean initialized; + boolean recording; + boolean takingPicture; + double previewWidth; + double previewHeight; + FlashMode flashMode = FlashMode.auto; + String errorDescription; + + CameraValue() { + } + + CameraValue copy() { + CameraValue v = new CameraValue(); + v.initialized = initialized; + v.recording = recording; + v.takingPicture = takingPicture; + v.previewWidth = previewWidth; + v.previewHeight = previewHeight; + v.flashMode = flashMode; + v.errorDescription = errorDescription; + return v; + } + + public boolean isInitialized() { + return initialized; + } + + public boolean isRecordingVideo() { + return recording; + } + + public boolean isTakingPicture() { + return takingPicture; + } + + public Size previewSize() { + return previewWidth > 0 ? new Size(previewWidth, previewHeight) : null; + } + + /** The plugin's convention: the landscape sensor's width over height. */ + public double aspectRatio() { + if (previewWidth <= 0 || previewHeight <= 0) { + return 1.0; + } + return Math.max(previewWidth, previewHeight) / Math.min(previewWidth, previewHeight); + } + + public FlashMode flashMode() { + return flashMode; + } + + public String errorDescription() { + return errorDescription; + } + + public boolean hasError() { + return errorDescription != null; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/Cameras.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/Cameras.java new file mode 100644 index 00000000000..a414435d1cf --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/Cameras.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.camera; + +import com.codename1.camera.Camera; +import com.codename1.camera.CameraFacing; +import com.codename1.camera.CameraInfo; +import com.codename1.ui.Display; + +import dart.async.Future; +import dart.core.DartList; + +/** camera's availableCameras(), from Codename One's Camera.getCameras(); empty headless or without camera support. */ +public final class Cameras { + private Cameras() { + } + + public static Future> availableCameras() { + DartList out = new DartList(); + if (Display.isInitialized() && Camera.isSupported()) { + CameraInfo[] all = Camera.getCameras(); + for (int i = 0; all != null && i < all.length; i++) { + CameraDescription d = new CameraDescription(); + d.info = all[i]; + d.name = all[i].getId(); + d.lensDirection = all[i].getFacing() == CameraFacing.FRONT ? CameraLensDirection.front + : all[i].getFacing() == CameraFacing.EXTERNAL ? CameraLensDirection.external + : CameraLensDirection.back; + d.sensorOrientation = 90; + out.add(d); + } + } + return Future.value(out); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/FlashMode.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/FlashMode.java new file mode 100644 index 00000000000..c1dbac9ba5a --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/FlashMode.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.camera; + +/** camera's FlashMode. */ +public enum FlashMode { + off, auto, always, torch +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/ImageFormatGroup.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/ImageFormatGroup.java new file mode 100644 index 00000000000..16515a15471 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/ImageFormatGroup.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.camera; + +/** camera's ImageFormatGroup: accepted; photos are JPEG. */ +public enum ImageFormatGroup { + unknown, yuv420, bgra8888, jpeg, nv21 +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/ResolutionPreset.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/ResolutionPreset.java new file mode 100644 index 00000000000..dbdb67c14fb --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/camera/ResolutionPreset.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.camera; + +/** camera's ResolutionPreset, mapped to a preview size in CameraController. */ +public enum ResolutionPreset { + low, medium, high, veryHigh, ultraHigh, max +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/Category.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/Category.java new file mode 100644 index 00000000000..13f7ff4f6f1 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/Category.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.emojipicker; + +/** emoji_picker_flutter's Category. */ +public enum Category { + RECENT, SMILEYS, ANIMALS, FOODS, ACTIVITIES, TRAVEL, OBJECTS, SYMBOLS, FLAGS +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/CategoryEmoji.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/CategoryEmoji.java new file mode 100644 index 00000000000..09a4c56507a --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/CategoryEmoji.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.emojipicker; + +import dart.core.DartList; + +/** emoji_picker_flutter's CategoryEmoji. */ +public class CategoryEmoji { + private final Category category; + private final DartList emoji; + + public CategoryEmoji(Category category, DartList emoji) { + this.category = category; + this.emoji = emoji; + } + + public Category category() { + return category; + } + + public DartList emoji() { + return emoji; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/Config.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/Config.java new file mode 100644 index 00000000000..5101692d60c --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/Config.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.emojipicker; + +/** emoji_picker_flutter's Config: the height is applied, everything else is accepted and ignored. */ +public class Config { + double height = 256; + + public Config() { + } + + public void height(Double v) { + if (v != null) { + height = v.doubleValue(); + } + } + + public void checkPlatformCompatibility(Object v) { + } + + public void emojiViewConfig(Object v) { + } + + public void skinToneConfig(Object v) { + } + + public void categoryViewConfig(Object v) { + } + + public void bottomActionBarConfig(Object v) { + } + + public void searchViewConfig(Object v) { + } + + public void emojiSet(Object v) { + } + + public void locale(Object v) { + } + + public void emojiTextStyle(Object v) { + } + + public void customBackspaceIcon(Object v) { + } + + public void customSearchIcon(Object v) { + } + + public void viewOrderConfig(Object v) { + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/Emoji.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/Emoji.java new file mode 100644 index 00000000000..a92ee83f473 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/Emoji.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.emojipicker; + +/** emoji_picker_flutter's Emoji. Skin tones are not offered. */ +public class Emoji { + private final String emoji; + private final String name; + private boolean hasSkinTone; + + public Emoji(String emoji, String name) { + this.emoji = emoji; + this.name = name; + } + + public void hasSkinTone(Boolean v) { + hasSkinTone = v != null && v.booleanValue(); + } + + public String emoji() { + return emoji; + } + + public String name() { + return name; + } + + public boolean hasSkinTone() { + return hasSkinTone; + } + + @Override + public String toString() { + return "Emoji(emoji: " + emoji + ", name: " + name + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiData.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiData.java new file mode 100644 index 00000000000..61156882b69 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiData.java @@ -0,0 +1,637 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.emojipicker; + +import java.util.ArrayList; +import java.util.List; + +import com.codename1.io.Preferences; +import com.codename1.ui.Display; + +import dart.core.DartList; + +/** + * The built-in emoji set and the recents. Every emoji is treated as supported: + * there is no portable way to ask a font whether it has a glyph, so + * checkPlatformCompatibility is not applied. + */ +final class EmojiData { + private static final String RECENTS = "cn1.emoji_picker.recent"; + private static final int MAX_RECENTS = 28; + private static DartList all; + + private static final String[] SMILEYS = { + "\uD83D\uDE00", "Grinning Face", + "\uD83D\uDE01", "Grinning Face With Smiling Eyes", + "\uD83D\uDE02", "Face With Tears Of Joy", + "\uD83D\uDE03", "Smiling Face With Open Mouth", + "\uD83D\uDE04", "Smiling Face With Open Mouth And Smiling Eyes", + "\uD83D\uDE05", "Smiling Face With Open Mouth And Cold Sweat", + "\uD83D\uDE06", "Smiling Face With Open Mouth And Tightly-closed Eyes", + "\uD83D\uDE07", "Smiling Face With Halo", + "\uD83D\uDE08", "Smiling Face With Horns", + "\uD83D\uDE09", "Winking Face", + "\uD83D\uDE0A", "Smiling Face With Smiling Eyes", + "\uD83D\uDE0B", "Face Savouring Delicious Food", + "\uD83D\uDE0C", "Relieved Face", + "\uD83D\uDE0D", "Smiling Face With Heart-shaped Eyes", + "\uD83D\uDE0E", "Smiling Face With Sunglasses", + "\uD83D\uDE0F", "Smirking Face", + "\uD83D\uDE10", "Neutral Face", + "\uD83D\uDE11", "Expressionless Face", + "\uD83D\uDE12", "Unamused Face", + "\uD83D\uDE13", "Face With Cold Sweat", + "\uD83D\uDE14", "Pensive Face", + "\uD83D\uDE15", "Confused Face", + "\uD83D\uDE16", "Confounded Face", + "\uD83D\uDE17", "Kissing Face", + "\uD83D\uDE18", "Face Throwing A Kiss", + "\uD83D\uDE19", "Kissing Face With Smiling Eyes", + "\uD83D\uDE1A", "Kissing Face With Closed Eyes", + "\uD83D\uDE1B", "Face With Stuck-out Tongue", + "\uD83D\uDE1C", "Face With Stuck-out Tongue And Winking Eye", + "\uD83D\uDE1D", "Face With Stuck-out Tongue And Tightly-closed Eyes", + "\uD83D\uDE1E", "Disappointed Face", + "\uD83D\uDE1F", "Worried Face", + "\uD83D\uDE20", "Angry Face", + "\uD83D\uDE21", "Pouting Face", + "\uD83D\uDE22", "Crying Face", + "\uD83D\uDE23", "Persevering Face", + "\uD83D\uDE24", "Face With Look Of Triumph", + "\uD83D\uDE25", "Disappointed But Relieved Face", + "\uD83D\uDE26", "Frowning Face With Open Mouth", + "\uD83D\uDE27", "Anguished Face", + "\uD83D\uDE28", "Fearful Face", + "\uD83D\uDE29", "Weary Face", + "\uD83D\uDE2A", "Sleepy Face", + "\uD83D\uDE2B", "Tired Face", + "\uD83D\uDE2C", "Grimacing Face", + "\uD83D\uDE2D", "Loudly Crying Face", + "\uD83D\uDE2E", "Face With Open Mouth", + "\uD83D\uDE2F", "Hushed Face", + "\uD83D\uDE30", "Face With Open Mouth And Cold Sweat", + "\uD83D\uDE31", "Face Screaming In Fear", + "\uD83D\uDE32", "Astonished Face", + "\uD83D\uDE33", "Flushed Face", + "\uD83D\uDE34", "Sleeping Face", + "\uD83D\uDE35", "Dizzy Face", + "\uD83D\uDE36", "Face Without Mouth", + "\uD83D\uDE37", "Face With Medical Mask", + "\uD83D\uDE38", "Grinning Cat Face With Smiling Eyes", + "\uD83D\uDE39", "Cat Face With Tears Of Joy", + "\uD83D\uDE3A", "Smiling Cat Face With Open Mouth", + "\uD83D\uDE3B", "Smiling Cat Face With Heart-shaped Eyes", + "\uD83D\uDE3C", "Cat Face With Wry Smile", + "\uD83D\uDE3D", "Kissing Cat Face With Closed Eyes", + "\uD83D\uDE3E", "Pouting Cat Face", + "\uD83D\uDE3F", "Crying Cat Face", + "\uD83D\uDE40", "Weary Cat Face", + "\uD83D\uDE41", "Slightly Frowning Face", + "\uD83D\uDE42", "Slightly Smiling Face", + "\uD83D\uDE43", "Upside-down Face", + "\uD83D\uDE44", "Face With Rolling Eyes", + "\uD83D\uDE45", "Face With No Good Gesture", + "\uD83D\uDE46", "Face With Ok Gesture", + "\uD83D\uDE47", "Person Bowing Deeply", + "\uD83D\uDE48", "See-no-evil Monkey", + "\uD83D\uDE49", "Hear-no-evil Monkey", + "\uD83D\uDE4A", "Speak-no-evil Monkey", + "\uD83D\uDE4B", "Happy Person Raising One Hand", + "\uD83D\uDE4C", "Person Raising Both Hands In Celebration", + "\uD83D\uDE4D", "Person Frowning", + "\uD83D\uDE4E", "Person With Pouting Face", + "\uD83D\uDE4F", "Person With Folded Hands", + }; + + private static final String[] ANIMALS = { + "\uD83D\uDC00", "Rat", + "\uD83D\uDC01", "Mouse", + "\uD83D\uDC02", "Ox", + "\uD83D\uDC03", "Water Buffalo", + "\uD83D\uDC04", "Cow", + "\uD83D\uDC05", "Tiger", + "\uD83D\uDC06", "Leopard", + "\uD83D\uDC07", "Rabbit", + "\uD83D\uDC08", "Cat", + "\uD83D\uDC09", "Dragon", + "\uD83D\uDC0A", "Crocodile", + "\uD83D\uDC0B", "Whale", + "\uD83D\uDC0C", "Snail", + "\uD83D\uDC0D", "Snake", + "\uD83D\uDC0E", "Horse", + "\uD83D\uDC0F", "Ram", + "\uD83D\uDC10", "Goat", + "\uD83D\uDC11", "Sheep", + "\uD83D\uDC12", "Monkey", + "\uD83D\uDC13", "Rooster", + "\uD83D\uDC14", "Chicken", + "\uD83D\uDC15", "Dog", + "\uD83D\uDC16", "Pig", + "\uD83D\uDC17", "Boar", + "\uD83D\uDC18", "Elephant", + "\uD83D\uDC19", "Octopus", + "\uD83D\uDC1A", "Spiral Shell", + "\uD83D\uDC1B", "Bug", + "\uD83D\uDC1C", "Ant", + "\uD83D\uDC1D", "Honeybee", + "\uD83D\uDC1E", "Lady Beetle", + "\uD83D\uDC1F", "Fish", + "\uD83D\uDC20", "Tropical Fish", + "\uD83D\uDC21", "Blowfish", + "\uD83D\uDC22", "Turtle", + "\uD83D\uDC23", "Hatching Chick", + "\uD83D\uDC24", "Baby Chick", + "\uD83D\uDC25", "Front-facing Baby Chick", + "\uD83D\uDC26", "Bird", + "\uD83D\uDC27", "Penguin", + "\uD83D\uDC28", "Koala", + "\uD83D\uDC29", "Poodle", + "\uD83D\uDC2A", "Dromedary Camel", + "\uD83D\uDC2B", "Bactrian Camel", + "\uD83D\uDC2C", "Dolphin", + "\uD83D\uDC2D", "Mouse Face", + "\uD83D\uDC2E", "Cow Face", + "\uD83D\uDC2F", "Tiger Face", + "\uD83D\uDC30", "Rabbit Face", + "\uD83D\uDC31", "Cat Face", + "\uD83D\uDC32", "Dragon Face", + "\uD83D\uDC33", "Spouting Whale", + "\uD83D\uDC34", "Horse Face", + "\uD83D\uDC35", "Monkey Face", + "\uD83D\uDC36", "Dog Face", + "\uD83D\uDC37", "Pig Face", + "\uD83D\uDC38", "Frog Face", + "\uD83D\uDC39", "Hamster Face", + "\uD83D\uDC3A", "Wolf Face", + "\uD83D\uDC3B", "Bear Face", + "\uD83D\uDC3C", "Panda Face", + "\uD83D\uDC3D", "Pig Nose", + "\uD83D\uDC3E", "Paw Prints", + }; + + private static final String[] FOODS = { + "\uD83C\uDF45", "Tomato", + "\uD83C\uDF46", "Aubergine", + "\uD83C\uDF47", "Grapes", + "\uD83C\uDF48", "Melon", + "\uD83C\uDF49", "Watermelon", + "\uD83C\uDF4A", "Tangerine", + "\uD83C\uDF4B", "Lemon", + "\uD83C\uDF4C", "Banana", + "\uD83C\uDF4D", "Pineapple", + "\uD83C\uDF4E", "Red Apple", + "\uD83C\uDF4F", "Green Apple", + "\uD83C\uDF50", "Pear", + "\uD83C\uDF51", "Peach", + "\uD83C\uDF52", "Cherries", + "\uD83C\uDF53", "Strawberry", + "\uD83C\uDF54", "Hamburger", + "\uD83C\uDF55", "Slice Of Pizza", + "\uD83C\uDF56", "Meat On Bone", + "\uD83C\uDF57", "Poultry Leg", + "\uD83C\uDF58", "Rice Cracker", + "\uD83C\uDF59", "Rice Ball", + "\uD83C\uDF5A", "Cooked Rice", + "\uD83C\uDF5B", "Curry And Rice", + "\uD83C\uDF5C", "Steaming Bowl", + "\uD83C\uDF5D", "Spaghetti", + "\uD83C\uDF5E", "Bread", + "\uD83C\uDF5F", "French Fries", + "\uD83C\uDF60", "Roasted Sweet Potato", + "\uD83C\uDF61", "Dango", + "\uD83C\uDF62", "Oden", + "\uD83C\uDF63", "Sushi", + "\uD83C\uDF64", "Fried Shrimp", + "\uD83C\uDF65", "Fish Cake With Swirl Design", + "\uD83C\uDF66", "Soft Ice Cream", + "\uD83C\uDF67", "Shaved Ice", + "\uD83C\uDF68", "Ice Cream", + "\uD83C\uDF69", "Doughnut", + "\uD83C\uDF6A", "Cookie", + "\uD83C\uDF6B", "Chocolate Bar", + "\uD83C\uDF6C", "Candy", + "\uD83C\uDF6D", "Lollipop", + "\uD83C\uDF6E", "Custard", + "\uD83C\uDF6F", "Honey Pot", + "\uD83C\uDF70", "Shortcake", + "\uD83C\uDF71", "Bento Box", + "\uD83C\uDF72", "Pot Of Food", + "\uD83C\uDF73", "Cooking", + "\uD83C\uDF74", "Fork And Knife", + "\uD83C\uDF75", "Teacup Without Handle", + "\uD83C\uDF76", "Sake Bottle And Cup", + "\uD83C\uDF77", "Wine Glass", + "\uD83C\uDF78", "Cocktail Glass", + "\uD83C\uDF79", "Tropical Drink", + "\uD83C\uDF7A", "Beer Mug", + "\uD83C\uDF7B", "Clinking Beer Mugs", + "\uD83C\uDF7C", "Baby Bottle", + "\uD83C\uDF7D", "Fork And Knife With Plate", + "\uD83C\uDF7E", "Bottle With Popping Cork", + "\uD83C\uDF7F", "Popcorn", + }; + + private static final String[] ACTIVITIES = { + "\uD83C\uDF80", "Ribbon", + "\uD83C\uDF81", "Wrapped Present", + "\uD83C\uDF82", "Birthday Cake", + "\uD83C\uDF83", "Jack-o-lantern", + "\uD83C\uDF84", "Christmas Tree", + "\uD83C\uDF85", "Father Christmas", + "\uD83C\uDF86", "Fireworks", + "\uD83C\uDF87", "Firework Sparkler", + "\uD83C\uDF88", "Balloon", + "\uD83C\uDF89", "Party Popper", + "\uD83C\uDF8A", "Confetti Ball", + "\uD83C\uDF8B", "Tanabata Tree", + "\uD83C\uDF8C", "Crossed Flags", + "\uD83C\uDF8D", "Pine Decoration", + "\uD83C\uDF8E", "Japanese Dolls", + "\uD83C\uDF8F", "Carp Streamer", + "\uD83C\uDF90", "Wind Chime", + "\uD83C\uDF91", "Moon Viewing Ceremony", + "\uD83C\uDF92", "School Satchel", + "\uD83C\uDF93", "Graduation Cap", + "\uD83C\uDFA0", "Carousel Horse", + "\uD83C\uDFA1", "Ferris Wheel", + "\uD83C\uDFA2", "Roller Coaster", + "\uD83C\uDFA3", "Fishing Pole And Fish", + "\uD83C\uDFA4", "Microphone", + "\uD83C\uDFA5", "Movie Camera", + "\uD83C\uDFA6", "Cinema", + "\uD83C\uDFA7", "Headphone", + "\uD83C\uDFA8", "Artist Palette", + "\uD83C\uDFA9", "Top Hat", + "\uD83C\uDFAA", "Circus Tent", + "\uD83C\uDFAB", "Ticket", + "\uD83C\uDFAC", "Clapper Board", + "\uD83C\uDFAD", "Performing Arts", + "\uD83C\uDFAE", "Video Game", + "\uD83C\uDFAF", "Direct Hit", + "\uD83C\uDFB0", "Slot Machine", + "\uD83C\uDFB1", "Billiards", + "\uD83C\uDFB2", "Game Die", + "\uD83C\uDFB3", "Bowling", + "\uD83C\uDFB4", "Flower Playing Cards", + "\uD83C\uDFB5", "Musical Note", + "\uD83C\uDFB6", "Multiple Musical Notes", + "\uD83C\uDFB7", "Saxophone", + "\uD83C\uDFB8", "Guitar", + "\uD83C\uDFB9", "Musical Keyboard", + "\uD83C\uDFBA", "Trumpet", + "\uD83C\uDFBB", "Violin", + "\uD83C\uDFBC", "Musical Score", + "\uD83C\uDFBD", "Running Shirt With Sash", + "\uD83C\uDFBE", "Tennis Racquet And Ball", + "\uD83C\uDFBF", "Ski And Ski Boot", + "\uD83C\uDFC0", "Basketball And Hoop", + "\uD83C\uDFC1", "Chequered Flag", + "\uD83C\uDFC2", "Snowboarder", + "\uD83C\uDFC3", "Runner", + "\uD83C\uDFC4", "Surfer", + "\uD83C\uDFC5", "Sports Medal", + "\uD83C\uDFC6", "Trophy", + "\uD83C\uDFC7", "Horse Racing", + "\uD83C\uDFC8", "American Football", + "\uD83C\uDFC9", "Rugby Football", + "\uD83C\uDFCA", "Swimmer", + }; + + private static final String[] TRAVEL = { + "\uD83D\uDE80", "Rocket", + "\uD83D\uDE81", "Helicopter", + "\uD83D\uDE82", "Steam Locomotive", + "\uD83D\uDE83", "Railway Car", + "\uD83D\uDE84", "High-speed Train", + "\uD83D\uDE85", "High-speed Train With Bullet Nose", + "\uD83D\uDE86", "Train", + "\uD83D\uDE87", "Metro", + "\uD83D\uDE88", "Light Rail", + "\uD83D\uDE89", "Station", + "\uD83D\uDE8A", "Tram", + "\uD83D\uDE8B", "Tram Car", + "\uD83D\uDE8C", "Bus", + "\uD83D\uDE8D", "Oncoming Bus", + "\uD83D\uDE8E", "Trolleybus", + "\uD83D\uDE8F", "Bus Stop", + "\uD83D\uDE90", "Minibus", + "\uD83D\uDE91", "Ambulance", + "\uD83D\uDE92", "Fire Engine", + "\uD83D\uDE93", "Police Car", + "\uD83D\uDE94", "Oncoming Police Car", + "\uD83D\uDE95", "Taxi", + "\uD83D\uDE96", "Oncoming Taxi", + "\uD83D\uDE97", "Automobile", + "\uD83D\uDE98", "Oncoming Automobile", + "\uD83D\uDE99", "Recreational Vehicle", + "\uD83D\uDE9A", "Delivery Truck", + "\uD83D\uDE9B", "Articulated Lorry", + "\uD83D\uDE9C", "Tractor", + "\uD83D\uDE9D", "Monorail", + "\uD83D\uDE9E", "Mountain Railway", + "\uD83D\uDE9F", "Suspension Railway", + "\uD83D\uDEA0", "Mountain Cableway", + "\uD83D\uDEA1", "Aerial Tramway", + "\uD83D\uDEA2", "Ship", + "\uD83D\uDEA3", "Rowboat", + "\uD83D\uDEA4", "Speedboat", + "\uD83D\uDEA5", "Horizontal Traffic Light", + "\uD83D\uDEA6", "Vertical Traffic Light", + "\uD83D\uDEA7", "Construction Sign", + "\uD83D\uDEA8", "Police Cars Revolving Light", + "\uD83D\uDEA9", "Triangular Flag On Post", + "\uD83D\uDEAA", "Door", + "\uD83D\uDEAB", "No Entry Sign", + "\uD83D\uDEAC", "Smoking Symbol", + "\uD83D\uDEAD", "No Smoking Symbol", + "\uD83D\uDEAE", "Put Litter In Its Place Symbol", + "\uD83D\uDEAF", "Do Not Litter Symbol", + "\uD83D\uDEB0", "Potable Water Symbol", + "\uD83D\uDEB1", "Non-potable Water Symbol", + "\uD83D\uDEB2", "Bicycle", + "\uD83D\uDEB3", "No Bicycles", + "\uD83D\uDEB4", "Bicyclist", + "\uD83D\uDEB5", "Mountain Bicyclist", + "\uD83D\uDEB6", "Pedestrian", + "\uD83D\uDEB7", "No Pedestrians", + "\uD83D\uDEB8", "Children Crossing", + "\uD83D\uDEB9", "Mens Symbol", + "\uD83D\uDEBA", "Womens Symbol", + "\uD83D\uDEBB", "Restroom", + "\uD83D\uDEBC", "Baby Symbol", + "\uD83D\uDEBD", "Toilet", + "\uD83D\uDEBE", "Water Closet", + "\uD83D\uDEBF", "Shower", + "\uD83D\uDEC0", "Bath", + "\uD83D\uDEC1", "Bathtub", + "\uD83D\uDEC2", "Passport Control", + "\uD83D\uDEC3", "Customs", + "\uD83D\uDEC4", "Baggage Claim", + "\uD83D\uDEC5", "Left Luggage", + }; + + private static final String[] OBJECTS = { + "\uD83D\uDCA0", "Diamond Shape With A Dot Inside", + "\uD83D\uDCA1", "Electric Light Bulb", + "\uD83D\uDCA2", "Anger Symbol", + "\uD83D\uDCA3", "Bomb", + "\uD83D\uDCA4", "Sleeping Symbol", + "\uD83D\uDCA5", "Collision Symbol", + "\uD83D\uDCA6", "Splashing Sweat Symbol", + "\uD83D\uDCA7", "Droplet", + "\uD83D\uDCA8", "Dash Symbol", + "\uD83D\uDCA9", "Pile Of Poo", + "\uD83D\uDCAA", "Flexed Biceps", + "\uD83D\uDCAB", "Dizzy Symbol", + "\uD83D\uDCAC", "Speech Balloon", + "\uD83D\uDCAD", "Thought Balloon", + "\uD83D\uDCAE", "White Flower", + "\uD83D\uDCAF", "Hundred Points Symbol", + "\uD83D\uDCB0", "Money Bag", + "\uD83D\uDCB1", "Currency Exchange", + "\uD83D\uDCB2", "Heavy Dollar Sign", + "\uD83D\uDCB3", "Credit Card", + "\uD83D\uDCB4", "Banknote With Yen Sign", + "\uD83D\uDCB5", "Banknote With Dollar Sign", + "\uD83D\uDCB6", "Banknote With Euro Sign", + "\uD83D\uDCB7", "Banknote With Pound Sign", + "\uD83D\uDCB8", "Money With Wings", + "\uD83D\uDCB9", "Chart With Upwards Trend And Yen Sign", + "\uD83D\uDCBA", "Seat", + "\uD83D\uDCBB", "Personal Computer", + "\uD83D\uDCBC", "Briefcase", + "\uD83D\uDCBD", "Minidisc", + "\uD83D\uDCBE", "Floppy Disk", + "\uD83D\uDCBF", "Optical Disc", + "\uD83D\uDCC0", "Dvd", + "\uD83D\uDCC1", "File Folder", + "\uD83D\uDCC2", "Open File Folder", + "\uD83D\uDCC3", "Page With Curl", + "\uD83D\uDCC4", "Page Facing Up", + "\uD83D\uDCC5", "Calendar", + "\uD83D\uDCC6", "Tear-off Calendar", + "\uD83D\uDCC7", "Card Index", + "\uD83D\uDCC8", "Chart With Upwards Trend", + "\uD83D\uDCC9", "Chart With Downwards Trend", + "\uD83D\uDCCA", "Bar Chart", + "\uD83D\uDCCB", "Clipboard", + "\uD83D\uDCCC", "Pushpin", + "\uD83D\uDCCD", "Round Pushpin", + "\uD83D\uDCCE", "Paperclip", + "\uD83D\uDCCF", "Straight Ruler", + "\uD83D\uDCD0", "Triangular Ruler", + "\uD83D\uDCD1", "Bookmark Tabs", + "\uD83D\uDCD2", "Ledger", + "\uD83D\uDCD3", "Notebook", + "\uD83D\uDCD4", "Notebook With Decorative Cover", + "\uD83D\uDCD5", "Closed Book", + "\uD83D\uDCD6", "Open Book", + "\uD83D\uDCD7", "Green Book", + "\uD83D\uDCD8", "Blue Book", + "\uD83D\uDCD9", "Orange Book", + "\uD83D\uDCDA", "Books", + "\uD83D\uDCDB", "Name Badge", + "\uD83D\uDCDC", "Scroll", + "\uD83D\uDCDD", "Memo", + "\uD83D\uDCDE", "Telephone Receiver", + "\uD83D\uDCDF", "Pager", + "\uD83D\uDCE0", "Fax Machine", + "\uD83D\uDCE1", "Satellite Antenna", + "\uD83D\uDCE2", "Public Address Loudspeaker", + "\uD83D\uDCE3", "Cheering Megaphone", + "\uD83D\uDCE4", "Outbox Tray", + "\uD83D\uDCE5", "Inbox Tray", + "\uD83D\uDCE6", "Package", + "\uD83D\uDCE7", "E-mail Symbol", + "\uD83D\uDCE8", "Incoming Envelope", + "\uD83D\uDCE9", "Envelope With Downwards Arrow Above", + "\uD83D\uDCEA", "Closed Mailbox With Lowered Flag", + "\uD83D\uDCEB", "Closed Mailbox With Raised Flag", + "\uD83D\uDCEC", "Open Mailbox With Raised Flag", + "\uD83D\uDCED", "Open Mailbox With Lowered Flag", + "\uD83D\uDCEE", "Postbox", + "\uD83D\uDCEF", "Postal Horn", + "\uD83D\uDCF0", "Newspaper", + "\uD83D\uDCF1", "Mobile Phone", + "\uD83D\uDCF2", "Mobile Phone With Rightwards Arrow At Left", + "\uD83D\uDCF3", "Vibration Mode", + "\uD83D\uDCF4", "Mobile Phone Off", + "\uD83D\uDCF5", "No Mobile Phones", + "\uD83D\uDCF6", "Antenna With Bars", + "\uD83D\uDCF7", "Camera", + "\uD83D\uDCF8", "Camera With Flash", + "\uD83D\uDCF9", "Video Camera", + "\uD83D\uDCFA", "Television", + "\uD83D\uDCFB", "Radio", + "\uD83D\uDCFC", "Videocassette", + }; + + private static final String[] SYMBOLS = { + "\u2764", "Heavy Black Heart", + "\uD83D\uDC93", "Beating Heart", + "\uD83D\uDC94", "Broken Heart", + "\uD83D\uDC95", "Two Hearts", + "\uD83D\uDC96", "Sparkling Heart", + "\uD83D\uDC97", "Growing Heart", + "\uD83D\uDC98", "Heart With Arrow", + "\uD83D\uDC99", "Blue Heart", + "\uD83D\uDC9A", "Green Heart", + "\uD83D\uDC9B", "Yellow Heart", + "\uD83D\uDC9C", "Purple Heart", + "\uD83D\uDC9D", "Heart With Ribbon", + "\uD83D\uDC9E", "Revolving Hearts", + "\uD83D\uDC9F", "Heart Decoration", + "\u2648", "Aries", + "\u2649", "Taurus", + "\u264A", "Gemini", + "\u264B", "Cancer", + "\u264C", "Leo", + "\u264D", "Virgo", + "\u264E", "Libra", + "\u264F", "Scorpius", + "\u2650", "Sagittarius", + "\u2651", "Capricorn", + "\u2652", "Aquarius", + "\u2653", "Pisces", + }; + + private static final String[] FLAGS = { + "\uD83C\uDDFA\uD83C\uDDF8", "Flag US", + "\uD83C\uDDEC\uD83C\uDDE7", "Flag GB", + "\uD83C\uDDE8\uD83C\uDDE6", "Flag CA", + "\uD83C\uDDE6\uD83C\uDDFA", "Flag AU", + "\uD83C\uDDEB\uD83C\uDDF7", "Flag FR", + "\uD83C\uDDE9\uD83C\uDDEA", "Flag DE", + "\uD83C\uDDEE\uD83C\uDDF9", "Flag IT", + "\uD83C\uDDEA\uD83C\uDDF8", "Flag ES", + "\uD83C\uDDF5\uD83C\uDDF9", "Flag PT", + "\uD83C\uDDF3\uD83C\uDDF1", "Flag NL", + "\uD83C\uDDF8\uD83C\uDDEA", "Flag SE", + "\uD83C\uDDF3\uD83C\uDDF4", "Flag NO", + "\uD83C\uDDE9\uD83C\uDDF0", "Flag DK", + "\uD83C\uDDEB\uD83C\uDDEE", "Flag FI", + "\uD83C\uDDEE\uD83C\uDDEA", "Flag IE", + "\uD83C\uDDEE\uD83C\uDDF1", "Flag IL", + "\uD83C\uDDEF\uD83C\uDDF5", "Flag JP", + "\uD83C\uDDF0\uD83C\uDDF7", "Flag KR", + "\uD83C\uDDE8\uD83C\uDDF3", "Flag CN", + "\uD83C\uDDEE\uD83C\uDDF3", "Flag IN", + "\uD83C\uDDE7\uD83C\uDDF7", "Flag BR", + "\uD83C\uDDF2\uD83C\uDDFD", "Flag MX", + "\uD83C\uDDE6\uD83C\uDDF7", "Flag AR", + "\uD83C\uDDFF\uD83C\uDDE6", "Flag ZA", + "\uD83C\uDDF3\uD83C\uDDEC", "Flag NG", + "\uD83C\uDDEA\uD83C\uDDEC", "Flag EG", + "\uD83C\uDDF9\uD83C\uDDF7", "Flag TR", + "\uD83C\uDDEC\uD83C\uDDF7", "Flag GR", + "\uD83C\uDDF5\uD83C\uDDF1", "Flag PL", + "\uD83C\uDDFA\uD83C\uDDE6", "Flag UA", + }; + + private EmojiData() { + } + + static synchronized DartList all() { + if (all == null) { + all = new DartList(); + add(Category.SMILEYS, SMILEYS); + add(Category.ANIMALS, ANIMALS); + add(Category.FOODS, FOODS); + add(Category.ACTIVITIES, ACTIVITIES); + add(Category.TRAVEL, TRAVEL); + add(Category.OBJECTS, OBJECTS); + add(Category.SYMBOLS, SYMBOLS); + add(Category.FLAGS, FLAGS); + } + return all; + } + + private static void add(Category c, String[] pairs) { + DartList list = new DartList(); + for (int i = 0; i + 1 < pairs.length; i += 2) { + list.add(new Emoji(pairs[i], pairs[i + 1])); + } + all.add(new CategoryEmoji(c, list)); + } + + static Emoji find(String e) { + for (CategoryEmoji c : all()) { + for (Emoji x : c.emoji()) { + if (x.emoji().equals(e)) { + return x; + } + } + } + return null; + } + + /** Recents, newest first; empty without a display (Preferences needs storage). */ + static DartList recents() { + DartList out = new DartList(); + if (!Display.isInitialized()) { + return out; + } + String s = Preferences.get(RECENTS, ""); + int start = 0; + while (start < s.length()) { + int end = s.indexOf('\n', start); + if (end < 0) { + end = s.length(); + } + Emoji e = find(s.substring(start, end)); + if (e != null) { + out.add(e); + } + start = end + 1; + } + return out; + } + + static void remember(Emoji e) { + if (!Display.isInitialized()) { + return; + } + List keep = new ArrayList(); + keep.add(e.emoji()); + for (Emoji r : recents()) { + if (!r.emoji().equals(e.emoji()) && keep.size() < MAX_RECENTS) { + keep.add(r.emoji()); + } + } + StringBuilder b = new StringBuilder(); + for (String k : keep) { + if (b.length() > 0) { + b.append('\n'); + } + b.append(k); + } + Preferences.set(RECENTS, b.toString()); + } + + static void clearRecents() { + if (Display.isInitialized()) { + Preferences.delete(RECENTS); + } + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiGrid.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiGrid.java new file mode 100644 index 00000000000..48e839b3c15 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiGrid.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.emojipicker; + +import com.codename1.ui.Button; +import com.codename1.ui.Container; +import com.codename1.ui.Tabs; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.layouts.GridLayout; + +import dart.core.DartList; + +/** Builds the tabs, grids and backspace button of an {@link EmojiPicker}. */ +final class EmojiGrid { + private static final int COLUMNS = 8; + + private EmojiGrid() { + } + + static Container create(final EmojiPicker picker) { + Tabs tabs = new Tabs(); + DartList recent = EmojiData.recents(); + tabs.addTab("\u23F1", grid(picker, Category.RECENT, recent)); + for (CategoryEmoji c : EmojiData.all()) { + tabs.addTab(c.emoji().get(0).emoji(), grid(picker, c.category(), c.emoji())); + } + if (recent.isEmpty()) { + tabs.setSelectedIndex(1); + } + Button back = new Button("\u232B"); + back.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + picker.backspace(); + } + }); + Container root = new Container(new BorderLayout()); + root.add(BorderLayout.CENTER, tabs); + root.add(BorderLayout.SOUTH, BorderLayout.east(back)); + return root; + } + + private static Container grid(final EmojiPicker picker, final Category cat, DartList list) { + int rows = Math.max(1, (list.size() + COLUMNS - 1) / COLUMNS); + Container g = new Container(new GridLayout(rows, COLUMNS)); + g.setScrollableY(true); + for (final Emoji e : list) { + Button b = new Button(e.emoji()); + b.setUIID("Label"); + b.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + picker.selected(cat, e); + } + }); + g.add(b); + } + return g; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiPicker.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiPicker.java new file mode 100644 index 00000000000..eb4885ac6ca --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiPicker.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.emojipicker; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.TextEditingController; +import com.codename1.flutter.rendering.Size; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * emoji_picker_flutter's EmojiPicker, drawn with Codename One components: a tab per + * category (recents first), a grid of emoji buttons and a backspace button. A + * selection is appended to the textEditingController when there is one, then + * reported; backspace removes the last emoji from it. Only Config.height is applied. + */ +public class EmojiPicker extends Widget { + Funcs.VoidFunc2 onEmojiSelected; + Funcs.VoidFunc0 onBackspacePressed; + TextEditingController textEditingController; + Config config = new Config(); + + public EmojiPicker() { + } + + /** The built-in set, a static getter in Dart. */ + public static final DartList defaultEmojiSet = EmojiData.all(); + + public void onEmojiSelected(Funcs.VoidFunc2 v) { + onEmojiSelected = v; + } + + public void onBackspacePressed(Funcs.VoidFunc0 v) { + onBackspacePressed = v; + } + + public void textEditingController(TextEditingController v) { + textEditingController = v; + } + + /** Accepted and ignored. */ + public void scrollController(Object v) { + } + + public void config(Config v) { + if (v != null) { + config = v; + } + } + + Size preferredLayoutSize() { + return new Size(360, config.height); + } + + void selected(Category c, Emoji e) { + EmojiData.remember(e); + if (textEditingController != null) { + String t = textEditingController.text(); + textEditingController.text((t == null ? "" : t) + e.emoji()); + } + if (onEmojiSelected != null) { + onEmojiSelected.call(c, e); + } + } + + void backspace() { + if (textEditingController != null) { + String t = textEditingController.text(); + if (t != null && t.length() > 0) { + int cut = t.length() - 1; + if (cut > 0 && Character.isLowSurrogate(t.charAt(cut)) && Character.isHighSurrogate(t.charAt(cut - 1))) { + cut--; + } + textEditingController.text(t.substring(0, cut)); + } + } + if (onBackspacePressed != null) { + onBackspacePressed.call(); + } + } + + @Override + public Element createElement() { + return new EmojiPickerRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiPickerRenderElement.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiPickerRenderElement.java new file mode 100644 index 00000000000..55686bc203e --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiPickerRenderElement.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.emojipicker; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; + +/** Hosts the Codename One emoji grid of an {@link EmojiPicker}; none headless. */ +public class EmojiPickerRenderElement extends RenderElement { + public EmojiPickerRenderElement(EmojiPicker widget) { + super(widget); + } + + @Override + protected Component createComponent() { + if (!com.codename1.ui.Display.isInitialized()) { + return null; + } + return EmojiGrid.create((EmojiPicker) widget()); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + Size want = ((EmojiPicker) widget()).preferredLayoutSize(); + double w = constraints.hasBoundedWidth() ? constraints.maxWidth() : Dp.px(want.width()); + double h = constraints.hasBoundedHeight() ? constraints.maxHeight() : Dp.px(want.height()); + return constraints.constrain(new Size(w, h)); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiPickerUtils.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiPickerUtils.java new file mode 100644 index 00000000000..27d3a7baeb3 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiPickerUtils.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.emojipicker; + +import dart.async.Future; +import dart.core.DartList; + +/** emoji_picker_flutter's EmojiPickerUtils. Search matches the keyword anywhere in the name, ignoring case. */ +public class EmojiPickerUtils { + public EmojiPickerUtils() { + } + + public Future> getRecentEmojis() { + return Future.value(EmojiData.recents()); + } + + public Future> searchEmoji(String keyword, DartList data) { + DartList out = new DartList(); + String k = keyword == null ? "" : keyword; + for (CategoryEmoji c : data == null ? EmojiData.all() : data) { + for (Emoji e : c.emoji()) { + if (contains(e.name(), k)) { + out.add(e); + } + } + } + return Future.value(out); + } + + private static boolean contains(String hay, String needle) { + for (int i = 0; i + needle.length() <= hay.length(); i++) { + if (hay.regionMatches(true, i, needle, 0, needle.length())) { + return true; + } + } + return false; + } + + public Future clearRecentEmojis() { + EmojiData.clearRecents(); + return Future.value(null); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/FilePicker.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/FilePicker.java new file mode 100644 index 00000000000..40a0544cbf9 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/FilePicker.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.filepicker; + +import com.codename1.io.FileSystemStorage; +import com.codename1.io.Util; +import com.codename1.io.filechooser.FileDialogResult; +import com.codename1.io.filechooser.FileTypeFilter; +import com.codename1.io.filechooser.OpenFileRequest; +import com.codename1.io.filechooser.SaveFileRequest; +import com.codename1.ui.Display; +import com.codename1.util.SuccessCallback; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +import dart.async.Completer; +import dart.async.Future; +import dart.core.DartList; +import dart.typed_data.Uint8List; + +/** + * file_picker's {@code FilePicker}, over {@code Display.showOpenDialog} and + * {@code showSaveDialog}. {@code type} and {@code allowedExtensions} become the dialog's + * filter, {@code withData} reads the bytes, {@code saveFile} writes {@code bytes} to the + * chosen path. Titles and the initial directory are passed on; compression, read streams, + * {@code onFileLoading} and {@code lockParentWindow} are accepted and not applied. A + * cancelled dialog, or no display to show one on, answers null, as the plugin does. + */ +public class FilePicker { + + /** {@code FilePicker.platform}. */ + public static FilePicker platform = new FilePicker(); + + public FilePicker() { + } + + static FileTypeFilter filter(FileType type, List extensions) { + if (type == FileType.image) { + return new FileTypeFilter("Images").mimeTypes("image/*"); + } + if (type == FileType.video) { + return new FileTypeFilter("Videos").mimeTypes("video/*"); + } + if (type == FileType.audio) { + return new FileTypeFilter("Audio").mimeTypes("audio/*"); + } + if (type == FileType.media) { + return new FileTypeFilter("Media").mimeTypes("image/*", "video/*"); + } + if (type == FileType.custom && extensions != null && !extensions.isEmpty()) { + String[] ext = new String[extensions.size()]; + for (int i = 0; i < ext.length; i++) { + String e = String.valueOf(extensions.get(i)); + ext[i] = e.startsWith(".") ? e.substring(1) : e; + } + return new FileTypeFilter("Files").extensions(ext); + } + return null; + } + + static String nameOf(String path) { + int slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); + return slash < 0 ? path : path.substring(slash + 1); + } + + static PlatformFile file(String path, boolean withData) { + FileSystemStorage fs = FileSystemStorage.getInstance(); + Uint8List bytes = null; + long size = 0; + try { + size = fs.getLength(path); + if (withData) { + InputStream in = fs.openInputStream(path); + try { + byte[] b = Util.readInputStream(in); + bytes = Uint8List.fromBytes(b); + size = b.length; + } finally { + Util.cleanup(in); + } + } + } catch (java.io.IOException e) { + com.codename1.io.Log.e(e); + } + return new PlatformFile(path, nameOf(path), size, bytes, null, path); + } + + public Future pickFiles(String dialogTitle, String initialDirectory, FileType type, + List allowedExtensions, Object onFileLoading, Boolean allowCompression, Long compressionQuality, + Boolean allowMultiple, Boolean withData, Boolean withReadStream, Boolean lockParentWindow, + Boolean readSequential) { + if (type == FileType.custom && (allowedExtensions == null || allowedExtensions.isEmpty())) { + return Future.error(new dart.core.ArgumentError( + "If you are using a custom extension filter, please use the FileType.custom instead.")); + } + if (!Display.isInitialized()) { + return Future.value(null); + } + OpenFileRequest req = new OpenFileRequest().multiSelect(Boolean.TRUE.equals(allowMultiple)); + if (dialogTitle != null) { + req.title(dialogTitle); + } + if (initialDirectory != null) { + req.initialDirectory(initialDirectory); + } + FileTypeFilter f = filter(type, allowedExtensions); + if (f != null) { + req.addFilter(f); + } + final boolean data = Boolean.TRUE.equals(withData); + final Completer done = new Completer(); + Display.getInstance().showOpenDialog(req, new SuccessCallback() { + @Override + public void onSucess(FileDialogResult r) { + String[] paths = r == null || r.isCancelled() ? null : r.getPaths(); + if (paths == null || paths.length == 0) { + done.complete(null); + return; + } + DartList files = new DartList(); + for (String p : paths) { + files.add(file(p, data)); + } + done.complete(new FilePickerResult(files)); + } + }); + return done.future(); + } + + public Future getDirectoryPath(String dialogTitle, Boolean lockParentWindow, String initialDirectory) { + if (!Display.isInitialized()) { + return Future.value(null); + } + OpenFileRequest req = new OpenFileRequest().directories(true); + if (dialogTitle != null) { + req.title(dialogTitle); + } + if (initialDirectory != null) { + req.initialDirectory(initialDirectory); + } + final Completer done = new Completer(); + Display.getInstance().showOpenDialog(req, new SuccessCallback() { + @Override + public void onSucess(FileDialogResult r) { + done.complete(r == null || r.isCancelled() ? null : r.getPath()); + } + }); + return done.future(); + } + + public Future saveFile(String dialogTitle, String fileName, String initialDirectory, FileType type, + List allowedExtensions, final Uint8List bytes, Boolean lockParentWindow) { + if (!Display.isInitialized()) { + return Future.value(null); + } + SaveFileRequest req = new SaveFileRequest(); + if (dialogTitle != null) { + req.title(dialogTitle); + } + if (fileName != null) { + req.suggestedName(fileName); + } + if (initialDirectory != null) { + req.initialDirectory(initialDirectory); + } + FileTypeFilter f = filter(type, allowedExtensions); + if (f != null) { + req.addFilter(f); + } + final Completer done = new Completer(); + Display.getInstance().showSaveDialog(req, new SuccessCallback() { + @Override + public void onSucess(FileDialogResult r) { + String path = r == null || r.isCancelled() ? null : r.getPath(); + if (path != null && bytes != null) { + OutputStream out = null; + try { + out = FileSystemStorage.getInstance().openOutputStream(path); + out.write(bytes.toBytes()); + } catch (java.io.IOException e) { + Util.cleanup(out); + out = null; + done.completeError(e); + return; + } finally { + Util.cleanup(out); + } + } + done.complete(path); + } + }); + return done.future(); + } + + /** The picked files are the platform's own, not copies, so there is nothing to clear. */ + public Future clearTemporaryFiles() { + return Future.value(Boolean.TRUE); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/FilePickerResult.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/FilePickerResult.java new file mode 100644 index 00000000000..7797c9c0448 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/FilePickerResult.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.filepicker; + +import dart.core.DartList; + +/** file_picker's {@code FilePickerResult}. */ +public class FilePickerResult { + private final DartList files; + + public FilePickerResult(DartList files) { + this.files = files; + } + + public DartList files() { + return files; + } + + public long count() { + return files.size(); + } + + public boolean isSinglePick() { + return files.size() == 1; + } + + public DartList paths() { + DartList out = new DartList(); + for (PlatformFile f : files) { + out.add(f.path()); + } + return out; + } + + public DartList names() { + DartList out = new DartList(); + for (PlatformFile f : files) { + out.add(f.name()); + } + return out; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/FileType.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/FileType.java new file mode 100644 index 00000000000..6b318a5fed0 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/FileType.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.filepicker; + +/** file_picker's {@code FileType}. */ +public enum FileType { + any, media, image, video, audio, custom +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/PlatformFile.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/PlatformFile.java new file mode 100644 index 00000000000..cb212ceb8dc --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/filepicker/PlatformFile.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.filepicker; + +import dart.typed_data.Uint8List; + +/** file_picker's {@code PlatformFile}: a picked file's path, name, size and, on request, bytes. */ +public class PlatformFile { + private String path; + private String name; + private long size; + private Uint8List bytes; + private String identifier; + + public PlatformFile() { + } + + public void path(String v) { + path = v; + } + + public void name(String v) { + name = v; + } + + public void size(long v) { + size = v; + } + + public void bytes(Uint8List v) { + bytes = v; + } + + public void identifier(String v) { + identifier = v; + } + + public PlatformFile(String path, String name, long size, Uint8List bytes, Object readStream, + String identifier) { + this.path = path; + this.name = name; + this.size = size; + this.bytes = bytes; + this.identifier = identifier; + } + + /** Accepted, not kept: files are read whole or by path. */ + public void readStream(Object v) { + } + + public String path() { + return path; + } + + public String name() { + return name; + } + + public long size() { + return size; + } + + public Uint8List bytes() { + return bytes; + } + + public String identifier() { + return identifier; + } + + public String extension() { + int dot = name == null ? -1 : name.lastIndexOf('.'); + return dot < 0 ? null : name.substring(dot + 1); + } + + @Override + public String toString() { + return "PlatformFile(path: " + path + ", name: " + name + ", size: " + size + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/AuthCredential.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/AuthCredential.java new file mode 100644 index 00000000000..b89c6793e74 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/AuthCredential.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebase; + +/** + * firebase_auth's {@code AuthCredential}: what {@code signInWithCredential} exchanges + * for a Firebase session. A federated one ({@link OAuthCredential}) carries the + * identity provider's tokens; an email one ({@link EmailAuthProvider#credential}) + * the address and password. + */ +public class AuthCredential { + final String providerId; + final String signInMethod; + String email; + String password; + + AuthCredential(String providerId, String signInMethod) { + this.providerId = providerId; + this.signInMethod = signInMethod; + } + + public String providerId() { + return providerId; + } + + public String signInMethod() { + return signInMethod; + } + + /** Always null: Firebase keys a credential by its tokens, not a number. */ + public Long token() { + return null; + } + + public String accessToken() { + return null; + } + + @Override + public String toString() { + return "AuthCredential(providerId: " + providerId + ", signInMethod: " + signInMethod + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/EmailAuthProvider.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/EmailAuthProvider.java new file mode 100644 index 00000000000..c6a7c4ec511 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/EmailAuthProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebase; + +/** firebase_auth's {@code EmailAuthProvider}. */ +public final class EmailAuthProvider { + public static final String PROVIDER_ID = "password"; + public static final String EMAIL_PASSWORD_SIGN_IN_METHOD = "password"; + + private EmailAuthProvider() { + } + + public static AuthCredential credential(String email, String password) { + AuthCredential c = new AuthCredential(PROVIDER_ID, EMAIL_PASSWORD_SIGN_IN_METHOD); + c.email = email; + c.password = password; + return c; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FederatedSignIn.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FederatedSignIn.java new file mode 100644 index 00000000000..61888301c61 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FederatedSignIn.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebase; + +import dart.async.Future; + +/** + * A provider {@code signInWithProvider} can run interactively: it shows the identity + * provider's own sign-in (Google's, Apple's sheet) and answers the credential to + * exchange. The providers that do this live in the firebaseoauth pack, so an app that + * signs in with a password never ships a reference to Google or Apple sign-in, and the + * builders never add their SDKs and entitlements to it. + */ +public interface FederatedSignIn { + /** Runs the provider's sign-in for {@code app}; a cancelled one fails the future. */ + Future obtainCredential(FirebaseApp app); +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FirebaseAuth.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FirebaseAuth.java index fb970dcf07d..d60147f8fad 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FirebaseAuth.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FirebaseAuth.java @@ -165,13 +165,66 @@ public Future signInAnonymously() { return signIn("signUp", DartMap.of("returnSecureToken", Boolean.TRUE), true); } + /// An email credential signs in with its password; a federated one is exchanged + /// through Identity Toolkit's `accounts:signInWithIdp`, which is what the native SDKs + /// call too. `requestUri` must be a URL, and Firebase only checks it for the + /// redirect flows, which a token exchange is not -- hence the fixed `localhost`. public Future signInWithCredential(Object credential) { - return Future.error(new FirebaseAuthException("operation-not-allowed", - "Only email/password and anonymous sign-in are supported in Codename One")); + if (credential instanceof OAuthCredential) { + OAuthCredential c = (OAuthCredential) credential; + if (c.idToken == null && c.accessTokenValue == null) { + return Future.error(new FirebaseAuthException("invalid-credential", + "The credential carries neither an ID token nor an access token")); + } + return signIn("signInWithIdp", DartMap.of("postBody", c.postBody(), + "requestUri", "http://localhost", "returnSecureToken", Boolean.TRUE, + "returnIdpCredential", Boolean.TRUE), false); + } + if (credential instanceof AuthCredential) { + AuthCredential c = (AuthCredential) credential; + if (EmailAuthProvider.PROVIDER_ID.equals(c.providerId)) { + return signInWithEmailAndPassword(c.email, c.password); + } + } + return Future.error(new FirebaseAuthException("invalid-credential", + "Unsupported credential " + credential)); } + /// Runs the provider's own sign-in (see [FederatedSignIn]) and exchanges what it + /// answers. A provider that cannot run interactively here fails, never hangs. public Future signInWithProvider(Object provider) { - return signInWithCredential(provider); + if (!(provider instanceof FederatedSignIn)) { + return Future.error(new FirebaseAuthException("operation-not-allowed", + "signInWithProvider supports the Google, Apple and OAuth providers")); + } + final Completer done = new Completer(); + ((FederatedSignIn) provider).obtainCredential(Firebase.find(appName)).then( + new Funcs.Func1() { + @Override + public Object call(OAuthCredential c) { + signInWithCredential(c).then(new Funcs.Func1() { + @Override + public Object call(UserCredential u) { + done.complete(u); + return null; + } + }, new Funcs.Func1() { + @Override + public Object call(Object err) { + done.completeError(err); + return null; + } + }); + return null; + } + }, new Funcs.Func1() { + @Override + public Object call(Object err) { + done.completeError(err); + return null; + } + }); + return done.future(); } public Future signOut() { @@ -196,6 +249,7 @@ private Future signIn(String op, Map body, final public void call(Map r) { User u = new User(FirebaseAuth.this, str(r.get("localId")), str(r.get("email")), anonymous); u.displayName = str(r.get("displayName")); + u.photoURL = str(r.get("photoUrl")); u.idToken = str(r.get("idToken")); u.refreshToken = str(r.get("refreshToken")); u.expiresAt = expiry(r.get("expiresIn")); @@ -523,6 +577,7 @@ private void persist() { Preferences.set(pref + "uid", current.uid); Preferences.set(pref + "email", current.email == null ? "" : current.email); Preferences.set(pref + "name", current.displayName == null ? "" : current.displayName); + Preferences.set(pref + "photo", current.photoURL == null ? "" : current.photoURL); Preferences.set(pref + "refresh", current.refreshToken == null ? "" : current.refreshToken); Preferences.set(pref + "anonymous", current.anonymous ? "true" : "false"); Preferences.set(pref + "verified", current.emailVerified ? "true" : "false"); @@ -540,6 +595,7 @@ private void restore() { User u = new User(this, uid, blankToNull(Preferences.get(pref + "email", "")), "true".equals(Preferences.get(pref + "anonymous", "false"))); u.displayName = blankToNull(Preferences.get(pref + "name", "")); + u.photoURL = blankToNull(Preferences.get(pref + "photo", "")); u.emailVerified = "true".equals(Preferences.get(pref + "verified", "false")); u.refreshToken = blankToNull(Preferences.get(pref + "refresh", "")); current = u; diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/OAuthCredential.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/OAuthCredential.java new file mode 100644 index 00000000000..0db2192523e --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/OAuthCredential.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebase; + +/** + * firebase_auth's {@code OAuthCredential}: an identity provider's ID token and/or + * access token, and for Apple the raw nonce whose hash the ID token carries. Built by + * {@code GoogleAuthProvider.credential} and {@code OAuthProvider(id).credential}. + */ +public class OAuthCredential extends AuthCredential { + final String idToken; + final String accessTokenValue; + final String rawNonce; + final String secret; + + public OAuthCredential(String providerId, String signInMethod, String idToken, String accessToken, + String rawNonce, String secret) { + super(providerId, signInMethod == null ? providerId : signInMethod); + this.idToken = idToken; + this.accessTokenValue = accessToken; + this.rawNonce = rawNonce; + this.secret = secret; + } + + public String idToken() { + return idToken; + } + + @Override + public String accessToken() { + return accessTokenValue; + } + + public String rawNonce() { + return rawNonce; + } + + public String secret() { + return secret; + } + + /// The `postBody` of Identity Toolkit's `accounts:signInWithIdp`, a form-encoded + /// list of what the provider issued. + String postBody() { + StringBuilder b = new StringBuilder("providerId=").append(encode(providerId)); + if (idToken != null) { + b.append("&id_token=").append(encode(idToken)); + } + if (accessTokenValue != null) { + b.append("&access_token=").append(encode(accessTokenValue)); + } + if (rawNonce != null) { + b.append("&nonce=").append(encode(rawNonce)); + } + if (secret != null) { + b.append("&oauth_token_secret=").append(encode(secret)); + } + return b.toString(); + } + + private static String encode(String v) { + return com.codename1.io.Util.encodeUrl(v); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/OAuthProvider.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/OAuthProvider.java new file mode 100644 index 00000000000..0775b9e927e --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/OAuthProvider.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebase; + +import dart.core.DartList; + +import java.util.Map; + +/** + * firebase_auth's {@code OAuthProvider}: builds the credential of any provider Firebase + * federates ({@code apple.com}, {@code microsoft.com}, ...) from tokens the app obtained + * itself. It cannot run a provider's sign-in, so {@code signInWithProvider} refuses it; + * Google's and Apple's interactive sign-in are {@code GoogleAuthProvider} and + * {@code AppleAuthProvider}, in packs of their own. + */ +public class OAuthProvider { + private final String providerId; + private final DartList scopes = new DartList(); + + public OAuthProvider(String providerId) { + this.providerId = providerId; + } + + public String providerId() { + return providerId; + } + + public DartList scopes() { + return scopes; + } + + public OAuthProvider addScope(String scope) { + scopes.add(scope); + return this; + } + + public OAuthProvider setCustomParameters(Map parameters) { + return this; + } + + public OAuthCredential credential(String accessToken, String secret, String idToken, String rawNonce, + String signInMethod) { + return new OAuthCredential(providerId, signInMethod, idToken, accessToken, rawNonce, secret); + } + + @Override + public String toString() { + return "OAuthProvider(" + providerId + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseapple/AppleAuthProvider.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseapple/AppleAuthProvider.java new file mode 100644 index 00000000000..4a7f119ecab --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseapple/AppleAuthProvider.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseapple; + +import com.codename1.flutter.plugins.firebase.FederatedSignIn; +import com.codename1.flutter.plugins.firebase.FirebaseApp; +import com.codename1.flutter.plugins.firebase.FirebaseAuthException; +import com.codename1.flutter.plugins.firebase.OAuthCredential; +import com.codename1.social.AppleSignIn; +import com.codename1.social.AppleSignInCallback; +import com.codename1.social.AppleSignInResult; +import com.codename1.ui.Display; + +import dart.async.Completer; +import dart.async.Future; +import dart.core.DartList; + +import java.util.Map; + +/** + * firebase_auth's {@code AppleAuthProvider}. Passed to {@code signInWithProvider} it runs + * {@link AppleSignIn}: the native sheet on iOS, which hands back the raw nonce Firebase + * checks the ID token's hashed one against, and Apple's web flow elsewhere. The web flow's + * token carries OIDC's own nonce rather than a hash of one, so there is no raw nonce to + * send and a Firebase project that enforces it refuses the exchange. A pack of its own, + * so that only an app that signs in with Apple carries the Sign in with Apple entitlement. + */ +public class AppleAuthProvider implements FederatedSignIn { + public static final String PROVIDER_ID = "apple.com"; + public static final String APPLE_SIGN_IN_METHOD = "apple.com"; + + private final DartList scopes = new DartList(); + + public static OAuthCredential credentialWithIDToken(String idToken, String rawNonce, Object fullName) { + return new OAuthCredential(PROVIDER_ID, APPLE_SIGN_IN_METHOD, idToken, null, rawNonce, null); + } + + public String providerId() { + return PROVIDER_ID; + } + + public DartList scopes() { + return scopes; + } + + public AppleAuthProvider addScope(String scope) { + scopes.add(scope); + return this; + } + + public AppleAuthProvider setCustomParameters(Map parameters) { + return this; + } + + @Override + public Future obtainCredential(FirebaseApp app) { + final Completer done = new Completer(); + if (!Display.isInitialized()) { + done.completeError(new FirebaseAuthException("operation-not-allowed", + "Sign in with Apple needs a display")); + return done.future(); + } + StringBuilder s = new StringBuilder(); + for (int i = 0; i < scopes.size(); i++) { + if (s.length() > 0) { + s.append(' '); + } + s.append(scopes.get(i)); + } + AppleSignIn.getInstance().signIn(s.length() == 0 ? null : s.toString(), new AppleSignInCallback() { + @Override + public void onSuccess(AppleSignInResult r) { + done.complete(credentialWithIDToken(r.getIdentityToken(), r.getRawNonce(), null)); + } + + @Override + public void onError(String error) { + done.completeError(new FirebaseAuthException("invalid-credential", error)); + } + + @Override + public void onCancel() { + done.completeError(new FirebaseAuthException("web-context-cancelled", + "Sign in with Apple was cancelled")); + } + }); + return done.future(); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebasegoogle/GoogleAuthProvider.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebasegoogle/GoogleAuthProvider.java new file mode 100644 index 00000000000..3d64e682183 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebasegoogle/GoogleAuthProvider.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebasegoogle; + +import com.codename1.flutter.plugins.firebase.FederatedSignIn; +import com.codename1.flutter.plugins.firebase.FirebaseApp; +import com.codename1.flutter.plugins.firebase.FirebaseAuthException; +import com.codename1.flutter.plugins.firebase.FirebaseOptions; +import com.codename1.flutter.plugins.firebase.OAuthCredential; +import com.codename1.flutter.plugins.googlesignin.GoogleSignIn; +import com.codename1.flutter.plugins.googlesignin.GoogleSignInAccount; +import com.codename1.flutter.plugins.googlesignin.GoogleSignInAuthentication; +import com.codename1.ui.Display; + +import dart.async.Completer; +import dart.async.Future; +import dart.core.DartList; +import dart.runtime.Funcs; + +import java.util.Map; + +/** + * firebase_auth's {@code GoogleAuthProvider}. {@code credential} wraps tokens from + * google_sign_in; passed to {@code signInWithProvider} it runs the Google sign-in of the + * googlesignin pack itself, with the client id of the Firebase app's options for this + * platform ({@code iosClientId} on iOS, {@code androidClientId} elsewhere) unless the + * {@code google.clientId} Display property names one. A pack of its own, so that only an + * app that signs in with Google ships Google sign-in. + */ +public class GoogleAuthProvider implements FederatedSignIn { + public static final String PROVIDER_ID = "google.com"; + public static final String GOOGLE_SIGN_IN_METHOD = "google.com"; + + private final DartList scopes = new DartList(); + + public static OAuthCredential credential(String idToken, String accessToken) { + return new OAuthCredential(PROVIDER_ID, GOOGLE_SIGN_IN_METHOD, idToken, accessToken, null, null); + } + + public String providerId() { + return PROVIDER_ID; + } + + public DartList scopes() { + return scopes; + } + + public GoogleAuthProvider addScope(String scope) { + scopes.add(scope); + return this; + } + + /** Accepted and not applied: the OIDC request carries no extra parameters. */ + public GoogleAuthProvider setCustomParameters(Map parameters) { + return this; + } + + @Override + public Future obtainCredential(FirebaseApp app) { + final Completer done = new Completer(); + GoogleSignIn g = new GoogleSignIn(); + g.scopes(scopes); + g.clientId(clientId(app)); + g.signIn().then(new Funcs.VoidFunc1() { + @Override + public void call(GoogleSignInAccount a) { + if (a == null) { + done.completeError(new FirebaseAuthException("web-context-cancelled", + "The Google sign-in was cancelled")); + return; + } + a.authentication().then(new Funcs.VoidFunc1() { + @Override + public void call(GoogleSignInAuthentication t) { + done.complete(credential(t.idToken(), t.accessToken())); + } + }); + } + }, new Funcs.VoidFunc1() { + @Override + public void call(Object err) { + done.completeError(err); + } + }); + return done.future(); + } + + private static String clientId(FirebaseApp app) { + if (Display.isInitialized()) { + String p = Display.getInstance().getProperty("google.clientId", null); + if (p != null) { + return p; + } + } + FirebaseOptions o = app == null ? null : app.options(); + if (o == null) { + return null; + } + boolean ios = Display.isInitialized() && "ios".equals(Display.getInstance().getPlatformName()); + String id = ios ? o.iosClientId() : o.androidClientId(); + return id != null ? id : (ios ? o.androidClientId() : o.iosClientId()); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/Geocoding.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/Geocoding.java new file mode 100644 index 00000000000..57bff5e2ba2 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/Geocoding.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.geocoding; + +import com.codename1.io.ConnectionRequest; +import com.codename1.io.JSONParser; +import com.codename1.io.NetworkManager; +import com.codename1.io.Util; +import com.codename1.ui.Display; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.List; +import java.util.Map; + +import dart.async.Completer; +import dart.async.Future; +import dart.core.DartList; +import dart.core.DateTime; + +/** + * geocoding's top-level functions. Codename One has no geocoder of its own, so these call + * Google's Geocoding REST API with the key in the Display property + * {@code google.geocoding.key} (set it with {@code Display.setProperty} before the first + * lookup). With no key, no display or no match the future fails with + * {@link NoResultFoundException}, the error the plugin raises for an empty answer. + * {@code setLocaleIdentifier} becomes the request's {@code language}. + */ +public final class Geocoding { + static final String KEY_PROPERTY = "google.geocoding.key"; + private static String locale; + + private Geocoding() { + } + + public static Future setLocaleIdentifier(String localeIdentifier) { + locale = localeIdentifier; + return Future.value(null); + } + + public static Future isPresent() { + return Future.value(Boolean.valueOf(apiKey() != null)); + } + + public static Future> placemarkFromCoordinates(double latitude, double longitude) { + final Completer> done = new Completer>(); + query("latlng=" + latitude + "," + longitude, new Answer() { + @Override + void results(List results) { + DartList out = new DartList(); + for (Object r : results) { + if (r instanceof Map) { + out.add(placemark((Map) r)); + } + } + done.complete(out); + } + + @Override + void failed(Object error) { + done.completeError(error); + } + }); + return done.future(); + } + + public static Future> locationFromAddress(String address) { + final Completer> done = new Completer>(); + query("address=" + Util.encodeUrl(address == null ? "" : address), new Answer() { + @Override + void results(List results) { + DartList out = new DartList(); + for (Object r : results) { + Object geo = r instanceof Map ? ((Map) r).get("geometry") : null; + Object loc = geo instanceof Map ? ((Map) geo).get("location") : null; + if (loc instanceof Map) { + out.add(new Location(num(((Map) loc).get("lat")), num(((Map) loc).get("lng")), + DateTime.now())); + } + } + done.complete(out); + } + + @Override + void failed(Object error) { + done.completeError(error); + } + }); + return done.future(); + } + + private static double num(Object o) { + return o instanceof Number ? ((Number) o).doubleValue() : 0; + } + + private static String apiKey() { + if (!Display.isInitialized()) { + return null; + } + String k = Display.getInstance().getProperty(KEY_PROPERTY, null); + return k == null || k.length() == 0 ? null : k; + } + + abstract static class Answer { + abstract void results(List results); + + abstract void failed(Object error); + } + + private static void query(String what, final Answer answer) { + String key = apiKey(); + if (key == null) { + answer.failed(new NoResultFoundException()); + return; + } + String url = "https://maps.googleapis.com/maps/api/geocode/json?" + what + "&key=" + Util.encodeUrl(key); + if (locale != null) { + url += "&language=" + Util.encodeUrl(locale.replace('_', '-')); + } + ConnectionRequest req = new ConnectionRequest() { + private List found; + + @Override + protected void readResponse(InputStream input) throws IOException { + Map m = new JSONParser().parseJSON(new InputStreamReader(input, "UTF-8")); + Object r = m == null ? null : m.get("results"); + found = r instanceof List ? (List) r : null; + } + + @Override + protected void postResponse() { + if (found == null || found.isEmpty()) { + answer.failed(new NoResultFoundException()); + } else { + answer.results(found); + } + } + + @Override + protected void handleException(Exception err) { + answer.failed(new NoResultFoundException(String.valueOf(err.getMessage()))); + } + + @Override + protected void handleErrorResponseCode(int code, String message) { + answer.failed(new NoResultFoundException("HTTP " + code)); + } + }; + req.setUrl(url); + req.setPost(false); + req.setFailSilently(true); + NetworkManager.getInstance().addToQueue(req); + } + + static Placemark placemark(Map r) { + String streetNumber = null; + String route = null; + String country = null; + String iso = null; + String postal = null; + String admin1 = null; + String admin2 = null; + String locality = null; + String subLocality = null; + Object comps = r.get("address_components"); + if (comps instanceof List) { + for (Object c : (List) comps) { + if (!(c instanceof Map)) { + continue; + } + Map m = (Map) c; + Object types = m.get("types"); + Object ln = m.get("long_name"); + Object sn = m.get("short_name"); + String longName = ln instanceof String ? (String) ln : null; + String shortName = sn instanceof String ? (String) sn : null; + if (!(types instanceof List)) { + continue; + } + List t = (List) types; + if (t.contains("street_number")) { + streetNumber = longName; + } else if (t.contains("route")) { + route = longName; + } else if (t.contains("country")) { + country = longName; + iso = shortName; + } else if (t.contains("postal_code")) { + postal = longName; + } else if (t.contains("administrative_area_level_1")) { + admin1 = longName; + } else if (t.contains("administrative_area_level_2")) { + admin2 = longName; + } else if (t.contains("locality")) { + locality = longName; + } else if (t.contains("sublocality") || t.contains("neighborhood")) { + subLocality = longName; + } + } + } + String street = route == null ? null : (streetNumber == null ? route : route + " " + streetNumber); + String name = streetNumber != null ? streetNumber : (route != null ? route : locality); + return new Placemark(name, street, iso, country, postal, admin1, admin2, locality, subLocality, + route, streetNumber); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/Location.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/Location.java new file mode 100644 index 00000000000..05e50c30ce9 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/Location.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.geocoding; + +import dart.core.DateTime; + +/** geocoding's {@code Location}: coordinates found for an address. */ +public class Location { + private double latitude; + private double longitude; + private DateTime timestamp; + + public Location() { + } + + public void latitude(double v) { + latitude = v; + } + + public void longitude(double v) { + longitude = v; + } + + public void timestamp(DateTime v) { + timestamp = v; + } + + public Location(double latitude, double longitude, DateTime timestamp) { + this.latitude = latitude; + this.longitude = longitude; + this.timestamp = timestamp; + } + + public double latitude() { + return latitude; + } + + public double longitude() { + return longitude; + } + + public DateTime timestamp() { + return timestamp; + } + + @Override + public String toString() { + return "Latitude: " + latitude + ",\n Longitude: " + longitude + ",\n Timestamp: " + timestamp; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/NoResultFoundException.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/NoResultFoundException.java new file mode 100644 index 00000000000..4cea60b8f24 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/NoResultFoundException.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.geocoding; + +/** geocoding's {@code NoResultFoundException}: the lookup found nothing. */ +public class NoResultFoundException extends RuntimeException { + + public NoResultFoundException() { + super("No result found."); + } + + public NoResultFoundException(String message) { + super(message); + } + + @Override + public String toString() { + return getMessage(); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/Placemark.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/Placemark.java new file mode 100644 index 00000000000..eb2318f968e --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/geocoding/Placemark.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.geocoding; + +/** geocoding's {@code Placemark}: the address parts found for coordinates. */ +public class Placemark { + private String name; + private String street; + private String isoCountryCode; + private String country; + private String postalCode; + private String administrativeArea; + private String subAdministrativeArea; + private String locality; + private String subLocality; + private String thoroughfare; + private String subThoroughfare; + + public Placemark() { + } + + public void name(String v) { + name = v; + } + + public void street(String v) { + street = v; + } + + public void isoCountryCode(String v) { + isoCountryCode = v; + } + + public void country(String v) { + country = v; + } + + public void postalCode(String v) { + postalCode = v; + } + + public void administrativeArea(String v) { + administrativeArea = v; + } + + public void subAdministrativeArea(String v) { + subAdministrativeArea = v; + } + + public void locality(String v) { + locality = v; + } + + public void subLocality(String v) { + subLocality = v; + } + + public void thoroughfare(String v) { + thoroughfare = v; + } + + public void subThoroughfare(String v) { + subThoroughfare = v; + } + + public Placemark(String name, String street, String isoCountryCode, String country, String postalCode, + String administrativeArea, String subAdministrativeArea, String locality, String subLocality, + String thoroughfare, String subThoroughfare) { + this.name = name; + this.street = street; + this.isoCountryCode = isoCountryCode; + this.country = country; + this.postalCode = postalCode; + this.administrativeArea = administrativeArea; + this.subAdministrativeArea = subAdministrativeArea; + this.locality = locality; + this.subLocality = subLocality; + this.thoroughfare = thoroughfare; + this.subThoroughfare = subThoroughfare; + } + + public String name() { + return name; + } + + public String street() { + return street; + } + + public String isoCountryCode() { + return isoCountryCode; + } + + public String country() { + return country; + } + + public String postalCode() { + return postalCode; + } + + public String administrativeArea() { + return administrativeArea; + } + + public String subAdministrativeArea() { + return subAdministrativeArea; + } + + public String locality() { + return locality; + } + + public String subLocality() { + return subLocality; + } + + public String thoroughfare() { + return thoroughfare; + } + + public String subThoroughfare() { + return subThoroughfare; + } + + @Override + public String toString() { + return "Name: " + name + ", Street: " + street + ", ISO Country Code: " + isoCountryCode + + ", Country: " + country + ", Postal code: " + postalCode + + ", Administrative area: " + administrativeArea + + ", Subadministrative area: " + subAdministrativeArea + ", Locality: " + locality + + ", Sublocality: " + subLocality + ", Thoroughfare: " + thoroughfare + + ", Subthoroughfare: " + subThoroughfare; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlesignin/GoogleSignIn.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlesignin/GoogleSignIn.java new file mode 100644 index 00000000000..93625a508b6 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlesignin/GoogleSignIn.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.googlesignin; + +import com.codename1.flutter.platform.PlatformException; +import com.codename1.io.oidc.OidcException; +import com.codename1.io.oidc.OidcTokens; +import com.codename1.social.GoogleConnect; +import com.codename1.ui.Display; +import com.codename1.util.SuccessCallback; + +import dart.async.Completer; +import dart.async.Future; +import dart.async.Stream; +import dart.async.StreamController; +import dart.core.DartList; + +/** + * google_sign_in's {@code GoogleSignIn} (the version 6 API), over + * {@link GoogleConnect#signIn(String, String, String...)}: Google's OIDC endpoints with + * PKCE in the system browser, on every platform. + * + *

The client id is the {@code clientId} argument, else the Display property + * {@code google.clientId}. The redirect is the Display property {@code google.redirectUri}, + * else the reversed client id scheme Google registers for an iOS or Android client + * ({@code com.googleusercontent.apps.:/oauth2redirect}). + * + *

Google issues no refresh token to a public client, so {@code signInSilently} answers + * the account of this run's last sign-in, and null after a restart; it never shows a + * browser. {@code hostedDomain}, {@code signInOption} and {@code forceCodeForRefreshToken} + * are accepted and not applied. A cancelled sign-in answers null, as the plugin does. + */ +public class GoogleSignIn { + private static final StreamController CHANGES = StreamController.broadcast(); + private static GoogleSignInAccount current; + + private final java.util.LinkedHashSet scopes = new java.util.LinkedHashSet(); + private String clientId; + private String serverClientId; + + public GoogleSignIn() { + scopes.add("openid"); + scopes.add("email"); + scopes.add("profile"); + } + + public void scopes(DartList v) { + if (v != null) { + for (int i = 0; i < v.size(); i++) { + scopes.add(v.get(i)); + } + } + } + + public void clientId(String v) { + clientId = v; + } + + public void serverClientId(String v) { + serverClientId = v; + } + + public void signInOption(Object v) { + } + + public void hostedDomain(String v) { + } + + public void forceCodeForRefreshToken(Boolean v) { + } + + /** The scopes a sign-in asks for, {@code openid email profile} first. */ + public DartList scopes() { + DartList l = new DartList(); + for (String s : scopes) { + l.add(s); + } + return l; + } + + public GoogleSignInAccount currentUser() { + return current; + } + + public Stream onCurrentUserChanged() { + return CHANGES.stream(); + } + + public Future signIn() { + final Completer done = new Completer(); + final String id = clientId(); + if (id == null) { + done.completeError(new PlatformException("sign_in_failed", + "No Google client id: pass clientId or set the google.clientId Display property", null)); + return done.future(); + } + if (!Display.isInitialized()) { + done.completeError(new PlatformException("sign_in_failed", "Google sign-in needs a display", null)); + return done.future(); + } + GoogleConnect.getInstance().signIn(id, redirectUri(id), scopes.toArray(new String[scopes.size()])).ready(new SuccessCallback() { + @Override + public void onSucess(OidcTokens t) { + done.complete(changeTo(new GoogleSignInAccount(GoogleSignIn.this, t))); + } + }).except(new SuccessCallback() { + @Override + public void onSucess(Throwable err) { + if (err instanceof OidcException + && OidcException.USER_CANCELLED.equals(((OidcException) err).getError())) { + done.complete(null); + } else { + done.completeError(new PlatformException("sign_in_failed", err.getMessage(), null)); + } + } + }); + return done.future(); + } + + public Future signInSilently(Boolean suppressErrors, Boolean reAuthenticate) { + return Future.value(current); + } + + public Future isSignedIn() { + return Future.value(Boolean.valueOf(current != null)); + } + + public Future signOut() { + changeTo(null); + return Future.value(null); + } + + /** Google's token revocation would need the access token sent back; it is dropped instead. */ + public Future disconnect() { + return signOut(); + } + + public Future requestScopes(DartList scopes) { + return Future.value(Boolean.valueOf(current != null)); + } + + public Future canAccessScopes(DartList scopes, String accessToken) { + return Future.value(Boolean.valueOf(current != null)); + } + + String serverClientId() { + return serverClientId; + } + + private String clientId() { + if (clientId != null) { + return clientId; + } + return Display.isInitialized() ? Display.getInstance().getProperty("google.clientId", null) : null; + } + + private static String redirectUri(String id) { + String r = Display.getInstance().getProperty("google.redirectUri", null); + if (r != null) { + return r; + } + String suffix = ".apps.googleusercontent.com"; + String prefix = id.endsWith(suffix) ? id.substring(0, id.length() - suffix.length()) : id; + return "com.googleusercontent.apps." + prefix + ":/oauth2redirect"; + } + + private static GoogleSignInAccount changeTo(GoogleSignInAccount a) { + current = a; + if (CHANGES.hasListener()) { + CHANGES.add(a); + } + return a; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlesignin/GoogleSignInAccount.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlesignin/GoogleSignInAccount.java new file mode 100644 index 00000000000..f6c8b634abb --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlesignin/GoogleSignInAccount.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.googlesignin; + +import com.codename1.io.oidc.OidcTokens; + +import dart.async.Future; +import dart.core.DartMap; + +import java.util.Map; + +/** google_sign_in's {@code GoogleSignInAccount}, read from the ID token's claims. */ +public class GoogleSignInAccount { + private final GoogleSignIn owner; + private final String id; + private final String email; + private final String displayName; + private final String photoUrl; + private final GoogleSignInAuthentication auth; + + GoogleSignInAccount(GoogleSignIn owner, OidcTokens t) { + this(owner, t.getSubject(), t.getEmail(), claim(t, "name"), claim(t, "picture"), + new GoogleSignInAuthentication(t.getIdToken(), t.getAccessToken())); + } + + GoogleSignInAccount(GoogleSignIn owner, String id, String email, String displayName, String photoUrl, + GoogleSignInAuthentication auth) { + this.owner = owner; + this.id = id; + this.email = email; + this.displayName = displayName; + this.photoUrl = photoUrl; + this.auth = auth; + } + + public String id() { + return id; + } + + public String email() { + return email; + } + + public String displayName() { + return displayName; + } + + public String photoUrl() { + return photoUrl; + } + + /** Always null: the PKCE flow exchanges the code on the device itself. */ + public String serverAuthCode() { + return null; + } + + public Future authentication() { + return Future.value(auth); + } + + public Future> authHeaders() { + DartMap h = new DartMap(); + if (auth.accessToken() != null) { + h.put("Authorization", "Bearer " + auth.accessToken()); + h.put("X-Goog-AuthUser", "0"); + } + return Future.>value(h); + } + + public Future clearAuthCache() { + return Future.value(null); + } + + @Override + public String toString() { + return "GoogleSignInAccount:{displayName: " + displayName + ", email: " + email + ", id: " + id + + ", photoUrl: " + photoUrl + "}"; + } + + private static String claim(OidcTokens t, String name) { + Map c = t.getIdTokenClaims(); + Object v = c == null ? null : c.get(name); + return v instanceof String ? (String) v : null; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlesignin/GoogleSignInAuthentication.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlesignin/GoogleSignInAuthentication.java new file mode 100644 index 00000000000..60821d7fef0 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlesignin/GoogleSignInAuthentication.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.googlesignin; + +/** google_sign_in's {@code GoogleSignInAuthentication}. */ +public class GoogleSignInAuthentication { + private final String idToken; + private final String accessToken; + + GoogleSignInAuthentication(String idToken, String accessToken) { + this.idToken = idToken; + this.accessToken = accessToken; + } + + public String idToken() { + return idToken; + } + + public String accessToken() { + return accessToken; + } + + public String serverAuthCode() { + return null; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagecompress/CompressFormat.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagecompress/CompressFormat.java new file mode 100644 index 00000000000..fe62b35a964 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagecompress/CompressFormat.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.imagecompress; + +/** flutter_image_compress's {@code CompressFormat}. */ +public enum CompressFormat { + jpeg, png, heic, webp +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagecompress/FlutterImageCompress.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagecompress/FlutterImageCompress.java new file mode 100644 index 00000000000..37b135b13ba --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagecompress/FlutterImageCompress.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.imagecompress; + +import com.codename1.flutter.FlutterAssets; +import com.codename1.flutter.plugins.crossfile.XFile; +import com.codename1.io.FileSystemStorage; +import com.codename1.io.Util; +import com.codename1.ui.Display; +import com.codename1.ui.Image; +import com.codename1.ui.util.ImageIO; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import dart.async.Future; +import dart.typed_data.Uint8List; + +/** + * flutter_image_compress's {@code FlutterImageCompress}, over {@code Image} and + * {@code ImageIO}. As in the plugin, {@code minWidth}/{@code minHeight} are the smallest + * size the result may have: the image is scaled down by the smaller of the two ratios and + * never up; {@code inSampleSize} divides it further; {@code rotate} turns it by a multiple + * of 90 degrees; {@code quality} is the JPEG quality. JPEG and PNG are written, HEIC and + * WebP only where the platform's ImageIO supports them (otherwise the future fails, as the + * plugin's does on an unsupported format). EXIF is not kept and + * {@code autoCorrectionAngle} is not applied. With no display there is no codec, and the + * future fails with {@code UnsupportedError} rather than answering the input as if it had + * been compressed. + */ +public final class FlutterImageCompress { + + private FlutterImageCompress() { + } + + private static long or(Long v, long def) { + return v == null ? def : v.longValue(); + } + + static String formatName(CompressFormat f) { + if (f == CompressFormat.png) { + return ImageIO.FORMAT_PNG; + } + if (f == CompressFormat.heic) { + return "heic"; + } + if (f == CompressFormat.webp) { + return "webp"; + } + return ImageIO.FORMAT_JPEG; + } + + static byte[] compress(byte[] src, Long minWidth, Long minHeight, Long inSampleSize, Long quality, Long rotate, + CompressFormat format) throws IOException { + ImageIO io = Display.isInitialized() ? ImageIO.getImageIO() : null; + if (io == null) { + throw new dart.core.UnsupportedError("flutter_image_compress: no image codec on this platform"); + } + String fmt = formatName(format); + if (!io.isFormatSupported(fmt)) { + throw new dart.core.UnsupportedError("flutter_image_compress: " + fmt + " is not supported here"); + } + Image img = Image.createImage(src, 0, src.length); + int w = img.getWidth(); + int h = img.getHeight(); + double minW = Math.max(1, or(minWidth, 1920)); + double minH = Math.max(1, or(minHeight, 1080)); + double scale = Math.max(1.0, Math.min(w / minW, h / minH)); + long sample = Math.max(1, or(inSampleSize, 1)); + int dw = Math.max(1, (int) (w / scale / sample)); + int dh = Math.max(1, (int) (h / scale / sample)); + if (dw != w || dh != h) { + img = img.scaled(dw, dh); + } + int r = (int) (or(rotate, 0) % 360); + if (r < 0) { + r += 360; + } + if (r == 90) { + img = img.rotate90Degrees(true); + } else if (r == 180) { + img = img.rotate180Degrees(true); + } else if (r == 270) { + img = img.rotate270Degrees(true); + } + float q = Math.max(0, Math.min(100, or(quality, 95))) / 100f; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + io.save(img, out, fmt, q); + return out.toByteArray(); + } + + private static byte[] readFile(String path) throws IOException { + InputStream in = FileSystemStorage.getInstance().openInputStream(path); + try { + return Util.readInputStream(in); + } finally { + Util.cleanup(in); + } + } + + public static Future compressWithFile(String path, Long minWidth, Long minHeight, Long inSampleSize, + Long quality, Long rotate, Boolean autoCorrectionAngle, CompressFormat format, Boolean keepExif, + Long numberOfRetries) { + try { + if (!Display.isInitialized()) { + return Future.error(new dart.core.UnsupportedError( + "flutter_image_compress: no image codec on this platform")); + } + if (!FileSystemStorage.getInstance().exists(path)) { + return Future.value(null); + } + return Future.value(Uint8List.fromBytes( + compress(readFile(path), minWidth, minHeight, inSampleSize, quality, rotate, format))); + } catch (Throwable t) { + return Future.error(t); + } + } + + public static Future compressAndGetFile(String path, String targetPath, Long minWidth, Long minHeight, + Long inSampleSize, Long quality, Long rotate, Boolean autoCorrectionAngle, CompressFormat format, + Boolean keepExif, Long numberOfRetries) { + if (path != null && path.equals(targetPath)) { + return Future.error(new dart.core.ArgumentError("Target path and source path cannot be the same.")); + } + try { + if (!Display.isInitialized()) { + return Future.error(new dart.core.UnsupportedError( + "flutter_image_compress: no image codec on this platform")); + } + FileSystemStorage fs = FileSystemStorage.getInstance(); + if (!fs.exists(path)) { + return Future.value(null); + } + byte[] out = compress(readFile(path), minWidth, minHeight, inSampleSize, quality, rotate, format); + OutputStream os = fs.openOutputStream(targetPath); + try { + os.write(out); + } finally { + Util.cleanup(os); + } + return Future.value(new XFile(targetPath)); + } catch (Throwable t) { + return Future.error(t); + } + } + + public static Future compressWithList(Uint8List image, Long minWidth, Long minHeight, Long quality, + Long rotate, Long inSampleSize, Boolean autoCorrectionAngle, CompressFormat format, Boolean keepExif) { + try { + return Future.value(Uint8List.fromBytes( + compress(image.toBytes(), minWidth, minHeight, inSampleSize, quality, rotate, format))); + } catch (Throwable t) { + return Future.error(t); + } + } + + public static Future compressAssetImage(String assetName, Long minWidth, Long minHeight, Long quality, + Long rotate, Boolean autoCorrectionAngle, CompressFormat format, Boolean keepExif) { + try { + if (!Display.isInitialized()) { + return Future.error(new dart.core.UnsupportedError( + "flutter_image_compress: no image codec on this platform")); + } + InputStream in = Display.getInstance().getResourceAsStream(FlutterImageCompress.class, + FlutterAssets.resourceName(assetName)); + if (in == null) { + return Future.value(null); + } + byte[] src; + try { + src = Util.readInputStream(in); + } finally { + Util.cleanup(in); + } + return Future.value(Uint8List.fromBytes(compress(src, minWidth, minHeight, null, quality, rotate, format))); + } catch (Throwable t) { + return Future.error(t); + } + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagepicker/ImagePickerAndroid.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagepicker/ImagePickerAndroid.java new file mode 100644 index 00000000000..055fe78f5ac --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagepicker/ImagePickerAndroid.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.imagepicker; + +/** + * image_picker_android's {@code ImagePickerAndroid}. {@code useAndroidPhotoPicker} is + * accepted and has no effect: Codename One's gallery already opens the system picker, + * which is the Android Photo Picker where the device has it. + */ +public class ImagePickerAndroid extends ImagePickerPlatform { + private boolean useAndroidPhotoPicker; + + public ImagePickerAndroid() { + } + + public static void registerWith() { + if (!(ImagePickerPlatform.instance instanceof ImagePickerAndroid)) { + ImagePickerPlatform.instance = new ImagePickerAndroid(); + } + } + + public boolean useAndroidPhotoPicker() { + return useAndroidPhotoPicker; + } + + public boolean useAndroidPhotoPicker(boolean v) { + useAndroidPhotoPicker = v; + return v; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagepicker/ImagePickerPlatform.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagepicker/ImagePickerPlatform.java new file mode 100644 index 00000000000..0db0421c67c --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagepicker/ImagePickerPlatform.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.imagepicker; + +import com.codename1.flutter.plugins.crossfile.XFile; + +import dart.async.Future; + +/** + * image_picker_platform_interface's {@code ImagePickerPlatform}. There is one + * implementation on every Codename One target, so {@code instance} starts as an + * {@link ImagePickerAndroid} and an app that installs its own replaces it, as the plugin + * allows. The pick goes through {@link ImagePicker}. + */ +public abstract class ImagePickerPlatform { + + /** {@code ImagePickerPlatform.instance}, read and written as a static field. */ + public static ImagePickerPlatform instance = new ImagePickerAndroid(); + + protected ImagePickerPlatform() { + } + + public Future getImageFromSource(ImageSource source, Object options) { + return new ImagePicker().pickImage(source, null, null, null, null, null); + } + + public Future retrieveLostData() { + return new ImagePicker().retrieveLostData(); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/installreferrer/AndroidPlayInstallReferrer.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/installreferrer/AndroidPlayInstallReferrer.java new file mode 100644 index 00000000000..29f4a30ec22 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/installreferrer/AndroidPlayInstallReferrer.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.installreferrer; + +import dart.async.Future; + +/** + * android_play_install_referrer's {@code AndroidPlayInstallReferrer}. The referrer comes + * from the Play Install Referrer service, which Codename One's Android port does not bind, + * so every platform answers what the plugin answers where the service is unavailable: a + * failed future. An app that awaits it inside a try keeps working, with no attribution. + */ +public final class AndroidPlayInstallReferrer { + + private AndroidPlayInstallReferrer() { + } + + /** {@code AndroidPlayInstallReferrer.installReferrer}, read as a static field. */ + public static final Future installReferrer = Future.error(new dart.core.UnsupportedError( + "android_play_install_referrer: the Play Install Referrer service is not available")); +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/installreferrer/ReferrerDetails.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/installreferrer/ReferrerDetails.java new file mode 100644 index 00000000000..b33db141a30 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/installreferrer/ReferrerDetails.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.installreferrer; + +/** android_play_install_referrer's {@code ReferrerDetails}. */ +public class ReferrerDetails { + private final String installReferrer; + private final long referrerClickTimestampSeconds; + private final long installBeginTimestampSeconds; + private final long referrerClickTimestampServerSeconds; + private final long installBeginTimestampServerSeconds; + private final String installVersion; + private final boolean googlePlayInstantParam; + + public ReferrerDetails(String installReferrer, long referrerClickTimestampSeconds, + long installBeginTimestampSeconds, long referrerClickTimestampServerSeconds, + long installBeginTimestampServerSeconds, String installVersion, boolean googlePlayInstantParam) { + this.installReferrer = installReferrer; + this.referrerClickTimestampSeconds = referrerClickTimestampSeconds; + this.installBeginTimestampSeconds = installBeginTimestampSeconds; + this.referrerClickTimestampServerSeconds = referrerClickTimestampServerSeconds; + this.installBeginTimestampServerSeconds = installBeginTimestampServerSeconds; + this.installVersion = installVersion; + this.googlePlayInstantParam = googlePlayInstantParam; + } + + public String installReferrer() { + return installReferrer; + } + + public long referrerClickTimestampSeconds() { + return referrerClickTimestampSeconds; + } + + public long installBeginTimestampSeconds() { + return installBeginTimestampSeconds; + } + + public long referrerClickTimestampServerSeconds() { + return referrerClickTimestampServerSeconds; + } + + public long installBeginTimestampServerSeconds() { + return installBeginTimestampServerSeconds; + } + + public String installVersion() { + return installVersion; + } + + public boolean googlePlayInstantParam() { + return googlePlayInstantParam; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/mixpanel/Json.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/mixpanel/Json.java new file mode 100644 index 00000000000..f1f20c0c6e2 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/mixpanel/Json.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.mixpanel; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +/** The JSON writer Mixpanel's ingestion bodies need: maps, lists, strings, numbers, booleans. */ +final class Json { + + private Json() { + } + + static String write(Object o) { + StringBuilder sb = new StringBuilder(); + write(sb, o); + return sb.toString(); + } + + static void write(StringBuilder sb, Object o) { + if (o == null) { + sb.append("null"); + } else if (o instanceof String) { + string(sb, (String) o); + } else if (o instanceof Boolean) { + sb.append(((Boolean) o).booleanValue() ? "true" : "false"); + } else if (o instanceof Double || o instanceof Float) { + double d = ((Number) o).doubleValue(); + if (Double.isNaN(d) || Double.isInfinite(d)) { + sb.append("null"); + } else if (d == Math.floor(d) && Math.abs(d) < 1e15) { + sb.append((long) d); + } else { + sb.append(d); + } + } else if (o instanceof Number) { + sb.append(((Number) o).longValue()); + } else if (o instanceof Map) { + sb.append('{'); + boolean first = true; + for (Object e : ((Map) o).entrySet()) { + Map.Entry en = (Map.Entry) e; + if (!first) { + sb.append(','); + } + first = false; + string(sb, String.valueOf(en.getKey())); + sb.append(':'); + write(sb, en.getValue()); + } + sb.append('}'); + } else if (o instanceof Collection) { + sb.append('['); + Iterator it = ((Collection) o).iterator(); + boolean first = true; + while (it.hasNext()) { + if (!first) { + sb.append(','); + } + first = false; + write(sb, it.next()); + } + sb.append(']'); + } else { + string(sb, o.toString()); + } + } + + private static void string(StringBuilder sb, String s) { + sb.append('"'); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '"': sb.append("\\\""); break; + case '\\': sb.append("\\\\"); break; + case '\n': sb.append("\\n"); break; + case '\r': sb.append("\\r"); break; + case '\t': sb.append("\\t"); break; + default: + if (c < 0x20) { + String h = Integer.toHexString(c); + sb.append("\\u"); + for (int p = h.length(); p < 4; p++) { + sb.append('0'); + } + sb.append(h); + } else { + sb.append(c); + } + } + } + sb.append('"'); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/mixpanel/Mixpanel.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/mixpanel/Mixpanel.java new file mode 100644 index 00000000000..e1ffd875761 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/mixpanel/Mixpanel.java @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.mixpanel; + +import com.codename1.io.ConnectionRequest; +import com.codename1.io.Log; +import com.codename1.io.NetworkManager; +import com.codename1.io.Preferences; +import com.codename1.ui.Display; + +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import dart.async.Future; +import dart.core.DartMap; + +/** + * mixpanel_flutter's {@code Mixpanel}, over Mixpanel's HTTP ingestion API: events are + * queued and POSTed as one JSON array to {@code /track}, profile updates to + * {@code /engage}, on {@code flush()} or once the queue reaches the flush batch size + * ({@code setServerURL} picks the region, e.g. {@code https://api-eu.mixpanel.com}). The + * distinct id is kept in Preferences per token. Nothing is sent without a display: the + * queue simply waits. {@code trackAutomaticEvents} is accepted and not applied, as are + * {@code config}, logging, groups and IP geolocation: only the app's own events are sent. + */ +public class Mixpanel { + private static final Random RANDOM = new Random(); + private final String token; + private String serverUrl = "https://api.mixpanel.com"; + private String distinctId; + private boolean optedOut; + private long flushBatchSize = 50; + private final Map superProperties = new LinkedHashMap(); + private final Map timedEvents = new LinkedHashMap(); + private final List events = new ArrayList(); + private final List profileUpdates = new ArrayList(); + private boolean flushing; + + Mixpanel(String token, boolean optedOut) { + this.token = token; + this.optedOut = optedOut; + String stored = Display.isInitialized() ? Preferences.get(key("distinct_id"), (String) null) : null; + distinctId = stored != null ? stored : randomId(); + if (stored == null && Display.isInitialized()) { + Preferences.set(key("distinct_id"), distinctId); + } + } + + public static Future init(String token, Boolean optOutTrackingDefault, Boolean trackAutomaticEvents, + Map superProperties, Map config) { + Mixpanel m = new Mixpanel(token, Boolean.TRUE.equals(optOutTrackingDefault)); + if (superProperties != null) { + m.registerSuperProperties(superProperties); + } + return Future.value(m); + } + + private String key(String what) { + return "mixpanel." + token + "." + what; + } + + private static String randomId() { + return "$device:" + Long.toHexString(RANDOM.nextLong() & Long.MAX_VALUE) + + Long.toHexString(System.currentTimeMillis()); + } + + static String isoNow() { + return dart.core.DateTime.now().toUtc().toIso8601String(); + } + + /** The events and profile updates waiting for the next flush; not part of the plugin's API. */ + public long cn1PendingCount() { + return events.size() + profileUpdates.size(); + } + + public void setServerURL(String url) { + if (url != null) { + serverUrl = url.endsWith("/") ? url.substring(0, url.length() - 1) : url; + } + } + + public void setLoggingEnabled(boolean enabled) { + } + + public void setUseIpAddressForGeolocation(boolean use) { + } + + public void setFlushBatchSize(long size) { + flushBatchSize = Math.max(1, size); + } + + public Future hasOptedOutTracking() { + return Future.value(Boolean.valueOf(optedOut)); + } + + public void optInTracking() { + optedOut = false; + } + + public void optOutTracking() { + optedOut = true; + events.clear(); + profileUpdates.clear(); + } + + public Future identify(String id) { + if (id != null && id.length() > 0 && !id.equals(distinctId)) { + Map props = new LinkedHashMap(); + props.put("$anon_distinct_id", distinctId); + String previous = distinctId; + distinctId = id; + if (Display.isInitialized()) { + Preferences.set(key("distinct_id"), id); + } + if (previous.startsWith("$device:")) { + queueEvent("$identify", props); + } + } + return Future.value(null); + } + + public void alias(String alias, String original) { + Map props = new LinkedHashMap(); + props.put("alias", alias); + props.put("distinct_id", original); + queueEvent("$create_alias", props); + } + + public Future getDistinctId() { + return Future.value(distinctId); + } + + public Future track(String eventName, Map properties) { + Map props = new LinkedHashMap(); + Long started = timedEvents.remove(eventName); + if (started != null) { + props.put("$duration", Double.valueOf((System.currentTimeMillis() - started.longValue()) / 1000.0)); + } + if (properties != null) { + for (Map.Entry e : properties.entrySet()) { + props.put(String.valueOf(e.getKey()), e.getValue()); + } + } + queueEvent(eventName, props); + return Future.value(null); + } + + public void timeEvent(String eventName) { + timedEvents.put(eventName, Long.valueOf(System.currentTimeMillis())); + } + + public Future eventElapsedTime(String eventName) { + Long started = timedEvents.get(eventName); + return Future.value(Double.valueOf(started == null ? 0 + : (System.currentTimeMillis() - started.longValue()) / 1000.0)); + } + + public void registerSuperProperties(Map properties) { + if (properties != null) { + for (Map.Entry e : properties.entrySet()) { + superProperties.put(String.valueOf(e.getKey()), e.getValue()); + } + } + } + + public void registerSuperPropertiesOnce(Map properties) { + if (properties != null) { + for (Map.Entry e : properties.entrySet()) { + String k = String.valueOf(e.getKey()); + if (!superProperties.containsKey(k)) { + superProperties.put(k, e.getValue()); + } + } + } + } + + public void unregisterSuperProperty(String name) { + superProperties.remove(name); + } + + public Future> getSuperProperties() { + DartMap out = new DartMap(); + out.putAll(superProperties); + return Future.value(out); + } + + public void clearSuperProperties() { + superProperties.clear(); + } + + public People getPeople() { + return new People(this); + } + + /** A new anonymous id; the queue and super properties are cleared, as in the plugin. */ + public void reset() { + events.clear(); + profileUpdates.clear(); + superProperties.clear(); + timedEvents.clear(); + distinctId = randomId(); + if (Display.isInitialized()) { + Preferences.set(key("distinct_id"), distinctId); + } + } + + private void queueEvent(String name, Map props) { + if (optedOut) { + return; + } + Map all = new LinkedHashMap(); + all.putAll(superProperties); + all.putAll(props); + all.put("token", token); + if (!all.containsKey("distinct_id")) { + all.put("distinct_id", distinctId); + } + all.put("time", Long.valueOf(new Date().getTime())); + all.put("$insert_id", Long.toHexString(RANDOM.nextLong() & Long.MAX_VALUE)); + all.put("mp_lib", "flutter"); + all.put("$os", Display.isInitialized() ? Display.getInstance().getPlatformName() : "headless"); + Map ev = new LinkedHashMap(); + ev.put("event", name); + ev.put("properties", all); + events.add(ev); + autoFlush(); + } + + void engage(Map operation) { + if (optedOut) { + return; + } + Map u = new LinkedHashMap(); + u.put("$token", token); + u.put("$distinct_id", distinctId); + u.putAll(operation); + profileUpdates.add(u); + autoFlush(); + } + + private void autoFlush() { + if (cn1PendingCount() >= flushBatchSize) { + flush(); + } + } + + public Future flush() { + if (!Display.isInitialized() || flushing || cn1PendingCount() == 0) { + return Future.value(null); + } + if (!events.isEmpty()) { + send("/track", events); + } + if (!profileUpdates.isEmpty()) { + send("/engage", profileUpdates); + } + return Future.value(null); + } + + /** POSTs the queued batch; it leaves the queue only once Mixpanel accepted it. */ + private void send(String path, final List queue) { + final List batch = new ArrayList(queue); + flushing = true; + ConnectionRequest req = new ConnectionRequest() { + @Override + protected void postResponse() { + flushing = false; + queue.removeAll(batch); + } + + @Override + protected void handleException(Exception err) { + flushing = false; + Log.p("mixpanel: flush failed, kept for the next one: " + err); + } + + @Override + protected void handleErrorResponseCode(int code, String message) { + flushing = false; + Log.p("mixpanel: " + code + " " + message); + if (code >= 400 && code < 500) { + // a rejected batch is never accepted later; resending it would block the queue + queue.removeAll(batch); + } + } + }; + req.setUrl(serverUrl + path + "?ip=1"); + req.setPost(true); + req.setHttpMethod("POST"); + req.setContentType("application/json"); + req.addRequestHeader("Accept", "text/plain"); + req.setRequestBody(Json.write(batch)); + req.setFailSilently(true); + NetworkManager.getInstance().addToQueue(req); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/mixpanel/People.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/mixpanel/People.java new file mode 100644 index 00000000000..9c01c2b4cba --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/mixpanel/People.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.mixpanel; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import dart.core.DartList; + +/** + * mixpanel_flutter's {@code People}: each call queues one {@code /engage} update for the + * current distinct id, sent with the events on the next flush. + */ +public class People { + private final Mixpanel mixpanel; + + People(Mixpanel mixpanel) { + this.mixpanel = mixpanel; + } + + private void op(String operation, Object value) { + Map m = new LinkedHashMap(); + m.put(operation, value); + mixpanel.engage(m); + } + + private static Map one(String prop, Object value) { + Map m = new LinkedHashMap(); + m.put(prop, value); + return m; + } + + public void set(String prop, Object to) { + op("$set", one(prop, to)); + } + + public void setOnce(String prop, Object to) { + op("$set_once", one(prop, to)); + } + + public void increment(String prop, double by) { + op("$add", one(prop, Double.valueOf(by))); + } + + public void append(String name, Object value) { + op("$append", one(name, value)); + } + + public void union(String name, List value) { + op("$union", one(name, value)); + } + + public void remove(String name, Object value) { + op("$remove", one(name, value)); + } + + public void unset(String name) { + DartList names = new DartList(); + names.add(name); + op("$unset", names); + } + + public void trackCharge(double amount, Map properties) { + Map charge = new LinkedHashMap(); + charge.put("$amount", Double.valueOf(amount)); + charge.put("$time", Mixpanel.isoNow()); + if (properties != null) { + for (Map.Entry e : properties.entrySet()) { + charge.put(String.valueOf(e.getKey()), e.getValue()); + } + } + op("$append", one("$transactions", charge)); + } + + public void clearCharges() { + op("$set", one("$transactions", new DartList())); + } + + public void deleteUser() { + op("$delete", ""); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/svg/ColorFilter.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/svg/ColorFilter.java new file mode 100644 index 00000000000..aada52ca7d6 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/svg/ColorFilter.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.svg; + +import com.codename1.flutter.BlendMode; +import com.codename1.flutter.Color; + +/** dart:ui's ColorFilter.mode as flutter_svg takes it; SvgPicture does not apply it (see there). */ +public class ColorFilter { + final Color color; + final BlendMode blendMode; + + ColorFilter(Color color, BlendMode blendMode) { + this.color = color; + this.blendMode = blendMode; + } + + public static ColorFilter mode(Color color, BlendMode blendMode) { + return new ColorFilter(color, blendMode); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/svg/SvgPicture.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/svg/SvgPicture.java new file mode 100644 index 00000000000..535677c884b --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/svg/SvgPicture.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.svg; + +import java.io.InputStream; + +import com.codename1.flutter.BoxFit; +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Element; +import com.codename1.flutter.FlutterAssets; +import com.codename1.flutter.Key; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.Size; +import com.codename1.io.ConnectionRequest; +import com.codename1.io.Log; +import com.codename1.io.NetworkManager; +import com.codename1.io.Util; +import com.codename1.ui.Display; +import com.codename1.ui.Image; + +import dart.runtime.Funcs; +import dart.typed_data.Uint8List; + +/** + * flutter_svg's SvgPicture over Codename One's {@link Image#createSVG}, which renders + * where the port supports SVG ({@link Image#isSVGSupported}). Where it does not, and + * headless, the picture is an empty box of the requested size: a pure-Java SVG + * rasterizer is not part of the runtime. colorFilter, placeholderBuilder, fit and + * alignment are accepted and not applied; the image is scaled to the box. + */ +public class SvgPicture extends Widget { + private final int kind; + private final Object source; + Double width; + Double height; + String semanticsLabel; + private byte[] data; + + static final int ASSET = 0; + static final int NETWORK = 1; + static final int STRING = 2; + static final int FILE = 3; + static final int MEMORY = 4; + + SvgPicture(int kind, Object source, Key key, Double width, Double height, String semanticsLabel) { + this.kind = kind; + this.source = source; + this.width = width; + this.height = height; + this.semanticsLabel = semanticsLabel; + if (key != null) { + key(key); + } + } + + public static SvgPicture asset(String assetName, Key key, Double width, Double height, BoxFit fit, Object alignment, String pkg, ColorFilter colorFilter, String semanticsLabel, Funcs.Func1 placeholderBuilder, Boolean matchTextDirection, Boolean excludeFromSemantics, Object clipBehavior, Object theme) { + return new SvgPicture(ASSET, assetName, key, width, height, semanticsLabel); + } + + public static SvgPicture network(String url, Key key, Double width, Double height, BoxFit fit, Object alignment, Object headers, ColorFilter colorFilter, String semanticsLabel, Funcs.Func1 placeholderBuilder, Boolean matchTextDirection, Boolean excludeFromSemantics, Object clipBehavior, Object theme) { + return new SvgPicture(NETWORK, url, key, width, height, semanticsLabel); + } + + public static SvgPicture string(String string, Key key, Double width, Double height, BoxFit fit, Object alignment, ColorFilter colorFilter, String semanticsLabel, Funcs.Func1 placeholderBuilder, Boolean matchTextDirection, Boolean excludeFromSemantics, Object clipBehavior, Object theme) { + return new SvgPicture(STRING, string, key, width, height, semanticsLabel); + } + + public static SvgPicture file(dart.io.File file, Key key, Double width, Double height, BoxFit fit, Object alignment, ColorFilter colorFilter, String semanticsLabel, Funcs.Func1 placeholderBuilder, Boolean matchTextDirection, Boolean excludeFromSemantics, Object clipBehavior, Object theme) { + return new SvgPicture(FILE, file == null ? null : file.path(), key, width, height, semanticsLabel); + } + + public static SvgPicture memory(Uint8List bytes, Key key, Double width, Double height, BoxFit fit, Object alignment, ColorFilter colorFilter, String semanticsLabel, Funcs.Func1 placeholderBuilder, Boolean matchTextDirection, Boolean excludeFromSemantics, Object clipBehavior, Object theme) { + return new SvgPicture(MEMORY, bytes == null ? null : bytes.toBytes(), key, width, height, semanticsLabel); + } + + public Double width() { + return width; + } + + public Double height() { + return height; + } + + Size preferredLayoutSize() { + double w = width == null ? (height == null ? 24 : height.doubleValue()) : width.doubleValue(); + double h = height == null ? w : height.doubleValue(); + return new Size(w, h); + } + + /** The SVG document's bytes, or null when they cannot be read (logged). */ + byte[] bytes() { + if (data != null || source == null) { + return data; + } + try { + if (kind == STRING) { + data = ((String) source).getBytes("UTF-8"); + } else if (kind == MEMORY) { + data = (byte[]) source; + } else if (kind == ASSET) { + FlutterAssets.Resolved r = FlutterAssets.open(SvgPicture.class, (String) source); + InputStream in = r == null ? null : r.stream(); + if (in != null) { + try { + data = Util.readInputStream(in); + } finally { + Util.cleanup(in); + } + } + } else if (kind == FILE) { + InputStream in = com.codename1.io.FileSystemStorage.getInstance().openInputStream((String) source); + try { + data = Util.readInputStream(in); + } finally { + Util.cleanup(in); + } + } else { + ConnectionRequest req = new ConnectionRequest((String) source, false); + NetworkManager.getInstance().addToQueueAndWait(req); + data = req.getResponseData(); + } + } catch (Exception e) { + Log.e(e); + } + return data; + } + + /** The rendered picture, or null where SVG is unsupported or headless. */ + Image image() { + if (!Display.isInitialized() || !Image.isSVGSupported()) { + return null; + } + byte[] b = bytes(); + if (b == null) { + return null; + } + try { + String base = kind == NETWORK || kind == FILE ? (String) source : ""; + return Image.createSVG(base, false, b); + } catch (Exception e) { + Log.e(e); + return null; + } + } + + @Override + public Element createElement() { + return new SvgRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/svg/SvgRenderElement.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/svg/SvgRenderElement.java new file mode 100644 index 00000000000..3150418f467 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/svg/SvgRenderElement.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.svg; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; + +/** Hosts a Label showing an {@link SvgPicture}; none headless, an empty Label where SVG is unsupported. */ +public class SvgRenderElement extends RenderElement { + public SvgRenderElement(SvgPicture widget) { + super(widget); + } + + @Override + protected Component createComponent() { + if (!com.codename1.ui.Display.isInitialized()) { + return null; + } + SvgPicture p = (SvgPicture) widget(); + com.codename1.ui.Label l = new com.codename1.ui.Label(); + l.setUIID("Container"); + com.codename1.ui.Image img = p.image(); + if (img != null) { + l.setIcon(img.scaled((int) com.codename1.flutter.rendering.Dp.px(p.preferredLayoutSize().width()), + (int) com.codename1.flutter.rendering.Dp.px(p.preferredLayoutSize().height()))); + } + if (p.semanticsLabel != null) { + l.setAccessibilityText(p.semanticsLabel); + } + return l; + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + Size want = ((SvgPicture) widget()).preferredLayoutSize(); + double w = constraints.hasBoundedWidth() ? constraints.maxWidth() : Dp.px(want.width()); + double h = constraints.hasBoundedHeight() ? constraints.maxHeight() : Dp.px(want.height()); + return constraints.constrain(new Size(w, h)); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/timezone/FlutterTimezone.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/timezone/FlutterTimezone.java new file mode 100644 index 00000000000..6e1d4e0cdaf --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/timezone/FlutterTimezone.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.timezone; + +import java.util.TimeZone; + +import dart.async.Future; +import dart.core.DartList; + +/** + * flutter_timezone's {@code FlutterTimezone}, over {@code java.util.TimeZone}. A device + * class library may know only the default zone, so the available list always holds the + * local one; a zone with no identifier answers {@code UTC}. + */ +public final class FlutterTimezone { + + private FlutterTimezone() { + } + + static String localId() { + TimeZone tz = TimeZone.getDefault(); + String id = tz == null ? null : tz.getID(); + return id == null || id.length() == 0 ? "UTC" : id; + } + + public static Future getLocalTimezone() { + return Future.value(localId()); + } + + public static Future> getAvailableTimezones() { + DartList out = new DartList(); + String local = localId(); + boolean hasLocal = false; + String[] ids = TimeZone.getAvailableIDs(); + if (ids != null) { + for (String id : ids) { + if (id != null && id.length() > 0) { + out.add(id); + hasLocal |= id.equals(local); + } + } + } + if (!hasLocal) { + out.add(local); + } + return Future.value(out); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/MediaInfo.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/MediaInfo.java new file mode 100644 index 00000000000..2c8caaf485b --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/MediaInfo.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.videocompress; + +/** video_compress's MediaInfo. Title, author and orientation are never known. */ +public class MediaInfo { + String path; + Long width; + Long height; + Long filesize; + Double duration; + + MediaInfo(String path) { + this.path = path; + } + + public String path() { + return path; + } + + public String title() { + return null; + } + + public String author() { + return null; + } + + public Long width() { + return width; + } + + public Long height() { + return height; + } + + public Long orientation() { + return null; + } + + public Long filesize() { + return filesize; + } + + /** Milliseconds, as the plugin reports it. */ + public Double duration() { + return duration; + } + + public Boolean isCancel() { + return Boolean.FALSE; + } + + public dart.io.File file() { + return path == null ? null : new dart.io.File(path); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/ObservableBuilder.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/ObservableBuilder.java new file mode 100644 index 00000000000..d4b7e669f64 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/ObservableBuilder.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.videocompress; + +import java.util.ArrayList; +import java.util.List; + +import dart.runtime.Funcs; + +/** video_compress's ObservableBuilder: a list of subscribers. onError and onDone are never called. */ +public class ObservableBuilder { + private final List subs = new ArrayList(); + + ObservableBuilder() { + } + + public Subscription subscribe(Funcs.VoidFunc1 onData, Funcs.VoidFunc0 onError, Funcs.VoidFunc0 onDone) { + Subscription s = new Subscription(this, onData); + subs.add(s); + return s; + } + + void remove(Subscription s) { + subs.remove(s); + } + + @SuppressWarnings("unchecked") + void next(T value) { + for (Subscription s : new ArrayList(subs)) { + if (s.handler instanceof Funcs.VoidFunc1) { + ((Funcs.VoidFunc1) s.handler).call(value); + } + } + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/Subscription.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/Subscription.java new file mode 100644 index 00000000000..82988f49c30 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/Subscription.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.videocompress; + +/** video_compress's Subscription to the progress observable. */ +public class Subscription { + private final ObservableBuilder owner; + final Object handler; + + Subscription(ObservableBuilder owner, Object handler) { + this.owner = owner; + this.handler = handler; + } + + public void unsubscribe() { + owner.remove(this); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/VideoCompress.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/VideoCompress.java new file mode 100644 index 00000000000..a6a28cec318 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/VideoCompress.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.videocompress; + +import com.codename1.io.FileSystemStorage; +import com.codename1.io.Log; +import com.codename1.media.Media; +import com.codename1.media.MediaManager; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.geom.Dimension; + +import dart.async.Future; +import dart.core.UnsupportedError; +import dart.typed_data.Uint8List; + +/** + * video_compress's VideoCompress. Codename One's core has no video transcoder, so + * compressVideo does NOT compress: it answers the MediaInfo of the original file, + * logs a warning saying so, and reports progress 100 at once. An app that uploads + * the result therefore uploads the original. Real compression needs a native + * interface per platform (AVAssetExportSession, MediaCodec). The thumbnail calls + * fail with an UnsupportedError for the same reason as video_thumbnail. + * deleteOrigin is never honoured -- the original is the result. + */ +public final class VideoCompress { + private static final ObservableBuilder PROGRESS = new ObservableBuilder(); + + /** The progress observable, a static getter in Dart. */ + public static final ObservableBuilder compressProgress$ = PROGRESS; + + /** Nothing is ever being compressed. */ + public static final boolean isCompressing = false; + + private VideoCompress() { + } + + public static Future compressVideo(String path, VideoQuality quality, Boolean deleteOrigin, + Long startTime, Long duration, Boolean includeAudio, Long frameRate) { + if (Display.isInitialized()) { + // Log needs the platform; headless the warning has nowhere to go + Log.p("video_compress: no transcoder in Codename One; answering the original file uncompressed: " + path, + Log.WARNING); + } + MediaInfo info = info(path); + PROGRESS.next(Double.valueOf(100)); + return Future.value(info); + } + + public static Future getMediaInfo(String path) { + return Future.value(info(path)); + } + + private static MediaInfo info(String path) { + MediaInfo info = new MediaInfo(path); + if (!Display.isInitialized() || path == null) { + return info; + } + try { + FileSystemStorage fs = FileSystemStorage.getInstance(); + if (fs.exists(path)) { + info.filesize = Long.valueOf(fs.getLength(path)); + } + Media m = MediaManager.createMedia(path, true); + if (m != null) { + info.duration = Double.valueOf(Math.max(0, m.getDuration())); + Component c = m.getVideoComponent(); + if (c != null) { + Dimension d = c.getPreferredSize(); + info.width = Long.valueOf(d.getWidth()); + info.height = Long.valueOf(d.getHeight()); + } + m.cleanup(); + } + } catch (Exception e) { + Log.e(e); + } + return info; + } + + public static Future getFileThumbnail(String path, Long quality, Long position) { + return Future.error(new UnsupportedError("video_compress: Codename One cannot extract a video frame without native code")); + } + + public static Future getByteThumbnail(String path, Long quality, Long position) { + return Future.error(new UnsupportedError("video_compress: Codename One cannot extract a video frame without native code")); + } + + public static Future cancelCompression() { + return Future.value(null); + } + + /** Nothing is ever cached, so there is nothing to delete. */ + public static Future deleteAllCache() { + return Future.value(Boolean.TRUE); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/VideoQuality.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/VideoQuality.java new file mode 100644 index 00000000000..5eb0d3c2ddd --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videocompress/VideoQuality.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.videocompress; + +/** video_compress's VideoQuality: accepted; nothing is transcoded. */ +public enum VideoQuality { + DefaultQuality, LowQuality, MediumQuality, HighestQuality, Res640x480Quality, Res960x540Quality, Res1280x720Quality, Res1920x1080Quality +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayer.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayer.java new file mode 100644 index 00000000000..233ca446198 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayer.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.videoplayer; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.Size; + +/** video_player's VideoPlayer: hosts the controller's Media video component. */ +public class VideoPlayer extends Widget { + final VideoPlayerController controller; + + public VideoPlayer(VideoPlayerController controller) { + this.controller = controller; + } + + Size preferredLayoutSize() { + VideoPlayerValue v = controller == null ? null : controller.value(); + if (v == null || v.width <= 0 || v.height <= 0) { + return new Size(320, 180); + } + return new Size(v.width, v.height); + } + + @Override + public Element createElement() { + return new VideoPlayerRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerController.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerController.java new file mode 100644 index 00000000000..62df08931f6 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerController.java @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.videoplayer; + +import java.io.InputStream; + +import com.codename1.flutter.FlutterAssets; +import com.codename1.flutter.foundation.ValueNotifier; +import com.codename1.io.Log; +import com.codename1.media.Media; +import com.codename1.media.MediaManager; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.Form; +import com.codename1.ui.geom.Dimension; +import com.codename1.ui.util.UITimer; +import com.codename1.util.AsyncResource; +import com.codename1.util.SuccessCallback; + +import dart.async.Completer; +import dart.async.Future; +import dart.core.DartUri; +import dart.core.Duration; +import dart.core.UnsupportedError; + +/** + * video_player's VideoPlayerController over a Codename One {@link Media} from + * {@link MediaManager}. While playing, the position is polled every 250 ms with a + * UITimer on the current form, and each change notifies the listeners. + * + * Accepted and not applied: httpHeaders (MediaManager opens a URL without them), + * formatHint, captions and setPlaybackSpeed, which is recorded in the value but + * not applied -- Media has no rate control. Without a display initialize fails as + * the plugin does with no platform player, and the value keeps its defaults. + */ +public class VideoPlayerController extends ValueNotifier { + private final String dataSource; + private final boolean asset; + private Media media; + private UITimer poll; + private boolean disposed; + + VideoPlayerController(String dataSource, boolean asset) { + super(new VideoPlayerValue()); + this.dataSource = dataSource; + this.asset = asset; + } + + public static VideoPlayerController networkUrl(DartUri url, VideoPlayerOptions options, Object httpHeaders, + Object formatHint, Object closedCaptionFile) { + return new VideoPlayerController(String.valueOf(url), false); + } + + public static VideoPlayerController network(String dataSource, VideoPlayerOptions options, Object httpHeaders, + Object formatHint, Object closedCaptionFile) { + return new VideoPlayerController(dataSource, false); + } + + public static VideoPlayerController file(dart.io.File file, VideoPlayerOptions options, Object httpHeaders, + Object closedCaptionFile) { + return new VideoPlayerController(file == null ? "" : file.path(), false); + } + + public static VideoPlayerController asset(String dataSource, String pkg, VideoPlayerOptions options, + Object closedCaptionFile) { + return new VideoPlayerController(dataSource, true); + } + + public String dataSource() { + return dataSource; + } + + public Future position() { + return Future.value(value().position()); + } + + Media media() { + return media; + } + + public Future initialize() { + if (!Display.isInitialized()) { + VideoPlayerValue v = value().copy(); + v.errorDescription = "no video player without a display"; + value(v); + return Future.error(new UnsupportedError(v.errorDescription)); + } + final Completer done = new Completer(); + Runnable onCompletion = new Runnable() { + @Override + public void run() { + completed(); + } + }; + AsyncResource res; + try { + if (asset) { + FlutterAssets.Resolved r = FlutterAssets.open(VideoPlayerController.class, dataSource); + InputStream in = r == null ? null : r.stream(); + if (in == null) { + return fail(done, "asset not found: " + dataSource); + } + res = MediaManager.createMediaAsync(in, mimeOf(dataSource), onCompletion); + } else { + res = MediaManager.createMediaAsync(dataSource, true, onCompletion); + } + } catch (Exception e) { + return fail(done, String.valueOf(e.getMessage())); + } + res.ready(new SuccessCallback() { + @Override + public void onSucess(Media m) { + if (disposed) { + m.cleanup(); + return; + } + media = m; + VideoPlayerValue v = value().copy(); + v.initialized = true; + v.durationMs = Math.max(0, m.getDuration()); + Component c = m.getVideoComponent(); + if (c != null) { + Dimension d = c.getPreferredSize(); + v.width = d.getWidth(); + v.height = d.getHeight(); + } + v.volume = m.getVolume() / 100.0; + value(v); + done.complete(null); + } + }); + res.except(new SuccessCallback() { + @Override + public void onSucess(Throwable t) { + fail(done, String.valueOf(t.getMessage())); + } + }); + return done.future(); + } + + private Future fail(Completer done, String why) { + VideoPlayerValue v = value().copy(); + v.errorDescription = why; + value(v); + done.completeError(new UnsupportedError(why)); + return done.future(); + } + + private static String mimeOf(String path) { + String p = path == null ? "" : path; + if (p.regionMatches(true, Math.max(0, p.length() - 5), ".webm", 0, 5)) { + return "video/webm"; + } + if (p.regionMatches(true, Math.max(0, p.length() - 4), ".mov", 0, 4)) { + return "video/quicktime"; + } + return "video/mp4"; + } + + private void completed() { + if (media == null) { + return; + } + if (value().looping) { + media.setTime(0); + media.play(); + return; + } + stopPolling(); + VideoPlayerValue v = value().copy(); + v.playing = false; + v.completed = true; + v.positionMs = v.durationMs; + value(v); + } + + public Future play() { + if (media != null) { + media.play(); + VideoPlayerValue v = value().copy(); + v.playing = true; + v.completed = false; + value(v); + startPolling(); + } + return Future.value(null); + } + + public Future pause() { + if (media != null) { + media.pause(); + stopPolling(); + VideoPlayerValue v = value().copy(); + v.playing = false; + v.positionMs = media.getTime(); + value(v); + } + return Future.value(null); + } + + public Future seekTo(Duration position) { + long ms = position == null ? 0 : position.inMilliseconds(); + VideoPlayerValue v = value().copy(); + v.positionMs = Math.max(0, v.durationMs > 0 ? Math.min(ms, v.durationMs) : ms); + if (media != null) { + media.setTime((int) v.positionMs); + } + value(v); + return Future.value(null); + } + + public Future setLooping(boolean looping) { + VideoPlayerValue v = value().copy(); + v.looping = looping; + value(v); + return Future.value(null); + } + + public Future setVolume(double volume) { + VideoPlayerValue v = value().copy(); + v.volume = Math.max(0, Math.min(1, volume)); + if (media != null) { + media.setVolume((int) Math.round(v.volume * 100)); + } + value(v); + return Future.value(null); + } + + public Future setPlaybackSpeed(double speed) { + VideoPlayerValue v = value().copy(); + v.playbackSpeed = speed; + value(v); + return Future.value(null); + } + + private void startPolling() { + if (poll != null || !Display.isInitialized()) { + return; + } + Form f = Display.getInstance().getCurrent(); + if (f == null) { + return; + } + poll = new UITimer(new Runnable() { + @Override + public void run() { + if (media == null) { + stopPolling(); + return; + } + VideoPlayerValue v = value().copy(); + v.positionMs = media.getTime(); + v.playing = media.isPlaying(); + if (v.durationMs <= 0) { + v.durationMs = Math.max(0, media.getDuration()); + } + value(v); + } + }); + poll.schedule(250, true, f); + } + + private void stopPolling() { + if (poll != null) { + poll.cancel(); + poll = null; + } + } + + @Override + public void dispose() { + disposed = true; + stopPolling(); + if (media != null) { + try { + media.cleanup(); + } catch (RuntimeException e) { + Log.e(e); + } + media = null; + } + super.dispose(); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerOptions.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerOptions.java new file mode 100644 index 00000000000..83069b6a576 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerOptions.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.videoplayer; + +/** video_player's VideoPlayerOptions: accepted, neither option is applied. */ +public class VideoPlayerOptions { + public VideoPlayerOptions() { + } + + public void mixWithOthers(Boolean v) { + } + + public void allowBackgroundPlayback(Boolean v) { + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerRenderElement.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerRenderElement.java new file mode 100644 index 00000000000..d664caf2c26 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerRenderElement.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.videoplayer; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; + +/** Hosts the Media video component of a {@link VideoPlayer}; none before initialize or headless. */ +public class VideoPlayerRenderElement extends RenderElement { + public VideoPlayerRenderElement(VideoPlayer widget) { + super(widget); + } + + @Override + protected Component createComponent() { + VideoPlayerController c = ((VideoPlayer) widget()).controller; + if (c == null || c.media() == null) { + return null; + } + return c.media().getVideoComponent(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + Size want = ((VideoPlayer) widget()).preferredLayoutSize(); + double w = constraints.hasBoundedWidth() ? constraints.maxWidth() : Dp.px(want.width()); + double h = constraints.hasBoundedHeight() ? constraints.maxHeight() : Dp.px(want.height()); + return constraints.constrain(new Size(w, h)); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerValue.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerValue.java new file mode 100644 index 00000000000..2fcde3c114f --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoPlayerValue.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.videoplayer; + +import com.codename1.flutter.rendering.Size; + +import dart.core.Duration; + +/** video_player's VideoPlayerValue: an immutable snapshot, copied on every change. */ +public class VideoPlayerValue { + long durationMs; + long positionMs; + double width; + double height; + boolean initialized; + boolean playing; + boolean looping; + boolean buffering; + boolean completed; + double volume = 1.0; + double playbackSpeed = 1.0; + String errorDescription; + + VideoPlayerValue() { + } + + VideoPlayerValue copy() { + VideoPlayerValue v = new VideoPlayerValue(); + v.durationMs = durationMs; + v.positionMs = positionMs; + v.width = width; + v.height = height; + v.initialized = initialized; + v.playing = playing; + v.looping = looping; + v.buffering = buffering; + v.completed = completed; + v.volume = volume; + v.playbackSpeed = playbackSpeed; + v.errorDescription = errorDescription; + return v; + } + + public Duration duration() { + return Duration.ofMicroseconds(durationMs * 1000); + } + + public Duration position() { + return Duration.ofMicroseconds(positionMs * 1000); + } + + public Size size() { + return new Size(width, height); + } + + public boolean isInitialized() { + return initialized; + } + + public boolean isPlaying() { + return playing; + } + + public boolean isLooping() { + return looping; + } + + public boolean isBuffering() { + return buffering; + } + + public boolean isCompleted() { + return completed; + } + + public double volume() { + return volume; + } + + public double playbackSpeed() { + return playbackSpeed; + } + + public boolean hasError() { + return errorDescription != null; + } + + public String errorDescription() { + return errorDescription; + } + + /** Width over height, 1.0 until the size is known, as the plugin answers. */ + public double aspectRatio() { + if (!initialized || width <= 0 || height <= 0) { + return 1.0; + } + return width / height; + } + + @Override + public String toString() { + return "VideoPlayerValue(duration: " + durationMs + "ms, position: " + positionMs + "ms, isInitialized: " + + initialized + ", isPlaying: " + playing + ", isLooping: " + looping + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoProgressColors.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoProgressColors.java new file mode 100644 index 00000000000..152862c8b56 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoProgressColors.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.videoplayer; + +import com.codename1.flutter.Color; + +/** video_player's VideoProgressColors. Only playedColor is drawn. */ +public class VideoProgressColors { + Color playedColor; + + public VideoProgressColors() { + } + + public void playedColor(Color v) { + playedColor = v; + } + + public void bufferedColor(Color v) { + } + + public void backgroundColor(Color v) { + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoProgressIndicator.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoProgressIndicator.java new file mode 100644 index 00000000000..af21a924fa2 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoProgressIndicator.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.videoplayer; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.Size; + +/** + * video_player's VideoProgressIndicator as a Codename One Slider that follows the + * controller's position; dragging it seeks when allowScrubbing is set. Padding is + * accepted and not applied. + */ +public class VideoProgressIndicator extends Widget { + final VideoPlayerController controller; + VideoProgressColors colors; + boolean allowScrubbing; + + public VideoProgressIndicator(VideoPlayerController controller) { + this.controller = controller; + } + + public void colors(VideoProgressColors v) { + colors = v; + } + + public void allowScrubbing(Boolean v) { + allowScrubbing = v != null && v.booleanValue(); + } + + public void padding(Object v) { + } + + Size preferredLayoutSize() { + return new Size(300, 8); + } + + @Override + public Element createElement() { + return new VideoProgressRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoProgressRenderElement.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoProgressRenderElement.java new file mode 100644 index 00000000000..e1a6d6c547f --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videoplayer/VideoProgressRenderElement.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.videoplayer; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.Slider; +import com.codename1.ui.events.DataChangedListener; + +import dart.core.Duration; +import dart.runtime.Funcs; + +/** Hosts a Slider for {@link VideoProgressIndicator}; none headless. */ +public class VideoProgressRenderElement extends RenderElement { + private Slider slider; + private boolean syncing; + private Funcs.VoidFunc0 listener; + + public VideoProgressRenderElement(VideoProgressIndicator widget) { + super(widget); + } + + private VideoProgressIndicator indicator() { + return (VideoProgressIndicator) widget(); + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized() || indicator().controller == null) { + return null; + } + slider = new Slider(); + slider.setMinValue(0); + slider.setMaxValue(1000); + slider.setEditable(indicator().allowScrubbing); + if (indicator().colors != null && indicator().colors.playedColor != null) { + slider.getSliderFullSelectedStyle().setBgColor(indicator().colors.playedColor.argb() & 0xffffff); + } + slider.addDataChangedListener(new DataChangedListener() { + @Override + public void dataChanged(int type, int index) { + if (syncing) { + return; + } + VideoPlayerValue v = indicator().controller.value(); + indicator().controller.seekTo(Duration.ofMicroseconds(v.durationMs * slider.getProgress())); + } + }); + listener = new Funcs.VoidFunc0() { + @Override + public void call() { + sync(); + } + }; + indicator().controller.addListener(listener); + sync(); + return slider; + } + + private void sync() { + if (slider == null) { + return; + } + VideoPlayerValue v = indicator().controller.value(); + syncing = true; + slider.setProgress(v.durationMs <= 0 ? 0 : (int) (v.positionMs * 1000 / v.durationMs)); + syncing = false; + } + + @Override + public void unmount() { + if (listener != null) { + indicator().controller.removeListener(listener); + listener = null; + } + super.unmount(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double w = constraints.hasBoundedWidth() ? constraints.maxWidth() : Dp.px(300); + return constraints.constrain(new Size(w, Dp.px(16))); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videothumbnail/ImageFormat.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videothumbnail/ImageFormat.java new file mode 100644 index 00000000000..99f2148dd2b --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videothumbnail/ImageFormat.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.videothumbnail; + +/** video_thumbnail's ImageFormat. */ +public enum ImageFormat { + JPEG, PNG, WEBP +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videothumbnail/VideoThumbnail.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videothumbnail/VideoThumbnail.java new file mode 100644 index 00000000000..e9a609338ea --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/videothumbnail/VideoThumbnail.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.videothumbnail; + +import dart.async.Future; +import dart.core.UnsupportedError; +import dart.typed_data.Uint8List; + +/** + * video_thumbnail's VideoThumbnail. Codename One's core has no portable way to + * decode a frame out of a video -- MediaManager plays media and Capture records it, + * and neither hands back a picture -- so both calls complete with an + * UnsupportedError naming that gap instead of answering a blank image. Frame + * extraction needs a native interface per platform (AVAssetImageGenerator, + * MediaMetadataRetriever). + */ +public final class VideoThumbnail { + static final String GAP = "video_thumbnail: Codename One cannot extract a video frame without native code"; + + private VideoThumbnail() { + } + + public static Future thumbnailData(String video, Object headers, ImageFormat imageFormat, + Long maxHeight, Long maxWidth, Long timeMs, Long quality) { + return Future.error(new UnsupportedError(GAP)); + } + + public static Future thumbnailFile(String video, Object headers, String thumbnailPath, + ImageFormat imageFormat, Long maxHeight, Long maxWidth, Long timeMs, Long quality) { + return Future.error(new UnsupportedError(GAP)); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webauth/FlutterWebAuth2.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webauth/FlutterWebAuth2.java new file mode 100644 index 00000000000..0dc5d5b3144 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webauth/FlutterWebAuth2.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.webauth; + +import com.codename1.flutter.platform.PlatformException; +import com.codename1.ui.BrowserComponent; +import com.codename1.ui.Command; +import com.codename1.ui.Display; +import com.codename1.ui.Form; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.BrowserNavigationCallback; +import com.codename1.ui.layouts.BorderLayout; + +import dart.async.Completer; +import dart.async.Future; + +/** + * flutter_web_auth_2's {@code FlutterWebAuth2}: the sign-in page opens in a + * BrowserComponent on a form of its own, and the first navigation to the callback scheme + * (for {@code https}, to {@code httpsHost} and {@code httpsPath}) completes the future with + * that URL and closes the form. Back or Cancel fails it with + * {@code PlatformException(code: 'CANCELED')}, as the plugin does. Without a display or a + * native browser it fails with {@code PlatformException(code: 'UNAVAILABLE')}. + */ +public final class FlutterWebAuth2 { + + private FlutterWebAuth2() { + } + + static boolean matches(String url, String scheme, FlutterWebAuth2Options options) { + if (url == null || scheme == null || url.length() <= scheme.length() + || url.charAt(scheme.length()) != ':' || !url.regionMatches(true, 0, scheme, 0, scheme.length())) { + return false; + } + boolean web = "https".equalsIgnoreCase(scheme) || "http".equalsIgnoreCase(scheme); + if (!web) { + return true; + } + // An http(s) callback is recognised by host (and path) only; matching on the + // scheme alone would end the flow on the sign-in page itself. + String host = options == null ? null : options.httpsHost(); + if (host == null) { + return false; + } + String rest = url.substring(scheme.length() + 1); + while (rest.startsWith("/")) { + rest = rest.substring(1); + } + if (!rest.regionMatches(true, 0, host, 0, host.length())) { + return false; + } + String afterHost = rest.substring(host.length()); + if (afterHost.length() > 0 && "/?#:".indexOf(afterHost.charAt(0)) < 0) { + return false; + } + String path = options.httpsPath(); + return path == null || afterHost.startsWith(path.startsWith("/") ? path : "/" + path); + } + + public static Future authenticate(final String url, final String callbackUrlScheme, + final FlutterWebAuth2Options options) { + if (!Display.isInitialized() || !BrowserComponent.isNativeBrowserSupported()) { + return Future.error(new PlatformException("UNAVAILABLE", + "No browser to show the sign-in page in", null)); + } + final Completer done = new Completer(); + final Form previous = Display.getInstance().getCurrent(); + final Form form = new Form("", new BorderLayout()); + final BrowserComponent browser = new BrowserComponent(); + final Command cancel = new Command("Cancel") { + @Override + public void actionPerformed(ActionEvent evt) { + finish(done, previous, null); + } + }; + browser.addBrowserNavigationCallback(new BrowserNavigationCallback() { + @Override + public boolean shouldNavigate(final String target) { + if (!matches(target, callbackUrlScheme, options)) { + return true; + } + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + finish(done, previous, target); + } + }); + return false; + } + }); + form.add(BorderLayout.CENTER, browser); + form.setBackCommand(cancel); + if (form.getToolbar() != null) { + form.getToolbar().addCommandToLeftBar(cancel); + } else { + form.addCommand(cancel); + } + browser.setURL(url); + form.show(); + return done.future(); + } + + static void finish(Completer done, Form previous, String callback) { + if (done.isCompleted()) { + return; + } + if (callback == null) { + done.completeError(new PlatformException("CANCELED", "User canceled login", null)); + } else { + done.complete(callback); + } + if (previous != null) { + previous.showBack(); + } + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webauth/FlutterWebAuth2Options.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webauth/FlutterWebAuth2Options.java new file mode 100644 index 00000000000..08032d9169b --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webauth/FlutterWebAuth2Options.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.webauth; + +/** + * flutter_web_auth_2's {@code FlutterWebAuth2Options}. {@code httpsHost} and + * {@code httpsPath} are used to recognise an https callback; the rest is accepted and not + * applied (the sign-in page always opens in an in-app browser). + */ +public class FlutterWebAuth2Options { + private String httpsHost; + private String httpsPath; + private Long timeout; + + public FlutterWebAuth2Options() { + } + + public void httpsHost(String v) { + httpsHost = v; + } + + public void httpsPath(String v) { + httpsPath = v; + } + + public void timeout(Long v) { + timeout = v; + } + + public FlutterWebAuth2Options(Boolean preferEphemeral, String debugOrigin, Long intentFlags, String windowName, + Long timeout, String landingPageHtml, Boolean silentAuth, Boolean useWebview, String httpsHost, + String httpsPath) { + this.httpsHost = httpsHost; + this.httpsPath = httpsPath; + this.timeout = timeout; + } + + /** Accepted, not applied. */ + public void preferEphemeral(Boolean v) { + } + + /** Accepted, not applied. */ + public void debugOrigin(String v) { + } + + /** Accepted, not applied. */ + public void intentFlags(Long v) { + } + + /** Accepted, not applied. */ + public void windowName(String v) { + } + + /** Accepted, not applied. */ + public void landingPageHtml(String v) { + } + + /** Accepted, not applied. */ + public void silentAuth(Boolean v) { + } + + /** Accepted, not applied. */ + public void useWebview(Boolean v) { + } + + public String httpsHost() { + return httpsHost; + } + + public String httpsPath() { + return httpsPath; + } + + public Long timeout() { + return timeout; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/JavaScriptMessage.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/JavaScriptMessage.java new file mode 100644 index 00000000000..5e4f8c7f4ab --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/JavaScriptMessage.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.webview; + +/** webview_flutter's JavaScriptMessage: what a page posted to a channel. */ +public class JavaScriptMessage { + private String message = ""; + + public JavaScriptMessage() { + } + + JavaScriptMessage(String message) { + this.message = message; + } + + public void message(String v) { + message = v == null ? "" : v; + } + + public String message() { + return message; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/JavaScriptMode.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/JavaScriptMode.java new file mode 100644 index 00000000000..cf9f9d916bb --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/JavaScriptMode.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.webview; + +/** webview_flutter's JavaScriptMode. Codename One cannot turn JavaScript off, so disabled is recorded only. */ +public enum JavaScriptMode { + disabled, unrestricted +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/LoadRequestMethod.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/LoadRequestMethod.java new file mode 100644 index 00000000000..d0f9c9e8306 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/LoadRequestMethod.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.webview; + +/** webview_flutter's LoadRequestMethod. */ +public enum LoadRequestMethod { + get, post +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/NavigationDecision.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/NavigationDecision.java new file mode 100644 index 00000000000..f2eda3b7c62 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/NavigationDecision.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.webview; + +/** webview_flutter's NavigationDecision. */ +public enum NavigationDecision { + prevent, navigate +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/NavigationDelegate.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/NavigationDelegate.java new file mode 100644 index 00000000000..12b0335415b --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/NavigationDelegate.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.webview; + +import dart.runtime.Funcs; + +/** + * webview_flutter's NavigationDelegate. onUrlChange and onHttpAuthRequest are + * accepted and never called: BrowserComponent reports neither. + */ +public class NavigationDelegate { + Funcs.Func1 onNavigationRequest; + Funcs.VoidFunc1 onPageStarted; + Funcs.VoidFunc1 onPageFinished; + Funcs.VoidFunc1 onProgress; + Funcs.VoidFunc1 onWebResourceError; + + public NavigationDelegate() { + } + + public void onNavigationRequest(Funcs.Func1 v) { + onNavigationRequest = v; + } + + public void onPageStarted(Funcs.VoidFunc1 v) { + onPageStarted = v; + } + + public void onPageFinished(Funcs.VoidFunc1 v) { + onPageFinished = v; + } + + public void onProgress(Funcs.VoidFunc1 v) { + onProgress = v; + } + + public void onWebResourceError(Funcs.VoidFunc1 v) { + onWebResourceError = v; + } + + /** Accepted and ignored. */ + public void onUrlChange(Object v) { + } + + /** Accepted and ignored. */ + public void onHttpAuthRequest(Object v) { + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/NavigationRequest.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/NavigationRequest.java new file mode 100644 index 00000000000..5a9b656567e --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/NavigationRequest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.webview; + +/** webview_flutter's NavigationRequest. Every request is reported as main frame. */ +public class NavigationRequest { + private final String url; + + NavigationRequest(String url) { + this.url = url; + } + + public String url() { + return url; + } + + public boolean isMainFrame() { + return true; + } + + @Override + public String toString() { + return "NavigationRequest(" + url + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebResourceError.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebResourceError.java new file mode 100644 index 00000000000..d499b59d9f6 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebResourceError.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.webview; + +/** webview_flutter's WebResourceError, built from BrowserComponent's onError event. */ +public class WebResourceError { + private final long errorCode; + private final String description; + private final String url; + + WebResourceError(long errorCode, String description, String url) { + this.errorCode = errorCode; + this.description = description; + this.url = url; + } + + public long errorCode() { + return errorCode; + } + + public String description() { + return description; + } + + public Boolean isForMainFrame() { + return Boolean.TRUE; + } + + public String url() { + return url; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebViewController.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebViewController.java new file mode 100644 index 00000000000..7fb480ccbfb --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebViewController.java @@ -0,0 +1,417 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.webview; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.codename1.flutter.Color; +import com.codename1.flutter.FlutterAssets; +import com.codename1.io.Log; +import com.codename1.io.Util; +import com.codename1.ui.BrowserComponent; +import com.codename1.ui.Display; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.events.BrowserNavigationCallback; +import com.codename1.util.SuccessCallback; + +import dart.async.Future; +import dart.core.ArgumentError; +import dart.core.DartUri; +import dart.core.UnsupportedError; +import dart.runtime.Funcs; +import dart.typed_data.Uint8List; + +/** + * webview_flutter's WebViewController over a {@link BrowserComponent}, created when + * a {@link WebViewWidget} first mounts; what was loaded before that is applied + * then. Navigation requests go through a BrowserNavigationCallback, JavaScript + * through execute/executeAndReturnString, and each channel is a window object whose + * postMessage reaches Java through addJSCallback, installed again after every page + * load. + * + * Accepted and not applied: JavaScriptMode.disabled (a BrowserComponent always runs + * JavaScript), a POST method and body (BrowserComponent loads a URL with GET), the + * cache and local-storage clears and enableZoom. + * + * Without a display there is no browser: the controller keeps the URL and history + * itself and consults the navigation delegate, firing onPageStarted, onProgress(100) + * and onPageFinished for an allowed load, so app logic around it still runs. + */ +public class WebViewController { + private BrowserComponent browser; + private NavigationDelegate delegate; + private JavaScriptMode jsMode = JavaScriptMode.disabled; + private String userAgent; + private Color background; + private String pendingUrl; + private Map pendingHeaders; + private String pendingHtml; + private String pendingBase; + private final List history = new ArrayList(); + private int index = -1; + private final Map> channels = + new LinkedHashMap>(); + private final Map> installed = + new HashMap>(); + + public WebViewController() { + } + + public Future loadRequest(DartUri uri, LoadRequestMethod method, Object headers, Uint8List body) { + String url = String.valueOf(uri); + if (method == LoadRequestMethod.post && Display.isInitialized()) { + Log.p("webview_flutter: BrowserComponent loads with GET; the POST method and body are not sent", + Log.WARNING); + } + Map h = null; + if (headers instanceof Map) { + h = new HashMap(); + for (Map.Entry e : ((Map) headers).entrySet()) { + h.put(String.valueOf(e.getKey()), String.valueOf(e.getValue())); + } + } + return load(url, h); + } + + private Future load(String url, Map headers) { + if (browser == null) { + pendingUrl = url; + pendingHeaders = headers; + pendingHtml = null; + navigateHeadless(url); + } else if (headers != null && !headers.isEmpty()) { + browser.setURL(url, headers); + } else { + browser.setURL(url); + } + return Future.value(null); + } + + public Future loadHtmlString(String html, String baseUrl) { + if (browser == null) { + pendingHtml = html == null ? "" : html; + pendingBase = baseUrl; + pendingUrl = null; + navigateHeadless(baseUrl == null ? "about:blank" : baseUrl); + } else { + browser.setPage(html == null ? "" : html, baseUrl); + } + return Future.value(null); + } + + public Future loadFlutterAsset(String key) { + FlutterAssets.Resolved r = FlutterAssets.open(WebViewController.class, key); + if (r == null || r.stream() == null) { + return Future.error(new ArgumentError("asset not found: " + key)); + } + InputStream in = r.stream(); + try { + return loadHtmlString(new String(Util.readInputStream(in), "UTF-8"), null); + } catch (IOException e) { + return Future.error(e); + } finally { + Util.cleanup(in); + } + } + + public Future loadFile(String absoluteFilePath) { + String p = absoluteFilePath == null ? "" : absoluteFilePath; + return load(p.startsWith("file:") ? p : "file://" + p, null); + } + + public Future setJavaScriptMode(JavaScriptMode mode) { + jsMode = mode; + return Future.value(null); + } + + public JavaScriptMode javaScriptMode() { + return jsMode; + } + + public Future setNavigationDelegate(NavigationDelegate d) { + delegate = d; + return Future.value(null); + } + + public Future runJavaScript(String js) { + if (browser != null && js != null) { + browser.execute(js); + } + return Future.value(null); + } + + public Future runJavaScriptReturningResult(String js) { + if (browser == null) { + return Future.error(new UnsupportedError("no web view is attached to evaluate JavaScript")); + } + return Future.value(browser.executeAndReturnString(js)); + } + + public Future addJavaScriptChannel(String name, Funcs.VoidFunc1 onMessageReceived) { + channels.put(name, onMessageReceived); + install(name); + return Future.value(null); + } + + public Future removeJavaScriptChannel(String name) { + channels.remove(name); + SuccessCallback cb = installed.remove(name); + if (browser != null && cb != null) { + browser.removeJSCallback(cb); + browser.execute("try { delete window[" + quote(name) + "]; } catch (e) {}"); + } + return Future.value(null); + } + + /** The registered channel names, in registration order. */ + public List channelNames() { + return new ArrayList(channels.keySet()); + } + + public Future reload() { + if (browser != null) { + browser.reload(); + } else if (index >= 0) { + pageLoaded(history.get(index)); + } + return Future.value(null); + } + + public Future goBack() { + if (browser != null) { + browser.back(); + } else if (index > 0) { + index--; + pageLoaded(history.get(index)); + } + return Future.value(null); + } + + public Future goForward() { + if (browser != null) { + browser.forward(); + } else if (index < history.size() - 1) { + index++; + pageLoaded(history.get(index)); + } + return Future.value(null); + } + + public Future canGoBack() { + return Future.value(Boolean.valueOf(browser != null ? browser.hasBack() : index > 0)); + } + + public Future canGoForward() { + return Future.value(Boolean.valueOf(browser != null ? browser.hasForward() : index < history.size() - 1)); + } + + public Future currentUrl() { + if (browser != null) { + return Future.value(browser.getURL()); + } + return Future.value(index >= 0 ? history.get(index) : null); + } + + public Future getTitle() { + if (browser == null) { + return Future.value(null); + } + return Future.value(browser.executeAndReturnString("document.title")); + } + + public Future setBackgroundColor(Color color) { + background = color; + applyBackground(); + return Future.value(null); + } + + public Future setUserAgent(String ua) { + userAgent = ua; + if (browser != null && ua != null) { + browser.setProperty("UserAgent", ua); + } + return Future.value(null); + } + + /** Accepted; BrowserComponent offers no cache clear. */ + public Future clearCache() { + return Future.value(null); + } + + /** Accepted; BrowserComponent offers no storage clear. */ + public Future clearLocalStorage() { + return Future.value(null); + } + + /** Accepted and ignored. */ + public Future enableZoom(boolean enabled) { + return Future.value(null); + } + + // --- headless -------------------------------------------------------- + + private boolean allowed(String url) { + if (delegate == null || delegate.onNavigationRequest == null) { + return true; + } + Object d = delegate.onNavigationRequest.call(new NavigationRequest(url)); + // A Future answer cannot be waited for inside a synchronous + // BrowserNavigationCallback, so only a NavigationDecision value can prevent. + return d != NavigationDecision.prevent; + } + + private void navigateHeadless(String url) { + if (!allowed(url)) { + return; + } + while (history.size() > index + 1) { + history.remove(history.size() - 1); + } + history.add(url); + index = history.size() - 1; + pageLoaded(url); + } + + private void pageLoaded(String url) { + if (delegate == null) { + return; + } + if (delegate.onPageStarted != null) { + delegate.onPageStarted.call(url); + } + if (delegate.onProgress != null) { + delegate.onProgress.call(Long.valueOf(100)); + } + if (delegate.onPageFinished != null) { + delegate.onPageFinished.call(url); + } + } + + // --- the peer -------------------------------------------------------- + + /** Creates the BrowserComponent on first use and applies what was loaded before it. */ + BrowserComponent attach() { + if (browser != null || !Display.isInitialized()) { + return browser; + } + browser = new BrowserComponent(); + browser.setBrowserNavigationCallback(new BrowserNavigationCallback() { + @Override + public boolean shouldNavigate(String url) { + return allowed(url); + } + }); + browser.addWebEventListener(BrowserComponent.onStart, new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + if (delegate != null && delegate.onPageStarted != null) { + delegate.onPageStarted.call(eventUrl(evt)); + } + } + }); + browser.addWebEventListener(BrowserComponent.onLoad, new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + installed.clear(); + for (String name : channels.keySet()) { + install(name); + } + if (delegate != null && delegate.onProgress != null) { + delegate.onProgress.call(Long.valueOf(100)); + } + if (delegate != null && delegate.onPageFinished != null) { + delegate.onPageFinished.call(eventUrl(evt)); + } + } + }); + browser.addWebEventListener(BrowserComponent.onError, new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + if (delegate != null && delegate.onWebResourceError != null) { + Object src = evt.getSource(); + delegate.onWebResourceError.call(new WebResourceError(-1, + src instanceof String ? (String) src : "the page failed to load", browser.getURL())); + } + } + }); + if (userAgent != null) { + browser.setProperty("UserAgent", userAgent); + } + applyBackground(); + if (pendingHtml != null) { + browser.setPage(pendingHtml, pendingBase); + } else if (pendingUrl != null) { + load(pendingUrl, pendingHeaders); + } + return browser; + } + + private String eventUrl(ActionEvent evt) { + Object src = evt.getSource(); + return src instanceof String ? (String) src : browser.getURL(); + } + + private void applyBackground() { + if (browser != null && background != null) { + browser.getAllStyles().setBgColor(background.argb() & 0xffffff); + browser.getAllStyles().setBgTransparency(background.alpha()); + } + } + + private void install(final String name) { + if (browser == null || installed.containsKey(name)) { + return; + } + SuccessCallback cb = new SuccessCallback() { + @Override + public void onSucess(BrowserComponent.JSRef value) { + Funcs.VoidFunc1 h = channels.get(name); + if (h != null) { + h.call(new JavaScriptMessage(value == null ? "" : value.getValue())); + } + } + }; + installed.put(name, cb); + browser.addJSCallback("window[" + quote(name) + "] = {postMessage: function(m) { callback.onSuccess(String(m)); }};", cb); + } + + private static String quote(String s) { + StringBuilder b = new StringBuilder("'"); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' || c == '\'') { + b.append('\\'); + } + b.append(c); + } + return b.append('\'').toString(); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebViewRenderElement.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebViewRenderElement.java new file mode 100644 index 00000000000..453a1b112f6 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebViewRenderElement.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.webview; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; + +/** Hosts the {@link WebViewController}'s BrowserComponent; none headless. */ +public class WebViewRenderElement extends RenderElement { + public WebViewRenderElement(WebViewWidget widget) { + super(widget); + } + + @Override + protected Component createComponent() { + WebViewController c = ((WebViewWidget) widget()).controller; + return c == null ? null : c.attach(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double w = constraints.hasBoundedWidth() ? constraints.maxWidth() : Dp.px(300); + double h = constraints.hasBoundedHeight() ? constraints.maxHeight() : Dp.px(300); + return constraints.constrain(new Size(w, h)); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebViewWidget.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebViewWidget.java new file mode 100644 index 00000000000..bbd4db68258 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/webview/WebViewWidget.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.webview; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** webview_flutter's WebViewWidget, hosting the controller's BrowserComponent. */ +public class WebViewWidget extends Widget { + WebViewController controller; + + public WebViewWidget() { + } + + public void controller(WebViewController v) { + controller = v; + } + + /** Accepted and ignored. */ + public void gestureRecognizers(Object v) { + } + + @Override + public Element createElement() { + return new WebViewRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/dart_io.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/dart_io.dart index 2244efceb76..3a0e3c52073 100644 --- a/maven/flutter-runtime/src/main/resources/META-INF/dart/dart_io.dart +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/dart_io.dart @@ -89,6 +89,46 @@ class File extends FileSystemEntity { external Future copy(String newPath); external File renameSync(String newPath); external Future rename(String newPath); + external RandomAccessFile openSync({FileMode mode = FileMode.read}); + external Future open({FileMode mode = FileMode.read}); +} + +@JavaName('dart.io.FileLock') +enum FileLock { shared, exclusive, blockingShared, blockingExclusive } + +// Held in memory while open and written back on flush/close; see +// dart.io.RandomAccessFile. +@JavaName('dart.io.RandomAccessFile') +class RandomAccessFile { + external String get path; + external int positionSync(); + external Future position(); + external void setPositionSync(int position); + external Future setPosition(int position); + external int lengthSync(); + external Future length(); + external int readByteSync(); + external Future readByte(); + external Uint8List readSync(int count); + external Future read(int count); + external int readIntoSync(List buffer, [int start = 0, int? end]); + external Future readInto(List buffer, [int start = 0, int? end]); + external RandomAccessFile writeByteSync(int value); + external Future writeByte(int value); + external RandomAccessFile writeFromSync(List buffer, [int start = 0, int? end]); + external Future writeFrom(List buffer, [int start = 0, int? end]); + external RandomAccessFile writeStringSync(String string, {Object? encoding}); + external Future writeString(String string, {Object? encoding}); + external RandomAccessFile truncateSync(int length); + external Future truncate(int length); + external RandomAccessFile flushSync(); + external Future flush(); + external void closeSync(); + external Future close(); + external RandomAccessFile lockSync([FileLock mode = FileLock.exclusive, int start = 0, int end = -1]); + external Future lock([FileLock mode = FileLock.exclusive, int start = 0, int end = -1]); + external RandomAccessFile unlockSync([int start = 0, int end = -1]); + external Future unlock([int start = 0, int end = -1]); } @JavaName('dart.io.Directory') diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/dartlib_developer.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/dartlib_developer.dart new file mode 100644 index 00000000000..d8738258533 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/dartlib_developer.dart @@ -0,0 +1,40 @@ +// Codename One Dart runtime API stubs -- dart:developer. +// +// A dartlib_.dart file is reached only through an import of +// dart:: dart:developer's log is not dart:math's, and the stubs are +// otherwise one global namespace. Backed by dart.developer (maven/dart-runtime): +// log goes to Codename One's Log; the timeline and the service protocol have +// nothing to talk to and do nothing. + +@JavaName('dart.developer.Developer.log') +external void log(String message, {DateTime? time, int? sequenceNumber, int level = 0, String name = '', + Object? zone, Object? error, StackTrace? stackTrace}); + +@JavaName('dart.developer.Developer.debugger') +external bool debugger({bool when = true, String? message}); + +@JavaName('dart.developer.Developer.inspect') +external Object? inspect(Object? object); + +@JavaName('dart.developer.Developer.postEvent') +external void postEvent(String eventKind, Map eventData, {String stream = 'Extension'}); + +@JavaName('dart.developer.Developer.registerExtension') +external void registerExtension(String method, Object handler); + +@JavaName('dart.developer.Timeline') +abstract class Timeline { + external static void startSync(String name, {Map? arguments, Object? flow}); + external static void finishSync(); + external static void instantSync(String name, {Map? arguments}); + external static T timeSync(String name, T Function() function, {Map? arguments, Object? flow}); +} + +@JavaName('dart.developer.TimelineTask') +class TimelineTask { + external TimelineTask({TimelineTask? parent, String? filterKey}); + external void start(String name, {Map? arguments}); + external void instant(String name, {Map? arguments}); + external void finish({Map? arguments}); + external TimelineTask pass(); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_android_play_install_referrer.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_android_play_install_referrer.dart new file mode 100644 index 00000000000..f9f8314a8c6 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_android_play_install_referrer.dart @@ -0,0 +1,18 @@ +// package:android_play_install_referrer. The Play Install Referrer service is not +// bound, so installReferrer fails as it does where the service is unavailable. + +@JavaName('com.codename1.flutter.plugins.installreferrer.ReferrerDetails') +class ReferrerDetails { + external String? get installReferrer; + external int get referrerClickTimestampSeconds; + external int get installBeginTimestampSeconds; + external int get referrerClickTimestampServerSeconds; + external int get installBeginTimestampServerSeconds; + external String? get installVersion; + external bool get googlePlayInstantParam; +} + +@JavaName('com.codename1.flutter.plugins.installreferrer.AndroidPlayInstallReferrer') +class AndroidPlayInstallReferrer { + external static Future get installReferrer; +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_app_settings.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_app_settings.dart new file mode 100644 index 00000000000..8cbaeef2965 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_app_settings.dart @@ -0,0 +1,17 @@ +// package:app_settings, over Display.execute: app-settings: on iOS, a settings +// intent on Android, nothing elsewhere. + +@JavaName('com.codename1.flutter.plugins.appsettings.AppSettingsType') +enum AppSettingsType { accessibility, alarm, apn, appLocale, batteryOptimization, bluetooth, cellular, + dataRoaming, date, developer, device, display, generalSettings, hotspot, internalStorage, location, + lockAndPassword, manageUnknownApps, nfc, notification, security, settings, sound, subscriptions, vpn, + wifi, wireless } + +@JavaName('com.codename1.flutter.plugins.appsettings.AppSettingsPanelType') +enum AppSettingsPanelType { internetConnectivity, nfc, volume, wifi } + +@JavaName('com.codename1.flutter.plugins.appsettings.AppSettings') +class AppSettings { + external static Future openAppSettings({AppSettingsType type, bool asAnotherTask}); + external static Future openAppSettingsPanel(AppSettingsPanelType type); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_audioplayers.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_audioplayers.dart new file mode 100644 index 00000000000..ab34b54b567 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_audioplayers.dart @@ -0,0 +1,75 @@ +// package:audioplayers, over MediaManager.createMedia. AssetSource paths are under +// the assets/ prefix, as with AudioCache. Without a display nothing sounds; the +// player state still follows the calls. + +@JavaName('com.codename1.flutter.plugins.audioplayers.PlayerState') +enum PlayerState { stopped, playing, paused, completed, disposed } + +@JavaName('com.codename1.flutter.plugins.audioplayers.ReleaseMode') +enum ReleaseMode { release, loop, stop } + +@JavaName('com.codename1.flutter.plugins.audioplayers.PlayerMode') +enum PlayerMode { mediaPlayer, lowLatency } + +@JavaName('com.codename1.flutter.plugins.audioplayers.Source') +abstract class Source { + external String? get mimeType; +} + +@JavaName('com.codename1.flutter.plugins.audioplayers.AssetSource') +class AssetSource extends Source { + external AssetSource(String path, {String? mimeType}); + external String get path; +} + +@JavaName('com.codename1.flutter.plugins.audioplayers.UrlSource') +class UrlSource extends Source { + external UrlSource(String url, {String? mimeType}); + external String get url; +} + +@JavaName('com.codename1.flutter.plugins.audioplayers.DeviceFileSource') +class DeviceFileSource extends Source { + external DeviceFileSource(String path, {String? mimeType}); + external String get path; +} + +@JavaName('com.codename1.flutter.plugins.audioplayers.BytesSource') +class BytesSource extends Source { + external BytesSource(Uint8List bytes, {String? mimeType}); + external Uint8List get bytes; +} + +@JavaName('com.codename1.flutter.plugins.audioplayers.AudioPlayer') +class AudioPlayer { + external AudioPlayer({String? playerId}); + external String get playerId; + external PlayerState get state; + external Source? get source; + external double get volume; + external ReleaseMode get releaseMode; + external Stream get onPlayerComplete; + external Stream get onPositionChanged; + external Stream get onDurationChanged; + external Stream get onPlayerStateChanged; + external Future setSource(Source source); + external Future setSourceUrl(String url, {String? mimeType}); + external Future setSourceDeviceFile(String path, {String? mimeType}); + external Future setSourceAsset(String path, {String? mimeType}); + external Future setSourceBytes(Uint8List bytes, {String? mimeType}); + external Future play(Source source, {double? volume, double? balance, Object? ctx, + Duration? position, PlayerMode? mode}); + external Future resume(); + external Future pause(); + external Future stop(); + external Future release(); + external Future seek(Duration position); + external Future setVolume(double volume); + external Future setBalance(double balance); + external Future setPlaybackRate(double playbackRate); + external Future setReleaseMode(ReleaseMode releaseMode); + external Future setPlayerMode(PlayerMode mode); + external Future getDuration(); + external Future getCurrentPosition(); + external Future dispose(); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_camera.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_camera.dart new file mode 100644 index 00000000000..019c42b5e60 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_camera.dart @@ -0,0 +1,70 @@ +// package:camera over com.codename1.camera: a live CameraView preview, photos +// and video from the CameraSession. Exposure, focus and image streams are not +// offered. + +@JavaName('com.codename1.flutter.plugins.camera.CameraLensDirection') +enum CameraLensDirection { front, back, external } + +@JavaName('com.codename1.flutter.plugins.camera.ResolutionPreset') +enum ResolutionPreset { low, medium, high, veryHigh, ultraHigh, max } + +@JavaName('com.codename1.flutter.plugins.camera.ImageFormatGroup') +enum ImageFormatGroup { unknown, yuv420, bgra8888, jpeg, nv21 } + +@JavaName('com.codename1.flutter.plugins.camera.FlashMode') +enum FlashMode { off, auto, always, torch } + +@JavaName('com.codename1.flutter.plugins.camera.CameraDescription') +class CameraDescription { + external CameraDescription({required String name, required CameraLensDirection lensDirection, + required int sensorOrientation}); + external String get name; + external CameraLensDirection get lensDirection; + external int get sensorOrientation; +} + +@JavaName('com.codename1.flutter.plugins.camera.CameraException') +class CameraException { + external CameraException(String code, String? description); + external String get code; + external String? get description; +} + +@JavaName('com.codename1.flutter.plugins.camera.CameraValue') +class CameraValue { + external bool get isInitialized; + external bool get isRecordingVideo; + external bool get isTakingPicture; + external Size? get previewSize; + external double get aspectRatio; + external FlashMode get flashMode; + external String? get errorDescription; + external bool get hasError; +} + +@JavaName('com.codename1.flutter.plugins.camera.Cameras.availableCameras') +external Future> availableCameras(); + +@JavaName('com.codename1.flutter.plugins.camera.CameraController') +class CameraController extends ValueNotifier { + external CameraController(CameraDescription description, ResolutionPreset resolutionPreset, + {bool enableAudio, ImageFormatGroup? imageFormatGroup}); + external CameraDescription get description; + external CameraValue get value; + external Future initialize(); + external Future takePicture(); + external Future startVideoRecording({dynamic onAvailable}); + external Future stopVideoRecording(); + external Future setFlashMode(FlashMode mode); + external Future setZoomLevel(double zoom); + external Future pausePreview(); + external Future resumePreview(); + external void addListener(VoidCallback listener); + external void removeListener(VoidCallback listener); + external Future dispose(); +} + +@JavaName('com.codename1.flutter.plugins.camera.CameraPreview') +class CameraPreview extends Widget { + external CameraPreview(CameraController controller, {Key? key, Widget? child}); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_emoji_picker_flutter.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_emoji_picker_flutter.dart new file mode 100644 index 00000000000..75dda30acdd --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_emoji_picker_flutter.dart @@ -0,0 +1,44 @@ +// package:emoji_picker_flutter, drawn with Codename One components from a +// built-in emoji set. Config is accepted; recents persist in Preferences. + +@JavaName('com.codename1.flutter.plugins.emojipicker.Category') +enum Category { RECENT, SMILEYS, ANIMALS, FOODS, ACTIVITIES, TRAVEL, OBJECTS, SYMBOLS, FLAGS } + +@JavaName('com.codename1.flutter.plugins.emojipicker.Emoji') +class Emoji { + external Emoji(String emoji, String name, {bool hasSkinTone}); + external String get emoji; + external String get name; + external bool get hasSkinTone; +} + +@JavaName('com.codename1.flutter.plugins.emojipicker.CategoryEmoji') +class CategoryEmoji { + external CategoryEmoji(Category category, List emoji); + external Category get category; + external List get emoji; +} + +@JavaName('com.codename1.flutter.plugins.emojipicker.Config') +class Config { + external Config({double height, bool checkPlatformCompatibility, dynamic emojiViewConfig, + dynamic skinToneConfig, dynamic categoryViewConfig, dynamic bottomActionBarConfig, + dynamic searchViewConfig, dynamic emojiSet, dynamic locale, dynamic emojiTextStyle, + dynamic customBackspaceIcon, dynamic customSearchIcon, dynamic viewOrderConfig}); +} + +@JavaName('com.codename1.flutter.plugins.emojipicker.EmojiPickerUtils') +class EmojiPickerUtils { + external EmojiPickerUtils(); + external Future> getRecentEmojis(); + external Future> searchEmoji(String keyword, List data); + external Future clearRecentEmojis(); +} + +@JavaName('com.codename1.flutter.plugins.emojipicker.EmojiPicker') +class EmojiPicker extends Widget { + external EmojiPicker({Key? key, void Function(Category? category, Emoji emoji)? onEmojiSelected, + void Function()? onBackspacePressed, TextEditingController? textEditingController, + dynamic scrollController, Config config}); + external static List get defaultEmojiSet; +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_file_picker.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_file_picker.dart new file mode 100644 index 00000000000..d2861a9ea9e --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_file_picker.dart @@ -0,0 +1,42 @@ +// package:file_picker, over Display.showOpenDialog / showSaveDialog. A cancelled +// dialog, or no display, answers null. + +@JavaName('com.codename1.flutter.plugins.filepicker.FileType') +enum FileType { any, media, image, video, audio, custom } + +@JavaName('com.codename1.flutter.plugins.filepicker.PlatformFile') +class PlatformFile { + external PlatformFile({String? path, required String name, required int size, Uint8List? bytes, + Object? readStream, String? identifier}); + external String? get path; + external String get name; + external int get size; + external Uint8List? get bytes; + external String? get identifier; + external String? get extension; +} + +@JavaName('com.codename1.flutter.plugins.filepicker.FilePickerResult') +class FilePickerResult { + external FilePickerResult(List files); + external List get files; + external int get count; + external bool get isSinglePick; + external List get paths; + external List get names; +} + +@JavaName('com.codename1.flutter.plugins.filepicker.FilePicker') +class FilePicker { + external static FilePicker get platform; + external static set platform(FilePicker instance); + external Future pickFiles({String? dialogTitle, String? initialDirectory, + FileType type, List? allowedExtensions, Function? onFileLoading, bool allowCompression, + int compressionQuality, bool allowMultiple, bool withData, bool withReadStream, + bool lockParentWindow, bool readSequential}); + external Future getDirectoryPath({String? dialogTitle, bool lockParentWindow, + String? initialDirectory}); + external Future saveFile({String? dialogTitle, String? fileName, String? initialDirectory, + FileType type, List? allowedExtensions, Uint8List? bytes, bool lockParentWindow}); + external Future clearTemporaryFiles(); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase.dart index 83fc59303c7..bd464f203fd 100644 --- a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase.dart +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase.dart @@ -1,6 +1,7 @@ // package:firebase_core, firebase_auth, firebase_analytics and firebase_messaging. // Auth talks to the Identity Toolkit REST API with the apiKey from -// Firebase.initializeApp (email/password and anonymous accounts). Analytics logs, +// Firebase.initializeApp (email/password, anonymous, and federated credentials +// through signInWithIdp; Google's and Apple's interactive sign-in are their own packs). Analytics logs, // and forwards screen views to AnalyticsService. Messaging reads the device push key // and delivers foreground pushes through onMessage. @@ -68,6 +69,63 @@ class UserCredential { external Object? get credential; } +@JavaName('com.codename1.flutter.plugins.firebase.AuthCredential') +class AuthCredential { + external String get providerId; + external String get signInMethod; + external int? get token; + external String? get accessToken; +} + +@JavaName('com.codename1.flutter.plugins.firebase.OAuthCredential') +class OAuthCredential extends AuthCredential { + external String? get idToken; + external String? get rawNonce; + external String? get secret; +} + +@JavaName('com.codename1.flutter.plugins.firebase.EmailAuthProvider') +class EmailAuthProvider { + external static String get PROVIDER_ID; + external static String get EMAIL_PASSWORD_SIGN_IN_METHOD; + external static AuthCredential credential({required String email, required String password}); +} + +@JavaName('com.codename1.flutter.plugins.firebase.OAuthProvider') +class OAuthProvider { + external OAuthProvider(String providerId); + external String get providerId; + external List get scopes; + external OAuthProvider addScope(String scope); + external OAuthProvider setCustomParameters(Map customOAuthParameters); + external OAuthCredential credential({String? accessToken, String? secret, String? idToken, String? rawNonce, + String? signInMethod}); +} + +@JavaName('com.codename1.flutter.plugins.firebasegoogle.GoogleAuthProvider') +class GoogleAuthProvider { + external GoogleAuthProvider(); + external static String get PROVIDER_ID; + external static String get GOOGLE_SIGN_IN_METHOD; + external static OAuthCredential credential({String? idToken, String? accessToken}); + external String get providerId; + external List get scopes; + external GoogleAuthProvider addScope(String scope); + external GoogleAuthProvider setCustomParameters(Map customOAuthParameters); +} + +@JavaName('com.codename1.flutter.plugins.firebaseapple.AppleAuthProvider') +class AppleAuthProvider { + external AppleAuthProvider(); + external static String get PROVIDER_ID; + external static String get APPLE_SIGN_IN_METHOD; + external static OAuthCredential credentialWithIDToken(String idToken, String rawNonce, Object? appleFullPersonName); + external String get providerId; + external List get scopes; + external AppleAuthProvider addScope(String scope); + external AppleAuthProvider setCustomParameters(Map customOAuthParameters); +} + @JavaName('com.codename1.flutter.plugins.firebase.FirebaseAuth') class FirebaseAuth { external static FirebaseAuth get instance; @@ -80,7 +138,7 @@ class FirebaseAuth { external Future createUserWithEmailAndPassword({required String email, required String password}); external Future signInAnonymously(); - external Future signInWithCredential(Object credential); + external Future signInWithCredential(AuthCredential credential); external Future signInWithProvider(Object provider); external Future signOut(); external Future sendPasswordResetEmail({required String email, Object? actionCodeSettings}); diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_image_compress.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_image_compress.dart new file mode 100644 index 00000000000..2f48aefca0c --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_image_compress.dart @@ -0,0 +1,20 @@ +// package:flutter_image_compress, over Image and ImageIO. With no display there is +// no codec and every call fails with UnsupportedError. + +@JavaName('com.codename1.flutter.plugins.imagecompress.CompressFormat') +enum CompressFormat { jpeg, png, heic, webp } + +@JavaName('com.codename1.flutter.plugins.imagecompress.FlutterImageCompress') +class FlutterImageCompress { + external static Future compressWithFile(String path, {int minWidth, int minHeight, + int inSampleSize, int quality, int rotate, bool autoCorrectionAngle, CompressFormat format, + bool keepExif, int numberOfRetries}); + external static Future compressAndGetFile(String path, String targetPath, {int minWidth, + int minHeight, int inSampleSize, int quality, int rotate, bool autoCorrectionAngle, + CompressFormat format, bool keepExif, int numberOfRetries}); + external static Future compressWithList(Uint8List image, {int minWidth, int minHeight, + int quality, int rotate, int inSampleSize, bool autoCorrectionAngle, CompressFormat format, + bool keepExif}); + external static Future compressAssetImage(String assetName, {int minWidth, int minHeight, + int quality, int rotate, bool autoCorrectionAngle, CompressFormat format, bool keepExif}); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_svg.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_svg.dart new file mode 100644 index 00000000000..e67f5222b30 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_svg.dart @@ -0,0 +1,33 @@ +// package:flutter_svg over Image.createSVG where the port renders SVG; elsewhere +// (and headless) the picture is a sized empty box. See SvgPicture.java. + +@JavaName('com.codename1.flutter.plugins.svg.ColorFilter') +class ColorFilter { + external static ColorFilter mode(Color color, BlendMode blendMode); +} + +@JavaName('com.codename1.flutter.plugins.svg.SvgPicture') +class SvgPicture extends Widget { + external static SvgPicture asset(String assetName, {Key? key, double? width, double? height, + BoxFit fit, dynamic alignment, String? package, ColorFilter? colorFilter, + String? semanticsLabel, Widget Function(BuildContext context)? placeholderBuilder, + bool matchTextDirection, bool excludeFromSemantics, dynamic clipBehavior, dynamic theme}); + external static SvgPicture network(String url, {Key? key, double? width, double? height, + BoxFit fit, dynamic alignment, Map? headers, ColorFilter? colorFilter, + String? semanticsLabel, Widget Function(BuildContext context)? placeholderBuilder, + bool matchTextDirection, bool excludeFromSemantics, dynamic clipBehavior, dynamic theme}); + external static SvgPicture string(String string, {Key? key, double? width, double? height, + BoxFit fit, dynamic alignment, ColorFilter? colorFilter, + String? semanticsLabel, Widget Function(BuildContext context)? placeholderBuilder, + bool matchTextDirection, bool excludeFromSemantics, dynamic clipBehavior, dynamic theme}); + external static SvgPicture file(File file, {Key? key, double? width, double? height, + BoxFit fit, dynamic alignment, ColorFilter? colorFilter, + String? semanticsLabel, Widget Function(BuildContext context)? placeholderBuilder, + bool matchTextDirection, bool excludeFromSemantics, dynamic clipBehavior, dynamic theme}); + external static SvgPicture memory(Uint8List bytes, {Key? key, double? width, double? height, + BoxFit fit, dynamic alignment, ColorFilter? colorFilter, + String? semanticsLabel, Widget Function(BuildContext context)? placeholderBuilder, + bool matchTextDirection, bool excludeFromSemantics, dynamic clipBehavior, dynamic theme}); + external double? get width; + external double? get height; +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_timezone.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_timezone.dart new file mode 100644 index 00000000000..0d68051ed3d --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_timezone.dart @@ -0,0 +1,7 @@ +// package:flutter_timezone, over java.util.TimeZone. + +@JavaName('com.codename1.flutter.plugins.timezone.FlutterTimezone') +class FlutterTimezone { + external static Future getLocalTimezone(); + external static Future> getAvailableTimezones(); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_web_auth_2.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_web_auth_2.dart new file mode 100644 index 00000000000..d3f7d77c75b --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_web_auth_2.dart @@ -0,0 +1,19 @@ +// package:flutter_web_auth_2: the sign-in page in a BrowserComponent on its own +// form; the callback URL completes the future, cancel fails it with +// PlatformException(code: 'CANCELED'). + +@JavaName('com.codename1.flutter.plugins.webauth.FlutterWebAuth2Options') +class FlutterWebAuth2Options { + external FlutterWebAuth2Options({bool? preferEphemeral, String? debugOrigin, int? intentFlags, + String? windowName, int? timeout, String? landingPageHtml, bool? silentAuth, bool? useWebview, + String? httpsHost, String? httpsPath}); + external String? get httpsHost; + external String? get httpsPath; + external int? get timeout; +} + +@JavaName('com.codename1.flutter.plugins.webauth.FlutterWebAuth2') +class FlutterWebAuth2 { + external static Future authenticate({required String url, required String callbackUrlScheme, + FlutterWebAuth2Options options}); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_geocoding.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_geocoding.dart new file mode 100644 index 00000000000..ca444f0d9d3 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_geocoding.dart @@ -0,0 +1,46 @@ +// package:geocoding, over Google's Geocoding REST API with the key in the Display +// property google.geocoding.key; no key, no display or no match fails with +// NoResultFoundException. + +@JavaName('com.codename1.flutter.plugins.geocoding.Location') +class Location { + external Location({required double latitude, required double longitude, required DateTime timestamp}); + external double get latitude; + external double get longitude; + external DateTime get timestamp; +} + +@JavaName('com.codename1.flutter.plugins.geocoding.Placemark') +class Placemark { + external Placemark({String? name, String? street, String? isoCountryCode, String? country, + String? postalCode, String? administrativeArea, String? subAdministrativeArea, String? locality, + String? subLocality, String? thoroughfare, String? subThoroughfare}); + external String? get name; + external String? get street; + external String? get isoCountryCode; + external String? get country; + external String? get postalCode; + external String? get administrativeArea; + external String? get subAdministrativeArea; + external String? get locality; + external String? get subLocality; + external String? get thoroughfare; + external String? get subThoroughfare; +} + +@JavaName('com.codename1.flutter.plugins.geocoding.NoResultFoundException') +class NoResultFoundException implements Exception { + external NoResultFoundException(); +} + +@JavaName('com.codename1.flutter.plugins.geocoding.Geocoding.placemarkFromCoordinates') +external Future> placemarkFromCoordinates(double latitude, double longitude); + +@JavaName('com.codename1.flutter.plugins.geocoding.Geocoding.locationFromAddress') +external Future> locationFromAddress(String address); + +@JavaName('com.codename1.flutter.plugins.geocoding.Geocoding.setLocaleIdentifier') +external Future setLocaleIdentifier(String localeIdentifier); + +@JavaName('com.codename1.flutter.plugins.geocoding.Geocoding.isPresent') +external Future isPresent(); diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_google_sign_in.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_google_sign_in.dart new file mode 100644 index 00000000000..07e5d8cc0f4 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_google_sign_in.dart @@ -0,0 +1,37 @@ +// package:google_sign_in (the version 6 API), over GoogleConnect's OIDC sign-in in the +// system browser. signInSilently answers this run's account only. + +@JavaName('com.codename1.flutter.plugins.googlesignin.GoogleSignIn') +class GoogleSignIn { + external GoogleSignIn({List scopes, Object? signInOption, String? hostedDomain, String? clientId, + String? serverClientId, bool forceCodeForRefreshToken}); + external List get scopes; + external GoogleSignInAccount? get currentUser; + external Stream get onCurrentUserChanged; + external Future signIn(); + external Future signInSilently({bool suppressErrors, bool reAuthenticate}); + external Future isSignedIn(); + external Future signOut(); + external Future disconnect(); + external Future requestScopes(List scopes); + external Future canAccessScopes(List scopes, {String? accessToken}); +} + +@JavaName('com.codename1.flutter.plugins.googlesignin.GoogleSignInAccount') +class GoogleSignInAccount { + external String get id; + external String get email; + external String? get displayName; + external String? get photoUrl; + external String? get serverAuthCode; + external Future get authentication; + external Future> get authHeaders; + external Future clearAuthCache(); +} + +@JavaName('com.codename1.flutter.plugins.googlesignin.GoogleSignInAuthentication') +class GoogleSignInAuthentication { + external String? get idToken; + external String? get accessToken; + external String? get serverAuthCode; +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_image_picker_platform.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_image_picker_platform.dart new file mode 100644 index 00000000000..34070e74091 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_image_picker_platform.dart @@ -0,0 +1,19 @@ +// package:image_picker_platform_interface and image_picker_android. One +// implementation serves every target; useAndroidPhotoPicker is accepted and has no +// effect, because Codename One's gallery already opens the system picker. + +@JavaName('com.codename1.flutter.plugins.imagepicker.ImagePickerPlatform') +abstract class ImagePickerPlatform { + external static ImagePickerPlatform get instance; + external static set instance(ImagePickerPlatform instance); + external Future getImageFromSource({required ImageSource source, Object? options}); + external Future retrieveLostData(); +} + +@JavaName('com.codename1.flutter.plugins.imagepicker.ImagePickerAndroid') +class ImagePickerAndroid extends ImagePickerPlatform { + external ImagePickerAndroid(); + external static void registerWith(); + external bool get useAndroidPhotoPicker; + external set useAndroidPhotoPicker(bool value); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_mixpanel_flutter.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_mixpanel_flutter.dart new file mode 100644 index 00000000000..dce06a9327e --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_mixpanel_flutter.dart @@ -0,0 +1,45 @@ +// package:mixpanel_flutter, over Mixpanel's HTTP ingestion API (/track and +// /engage, JSON, batched until flush). cn1PendingCount is Codename One's, not the +// plugin's: the queued events and profile updates. + +@JavaName('com.codename1.flutter.plugins.mixpanel.People') +class People { + external void set(String prop, dynamic to); + external void setOnce(String prop, dynamic to); + external void increment(String prop, double by); + external void append(String name, dynamic value); + external void union(String name, List value); + external void remove(String name, dynamic value); + external void unset(String name); + external void trackCharge(double amount, {Map? properties}); + external void clearCharges(); + external void deleteUser(); +} + +@JavaName('com.codename1.flutter.plugins.mixpanel.Mixpanel') +class Mixpanel { + external static Future init(String token, {bool optOutTrackingDefault, + required bool trackAutomaticEvents, Map? superProperties, Map? config}); + external int get cn1PendingCount; + external void setServerURL(String serverURL); + external void setLoggingEnabled(bool loggingEnabled); + external void setUseIpAddressForGeolocation(bool useIpAddressForGeolocation); + external void setFlushBatchSize(int flushBatchSize); + external Future hasOptedOutTracking(); + external void optInTracking(); + external void optOutTracking(); + external Future identify(String distinctId); + external void alias(String alias, String distinctId); + external Future getDistinctId(); + external Future track(String eventName, {Map? properties}); + external void timeEvent(String eventName); + external Future eventElapsedTime(String eventName); + external void registerSuperProperties(Map properties); + external void registerSuperPropertiesOnce(Map properties); + external void unregisterSuperProperty(String propertyName); + external Future> getSuperProperties(); + external void clearSuperProperties(); + external People getPeople(); + external void reset(); + external Future flush(); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_compress.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_compress.dart new file mode 100644 index 00000000000..272791e64d3 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_compress.dart @@ -0,0 +1,43 @@ +// package:video_compress. Codename One has no transcoder: compressVideo answers +// the ORIGINAL file's MediaInfo and logs a warning; see VideoCompress.java. + +@JavaName('com.codename1.flutter.plugins.videocompress.VideoQuality') +enum VideoQuality { DefaultQuality, LowQuality, MediumQuality, HighestQuality, Res640x480Quality, + Res960x540Quality, Res1280x720Quality, Res1920x1080Quality } + +@JavaName('com.codename1.flutter.plugins.videocompress.MediaInfo') +class MediaInfo { + external String? get path; + external String? get title; + external String? get author; + external int? get width; + external int? get height; + external int? get orientation; + external int? get filesize; + external double? get duration; + external bool? get isCancel; + external File? get file; +} + +@JavaName('com.codename1.flutter.plugins.videocompress.Subscription') +class Subscription { + external void unsubscribe(); +} + +@JavaName('com.codename1.flutter.plugins.videocompress.ObservableBuilder') +class ObservableBuilder { + external Subscription subscribe(void Function(T event) onData, {void Function()? onError, void Function()? onDone}); +} + +@JavaName('com.codename1.flutter.plugins.videocompress.VideoCompress') +class VideoCompress { + external static ObservableBuilder get compressProgress$; + external static bool get isCompressing; + external static Future compressVideo(String path, {VideoQuality quality, bool deleteOrigin, + int? startTime, int? duration, bool? includeAudio, int frameRate}); + external static Future getMediaInfo(String path); + external static Future getFileThumbnail(String path, {int quality, int position}); + external static Future getByteThumbnail(String path, {int quality, int position}); + external static Future cancelCompression(); + external static Future deleteAllCache(); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_player.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_player.dart new file mode 100644 index 00000000000..8a950691f17 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_player.dart @@ -0,0 +1,66 @@ +// package:video_player over Codename One's MediaManager: the VideoPlayer widget +// hosts the Media's video component, and the position is polled while playing. +// Captions and mixWithOthers are accepted and not applied. + +@JavaName('com.codename1.flutter.plugins.videoplayer.VideoPlayerOptions') +class VideoPlayerOptions { + external VideoPlayerOptions({bool mixWithOthers, bool allowBackgroundPlayback}); +} + +@JavaName('com.codename1.flutter.plugins.videoplayer.VideoPlayerValue') +class VideoPlayerValue { + external Duration get duration; + external Duration get position; + external Size get size; + external bool get isInitialized; + external bool get isPlaying; + external bool get isLooping; + external bool get isBuffering; + external bool get isCompleted; + external double get volume; + external double get playbackSpeed; + external bool get hasError; + external String? get errorDescription; + external double get aspectRatio; +} + +@JavaName('com.codename1.flutter.plugins.videoplayer.VideoPlayerController') +class VideoPlayerController extends ValueNotifier { + external static VideoPlayerController networkUrl(Uri url, {VideoPlayerOptions? videoPlayerOptions, + Map httpHeaders, dynamic formatHint, dynamic closedCaptionFile}); + external static VideoPlayerController network(String dataSource, {VideoPlayerOptions? videoPlayerOptions, + Map httpHeaders, dynamic formatHint, dynamic closedCaptionFile}); + external static VideoPlayerController file(File file, {VideoPlayerOptions? videoPlayerOptions, + Map httpHeaders, dynamic closedCaptionFile}); + external static VideoPlayerController asset(String dataSource, {String? package, + VideoPlayerOptions? videoPlayerOptions, dynamic closedCaptionFile}); + external String get dataSource; + external VideoPlayerValue get value; + external Future get position; + external Future initialize(); + external Future play(); + external Future pause(); + external Future seekTo(Duration position); + external Future setLooping(bool looping); + external Future setVolume(double volume); + external Future setPlaybackSpeed(double speed); + external void addListener(VoidCallback listener); + external void removeListener(VoidCallback listener); + external Future dispose(); +} + +@JavaName('com.codename1.flutter.plugins.videoplayer.VideoPlayer') +class VideoPlayer extends Widget { + external VideoPlayer(VideoPlayerController controller, {Key? key}); +} + +@JavaName('com.codename1.flutter.plugins.videoplayer.VideoProgressColors') +class VideoProgressColors { + external VideoProgressColors({Color playedColor, Color bufferedColor, Color backgroundColor}); +} + +@JavaName('com.codename1.flutter.plugins.videoplayer.VideoProgressIndicator') +class VideoProgressIndicator extends Widget { + external VideoProgressIndicator(VideoPlayerController controller, {Key? key, + VideoProgressColors? colors, required bool allowScrubbing, EdgeInsets? padding}); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_thumbnail.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_thumbnail.dart new file mode 100644 index 00000000000..e3393ec438f --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_thumbnail.dart @@ -0,0 +1,13 @@ +// package:video_thumbnail. Codename One has no portable frame extractor, so every +// call completes with an error naming the gap; see VideoThumbnail.java. + +@JavaName('com.codename1.flutter.plugins.videothumbnail.ImageFormat') +enum ImageFormat { JPEG, PNG, WEBP } + +@JavaName('com.codename1.flutter.plugins.videothumbnail.VideoThumbnail') +class VideoThumbnail { + external static Future thumbnailData({required String video, Map? headers, + ImageFormat imageFormat, int maxHeight, int maxWidth, int timeMs, int quality}); + external static Future thumbnailFile({required String video, Map? headers, + String? thumbnailPath, ImageFormat imageFormat, int maxHeight, int maxWidth, int timeMs, int quality}); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_webview_flutter.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_webview_flutter.dart new file mode 100644 index 00000000000..cf9b8fbd049 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_webview_flutter.dart @@ -0,0 +1,74 @@ +// package:webview_flutter (the v4 controller API), hosting Codename One's +// BrowserComponent. Navigation decisions go through BrowserNavigationCallback, +// JavaScript through execute/executeAndReturnString and channels through +// addJSCallback. Without a display the controller records its state and consults +// the navigation delegate, so app logic around it still runs. + +@JavaName('com.codename1.flutter.plugins.webview.JavaScriptMode') +enum JavaScriptMode { disabled, unrestricted } + +@JavaName('com.codename1.flutter.plugins.webview.NavigationDecision') +enum NavigationDecision { prevent, navigate } + +@JavaName('com.codename1.flutter.plugins.webview.LoadRequestMethod') +enum LoadRequestMethod { get, post } + +@JavaName('com.codename1.flutter.plugins.webview.NavigationRequest') +class NavigationRequest { + external String get url; + external bool get isMainFrame; +} + +@JavaName('com.codename1.flutter.plugins.webview.WebResourceError') +class WebResourceError { + external int get errorCode; + external String get description; + external bool? get isForMainFrame; + external String? get url; +} + +@JavaName('com.codename1.flutter.plugins.webview.JavaScriptMessage') +class JavaScriptMessage { + external JavaScriptMessage({required String message}); + external String get message; +} + +@JavaName('com.codename1.flutter.plugins.webview.NavigationDelegate') +class NavigationDelegate { + external NavigationDelegate({dynamic Function(NavigationRequest request)? onNavigationRequest, + void Function(String url)? onPageStarted, void Function(String url)? onPageFinished, + void Function(int progress)? onProgress, void Function(WebResourceError error)? onWebResourceError, + void Function(dynamic change)? onUrlChange, void Function(dynamic request)? onHttpAuthRequest}); +} + +@JavaName('com.codename1.flutter.plugins.webview.WebViewController') +class WebViewController { + external WebViewController(); + external Future loadRequest(Uri uri, {LoadRequestMethod method, Map headers, Uint8List? body}); + external Future loadHtmlString(String html, {String? baseUrl}); + external Future loadFlutterAsset(String key); + external Future loadFile(String absoluteFilePath); + external Future setJavaScriptMode(JavaScriptMode javaScriptMode); + external Future setNavigationDelegate(NavigationDelegate delegate); + external Future runJavaScript(String javaScript); + external Future runJavaScriptReturningResult(String javaScript); + external Future addJavaScriptChannel(String name, {required void Function(JavaScriptMessage message) onMessageReceived}); + external Future removeJavaScriptChannel(String javaScriptChannelName); + external Future reload(); + external Future goBack(); + external Future goForward(); + external Future canGoBack(); + external Future canGoForward(); + external Future currentUrl(); + external Future getTitle(); + external Future setBackgroundColor(Color color); + external Future setUserAgent(String? userAgent); + external Future clearCache(); + external Future clearLocalStorage(); + external Future enableZoom(bool enabled); +} + +@JavaName('com.codename1.flutter.plugins.webview.WebViewWidget') +class WebViewWidget extends Widget { + external WebViewWidget({Key? key, required WebViewController controller, dynamic gestureRecognizers}); +} From 5a465f7c24770ab170f436f2561f4c8dc84b64d2 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:17:43 +0200 Subject: [PATCH 3/8] Flutter interop: FirebaseUI, firebase_ai, App Check, l10n/RTL, package-scoped stubs - FirebaseUI auth screens, Google/Apple providers, google_sign_in, federated firebase_auth credentials; App Check; firebase_ai over the AI Logic REST API. - Stub files can declare `// cn1:package`, so two packages exporting one name resolve per importing library, and an app's own class is never shadowed. - @Unapplied stub parameters warn (W0002); stub packs declare import coverage. - en/es/he Material and Cupertino localizations; RTL mirroring of rows, wraps, directional alignment, stacks, text, list tiles, app bars and arrow icons. - cached_network_image placeholder/progress/error widgets, polyline and circle taps, ui.Image prefix, ByteData.buffer, depth for dio, url_launcher, image_picker, messaging, notifications and maps. Co-Authored-By: Claude Opus 5.5 --- docs/developer-guide/Flutter-Interop.asciidoc | 258 ++++++++++++- .../maven/FlutterProjectImporter.java | 215 ++++++++++- .../maven/FlutterProjectImporterTest.java | 38 ++ .../maven/ImportFlutterProjectMojo.java | 16 +- .../src/main/java/dart/io/WebSocket.java | 19 +- .../main/java/dart/typed_data/ByteBuffer.java | 50 +++ .../main/java/dart/typed_data/ByteData.java | 16 + .../main/java/dart/typed_data/Uint8List.java | 5 + .../dart/transpiler/analyze/StubRegistry.java | 67 ++++ .../codename1/dart/transpiler/ast/Ast.java | 2 + .../dart/transpiler/codegen/JavaEmitter.java | 73 +++- .../dart/transpiler/parser/AstBuilder.java | 28 ++ .../UnappliedParameterWarningTest.java | 86 +++++ .../behavior/flutter_l10n_rtl/expect.txt | 19 + .../behavior/flutter_l10n_rtl/main.dart | 28 ++ .../behavior/plugin_depth_gaps/expect.txt | 22 ++ .../behavior/plugin_depth_gaps/main.dart | 89 +++++ .../behavior/plugin_depth_net/expect.txt | 14 + .../behavior/plugin_depth_net/main.dart | 67 ++++ .../behavior/plugin_depth_platform/expect.txt | 22 ++ .../behavior/plugin_depth_platform/main.dart | 82 +++++ .../behavior/plugin_device_packs/expect.txt | 2 +- .../behavior/plugin_firebase_ai/expect.txt | 16 + .../behavior/plugin_firebase_ai/main.dart | 73 ++++ .../behavior/plugin_firebase_ui/expect.txt | 6 + .../plugin_firebase_ui/fui_app_check.dart | 3 + .../behavior/plugin_firebase_ui/main.dart | 34 ++ .../flutter/AlignmentDirectional.java | 8 + .../java/com/codename1/flutter/Canvas.java | 119 ++++++ .../codename1/flutter/ImageByteFormat.java | 29 ++ .../java/com/codename1/flutter/Picture.java | 89 +++++ .../codename1/flutter/PictureRecorder.java | 61 ++++ .../com/codename1/flutter/RenderElement.java | 18 + .../java/com/codename1/flutter/UiImage.java | 204 +++++++++++ .../flutter/l10n/CupertinoLocalizations.java | 188 ++++++++++ .../l10n/GlobalCupertinoLocalizations.java | 2 +- .../l10n/GlobalMaterialLocalizations.java | 2 +- .../flutter/l10n/MaterialLocalizations.java | 342 +++++++++++++++++- .../flutter/material/AppBarRenderElement.java | 9 + .../flutter/material/LicensePage.java | 3 +- .../material/ListTileRenderElement.java | 7 + .../flutter/services/LaunchMode.java | 29 ++ .../flutter/services/UrlLauncher.java | 47 ++- .../flutter/widgets/AlignRenderElement.java | 3 +- .../flutter/widgets/FlexRenderElement.java | 8 +- .../flutter/widgets/IconRenderElement.java | 45 ++- .../com/codename1/flutter/widgets/Image.java | 11 + .../widgets/RichTextRenderElement.java | 8 +- .../flutter/widgets/StackRenderElement.java | 7 +- .../flutter/widgets/TextRenderElement.java | 12 +- .../flutter/widgets/WrapRenderElement.java | 7 + .../CachedNetworkImage.java | 299 +++++++++++++-- .../cachednetworkimage/DownloadProgress.java | 61 ++++ .../flutter/plugins/dio/CancelToken.java | 5 + .../codename1/flutter/plugins/dio/Dio.java | 106 +++++- .../flutter/plugins/dio/FormData.java | 56 ++- .../flutter/plugins/dio/RequestOptions.java | 4 + .../plugins/firebase/AndroidProvider.java | 29 ++ .../plugins/firebase/AppleProvider.java | 29 ++ .../plugins/firebase/EmailAuthProvider.java | 7 +- .../plugins/firebase/FirebaseAppCheck.java | 126 +++++++ .../plugins/firebase/FirebaseMessaging.java | 75 +++- .../plugins/firebase/ReCaptchaV3Provider.java | 37 ++ .../plugins/firebaseai/AiTransport.java | 140 +++++++ .../flutter/plugins/firebaseai/Candidate.java | 63 ++++ .../plugins/firebaseai/ChatSession.java | 81 +++++ .../flutter/plugins/firebaseai/Content.java | 117 ++++++ .../firebaseai/CountTokensResponse.java | 43 +++ .../plugins/firebaseai/FinishReason.java | 49 +++ .../plugins/firebaseai/FirebaseAI.java | 89 +++++ .../firebaseai/FirebaseAIException.java | 40 ++ .../firebaseai/GenerateContentResponse.java | 128 +++++++ .../plugins/firebaseai/GenerationConfig.java | 99 +++++ .../plugins/firebaseai/GenerativeModel.java | 285 +++++++++++++++ .../firebaseai/HarmBlockThreshold.java | 41 +++ .../plugins/firebaseai/HarmCategory.java | 42 +++ .../plugins/firebaseai/InlineDataPart.java | 64 ++++ .../flutter/plugins/firebaseai/Part.java | 33 ++ .../plugins/firebaseai/SafetySetting.java | 53 +++ .../flutter/plugins/firebaseai/TextPart.java | 47 +++ .../flutter/plugins/firebaseai/Tool.java | 62 ++++ .../plugins/firebaseai/UsageMetadata.java | 49 +++ .../firebasegoogle/GoogleAuthProvider.java | 10 +- .../plugins/firebaseui/AuthAction.java | 29 ++ .../plugins/firebaseui/AuthFailed.java | 37 ++ .../flutter/plugins/firebaseui/AuthState.java | 28 ++ .../firebaseui/AuthStateChangeAction.java | 67 ++++ .../plugins/firebaseui/FirebaseUIAuth.java | 48 +++ .../firebaseui/OAuthProviderButton.java | 89 +++++ .../plugins/firebaseui/ProfileScreen.java | 115 ++++++ .../plugins/firebaseui/SignInScreen.java | 302 ++++++++++++++++ .../flutter/plugins/firebaseui/SignedIn.java | 39 ++ .../plugins/firebaseui/SignedOutAction.java | 48 +++ .../flutter/plugins/firebaseui/Ui.java | 69 ++++ .../plugins/firebaseui/UiOAuthProvider.java | 36 ++ .../plugins/firebaseui/UserCreated.java | 39 ++ .../firebaseuiapple/AppleProvider.java | 65 ++++ .../firebaseuigoogle/GoogleProvider.java | 77 ++++ .../plugins/googlemaps/BitmapDescriptor.java | 111 +++++- .../flutter/plugins/googlemaps/Circle.java | 139 +++++++ .../flutter/plugins/googlemaps/CircleId.java | 37 ++ .../flutter/plugins/googlemaps/GoogleMap.java | 11 +- .../googlemaps/GoogleMapRenderElement.java | 77 ++++ .../flutter/plugins/googlemaps/MapHit.java | 81 +++++ .../flutter/plugins/googlemaps/Marker.java | 9 +- .../flutter/plugins/googlemaps/Polyline.java | 161 +++++++++ .../plugins/googlemaps/PolylineId.java | 37 ++ .../plugins/imagepicker/ImagePicker.java | 123 ++++++- .../FlutterLocalNotificationsPlugin.java | 74 +++- .../RawResourceAndroidNotificationSound.java | 60 +++ .../main/resources/META-INF/dart/dart_io.dart | 1 + .../META-INF/dart/dart_ui_picture.dart | 33 ++ .../META-INF/dart/gallery_dartCore.dart | 63 +++- .../dart/gallery_p2_geometryPaint.dart | 1 + .../META-INF/dart/gallery_p4_apitail.dart | 3 + .../dart/plugin_cached_network_image.dart | 17 +- .../resources/META-INF/dart/plugin_dio.dart | 16 +- .../dart/plugin_emoji_picker_flutter.dart | 1 + .../META-INF/dart/plugin_firebase.dart | 1 + .../META-INF/dart/plugin_firebase_ai.dart | 132 +++++++ .../dart/plugin_firebase_app_check.dart | 34 ++ .../META-INF/dart/plugin_firebase_ui.dart | 85 +++++ .../plugin_flutter_local_notifications.dart | 13 +- .../META-INF/dart/plugin_flutter_svg.dart | 5 + .../dart/plugin_google_maps_flutter.dart | 47 +++ .../META-INF/dart/plugin_video_player.dart | 3 + .../codename1/flutter/RtlMirroringTest.java | 125 +++++++ 127 files changed, 7420 insertions(+), 145 deletions(-) create mode 100644 maven/dart-runtime/src/main/java/dart/typed_data/ByteBuffer.java create mode 100644 maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/UnappliedParameterWarningTest.java create mode 100644 maven/dart-transpiler/src/test/resources/behavior/flutter_l10n_rtl/expect.txt create mode 100644 maven/dart-transpiler/src/test/resources/behavior/flutter_l10n_rtl/main.dart create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_depth_gaps/expect.txt create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_depth_gaps/main.dart create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_depth_net/expect.txt create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_depth_net/main.dart create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_depth_platform/expect.txt create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_depth_platform/main.dart create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ai/expect.txt create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ai/main.dart create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ui/expect.txt create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ui/fui_app_check.dart create mode 100644 maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ui/main.dart create mode 100644 maven/flutter-runtime/src/main/java/com/codename1/flutter/ImageByteFormat.java create mode 100644 maven/flutter-runtime/src/main/java/com/codename1/flutter/Picture.java create mode 100644 maven/flutter-runtime/src/main/java/com/codename1/flutter/PictureRecorder.java create mode 100644 maven/flutter-runtime/src/main/java/com/codename1/flutter/UiImage.java create mode 100644 maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/CupertinoLocalizations.java create mode 100644 maven/flutter-runtime/src/main/java/com/codename1/flutter/services/LaunchMode.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/cachednetworkimage/DownloadProgress.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/AndroidProvider.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/AppleProvider.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FirebaseAppCheck.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/ReCaptchaV3Provider.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/AiTransport.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Candidate.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/ChatSession.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Content.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/CountTokensResponse.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/FinishReason.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/FirebaseAI.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/FirebaseAIException.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/GenerateContentResponse.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/GenerationConfig.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/GenerativeModel.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/HarmBlockThreshold.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/HarmCategory.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/InlineDataPart.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Part.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/SafetySetting.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/TextPart.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Tool.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/UsageMetadata.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthAction.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthFailed.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthState.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthStateChangeAction.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/FirebaseUIAuth.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/OAuthProviderButton.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/ProfileScreen.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/SignInScreen.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/SignedIn.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/SignedOutAction.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/Ui.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/UiOAuthProvider.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/UserCreated.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseuiapple/AppleProvider.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseuigoogle/GoogleProvider.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/Circle.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/CircleId.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/MapHit.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/Polyline.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/PolylineId.java create mode 100644 maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/notifications/RawResourceAndroidNotificationSound.java create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/dart_ui_picture.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase_ai.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase_app_check.dart create mode 100644 maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase_ui.dart create mode 100644 maven/flutter-runtime/src/test/java/com/codename1/flutter/RtlMirroringTest.java diff --git a/docs/developer-guide/Flutter-Interop.asciidoc b/docs/developer-guide/Flutter-Interop.asciidoc index f5f9a0b0226..2ba277c0fd0 100644 --- a/docs/developer-guide/Flutter-Interop.asciidoc +++ b/docs/developer-guide/Flutter-Interop.asciidoc @@ -108,10 +108,55 @@ by hand would take: * Carries the application name and version over from `pubspec.yaml`. It then lists the application's pub dependencies and says which of them -Codename One provides. The transpiler doesn't compile a package's Dart; a -package works when the Flutter runtime implements its API, as it does for -`provider`, `intl`, `animations` and other common ones. A dependency listed as -not covered is the first thing to look at if a screen misbehaves. +Codename One provides. A package is provided one of two ways. A pure Dart +package is copied and transpiled with the application, file by file, as far as +the application's imports reach into it. A plugin, or a package the runtime +implements natively (`provider`, `intl`, `go_router`, `firebase_auth` and the +others in <>), is provided by a stub pack: Dart +declarations of the package's API backed by Java. A dependency listed as not +covered is the first thing to look at if a screen misbehaves. + +[[flutter-import-report]] +==== The import report + +The same report is written as JSON to `common/target/flutter-import-report.json`, +for a script or a CI step to read. Each package has a status (`covered` or +`unsupported`), the note or the reason, and the application's own files that +reach it. An unsupported package that is only unsupported because of something +it depends on carries the chain that verdict came through, ending with the +package that is unsupported itself: + +[source,json] +---- +{"package": "ui_kit", "status": "unsupported", + "note": "depends on vector_graphics_compiler, which needs dart:ffi", + "cause": "needs dart:ffi", + "chain": ["ui_kit", "vector_graphics_compiler"], + "reachedFrom": ["lib/login/login_screen.dart"]} +---- + +The report also lists, under `googleFonts`, every Google Fonts family the +application names. The `google_fonts` stub draws only fonts bundled with the +application, so those are the families to add as assets. + +A few kinds of imports are resolved rather than reported: + +* A conditional import (`if (dart.library.html) ...`) takes the branch for a + library the runtime has, so a web-only branch and everything it imports are + never read. +* A plugin that implements only desktop or web platforms is transpiled as Dart. + Its calls into the platform answer `MissingPluginException`, as they would on + a mobile device in Flutter. + +==== Parameters that are accepted and not applied + +Some stub parameters are accepted so the application compiles, and then have no +effect: an HTTP header a media player can't send, a fit mode an image renderer +lacks. A call that passes one gets a build warning at that argument: + +---- +[WARNING] src/main/flutter/feed/post.dart:[41,6] 'httpHeaders' of networkUrl is accepted but not applied on Codename One (dart2java:W0002) +---- Re-running the command later picks up changes made on the Flutter side. A file you edited after the earlier import is left as you edited it, and a file the @@ -345,6 +390,211 @@ The generated package and the paths are configurable: `com.codename1.generated.flutter`. |=== +==== Localization and right-to-left layout + +`GlobalMaterialLocalizations.delegate` and `GlobalCupertinoLocalizations.delegate` +supply English, Spanish and Hebrew strings for the built-in widgets, picked by the +locale's language code; any other language falls back to English. Your own +delegates and ARB-generated localizations work as they do in Flutter. + +Under a right-to-left `Directionality`, rows, +wraps, `AlignmentDirectional`, stacks, text alignment, list tiles and app bars are +mirrored, and the directional arrow and chevron icons swap. A plain `Alignment` +never mirrors, as in Flutter. + +[[flutter-plugin-coverage]] +=== Plugin coverage + +These are the pub packages the runtime provides, and how. A package that is not +listed and is pure Dart is transpiled from its own sources. + +[cols="2,4,3", options="header"] +|=== +|Package |Backed by |Accepted and not applied + +|`firebase_core`, `firebase_auth` +|The Identity Toolkit REST API: email and password, anonymous accounts, and +Google, Apple and OAuth credentials exchanged through `signInWithIdp`. +`signInWithProvider` runs Google's and Apple's sign-in. +|Phone sign-in, multi-factor, account linking + +|`google_sign_in` +|`GoogleConnect`, Google's OIDC sign-in with PKCE in the system browser +|`signInSilently` answers the current run's account only; `hostedDomain` + +|`firebase_ui_auth`, `firebase_ui_oauth_google`, `firebase_ui_oauth_apple` +|`SignInScreen` and `ProfileScreen` built from the runtime's Material widgets, +over `firebase_auth`; the Google and Apple buttons sign in as `signInWithProvider` does +|`styles`, `sideBuilder`, `breakpoint`, `headerMaxExtent`; reported as W0002 warnings + +|`firebase_analytics`, `firebase_messaging` +|Logged events and `AnalyticsService`; Codename One push, including +`onBackgroundMessage` and provisional permission +| + +|`firebase_ai` +|The Firebase AI Logic REST API, Gemini through Google AI or Vertex AI, with the +App Check token and the signed-in user's ID token +|Function calling, `responseSchema`, `toolConfig`; `generateContentStream` +delivers the whole reply as one event; Live and Imagen models + +|`firebase_app_check` +|The debug provider's token, from the `firebase.appCheck.debugToken` Display property +|Play Integrity, DeviceCheck and App Attest: there is no attestation, so they yield no token + +|`dio` +|`ConnectionRequest`: interceptors, `DioException` types, `CancelToken`, +`FormData`, downloads +| + +|`go_router` +|The runtime's navigator: routes, redirects, `extra`, transitions +| + +|`cached_network_image` +|`ConnectionRequest` with an in-memory cache: `placeholder`, +`progressIndicatorBuilder` and `errorWidget` are built +|`fadeInDuration` + +|`google_maps_flutter` +|`MapView`: markers with hued or byte icons, polylines, circles, camera, taps +| + +|`image_picker`, `camera` +|`Capture` and `com.codename1.camera`; `maxWidth`, `maxHeight` and `imageQuality` +re-encode the picked image +|The `child` overlay of `CameraPreview` + +|`video_player` +|`MediaManager` +|`httpHeaders`, captions, playback speed + +|`video_thumbnail`, `video_compress` +|No frame extraction or transcoding in the core: every thumbnail call fails, +and `compressVideo` answers the original file with a warning +| + +|`webview_flutter` +|`BrowserComponent`, JavaScript channels through `addJSCallback` +|A POST body, `JavaScriptMode.disabled`, `clearCache` + +|`flutter_svg` +|`Image.createSVG` where the port renders SVG, otherwise an empty box of the +requested size +|`colorFilter`, `fit`, `alignment`, `placeholderBuilder` + +|`file_picker` +|The native open and save dialogs +| + +|`audioplayers` +|`MediaManager` +| + +|`flutter_image_compress` +|`Image` and `ImageIO`, JPEG and PNG +|HEIC and WebP where the platform can't encode them + +|`geocoding` +|The Google Geocoding REST API, keyed by the `google.geocoding.key` Display +property +| + +|`mixpanel_flutter` +|Mixpanel's HTTP ingestion API, with `setServerURL` for EU residency +| + +|`flutter_web_auth_2` +|An in-app browser that watches for the callback URL +| + +|`emoji_picker_flutter` +|A Codename One emoji grid with recents and search +|`Config`, except `height` + +|`app_settings`, `flutter_timezone` +|`Display.execute` with the platform's settings URL; `java.util.TimeZone` +| + +|`android_play_install_referrer` +|Nothing: `installReferrer` always fails with `UnsupportedError` +| + +|`shared_preferences`, `path_provider`, `package_info_plus`, +`device_info_plus`, `connectivity_plus`, `geolocator`, `share_plus`, +`url_launcher`, `app_links`, `flutter_local_notifications` +|`Preferences`, `FileSystemStorage`, the build and Display properties, +`NetworkManager`, `LocationManager`, `Display.share`, `Display.execute` (an in-app +`BrowserComponent` for `LaunchMode.inAppWebView`), `LocalNotification` with +Android channels, raw sounds and a background tap callback +| +|=== + +[[flutter-stub-packs]] +=== Writing a stub pack + +A package the runtime doesn't provide can be provided by a stub pack of your +own, in the application or in a library jar. A pack has two parts. + +The Dart half is a file under `META-INF/dart/` on the compile class path -- +in an application, `common/src/main/resources/META-INF/dart/`. It declares the +package's API with `external` members, and names the Java class behind each type +with `@JavaName`. The `cn1:covers` line tells the import report that the pack +stands in for the package: + +[source,dart] +---- +// cn1:covers lottie Lottie.asset over a Codename One animation +@JavaName('com.example.lottie.Lottie') +class Lottie extends Widget { + @Unapplied('fit') + external static Lottie asset(String name, {double? width, double? height, + bool repeat, BoxFit? fit}); +} +---- + +Stub types share one namespace. When two packages export the same name -- the +`AppleProvider` of `firebase_app_check` and of `firebase_ui_oauth_apple` -- give +each file a `// cn1:package ` line: a library then sees the declaration +of the package it imports, and a class of the application's own with the same +name is never shadowed by a stub of a package it does not import. + +The Java half is an ordinary class with the members the declarations map to: + +[source,java] +---- +package com.example.lottie; + +public class Lottie extends com.codename1.flutter.Widget { + public static Lottie asset(String name, Double width, Double height, + Boolean repeat, Object fit) { + // ... + } +} +---- + +The mapping rules: + +* Named parameters become positional Java parameters, in the order they are + declared. A parameter the call leaves out is passed as `null`. +* `int` is `Long` (or `long` when it can't be null), `double` is `Double`, + `bool` is `Boolean`, `List` and `Map` are `dart.core.DartList` and + `dart.core.DartMap`, `Future` and `Stream` are `dart.async.Future` and + `dart.async.Stream`, and a function is a `dart.runtime.Funcs` interface. +* A constructor whose parameters are all named is a public no-argument + constructor plus one method per parameter, named after it, which the + generated code calls for each argument passed. +* An instance getter is a method with no arguments. A static getter is a + public static field. +* `@Unapplied('a, b')` on a constructor or method names the parameters that + are accepted and not applied, so that the build warns where one is passed. + +Every stub on the class path shares one namespace, so a type that another stub +already declares mustn't be declared again. The transpiler ignores the import +of the package itself. For a pack outside the application, pass its jar to the +import with `-Dcn1.flutter.stubPacks=path/to/pack.jar` so that the report counts +it. + [[flutter-benchmark]] === How the numbers are measured diff --git a/maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectImporter.java b/maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectImporter.java index 1c156ef2fa6..69f3d25c238 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectImporter.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectImporter.java @@ -27,6 +27,7 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.nio.charset.Charset; import java.nio.file.Files; @@ -72,7 +73,8 @@ public final class FlutterProjectImporter { COVERED.put("animations", "OpenContainer/PageTransitionSwitcher/SharedAxisTransition stubs"); COVERED.put("intl", "DateFormat/NumberFormat/Intl stubs"); COVERED.put("google_fonts", "GoogleFonts stub (bundled/asset fonts only, no network fetch)"); - COVERED.put("url_launcher", "launchUrl/launchUrlString stubs"); + COVERED.put("url_launcher", "launchUrl/launchUrlString/canLaunchUrl; LaunchMode.inAppWebView and " + + "inAppBrowserView open an in-app BrowserComponent, the others the system handler"); COVERED.put("scoped_model", "ScopedModel/ScopedModelDescendant stubs"); COVERED.put("vector_math", "Matrix4/Vector3 stubs"); COVERED.put("flutter_localized_locales", "LocaleNames/LocaleNamesLocalizationsDelegate stubs"); @@ -95,7 +97,7 @@ public final class FlutterProjectImporter { COVERED.put("go_router", "GoRouter/GoRoute/ShellRoute/GoRouterState and the context.go/push/pop " + "extension; MaterialApp.router (plugins.gorouter)"); COVERED.put("cached_network_image", "CachedNetworkImage/CachedNetworkImageProvider over " - + "Image.network (plugins.cachednetworkimage)"); + + "ConnectionRequest with placeholder, progress and error widgets (plugins.cachednetworkimage)"); COVERED.put("cross_file", "XFile over FileSystemStorage or in-memory bytes (plugins.crossfile)"); COVERED.put("share_plus", "SharePlus.instance.share and the static Share API over Display.share " + "(plugins.share)"); @@ -107,20 +109,71 @@ public final class FlutterProjectImporter { COVERED.put("geolocator", "Geolocator positions, permissions and distance/bearing over " + "LocationManager (plugins.geolocator)"); COVERED.put("image_picker", "ImagePicker pickImage/pickVideo/pickMedia/pickMultiImage over Capture and " - + "the gallery (plugins.imagepicker)"); + + "the gallery, with maxWidth/maxHeight resizing and imageQuality (plugins.imagepicker)"); COVERED.put("app_links", "AppLinks initial link and link streams from the AppArg launch property " + "(plugins.applinks)"); COVERED.put("flutter_local_notifications", "show/periodicallyShow/zonedSchedule/cancel and tap " - + "callbacks over LocalNotification (plugins.notifications)"); + + "callbacks over LocalNotification, background tap callback, Android channels and raw " + + "sounds (plugins.notifications)"); COVERED.put("firebase_core", "Firebase.initializeApp/FirebaseOptions (plugins.firebase)"); - COVERED.put("firebase_auth", "FirebaseAuth email/password and anonymous accounts over the Identity " - + "Toolkit REST API (plugins.firebase)"); + COVERED.put("firebase_auth", "FirebaseAuth email/password, anonymous and federated credentials " + + "(Google, Apple, OAuthProvider tokens through signInWithIdp) over the Identity Toolkit REST " + + "API; signInWithProvider for Google and Apple (plugins.firebase, firebasegoogle, firebaseapple)"); + COVERED.put("google_sign_in", "GoogleSignIn signIn/signOut/currentUser/onCurrentUserChanged over " + + "GoogleConnect's OIDC sign-in; signInSilently answers this run's account only (plugins.googlesignin)"); COVERED.put("firebase_analytics", "FirebaseAnalytics events logged, screen views to AnalyticsService " + "(plugins.firebase)"); - COVERED.put("firebase_messaging", "FirebaseMessaging token and foreground onMessage over Codename One " - + "push (plugins.firebase)"); - COVERED.put("google_maps_flutter", "GoogleMap widget with markers, camera and taps over MapView " + COVERED.put("firebase_messaging", "FirebaseMessaging token, onMessage, onBackgroundMessage and " + + "provisional permission over Codename One push (plugins.firebase)"); + COVERED.put("firebase_ai", "FirebaseAI googleAI/vertexAI, generateContent, countTokens and chat over " + + "the Firebase AI Logic REST API with App Check and auth headers; streams are one-shot and " + + "function calling is not run (plugins.firebaseai)"); + COVERED.put("firebase_app_check", "FirebaseAppCheck activate/getToken; only the debug provider yields " + + "a token, the attestation providers are accepted (plugins.firebase)"); + COVERED.put("firebase_ui_auth", "SignInScreen, ProfileScreen, auth state actions and " + + "FirebaseUIAuth.configureProviders over firebase_auth (plugins.firebaseui)"); + COVERED.put("firebase_ui_oauth", "OAuthProviderButton over the configured providers (plugins.firebaseui)"); + COVERED.put("firebase_ui_oauth_google", "GoogleProvider over GoogleConnect (plugins.firebaseuigoogle)"); + COVERED.put("firebase_ui_oauth_apple", "AppleProvider over AppleSignIn (plugins.firebaseuiapple)"); + COVERED.put("google_maps_flutter", "GoogleMap widget with markers, polylines, circles, hued and byte marker icons, camera " + + "and taps over MapView " + "(plugins.googlemaps)"); + COVERED.put("flutter_timezone", "local zone id and available zones from java.util.TimeZone " + + "(plugins.timezone)"); + COVERED.put("app_settings", "openAppSettings/openAppSettingsPanel through Display.execute " + + "(plugins.appsettings)"); + COVERED.put("android_play_install_referrer", "API only; installReferrer always fails with " + + "UnsupportedError (plugins.installreferrer)"); + COVERED.put("geocoding", "placemarkFromCoordinates/locationFromAddress over the Google Geocoding REST " + + "API, key in the google.geocoding.key Display property (plugins.geocoding)"); + COVERED.put("file_picker", "FilePicker.platform pickFiles/saveFile/getDirectoryPath over the native " + + "file dialogs (plugins.filepicker)"); + COVERED.put("audioplayers", "AudioPlayer with asset/url/file/bytes sources and its streams over " + + "MediaManager (plugins.audioplayers)"); + COVERED.put("mixpanel_flutter", "track/identify/alias/super properties/People/flush over the Mixpanel " + + "HTTP API, setServerURL for EU residency (plugins.mixpanel)"); + COVERED.put("flutter_web_auth_2", "authenticate through an in-app browser; cancel is a CANCELED " + + "PlatformException (plugins.webauth)"); + COVERED.put("flutter_image_compress", "compressWithFile/List/AndGetFile/AssetImage over Image and " + + "ImageIO, JPEG and PNG (plugins.imagecompress)"); + COVERED.put("image_picker_platform_interface", "ImagePickerPlatform.instance and getImageFromSource " + + "(plugins.imagepicker)"); + COVERED.put("image_picker_android", "ImagePickerAndroid.registerWith; useAndroidPhotoPicker accepted " + + "with no effect (plugins.imagepicker)"); + COVERED.put("webview_flutter", "WebViewController/NavigationDelegate/WebViewWidget over BrowserComponent; " + + "a POST is sent as a GET (plugins.webview)"); + COVERED.put("video_player", "VideoPlayerController/VideoPlayer/VideoProgressIndicator over MediaManager; " + + "no captions, speed or headers (plugins.videoplayer)"); + COVERED.put("video_thumbnail", "API only; every call fails with UnsupportedError, core extracts no " + + "frames (plugins.videothumbnail)"); + COVERED.put("video_compress", "API only; compressVideo answers the ORIGINAL file uncompressed with a " + + "warning (plugins.videocompress)"); + COVERED.put("camera", "availableCameras/CameraController/CameraPreview over com.codename1.camera " + + "(plugins.camera)"); + COVERED.put("emoji_picker_flutter", "EmojiPicker as a tabbed grid of built-in emoji with recents and " + + "search; Config ignored except height (plugins.emojipicker)"); + COVERED.put("flutter_svg", "SvgPicture.asset/network/string/file/memory through Image.createSVG where " + + "the port renders SVG, otherwise a sized empty box (plugins.svg)"); // NOT covered, checked and confirmed absent: "collection" (no ListEquality/MapEquality/ // SetEquality stub -- the one hit on that name is a comment, not an implementation), // "flutter_staggered_grid_view" and "nested" (provider's internal composition helper, @@ -182,6 +235,10 @@ public static final class Result { /// Every package the import reached, with the app's own `lib/` files that reach /// it, directly or through other packages. public final Map> reachedFrom = new TreeMap>(); + /// The Google Fonts families the app's own files name (`GoogleFonts.openSans(...)`, + /// `GoogleFonts.latoTextTheme()`, `GoogleFonts.getFont('Rubik')`). The stub draws + /// only fonts bundled with the app, so these are the ones to add as assets. + public final Set googleFonts = new java.util.TreeSet(); public String appName; public String appVersion; public int copiedFiles; @@ -223,7 +280,8 @@ public String toJson() { b.append("\n ],\n"); b.append(" \"missingAssets\": ").append(jsonArray(missingAssets)).append(",\n"); b.append(" \"unresolvedPackages\": ").append(jsonArray(unresolvedPackages)).append(",\n"); - b.append(" \"assetCollisions\": ").append(jsonArray(assetCollisions)).append("\n"); + b.append(" \"assetCollisions\": ").append(jsonArray(assetCollisions)).append(",\n"); + b.append(" \"googleFonts\": ").append(jsonArray(googleFonts)).append("\n"); return b.append("}\n").toString(); } @@ -272,6 +330,84 @@ public FlutterProjectImporter(Log log) { /// only when the project has not overridden `cn1.flutter.package`. A /// caller that can see that configuration (`ImportFlutterProjectMojo`) /// must use the five-argument overload instead. + /// [#COVERED] plus what the stub packs of this import declare. + private final Map coverage = new LinkedHashMap(); + private final List stubPacks = new ArrayList(); + + /// Stub packs beyond the Flutter runtime's own -- jars, or class/resource directories, + /// with `META-INF/dart/*.dart` stubs -- whose `// cn1:covers` lines (see + /// [#declaredCoverage]) count as covered. The application's own + /// `common/src/main/resources` is always read. + public FlutterProjectImporter withStubPacks(java.util.Collection packs) { + stubPacks.addAll(packs); + return this; + } + + private static final Pattern COVERS = Pattern.compile("(?m)^//\\s*cn1:covers\\s+([A-Za-z0-9_]+)\\s*(.*)$"); + + /// A stub file declares the pub packages it stands in for, one line each, so a third-party + /// pack is reported -- and its package not followed into -- like a built-in one: + /// + /// ``` + /// // cn1:covers lottie Lottie.asset/network over a Codename One animation + /// ``` + static void declaredCoverage(List packs, Map out) { + for (File pack : packs) { + try { + if (pack.isDirectory()) { + File[] stubs = new File(pack, "META-INF/dart").listFiles(); + if (stubs != null) { + for (File f : stubs) { + if (f.isFile() && f.getName().endsWith(".dart")) { + covers(new String(Files.readAllBytes(f.toPath()), Charset.forName("UTF-8")), out); + } + } + } + } else if (pack.isFile()) { + java.util.zip.ZipFile z = new java.util.zip.ZipFile(pack); + try { + java.util.Enumeration e = z.entries(); + while (e.hasMoreElements()) { + java.util.zip.ZipEntry ze = e.nextElement(); + String n = ze.getName(); + if (!ze.isDirectory() && n.startsWith("META-INF/dart/") && n.endsWith(".dart") + && n.indexOf('/', "META-INF/dart/".length()) < 0) { + InputStream in = z.getInputStream(ze); + try { + covers(new String(readAll(in), Charset.forName("UTF-8")), out); + } finally { + in.close(); + } + } + } + } finally { + z.close(); + } + } + } catch (IOException ex) { + // An unreadable pack declares nothing; the import reports its packages as it would without it. + } + } + } + + private static void covers(String stub, Map out) { + Matcher m = COVERS.matcher(stub); + while (m.find()) { + String note = m.group(2).trim(); + out.put(m.group(1), note.isEmpty() ? "a stub pack on the class path" : note); + } + } + + private static byte[] readAll(InputStream in) throws IOException { + java.io.ByteArrayOutputStream b = new java.io.ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int n; + while ((n = in.read(buf)) > 0) { + b.write(buf, 0, n); + } + return b.toByteArray(); + } + public Result importProject(File source, File commonDir, String mainPackage, String mainClass) throws BuildException { return importProject(source, commonDir, mainPackage, mainClass, DEFAULT_FLUTTER_PACKAGE); @@ -288,6 +424,11 @@ public Result importProject(File source, File commonDir, String mainPackage, Str public Result importProject(File source, File commonDir, String mainPackage, String mainClass, String flutterPackage) throws BuildException { String entryPointPackage = flutterPackage == null ? DEFAULT_FLUTTER_PACKAGE : flutterPackage; + coverage.clear(); + coverage.putAll(COVERED); + List packs = new ArrayList(stubPacks); + packs.add(new File(commonDir, "src/main/resources")); + declaredCoverage(packs, coverage); File pubspecFile = new File(source, "pubspec.yaml"); File libDir = new File(source, "lib"); if (!pubspecFile.isFile()) { @@ -443,7 +584,7 @@ public Result importProject(File source, File commonDir, String mainPackage, Str // API package, which is only covered when flutter-runtime actually stubs its classes. Map assetOnlyCovered = new LinkedHashMap(); for (String pkgName : packagesWithCopiedAssets) { - if (COVERED.containsKey(pkgName)) { + if (coverage.containsKey(pkgName)) { continue; } File pkgLibDir = resolvedPackageLibDirs.get(pkgName); @@ -464,10 +605,13 @@ public Result importProject(File source, File commonDir, String mainPackage, Str if (ignored) { continue; } - String note = COVERED.get(dep); + String note = coverage.get(dep); if (note == null) { note = assetOnlyCovered.get(dep); } + if ("google_fonts".equals(dep) && note != null && !r.googleFonts.isEmpty()) { + note += "; bundle these families as assets: " + String.join(", ", r.googleFonts); + } if (note == null && r.foreignPlugins.containsKey(dep) && r.transpiledPackages.containsKey(dep)) { note = "a " + r.foreignPlugins.get(dep) + "-only plugin, transpiled as Dart; its platform " + "calls throw MissingPluginException"; @@ -509,6 +653,48 @@ public Result importProject(File source, File commonDir, String mainPackage, Str static final Set SUPPORTED_DART_LIBRARIES = new HashSet(java.util.Arrays.asList( "core", "async", "collection", "convert", "developer", "io", "math", "typed_data", "ui")); + private static final Pattern GOOGLE_FONT = Pattern.compile( + "GoogleFonts\\s*\\.\\s*([a-z][A-Za-z0-9]*)\\s*\\(\\s*(?:['\"]([^'\"]+)['\"])?"); + + /// google_fonts' members that are not a family. + private static final Set GOOGLE_FONTS_API = new java.util.HashSet(java.util.Arrays.asList( + "getFont", "getTextTheme", "asMap", "pendingFonts", "config")); + + /// Adds the families `source` names through google_fonts: a family method is the + /// family in lower camel case (`notoSansHebrew` is Noto Sans Hebrew, `baloo2` is + /// Baloo 2), and its `TextTheme` variant names the same one. + static void googleFontFamilies(String source, Set out) { + Matcher m = GOOGLE_FONT.matcher(source); + while (m.find()) { + String member = m.group(1); + if ("getFont".equals(member) || "getTextTheme".equals(member)) { + if (m.group(2) != null) { + out.add(m.group(2)); + } + continue; + } + if (GOOGLE_FONTS_API.contains(member)) { + continue; + } + if (member.endsWith("TextTheme") && member.length() > "TextTheme".length()) { + member = member.substring(0, member.length() - "TextTheme".length()); + } + StringBuilder family = new StringBuilder(); + for (int i = 0; i < member.length(); i++) { + char c = member.charAt(i); + boolean digit = c >= '0' && c <= '9'; + boolean upper = c >= 'A' && c <= 'Z'; + char prev = i == 0 ? ' ' : member.charAt(i - 1); + boolean prevDigit = prev >= '0' && prev <= '9'; + if (i > 0 && (upper || (digit && !prevDigit))) { + family.append(' '); + } + family.append(i == 0 && c >= 'a' && c <= 'z' ? (char) (c - 'a' + 'A') : c); + } + out.add(family.toString()); + } + } + private static final Pattern DIRECTIVE = Pattern.compile( "(?m)^\\s*(import|export|part)\\s+(?!of\\b)(['\"])([^'\"]+)\\2([^;]*);"); private static final Pattern CONDITIONAL = Pattern.compile( @@ -569,6 +755,9 @@ private int importPackageSources(File libDir, String ownName, Map Integer c = counts.get(pkg); counts.put(pkg, Integer.valueOf(c == null ? 1 : c.intValue() + 1)); } + if (pkg == null) { + googleFontFamilies(stripComments(text), r.googleFonts); + } Matcher m = DIRECTIVE.matcher(stripComments(text)); while (m.find()) { String uri = selectUri(m.group(3), m.group(4)); @@ -621,7 +810,7 @@ private int importPackageSources(File libDir, String ownName, Map } by.add(pkg); } - if (SDK_PACKAGES.contains(nextPkg) || COVERED.containsKey(nextPkg) + if (SDK_PACKAGES.contains(nextPkg) || coverage.containsKey(nextPkg) || r.unsupportedPackages.containsKey(nextPkg)) { continue; } diff --git a/maven/build-engine/src/test/java/com/codename1/maven/FlutterProjectImporterTest.java b/maven/build-engine/src/test/java/com/codename1/maven/FlutterProjectImporterTest.java index c4a7e4ad886..30e3fa00b35 100644 --- a/maven/build-engine/src/test/java/com/codename1/maven/FlutterProjectImporterTest.java +++ b/maven/build-engine/src/test/java/com/codename1/maven/FlutterProjectImporterTest.java @@ -784,6 +784,44 @@ public void aDesktopOnlyPluginIsTranspiledNotBlamed() throws Exception { FlutterProjectImporter.pluginPlatforms(new File(mobile, "lib")))); } + /// A third-party stub pack's `// cn1:covers` line makes its package covered: reported + /// with the pack's note, and its platform-plugin sources never followed. + @Test + public void aStubPackDeclaresTheCoverageOfItsPackage() throws Exception { + File root = Files.createTempDirectory("flutter-stub-pack").toFile(); + File app = new File(root, "demo"); + write(new File(app, "pubspec.yaml"), "name: demo\nversion: 1.0.0\n" + + "dependencies:\n flutter:\n sdk: flutter\n lottie_like: 1.0.0\n"); + write(new File(app, "lib/main.dart"), "import 'package:lottie_like/lottie_like.dart';\nvoid main() {}\n"); + File plugin = new File(root, "cache/lottie_like-1.0.0"); + write(new File(plugin, "pubspec.yaml"), "name: lottie_like\nflutter:\n plugin:\n platforms:\n" + + " android:\n package: a.b\n pluginClass: P\n"); + write(new File(plugin, "lib/lottie_like.dart"), "int l() => 1;\n"); + packageConfig(app, "lottie_like", plugin, "lottie_like", plugin); + File common = new File(root, "common"); + write(new File(common, "src/main/resources/META-INF/dart/plugin_lottie_like.dart"), + "// cn1:covers lottie_like animations over a Codename One player\n"); + + FlutterProjectImporter.Result r = new FlutterProjectImporter(new com.codename1.build.SystemStreamLog()) + .importProject(app, common, null, null); + + assertTrue(r.unsupportedPackages.toString(), r.unsupportedPackages.isEmpty()); + assertTrue(r.covered.toString(), r.covered.contains("lottie_like (animations over a Codename One player)")); + assertFalse(new File(common, "src/main/flutter/packages/lottie_like/lottie_like.dart").exists()); + } + + /// The families named through google_fonts, which the stub can only draw bundled. + @Test + public void googleFontFamiliesAreNamedFromTheirMethods() { + java.util.Set f = new java.util.TreeSet(); + FlutterProjectImporter.googleFontFamilies("final a = GoogleFonts.openSans(fontSize: 12);\n" + + "final b = GoogleFonts.notoSansHebrewTextTheme(base);\n" + + "final c = GoogleFonts.baloo2();\n" + + "final d = GoogleFonts.getFont('Rubik', fontSize: 3);\n" + + "final e = GoogleFonts.asMap(); GoogleFonts.config.allowRuntimeFetching = false;\n", f); + assertEquals("[Baloo 2, Noto Sans Hebrew, Open Sans, Rubik]", f.toString()); + } + /// The JSON report names the chain behind a verdict and the app file behind it. @Test public void theReportCarriesTheTransitiveCause() throws Exception { diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/ImportFlutterProjectMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/ImportFlutterProjectMojo.java index dc4a408fefe..5efc55e48be 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/ImportFlutterProjectMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/ImportFlutterProjectMojo.java @@ -56,6 +56,12 @@ public class ImportFlutterProjectMojo extends AbstractCN1Mojo { @Parameter(property = "cn1.flutter.package", defaultValue = "com.codename1.generated.flutter") private String flutterPackage; + /// Third-party stub packs (jars or directories with `META-INF/dart` stubs), comma + /// separated, whose `// cn1:covers` lines the report counts as covered. Stubs under the + /// application's own `common/src/main/resources/META-INF/dart` are always read. + @Parameter(property = "cn1.flutter.stubPacks") + private String stubPacks; + @Override protected void executeImpl() throws MojoExecutionException, MojoFailureException { File common = getCN1ProjectDir(); @@ -67,7 +73,15 @@ protected void executeImpl() throws MojoExecutionException, MojoFailureException String main = properties == null ? null : properties.getProperty("codename1.mainName"); FlutterProjectImporter.Result r; try { - r = new FlutterProjectImporter(MavenLog.of(getLog())).importProject(source, common, pkg, main, + java.util.List packs = new java.util.ArrayList(); + if (stubPacks != null) { + for (String p : stubPacks.split(",")) { + if (!p.trim().isEmpty()) { + packs.add(new File(p.trim())); + } + } + } + r = new FlutterProjectImporter(MavenLog.of(getLog())).withStubPacks(packs).importProject(source, common, pkg, main, flutterPackage); } catch (com.codename1.builders.BuildException e) { throw new MojoFailureException(e.getMessage(), e); diff --git a/maven/dart-runtime/src/main/java/dart/io/WebSocket.java b/maven/dart-runtime/src/main/java/dart/io/WebSocket.java index fdd9fcd7037..a2c76705868 100644 --- a/maven/dart-runtime/src/main/java/dart/io/WebSocket.java +++ b/maven/dart-runtime/src/main/java/dart/io/WebSocket.java @@ -33,7 +33,14 @@ /** * Dart's {@code WebSocket} over {@link com.codename1.io.WebSocket}: a stream of the * messages received (a String per text frame, a Uint8List per binary frame) that the - * app writes to with {@link #add}. Events reach listeners on the EDT. + * app writes to with {@link #add}. Events reach listeners on the EDT. The + * {@code headers} of {@code connect} go on the opening handshake. {@code pingInterval} + * is kept and read back but sends no ping frames, deliberately: {@code + * com.codename1.io.WebSocket} offers only text and binary {@code send}, and each port's + * socket answers a server's ping with a pong on its own but has no entry point for + * sending a ping. A text or binary message on the interval would not be a ping -- the + * server would deliver it to the application -- so nothing is sent; keep-alive is the + * server's pings, answered by the port. */ public class WebSocket extends Stream { public static final long connecting = 0; @@ -231,6 +238,14 @@ public String extensions() { return ""; } - public void pingInterval(Object interval) { + private Object pingInterval; + + public Object pingInterval() { + return pingInterval; + } + + public Object pingInterval(Object interval) { + pingInterval = interval; + return interval; } } diff --git a/maven/dart-runtime/src/main/java/dart/typed_data/ByteBuffer.java b/maven/dart-runtime/src/main/java/dart/typed_data/ByteBuffer.java new file mode 100644 index 00000000000..7670cccb01c --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/typed_data/ByteBuffer.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.typed_data; + +/// Dart's {@code dart:typed_data} ByteBuffer: the bytes a {@link ByteData} or a +/// {@link Uint8List} is a view over. Views made from it share those bytes, as Dart's do; +/// offsets and lengths cover the whole buffer. +public final class ByteBuffer { + + private final byte[] bytes; + + ByteBuffer(byte[] bytes) { + this.bytes = bytes; + } + + public long lengthInBytes() { + return bytes.length; + } + + /// {@code buffer.asUint8List()}: the bytes as a list, shared with this buffer. + public Uint8List asUint8List() { + return Uint8List.view(bytes); + } + + /// {@code buffer.asByteData()}: the bytes as a ByteData, shared with this buffer. + public ByteData asByteData() { + return ByteData.fromBytes(bytes); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/typed_data/ByteData.java b/maven/dart-runtime/src/main/java/dart/typed_data/ByteData.java index 6ded6c563bc..b64df75643a 100644 --- a/maven/dart-runtime/src/main/java/dart/typed_data/ByteData.java +++ b/maven/dart-runtime/src/main/java/dart/typed_data/ByteData.java @@ -40,6 +40,22 @@ public ByteData(long length) { this.buffer = new byte[(int) length]; } + private ByteData(byte[] buffer) { + this.buffer = buffer; + } + + /// A ByteData holding {@code bytes} itself: how a Java encoder's output crosses into + /// Dart without a copy (`ui.Image.toByteData`). + public static ByteData fromBytes(byte[] bytes) { + return new ByteData(bytes == null ? new byte[0] : bytes); + } + + /// {@code byteData.buffer}: the bytes this view is over, shared, so + /// {@code buffer.asUint8List()} reads what the ByteData holds. + public ByteBuffer buffer() { + return new ByteBuffer(buffer); + } + /** * The offset as an array index, after checking the whole access fits -- on the * long, before narrowing. The cast came first before, so an offset of 2^32 wrapped diff --git a/maven/dart-runtime/src/main/java/dart/typed_data/Uint8List.java b/maven/dart-runtime/src/main/java/dart/typed_data/Uint8List.java index 106c1e0ac54..b0dec65b7bb 100644 --- a/maven/dart-runtime/src/main/java/dart/typed_data/Uint8List.java +++ b/maven/dart-runtime/src/main/java/dart/typed_data/Uint8List.java @@ -46,6 +46,11 @@ private Uint8List(byte[] bytes) { this.bytes = bytes; } + /// A Uint8List over {@code bytes} itself, not a copy: a ByteBuffer's view. + static Uint8List view(byte[] bytes) { + return new Uint8List(bytes); + } + /// A Uint8List holding a copy of {@code bytes}: how a Java byte buffer crosses into /// Dart (a platform channel's binary result, as Flutter's codec delivers it). public static Uint8List fromBytes(byte[] bytes) { diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/StubRegistry.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/StubRegistry.java index 88eda6badef..c1c51b57f61 100644 --- a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/StubRegistry.java +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/StubRegistry.java @@ -156,15 +156,19 @@ public void loadResource(String resource, Diagnostics diags) { public void load(String name, String source, Diagnostics diags) { AstBuilder builder = new AstBuilder(diags); Ast.Library lib = builder.parse(name, source); + java.util.regex.Matcher pm = PACKAGE_LINE.matcher(source); + String pkg = pm.find() ? pm.group(1) : null; for (Ast.ClassDecl c : lib.classes) { if (c.extensionOn != null) { extensions.add(c); } else { classes.put(c.name, c); + variant(c.name, pkg, c); } } for (Ast.EnumDecl e : lib.enums) { enums.put(e.name, e); + variant(e.name, pkg, e); } String scoped = scopedLibrary(name); for (Ast.FunctionDecl f : lib.functions) { @@ -186,6 +190,69 @@ public void load(String name, String source, Diagnostics diags) { } } + /// `// cn1:package firebase_app_check` at the top of a stub file: the pub package whose + /// API the file declares. + private static final java.util.regex.Pattern PACKAGE_LINE = + java.util.regex.Pattern.compile("(?m)^//\\s*cn1:package\\s+([A-Za-z0-9_]+)"); + + /// A type name and every stub file's declaration of it, with the package that file + /// declares. Stubs share one namespace, but two pub packages can both export a name: + /// firebase_app_check's `AppleProvider` enum and firebase_ui_oauth_apple's + /// `AppleProvider` class. Dart keeps them apart by what a file imports, and so does + /// [#viewFrom]. + private final Map> variants = new LinkedHashMap>(); + + private void variant(String name, String pkg, Object decl) { + List l = variants.get(name); + if (l == null) { + l = new java.util.ArrayList(); + variants.put(name, l); + } + l.add(new Object[] {pkg, decl}); + } + + /// Makes every type name more than one package declares resolve to the declaration of + /// the package `importedPackages` contains, for the library about to be emitted. A + /// library importing neither keeps whichever was loaded last. + public void viewFrom(java.util.Collection importedPackages) { + viewFrom(importedPackages, java.util.Collections.emptySet()); + } + + /// As [#viewFrom(java.util.Collection)], and a type the program itself declares in + /// `programTypes` hides a stub of a package the library does not import: emoji_picker's + /// `Config` must not shadow an application's own `Config`. + public void viewFrom(java.util.Collection importedPackages, java.util.Set programTypes) { + for (Map.Entry> e : variants.entrySet()) { + List l = e.getValue(); + if (programTypes.contains(e.getKey())) { + boolean imported = false; + for (Object[] v : l) { + imported |= v[0] == null || importedPackages.contains(v[0]); + } + if (!imported) { + classes.remove(e.getKey()); + enums.remove(e.getKey()); + continue; + } + } + if (l.size() < 2) { + continue; + } + for (Object[] v : l) { + if (v[0] != null && importedPackages.contains(v[0])) { + classes.remove(e.getKey()); + enums.remove(e.getKey()); + if (v[1] instanceof Ast.ClassDecl) { + classes.put(e.getKey(), (Ast.ClassDecl) v[1]); + } else { + enums.put(e.getKey(), (Ast.EnumDecl) v[1]); + } + break; + } + } + } + } + /** {@code developer} for {@code .../dartlib_developer.dart}, else null. */ static String scopedLibrary(String stubName) { String base = stubName.replace('\\', '/'); diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/ast/Ast.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/ast/Ast.java index 76a5faafa61..be60e8e4212 100644 --- a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/ast/Ast.java +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/ast/Ast.java @@ -329,6 +329,7 @@ public static class CtorDecl extends Node { public SuperInit superInit; // nullable public SuperInit redirect; // `: this(...)` / `: this.named(...)`; nullable public Block body; // nullable (";" body) + public java.util.Set unapplied; // stub @Unapplied('a, b'): accepted, not applied } public static class FieldInit extends Node { @@ -342,6 +343,7 @@ public static class SuperInit extends Node { } public static class MethodDecl extends Node { + public java.util.Set unapplied; // stub @Unapplied('a, b'): accepted, not applied public TypeRef returnType; // may be VAR (=> inferred) or VOID public String name; public List typeParams = new ArrayList(); // generic method: m(...) diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java index ba4a6463ac3..3b3de6d670d 100644 --- a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java @@ -148,6 +148,19 @@ public List emit() { return mapped; } + /// The pub packages `lib` imports, `firebase_app_check` for + /// `package:firebase_app_check/firebase_app_check.dart`. + private static java.util.Set importedPackages(Library lib) { + java.util.Set out = new java.util.HashSet(); + for (String uri : lib.imports) { + if (uri != null && uri.startsWith("package:")) { + int slash = uri.indexOf('/'); + out.add(slash < 0 ? uri.substring(8) : uri.substring(8, slash)); + } + } + return out; + } + /// Adds `g` to `out`, remembering the Dart file it came from for its line table. private void addMapped(List out, GeneratedFile g, String dartFile) { out.add(g); @@ -175,6 +188,7 @@ private List emitAll() { // of the two sorts last, silently, with no diagnostic. List mainCandidates = new ArrayList(); for (Library lib : program.libraries) { + stubs.viewFrom(importedPackages(lib), program.classes.keySet()); for (ClassDecl c : lib.classes) { try { String dart = c.file != null ? c.file : lib.fileName; @@ -9955,6 +9969,17 @@ private static boolean publishesValueByType(String className) { || className.equals("BlocProvider") || className.equals("RepositoryProvider"); } + /** + * Stub classes whose behaviour depends on their type argument and that are made with + * positional arguments only: firebase_ui_auth's {@code AuthStateChangeAction(cb)} + * must run its callback for a SignedIn and never hand it a UserCreated, which the erased + * callback would read as the wrong class. They get {@code .providedType(A.class)}, which + * answers the object itself. + */ + private static boolean actsOnTypeArgument(String className) { + return className.equals("AuthStateChangeAction"); + } + /** {@code A.class} for the first type argument when it is a plain class, else null. */ private String providedTypeToken(List typeArgs, Ctx ctx) { if (typeArgs == null || typeArgs.isEmpty() || typeArgs.get(0) == null) { @@ -10061,6 +10086,7 @@ private Out emitCtorCall(String className, List typeArgs, Args args, No ? new TypeRef(className) : TypeRef.of(className, resultArgs.toArray(new TypeRef[0])); Ast.CtorDecl ct = sc.defaultCtor(); + warnUnapplied(ct == null ? null : ct.unapplied, className, args); // positional args -> Java constructor arguments StringBuilder posArgs = new StringBuilder(); List positionalParams = new ArrayList(); @@ -10102,7 +10128,9 @@ private Out emitCtorCall(String className, List typeArgs, Args args, No } } if (args.named.isEmpty()) { - return new Out("new " + simple + diamond + "(" + posArgs + ")", stubResultType); + String created = "new " + simple + diamond + "(" + posArgs + ")"; + String token = actsOnTypeArgument(className) ? providedTypeToken(typeArgs, ctx) : null; + return new Out(token == null ? created : created + ".providedType(" + token + ")", stubResultType); } // allocate-then-setters (ANF) String tmp = ctx.newTemp(); @@ -10799,7 +10827,26 @@ private String witnessArgs(Ast.MethodDecl m, Call c, Ctx ctx) { return args.isEmpty() ? token : args + ", " + token; } + /** + * W0002: the call passes a named argument the stub declares {@code @Unapplied} -- one + * the runtime accepts so the program compiles, and then does nothing with + * ({@code memCacheWidth}, a progress callback a transport cannot report). Reported at + * the argument, so the build log names the line that expects more than it will get. + */ + private void warnUnapplied(java.util.Set unapplied, String owner, Args args) { + if (unapplied == null || args == null) { + return; + } + for (Ast.NamedArg a : args.named) { + if (unapplied.contains(a.name)) { + diags.warn(a.value, "W0002", "'" + a.name + "' of " + owner + + " is accepted but not applied on Codename One"); + } + } + } + private String stubMethodArgs(Ast.MethodDecl m, Args args, Ctx ctx) { + warnUnapplied(m.unapplied, m.name, args); CtorDecl fake = new CtorDecl(); fake.params = m.params; return canonicalArgs(fake, args, ctx); @@ -12098,12 +12145,34 @@ private String stripImportPrefix(String name) { if (name != null) { int dot = name.indexOf('.'); if (dot > 0 && program.importPrefixes.contains(name.substring(0, dot))) { - return name.substring(dot + 1); + String base = name.substring(dot + 1); + // dart:ui's Image is UiImage in the stubs, since `Image` is the widget. + if ("Image".equals(base) && dartUiPrefixes().contains(name.substring(0, dot))) { + return "UiImage"; + } + return base; } } return name; } + private java.util.Set dartUiPrefixes; + + /// Every `import 'dart:ui' as p` prefix in the program. + private java.util.Set dartUiPrefixes() { + if (dartUiPrefixes == null) { + dartUiPrefixes = new java.util.HashSet(); + for (Library l : program.libraries) { + for (Ast.Directive d : l.importDirectives) { + if ("dart:ui".equals(d.uri) && d.prefix != null) { + dartUiPrefixes.add(d.prefix); + } + } + } + } + return dartUiPrefixes; + } + /** Pseudo-type marking a reference to a class itself (for static access). */ private TypeRef classRef(String className) { TypeRef t = new TypeRef("$class"); diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/parser/AstBuilder.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/parser/AstBuilder.java index d2eb054dbed..87a37946702 100644 --- a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/parser/AstBuilder.java +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/parser/AstBuilder.java @@ -533,8 +533,36 @@ private EnumDecl buildEnum(Dart2Parser.EnumTypeContext ctx) { return ed; } + /** + * A stub's {@code @Unapplied('a, b')} names the parameters of the constructor or method + * below it that are accepted and not applied, so a call passing one is reported (see + * {@code JavaEmitter.warnUnapplied}) instead of silently doing less than it says. + */ private void buildMember(Dart2Parser.ClassMemberDeclarationContext ctx, Dart2Parser.MetadataContext meta, ClassDecl cd) { + int ctors = cd.ctors.size(); + int methods = cd.methods.size(); + buildMemberDecl(ctx, meta, cd); + String unapplied = annotationArg(meta, "Unapplied"); + if (unapplied == null) { + return; + } + java.util.Set names = new java.util.LinkedHashSet(); + for (String n : unapplied.split(",")) { + if (!n.trim().isEmpty()) { + names.add(n.trim()); + } + } + for (int i = ctors; i < cd.ctors.size(); i++) { + cd.ctors.get(i).unapplied = names; + } + for (int i = methods; i < cd.methods.size(); i++) { + cd.methods.get(i).unapplied = names; + } + } + + private void buildMemberDecl(Dart2Parser.ClassMemberDeclarationContext ctx, + Dart2Parser.MetadataContext meta, ClassDecl cd) { boolean override = hasAnnotation(meta, "override"); if (ctx.methodSignature() != null) { buildMethodWithBody(ctx.methodSignature(), ctx.functionBody(), override, cd); diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/UnappliedParameterWarningTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/UnappliedParameterWarningTest.java new file mode 100644 index 00000000000..27d8ce842f0 --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/UnappliedParameterWarningTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler; + +import com.codename1.dart.transpiler.api.Diagnostic; +import com.codename1.dart.transpiler.harness.TestSupport; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * A stub's {@code @Unapplied} parameters are accepted so the program compiles and then + * do nothing; a call that passes one is reported as W0002 at that argument's line, and a + * call that does not pass one is not. + */ +public class UnappliedParameterWarningTest { + + private static final String STUB = + "@JavaName('com.example.Thumb')\n" + + "class Thumb {\n" + + " @Unapplied('memCacheWidth')\n" + + " external Thumb({String? url, int? memCacheWidth});\n" + + " @Unapplied('onSendProgress')\n" + + " external void send(String path, {Object? onSendProgress, int? retries});\n" + + "}\n"; + + private static List w0002(String src) { + TestSupport.Result r = TestSupport.transpile(new String[][] {{"main.dart", src}}, + Collections.emptyList(), + Collections.singletonList(new String[] {"thumb.stub.dart", STUB})); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + List out = new ArrayList(); + for (Diagnostic d : r.diags.asList()) { + if (d.code.equals("W0002")) { + out.add(d); + } + } + return out; + } + + @Test + public void passingAnUnappliedArgumentIsReportedAtItsLine() { + List w = w0002("void main() {\n" + + " final t = Thumb(url: 'u',\n" + + " memCacheWidth: 64);\n" + + " t.send('p', onSendProgress: (a, b) {});\n" + + "}\n"); + assertEquals(2, w.size(), String.valueOf(w)); + assertEquals(3, w.get(0).line); + assertEquals(4, w.get(1).line); + assertEquals("'memCacheWidth' of Thumb is accepted but not applied on Codename One", w.get(0).message); + } + + @Test + public void appliedArgumentsAreNotReported() { + assertEquals(0, w0002("void main() {\n" + + " Thumb(url: 'u').send('p', retries: 2);\n" + + "}\n").size()); + } +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/flutter_l10n_rtl/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/flutter_l10n_rtl/expect.txt new file mode 100644 index 00000000000..2bf6a126976 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/flutter_l10n_rtl/expect.txt @@ -0,0 +1,19 @@ +en ok=OK cancel=Cancel +en back=Back search=Search +en licenses=Licenses date=Select date +en first=0 narrow=SMTWTFS +en monthYear=October 2026 +en cupertino today=Today paste=Paste +es ok=ACEPTAR cancel=Cancelar +es back=Atrás search=Buscar +es licenses=Licencias date=Seleccionar fecha +es first=1 narrow=DLMXJVS +es monthYear=octubre de 2026 +es cupertino today=Hoy paste=Pegar +he ok=אישור cancel=ביטול +he back=הקודם search=חיפוש +he licenses=רישיונות date=בחירת תאריך +he first=0 narrow=אבגדהוש +he monthYear=אוקטובר 2026 +he cupertino today=היום paste=הדבקה +fallback ok=OK diff --git a/maven/dart-transpiler/src/test/resources/behavior/flutter_l10n_rtl/main.dart b/maven/dart-transpiler/src/test/resources/behavior/flutter_l10n_rtl/main.dart new file mode 100644 index 00000000000..d3d7e84060e --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/flutter_l10n_rtl/main.dart @@ -0,0 +1,28 @@ +// The framework strings an app gets from flutter_localizations, per locale. +// +// An app that lists GlobalMaterialLocalizations.delegate and runs in Hebrew or Spanish +// expects the dialogs, tooltips and pickers the framework draws to speak that language. +// This loads each delegate the way Localizations does and prints what it answers. The +// right-to-left layout half (a Row mirrored under Hebrew) needs a mounted tree and is +// held by RtlMirroringTest in flutter-runtime. +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +Future main() async { + for (final String lang in ['en', 'es', 'he']) { + final MaterialLocalizations m = + await GlobalMaterialLocalizations.delegate.load(Locale(lang)); + print('$lang ok=${m.okButtonLabel} cancel=${m.cancelButtonLabel}'); + print('$lang back=${m.backButtonTooltip} search=${m.searchFieldLabel}'); + print('$lang licenses=${m.licensesPageTitle} date=${m.datePickerHelpText}'); + print('$lang first=${m.firstDayOfWeekIndex} narrow=${m.narrowWeekdays.join()}'); + print('$lang monthYear=${m.formatMonthYear(DateTime(2026, 10, 29))}'); + final CupertinoLocalizations c = + await GlobalCupertinoLocalizations.delegate.load(Locale(lang)); + print('$lang cupertino today=${c.todayLabel} paste=${c.pasteButtonLabel}'); + } + final MaterialLocalizations fr = + await GlobalMaterialLocalizations.delegate.load(const Locale('fr')); + print('fallback ok=${fr.okButtonLabel}'); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_gaps/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_gaps/expect.txt new file mode 100644 index 00000000000..99722908229 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_gaps/expect.txt @@ -0,0 +1,22 @@ +true +err http://127.0.0.1:9/a.png true +true +true +err http://127.0.0.1:9/a.png true|empty|ph u1|p 50 200 0.25 u2|p 7 null null u2 +true +5 +true +false +true +false +true +true +false +true +false +true +false +true +false +false +line,circle diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_gaps/main.dart b/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_gaps/main.dart new file mode 100644 index 00000000000..5ad704a421c --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_gaps/main.dart @@ -0,0 +1,89 @@ +// Plugin gaps closed, headless: CachedNetworkImage builds its errorWidget when a load +// cannot start, its placeholder / progress builder while loading and the picture from +// cached bytes; polylines and circles hit-test taps in map coordinates and call onTap +// when they consume them. +import 'dart:typed_data'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +final log = []; + +Widget mark(String s) { + log.add(s); + return const Text('x'); +} + +void main() { + final err = CachedNetworkImage( + imageUrl: 'http://127.0.0.1:9/a.png', + placeholder: (c, u) => mark('ph'), + errorWidget: (c, u, e) => mark('err $u ${e != null}'), + ); + final w = err.buildOnce(null); + print(w is Text); + print(log.join('|')); + + // no builders: an empty box, not a throw + print(CachedNetworkImage(imageUrl: 'http://127.0.0.1:9/b.png').buildOnce(null) is SizedBox); + // an empty url fails as well + print(CachedNetworkImage(imageUrl: '', errorWidget: (c, u, e) => mark('empty')).buildOnce(null) is Text); + + final loading = CachedNetworkImage( + imageUrl: 'u1', + placeholder: (c, u) => mark('ph $u'), + ); + loading.buildLoading(null, 0, null); + final prog = CachedNetworkImage( + imageUrl: 'u2', + placeholder: (c, u) => mark('ph'), + progressIndicatorBuilder: (c, u, p) => mark('p ${p.downloaded} ${p.totalSize} ${p.progress} ${p.originalUrl}'), + ); + prog.buildLoading(null, 50, 200); + prog.buildLoading(null, 7, null); + print(log.join('|')); + + final cached = CachedNetworkImage(imageUrl: 'u3', cacheKey: 'k3', width: 10, height: 10, + errorWidget: (c, u, e) => mark('bad')); + cached.seedCache(Uint8List.fromList([1, 2, 3])); + print(cached.buildOnce(null) is Image); + print(log.length); + + final taps = []; + final line = Polyline( + polylineId: const PolylineId('l'), + points: const [LatLng(10, 10), LatLng(10, 10.01)], + consumeTapEvents: true, + onTap: () => taps.add('line'), + ); + // 0.0001 degrees of latitude is about 11 m + print(line.hitTest(const LatLng(10, 10.005), 5)); + print(line.hitTest(const LatLng(10.0001, 10.005), 5)); + print(line.hitTest(const LatLng(10.0001, 10.005), 20)); + print(line.hitTest(const LatLng(10, 10.02), 20)); + print(line.hitTest(const LatLng(10, 10.0101), 200)); + print(line.tapAt(const LatLng(10, 10.005), 5)); + print(line.tapAt(const LatLng(11, 10.005), 5)); + + final quiet = Polyline(polylineId: const PolylineId('q'), + points: const [LatLng(0, 0), LatLng(0, 1)], onTap: () => taps.add('quiet')); + print(quiet.hitTest(const LatLng(0, 0.5), 5)); + print(quiet.tapAt(const LatLng(0, 0.5), 5)); + + final circle = Circle( + circleId: const CircleId('c'), + center: const LatLng(48, 2), + radius: 1000, + consumeTapEvents: true, + onTap: () => taps.add('circle'), + ); + print(circle.hitTest(const LatLng(48.005, 2))); + print(circle.hitTest(const LatLng(48.02, 2))); + print(circle.tapAt(const LatLng(48, 2.005))); + print(circle.tapAt(const LatLng(48, 2.05))); + final hidden = Circle(circleId: const CircleId('h'), center: const LatLng(0, 0), radius: 1000, + consumeTapEvents: true, visible: false, onTap: () => taps.add('hidden')); + print(hidden.tapAt(const LatLng(0, 0))); + print(taps.join(',')); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_net/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_net/expect.txt new file mode 100644 index 00000000000..2aeaf615134 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_net/expect.txt @@ -0,0 +1,14 @@ +conn DioExceptionType.connectionError null +req1,req2,err1 DioExceptionType.connectionError,err2 +cancel DioExceptionType.cancel true +post DioExceptionType.connectionError progress=0 +download DioExceptionType.connectionError +form 1 1 +types ResponseType.json ResponseType.plain ResponseType.bytes +kinds DioExceptionType.receiveTimeout DioExceptionType.sendTimeout DioExceptionType.badResponse +modes 5 inAppBrowserView +canLaunch false +launch false +share ShareResultStatus.unavailable +picked null +many 0 diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_net/main.dart b/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_net/main.dart new file mode 100644 index 00000000000..2fb61fbc205 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_net/main.dart @@ -0,0 +1,67 @@ +// Deeper network plugin packs, headless: dio's interceptor order, error types, +// CancelToken, FormData, download and progress paths; url_launcher's LaunchMode; +// share_plus's ShareParams; image_picker with size options and no display. +import 'package:dio/dio.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; + +Future main() async { + try { await run(); } catch (e) { print('ERR $e'); } +} + +Future run() async { + final dio = Dio(BaseOptions(connectTimeout: const Duration(seconds: 2), headers: {'X-A': '1'})); + final log = []; + dio.interceptors.add(InterceptorsWrapper( + onRequest: (o, h) { log.add('req1'); h.next(o); }, + onError: (e, h) { log.add('err1 ${e.type}'); h.next(e); }, + )); + dio.interceptors.add(InterceptorsWrapper( + onRequest: (o, h) { log.add('req2'); h.next(o); }, + onError: (e, h) { log.add('err2'); h.next(e); }, + )); + try { + await dio.get('http://127.0.0.1:9/x'); + } on DioException catch (e) { + print('conn ${e.type} ${e.response?.statusCode}'); + } + print(log.join(',')); + + final token = CancelToken(); + token.cancel('bye'); + try { + await dio.get('http://127.0.0.1:9/y', cancelToken: token); + } on DioException catch (e) { + print('cancel ${e.type} ${CancelToken.isCancel(e)}'); + } + + var sent = 0; + try { + await dio.post('http://127.0.0.1:9/z', data: {'a': 1}, onSendProgress: (c, t) { sent++; }); + } on DioException catch (e) { + print('post ${e.type} progress=$sent'); + } + try { + await dio.download('http://127.0.0.1:9/f', '/tmp/never', onReceiveProgress: (c, t) {}); + } on DioException catch (e) { + print('download ${e.type}'); + } + + final form = FormData.fromMap({'name': 'x', 'file': MultipartFile.fromBytes([1, 2, 3], filename: 'a.bin')}); + print('form ${form.fields.length} ${form.files.length}'); + print('types ${ResponseType.json} ${ResponseType.plain} ${ResponseType.bytes}'); + print('kinds ${DioExceptionType.receiveTimeout} ${DioExceptionType.sendTimeout} ${DioExceptionType.badResponse}'); + + print('modes ${LaunchMode.values.length} ${LaunchMode.inAppBrowserView.name}'); + print('canLaunch ${await canLaunchUrl(Uri.parse('tel:123'))}'); + print('launch ${await launchUrl(Uri.parse('https://example.com'), mode: LaunchMode.inAppWebView)}'); + + final r = await SharePlus.instance.share(ShareParams(text: 'hi', subject: 's')); + print('share ${r.status}'); + + final picked = await ImagePicker().pickImage(source: ImageSource.gallery, maxWidth: 100, imageQuality: 50); + print('picked $picked'); + final many = await ImagePicker().pickMultiImage(maxWidth: 10); + print('many ${many.length}'); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_platform/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_platform/expect.txt new file mode 100644 index 00000000000..90696495098 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_platform/expect.txt @@ -0,0 +1,22 @@ +recording true +ops 1 recording false +image 4x3 +png 137 80 78 71 +marker bytes 119 true +raw 48 +azure 210.0 rgb 65280 +rose 330.0 +bytes true default -1.0 +marker icon 210.0 +line p 4 circle c 100.0 +launched false null +sound bells /notification_sound_bells.mp3 +perm true +shown +before AuthorizationStatus.notDetermined +provisional AuthorizationStatus.provisional +full AuthorizationStatus.authorized +initial null +appcheck null +auto false +limited failed diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_platform/main.dart b/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_platform/main.dart new file mode 100644 index 00000000000..678a4990a5f --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_depth_platform/main.dart @@ -0,0 +1,82 @@ +// Deeper platform plugin paths, headless: picture-to-PNG, marker hues and overlays, +// notification launch details and channel sounds, messaging permissions, App Check. +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +Future main() async { + try { await run(); } catch (e) { print('ERR $e'); } +} + +Future run() async { + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + canvas.drawRect(Rect.fromLTWH(0, 0, 4, 4), Paint()..color = const Color(0xFFFF0000)); + print('recording ${recorder.isRecording}'); + final picture = recorder.endRecording(); + print('ops ${picture.approximateBytesUsed} recording ${recorder.isRecording}'); + final ui.Image img = await picture.toImage(4, 3); + print('image ${img.width}x${img.height}'); + final png = await img.toByteData(format: ui.ImageByteFormat.png); + print('png ${png!.getUint8(0)} ${png.getUint8(1)} ${png.getUint8(2)} ${png.getUint8(3)}'); + final Uint8List pngBytes = png.buffer.asUint8List(); + print('marker bytes ${pngBytes.length} ${BitmapDescriptor.fromBytes(pngBytes).hasBytes()}'); + final raw = await img.toByteData(); + print('raw ${raw!.lengthInBytes}'); + + final azure = BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueAzure); + print('azure ${azure.hue()} rgb ${BitmapDescriptor.hueToRgb(BitmapDescriptor.hueGreen)}'); + print('rose ${BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRose).hue()}'); + final bytes = BitmapDescriptor.fromBytes(Uint8List.fromList([1, 2, 3])); + print('bytes ${bytes.hasBytes()} default ${BitmapDescriptor.defaultMarker.hue()}'); + final m = Marker(markerId: const MarkerId('a'), position: const LatLng(1, 2), icon: azure, + infoWindow: const InfoWindow(title: 'A'), onTap: () {}); + print('marker icon ${m.icon!.hue()}'); + final line = Polyline(polylineId: const PolylineId('p'), + points: const [LatLng(0, 0), LatLng(1, 1)], color: const Color(0xFF0000FF), width: 4); + final circle = Circle(circleId: const CircleId('c'), center: const LatLng(0, 0), radius: 100); + print('line ${line.polylineId.value} ${line.width} circle ${circle.circleId.value} ${circle.radius}'); + + final plugin = FlutterLocalNotificationsPlugin(); + await plugin.initialize(const InitializationSettings(), + onDidReceiveNotificationResponse: (r) => print('tap ${r.payload}'), + onDidReceiveBackgroundNotificationResponse: (r) => print('bg ${r.payload}')); + final launch = await plugin.getNotificationAppLaunchDetails(); + print('launched ${launch!.didNotificationLaunchApp} ${launch.notificationResponse?.payload}'); + final android = plugin.resolvePlatformSpecificImplementation(); + final sound = RawResourceAndroidNotificationSound('bells'); + print('sound ${sound.sound} ${sound.alertSoundPath()}'); + await android?.createNotificationChannel(AndroidNotificationChannel('ch', 'Channel', sound: sound)); + print('perm ${await android?.requestNotificationsPermission()}'); + await plugin.show(1, 'T', 'B', null, payload: 'p1'); + print('shown'); + + final messaging = FirebaseMessaging.instance; + print('before ${(await messaging.getNotificationSettings()).authorizationStatus}'); + final prov = await messaging.requestPermission(alert: true, badge: true, sound: true, provisional: true); + print('provisional ${prov.authorizationStatus}'); + final full = await messaging.requestPermission(alert: true, badge: true, sound: true); + print('full ${full.authorizationStatus}'); + FirebaseMessaging.onBackgroundMessage(_bg); + print('initial ${await messaging.getInitialMessage()}'); + + await FirebaseAppCheck.instance.activate(androidProvider: AndroidProvider.debug, + appleProvider: AppleProvider.appAttest); + print('appcheck ${await FirebaseAppCheck.instance.getToken()}'); + await FirebaseAppCheck.instance.setTokenAutoRefreshEnabled(false); + print('auto ${FirebaseAppCheck.instance.isTokenAutoRefreshEnabled}'); + try { + await FirebaseAppCheck.instance.getLimitedUseToken(); + } catch (e) { + print('limited failed'); + } +} + +Future _bg(RemoteMessage message) async { + print('background ${message.data}'); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_device_packs/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/plugin_device_packs/expect.txt index 6e11fb5820e..c91292b6cda 100644 --- a/maven/dart-transpiler/src/test/resources/behavior/plugin_device_packs/expect.txt +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_device_packs/expect.txt @@ -4,7 +4,7 @@ Pets 1.2.3+45 offline true a.txt text/plain hello 5 details true -platform impl true +platform impl false before init: no-app apps 1 p headless: network-request-failed diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ai/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ai/expect.txt new file mode 100644 index 00000000000..6e5417333c4 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ai/expect.txt @@ -0,0 +1,16 @@ +[DEFAULT] null europe-west4 +{"candidateCount":1,"maxOutputTokens":256,"temperature":0.5,"topP":0.9,"topK":40,"stopSequences":["END"],"responseMimeType":"text/plain"} +{"category":"HARM_CATEGORY_HATE_SPEECH","threshold":"BLOCK_MEDIUM_AND_ABOVE"} +{"role":"user","parts":[{"text":"hi"}]} +{"role":"model","parts":[{"text":"yo"}]} +{"role":"user","parts":[{"text":"a"},{"inlineData":{"mimeType":"image/png","data":"AQID"}}]} +Hello world 2 +true true +model 2 +3 5 8 +error API key not valid +blocked Response was blocked due to SAFETY +generate failed +stream failed +count failed +chat failed 1 diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ai/main.dart b/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ai/main.dart new file mode 100644 index 00000000000..bceedb7e7e4 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ai/main.dart @@ -0,0 +1,73 @@ +// firebase_ai, headless: models, contents and configs are built and serialised as the REST +// API wants them, a canned reply is parsed, and a request fails at once with the plugin's +// own FirebaseAIException instead of hanging. +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:firebase_core/firebase_core.dart'; + +Future main() async { + await Firebase.initializeApp( + options: FirebaseOptions(apiKey: 'k', appId: 'a', messagingSenderId: 'm', projectId: 'proj')); + final ai = FirebaseAI.googleAI(); + final vertex = FirebaseAI.vertexAI(location: 'europe-west4'); + print('${ai.app.name} ${ai.location} ${vertex.location}'); + + final config = GenerationConfig(temperature: 0.5, maxOutputTokens: 256, topP: 0.9, topK: 40, + responseMimeType: 'text/plain', candidateCount: 1, stopSequences: ['END']); + print(jsonEncode(config.toJson())); + print(jsonEncode(SafetySetting(HarmCategory.hateSpeech, HarmBlockThreshold.medium).toJson())); + + print(jsonEncode(Content.text('hi').toJson())); + print(jsonEncode(Content.model([TextPart('yo')]).toJson())); + print(jsonEncode(Content('user', [TextPart('a'), InlineDataPart('image/png', Uint8List.fromList([1, 2, 3]))]).toJson())); + + final model = ai.generativeModel( + model: 'gemini-2.0-flash', + generationConfig: config, + safetySettings: [SafetySetting(HarmCategory.harassment, HarmBlockThreshold.low)], + systemInstruction: Content.text('be brief')); + + final reply = GenerateContentResponse.cn1FromJson('{"candidates":[{"content":{"role":"model",' + '"parts":[{"text":"Hello "},{"text":"world"}]},"finishReason":"STOP"},' + '{"content":{"role":"model","parts":[{"text":"x"}]},"finishReason":"MAX_TOKENS"}],' + '"usageMetadata":{"promptTokenCount":3,"candidatesTokenCount":5,"totalTokenCount":8}}'); + print('${reply.text} ${reply.candidates.length}'); + print('${reply.candidates[0].finishReason == FinishReason.stop} ${reply.candidates[1].finishReason == FinishReason.maxTokens}'); + print('${reply.candidates[0].content.role} ${reply.candidates[0].content.parts.length}'); + final u = reply.usageMetadata!; + print('${u.promptTokenCount} ${u.candidatesTokenCount} ${u.totalTokenCount}'); + + try { + GenerateContentResponse.cn1FromJson('{"error":{"code":403,"message":"API key not valid"}}'); + } on FirebaseAIException catch (e) { + print('error ${e.message}'); + } + try { + GenerateContentResponse.cn1FromJson('{"promptFeedback":{"blockReason":"SAFETY"}}').text; + } on FirebaseAIException catch (e) { + print('blocked ${e.message}'); + } + + try { + await model.generateContent([Content.text('hello')]); + } on FirebaseAIException catch (e) { + print('generate failed'); + } + try { + await model.generateContentStream([Content.text('hello')]).toList(); + } on FirebaseAIException catch (e) { + print('stream failed'); + } + try { + await model.countTokens([Content.text('hello')]); + } on FirebaseAIException catch (e) { + print('count failed'); + } + final chat = model.startChat(history: [Content.text('earlier')]); + try { + await chat.sendMessage(Content.text('next')); + } on FirebaseAIException catch (e) { + print('chat failed ${chat.history.length}'); + } +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ui/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ui/expect.txt new file mode 100644 index 00000000000..cc12e556663 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ui/expect.txt @@ -0,0 +1,6 @@ +3 +google.com apple.com +AppleProvider.appAttest +true +true +true diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ui/fui_app_check.dart b/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ui/fui_app_check.dart new file mode 100644 index 00000000000..d29bbff6985 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ui/fui_app_check.dart @@ -0,0 +1,3 @@ +import 'package:firebase_app_check/firebase_app_check.dart'; + +String appCheckProviderName() => AppleProvider.appAttest.toString(); diff --git a/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ui/main.dart b/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ui/main.dart new file mode 100644 index 00000000000..2a40dadf033 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/plugin_firebase_ui/main.dart @@ -0,0 +1,34 @@ +// FirebaseUI's screens and providers, headless: they are made, configured and carry +// their provider ids; App Check's AppleProvider enum shares its name with FirebaseUI's +// Apple button; each library sees the one its imports name. +import 'fui_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart' hide EmailAuthProvider; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_oauth_apple/firebase_ui_oauth_apple.dart'; +import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart'; +import 'package:flutter/material.dart'; + +Future main() async { + await Firebase.initializeApp( + options: const FirebaseOptions(apiKey: 'k', appId: 'a', messagingSenderId: 'm', projectId: 'p')); + final providers = [EmailAuthProvider(), GoogleProvider(clientId: 'id.apps.googleusercontent.com'), + AppleProvider()]; + FirebaseUIAuth.configureProviders(providers); + print(FirebaseUIAuth.providersFor(Firebase.app()).length); + print('${(providers[1] as GoogleProvider).providerId} ${(providers[2] as AppleProvider).providerId}'); + print(appCheckProviderName()); + final screen = SignInScreen( + providers: providers, + actions: [ + AuthStateChangeAction((context, state) => print('signed in ${state.user?.uid}')), + AuthStateChangeAction((context, state) => print('created')), + SignedOutAction((context) => print('out')), + ], + headerBuilder: (context, constraints, shrink) => const Text('header'), + ); + print(screen is StatefulWidget); + final profile = ProfileScreen(actions: [SignedOutAction((context) {})], children: const [Text('x')]); + print(profile is StatelessWidget); + print(OAuthProviderButton(provider: providers[1]) is Widget); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/AlignmentDirectional.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/AlignmentDirectional.java index 88e5344e800..1ac11464002 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/AlignmentDirectional.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/AlignmentDirectional.java @@ -48,6 +48,14 @@ public AlignmentDirectional(double start, double y) { super(start, y); } + /** + * The horizontal coordinate {@code a} resolves to: a directional alignment's start + * is the right edge under right-to-left text, a plain {@link Alignment} never moves. + */ + public static double xFor(Alignment a, boolean rtl) { + return rtl && a instanceof AlignmentDirectional ? -a.x() : a.x(); + } + public double start() { return x(); } diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Canvas.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Canvas.java index 61838580830..b1a4b421c46 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Canvas.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Canvas.java @@ -32,69 +32,188 @@ */ public class Canvas { + /// The recorder this canvas writes into, when it was made as + /// {@code Canvas(recorder)}; null for a canvas a painter is handed. + private PictureRecorder recorder; + + public Canvas() { + } + + /// {@code Canvas(recorder, [cullRect])}: every call is recorded into the recorder + /// and replayed when the resulting Picture is drawn or turned into an image. The + /// cull rect is accepted and not applied. + public Canvas(PictureRecorder recorder, Rect cullRect) { + this.recorder = recorder; + if (recorder != null) { + recorder.begin(); + } + } + + private void rec(String op, Object... args) { + if (recorder != null) { + recorder.record(op, args); + } + } + public void drawPath(Path path, Paint paint) { + rec("drawPath", path, paint); } public void drawRect(Rect rect, Paint paint) { + rec("drawRect", rect, paint); } public void drawRRect(RRect rrect, Paint paint) { + rec("drawRRect", rrect, paint); } public void drawCircle(Offset c, double radius, Paint paint) { + rec("drawCircle", c, radius, paint); } public void drawOval(Rect rect, Paint paint) { + rec("drawOval", rect, paint); } public void drawLine(Offset p1, Offset p2, Paint paint) { + rec("drawLine", p1, p2, paint); } public void drawArc(Rect rect, double startAngle, double sweepAngle, boolean useCenter, Paint paint) { + rec("drawArc", rect, startAngle, sweepAngle, useCenter, paint); } public void drawPoints(Object pointMode, Object points, Paint paint) { + rec("drawPoints", pointMode, points, paint); } public void drawColor(Color color, Object blendMode) { + rec("drawColor", color, blendMode); } public void drawShadow(Path path, Color color, double elevation, boolean transparentOccluder) { + rec("drawShadow", path, color, elevation, transparentOccluder); } public void drawVertices(Object vertices, Object blendMode, Paint paint) { + rec("drawVertices", vertices, blendMode, paint); } public void drawImage(Object image, Offset offset, Paint paint) { + rec("drawImage", image, offset, paint); } public void translate(double dx, double dy) { + rec("translate", dx, dy); } public void scale(double sx, double sy) { + rec("scale", sx, sy); } public void rotate(double radians) { + rec("rotate", radians); } public void skew(double sx, double sy) { + rec("skew", sx, sy); } public void save() { + rec("save"); } public void saveLayer(Rect bounds, Paint paint) { + rec("saveLayer", bounds, paint); } public void restore() { + rec("restore"); } public void clipRect(Rect rect) { + rec("clipRect", rect); } public void clipRRect(RRect rrect) { + rec("clipRRect", rrect); } public void clipPath(Path path) { + rec("clipPath", path); + } + + /// Replays one recorded call onto {@code target}. + static void replay(Canvas target, String op, Object[] a) { + switch (op) { + case "drawPath": + target.drawPath((Path) a[0], (Paint) a[1]); + break; + case "drawRect": + target.drawRect((Rect) a[0], (Paint) a[1]); + break; + case "drawRRect": + target.drawRRect((RRect) a[0], (Paint) a[1]); + break; + case "drawCircle": + target.drawCircle((Offset) a[0], ((Double) a[1]).doubleValue(), (Paint) a[2]); + break; + case "drawOval": + target.drawOval((Rect) a[0], (Paint) a[1]); + break; + case "drawLine": + target.drawLine((Offset) a[0], (Offset) a[1], (Paint) a[2]); + break; + case "drawArc": + target.drawArc((Rect) a[0], ((Double) a[1]).doubleValue(), ((Double) a[2]).doubleValue(), ((Boolean) a[3]).booleanValue(), (Paint) a[4]); + break; + case "drawPoints": + target.drawPoints(a[0], a[1], (Paint) a[2]); + break; + case "drawColor": + target.drawColor((Color) a[0], a[1]); + break; + case "drawShadow": + target.drawShadow((Path) a[0], (Color) a[1], ((Double) a[2]).doubleValue(), ((Boolean) a[3]).booleanValue()); + break; + case "drawVertices": + target.drawVertices(a[0], a[1], (Paint) a[2]); + break; + case "drawImage": + target.drawImage(a[0], (Offset) a[1], (Paint) a[2]); + break; + case "translate": + target.translate(((Double) a[0]).doubleValue(), ((Double) a[1]).doubleValue()); + break; + case "scale": + target.scale(((Double) a[0]).doubleValue(), ((Double) a[1]).doubleValue()); + break; + case "rotate": + target.rotate(((Double) a[0]).doubleValue()); + break; + case "skew": + target.skew(((Double) a[0]).doubleValue(), ((Double) a[1]).doubleValue()); + break; + case "save": + target.save(); + break; + case "saveLayer": + target.saveLayer((Rect) a[0], (Paint) a[1]); + break; + case "restore": + target.restore(); + break; + case "clipRect": + target.clipRect((Rect) a[0]); + break; + case "clipRRect": + target.clipRRect((RRect) a[0]); + break; + case "clipPath": + target.clipPath((Path) a[0]); + break; + default: + break; + } } } diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/ImageByteFormat.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ImageByteFormat.java new file mode 100644 index 00000000000..6dfda9b9fd9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ImageByteFormat.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** dart:ui's {@code ImageByteFormat}. */ +public enum ImageByteFormat { + rawRgba, rawStraightRgba, rawUnmodified, rawExtendedRgba128, png +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Picture.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Picture.java new file mode 100644 index 00000000000..e0e6d67a670 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Picture.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import java.util.List; + +import com.codename1.flutter.rendering.GraphicsCanvas; +import com.codename1.ui.Display; +import com.codename1.ui.Graphics; +import com.codename1.ui.Image; + +import dart.async.Future; + +/** + * dart:ui's {@code Picture}: the calls a {@link PictureRecorder} captured. {@link #toImage} + * replays them onto a mutable Codename One image ({@code Image.createImage(w, h, 0)}) of + * that size, transparent where nothing was drawn. Headless (no Display) there is nothing + * to draw on, so the image is fully transparent, still {@code width x height}, and still + * encodes to a valid PNG. + */ +public class Picture { + private final List ops; + private boolean disposed; + + Picture(List ops) { + this.ops = ops; + } + + /** How many drawing calls were recorded. */ + public long approximateBytesUsed() { + return ops.size(); + } + + public Future toImage(long width, long height) { + try { + return Future.value(toImageSync(width, height)); + } catch (RuntimeException e) { + return Future.error(e); + } + } + + public UiImage toImageSync(long width, long height) { + if (disposed) { + throw new dart.core.StateError("Picture has been disposed"); + } + int w = (int) Math.max(1, width); + int h = (int) Math.max(1, height); + if (!Display.isInitialized()) { + return new UiImage(w, h, null); + } + Image img = Image.createImage(w, h, 0); + Graphics g = img.getGraphics(); + g.setAntiAliased(true); + Canvas target = new GraphicsCanvas(g, 0, 0, 1); + for (Object[] op : ops) { + Canvas.replay(target, (String) op[0], (Object[]) op[1]); + } + return new UiImage(w, h, img); + } + + public void dispose() { + disposed = true; + } + + public boolean debugDisposed() { + return disposed; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/PictureRecorder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/PictureRecorder.java new file mode 100644 index 00000000000..4cae0d784d4 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/PictureRecorder.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import java.util.ArrayList; +import java.util.List; + +/** + * dart:ui's {@code PictureRecorder}. A {@code Canvas(recorder)} writes its calls here; + * {@link #endRecording} hands them to a {@link Picture}, which replays them onto a + * Codename One {@code Graphics} when it is turned into an image. + */ +public class PictureRecorder { + private List ops = new ArrayList(); + private boolean recording; + + public PictureRecorder() { + } + + void begin() { + recording = true; + } + + void record(String op, Object[] args) { + ops.add(new Object[] {op, args}); + } + + /** {@code isRecording}: a canvas was attached and endRecording has not run. */ + public boolean isRecording() { + return recording; + } + + /** Ends the recording; later calls on the canvas are recorded into a fresh one. */ + public Picture endRecording() { + Picture p = new Picture(ops); + ops = new ArrayList(); + recording = false; + return p; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RenderElement.java index 898886677f2..4b51a8a2f48 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RenderElement.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RenderElement.java @@ -796,6 +796,24 @@ protected void positionChildren(int x, int y) { } } + /** + * Whether the nearest enclosing {@code Directionality} is right-to-left. Layouts that + * place children along a start/end axis mirror under it, as Flutter's do. + */ + protected final boolean isRtl() { + return com.codename1.flutter.widgets.Directionality.of(this) == TextDirection.rtl; + } + + /** + * Mirrors the x offset {@code child} was given in a box {@code width} wide, so a + * layout written for left-to-right places the child at the mirrored position. + */ + protected final void mirrorChildX(RenderElement child, double width) { + if (child != null && child.size() != null) { + child.relX = width - child.relX - child.size().width(); + } + } + protected void setChildOffset(RenderElement child, double dx, double dy) { child.relX = dx; child.relY = dy; diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/UiImage.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/UiImage.java new file mode 100644 index 00000000000..dd047173542 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/UiImage.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import com.codename1.ui.Image; +import com.codename1.ui.util.ImageIO; + +import dart.async.Future; +import dart.typed_data.ByteData; + +/** + * dart:ui's {@code Image} as {@code Picture.toImage} returns it. (The name {@code Image} + * is the widget's in these stubs, so code that writes the type as {@code ui.Image} + * should use {@code final} or {@code var}; the members are dart:ui's.) It wraps a + * Codename One image, or none headless, where it is fully transparent. + * + *

{@code toByteData(format: ImageByteFormat.png)} encodes with the port's + * {@code ImageIO} when it has one, and otherwise with a small built-in encoder + * (uncompressed deflate), so the bytes are a valid PNG everywhere, headless included. + * {@code rawRgba} and {@code rawStraightRgba} answer the pixels row by row; the other + * raw formats answer the same bytes. {@code ByteData.buffer} is not provided by the + * runtime's ByteData, so read the bytes with {@code getUint8}. + */ +public class UiImage { + private final int width; + private final int height; + private final Image image; + private boolean disposed; + + UiImage(int width, int height, Image image) { + this.width = width; + this.height = height; + this.image = image; + } + + public long width() { + return width; + } + + public long height() { + return height; + } + + /** The Codename One image, or null headless. */ + public Image cn1Image() { + return image; + } + + public void dispose() { + disposed = true; + } + + public boolean debugDisposed() { + return disposed; + } + + public Future toByteData(ImageByteFormat format) { + try { + byte[] b = format == ImageByteFormat.png ? png() : rgba(); + return Future.value(ByteData.fromBytes(b)); + } catch (IOException e) { + return Future.error(new dart.core.StateError("Could not encode the image: " + e.getMessage())); + } + } + + private int[] argb() { + return image == null ? new int[width * height] : image.getRGB(); + } + + private byte[] rgba() { + int[] px = argb(); + byte[] out = new byte[px.length * 4]; + for (int i = 0; i < px.length; i++) { + int p = px[i]; + out[i * 4] = (byte) (p >> 16); + out[i * 4 + 1] = (byte) (p >> 8); + out[i * 4 + 2] = (byte) p; + out[i * 4 + 3] = (byte) (p >>> 24); + } + return out; + } + + private byte[] png() throws IOException { + ImageIO io = image == null ? null : ImageIO.getImageIO(); + if (io != null && io.isFormatSupported(ImageIO.FORMAT_PNG)) { + ByteArrayOutputStream bo = new ByteArrayOutputStream(); + io.save(image, bo, ImageIO.FORMAT_PNG, 1f); + byte[] b = bo.toByteArray(); + if (b.length > 8) { + return b; + } + } + return encodePng(width, height, argb()); + } + + /// A PNG of the ARGB pixels: 8-bit RGBA, filter 0, stored (uncompressed) deflate. + static byte[] encodePng(int w, int h, int[] px) throws IOException { + int stride = w * 4 + 1; + byte[] raw = new byte[stride * h]; + for (int y = 0; y < h; y++) { + int o = y * stride; + raw[o] = 0; + for (int x = 0; x < w; x++) { + int p = px[y * w + x]; + int q = o + 1 + x * 4; + raw[q] = (byte) (p >> 16); + raw[q + 1] = (byte) (p >> 8); + raw[q + 2] = (byte) p; + raw[q + 3] = (byte) (p >>> 24); + } + } + ByteArrayOutputStream z = new ByteArrayOutputStream(); + z.write(0x78); + z.write(0x01); + int pos = 0; + do { + int n = Math.min(65535, raw.length - pos); + boolean last = pos + n >= raw.length; + z.write(last ? 1 : 0); + z.write(n & 0xff); + z.write((n >> 8) & 0xff); + z.write(~n & 0xff); + z.write((~n >> 8) & 0xff); + z.write(raw, pos, n); + pos += n; + } while (pos < raw.length); + long a = 1; + long b = 0; + for (int i = 0; i < raw.length; i++) { + a = (a + (raw[i] & 0xff)) % 65521; + b = (b + a) % 65521; + } + writeInt(z, (int) ((b << 16) | a)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(new byte[] {(byte) 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}); + ByteArrayOutputStream ihdr = new ByteArrayOutputStream(); + writeInt(ihdr, w); + writeInt(ihdr, h); + ihdr.write(8); + ihdr.write(6); + ihdr.write(0); + ihdr.write(0); + ihdr.write(0); + chunk(out, "IHDR", ihdr.toByteArray()); + chunk(out, "IDAT", z.toByteArray()); + chunk(out, "IEND", new byte[0]); + return out.toByteArray(); + } + + private static void writeInt(ByteArrayOutputStream o, int v) { + o.write((v >>> 24) & 0xff); + o.write((v >>> 16) & 0xff); + o.write((v >>> 8) & 0xff); + o.write(v & 0xff); + } + + private static void chunk(ByteArrayOutputStream out, String type, byte[] data) throws IOException { + byte[] t = new byte[] {(byte) type.charAt(0), (byte) type.charAt(1), (byte) type.charAt(2), + (byte) type.charAt(3)}; + writeInt(out, data.length); + out.write(t); + out.write(data); + int crc = 0xffffffff; + crc = crc(crc, t); + crc = crc(crc, data); + writeInt(out, ~crc); + } + + private static int crc(int crc, byte[] b) { + int c = crc; + for (int i = 0; i < b.length; i++) { + c ^= b[i] & 0xff; + for (int k = 0; k < 8; k++) { + c = (c & 1) != 0 ? (c >>> 1) ^ 0xedb88320 : c >>> 1; + } + } + return c; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/CupertinoLocalizations.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/CupertinoLocalizations.java new file mode 100644 index 00000000000..0f4402df1d9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/CupertinoLocalizations.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.l10n; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Locale; + +/** + * Localized strings for Cupertino widgets, mirroring Flutter's + * {@code CupertinoLocalizations}: English, Spanish and Hebrew, chosen by the + * ambient locale's language code, English for any other. + */ +public class CupertinoLocalizations { + + private static final int K_ALERTDIALOGLABEL = 0; + private static final int K_ANTEMERIDIEMABBREVIATION = 1; + private static final int K_POSTMERIDIEMABBREVIATION = 2; + private static final int K_COPYBUTTONLABEL = 3; + private static final int K_CUTBUTTONLABEL = 4; + private static final int K_PASTEBUTTONLABEL = 5; + private static final int K_SELECTALLBUTTONLABEL = 6; + private static final int K_SEARCHTEXTFIELDPLACEHOLDERLABEL = 7; + private static final int K_MODALBARRIERDISMISSLABEL = 8; + private static final int K_TODAYLABEL = 9; + private static final int K_CANCELBUTTONLABEL = 10; + private static final int K_BACKBUTTONLABEL = 11; + + private static final String[][] TABLES = { + // en + { + "Alert", + "AM", + "PM", + "Copy", + "Cut", + "Paste", + "Select All", + "Search", + "Dismiss", + "Today", + "Cancel", + "Back" + }, + // es + { + "Alerta", + "a. m.", + "p. m.", + "Copiar", + "Cortar", + "Pegar", + "Seleccionar todo", + "Buscar", + "Cerrar", + "Hoy", + "Cancelar", + "Atr\u00e1s" + }, + // he + { + "\u05d4\u05ea\u05e8\u05d0\u05d4", + "AM", + "PM", + "\u05d4\u05e2\u05ea\u05e7\u05d4", + "\u05d2\u05d6\u05d9\u05e8\u05d4", + "\u05d4\u05d3\u05d1\u05e7\u05d4", + "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05db\u05d5\u05dc", + "\u05d7\u05d9\u05e4\u05d5\u05e9", + "\u05e1\u05d2\u05d9\u05e8\u05d4", + "\u05d4\u05d9\u05d5\u05dd", + "\u05d1\u05d9\u05d8\u05d5\u05dc", + "\u05d4\u05e7\u05d5\u05d3\u05dd" + }, + }; + + private static final CupertinoLocalizations[] INSTANCES = { + new CupertinoLocalizations(0), new CupertinoLocalizations(1), new CupertinoLocalizations(2) + }; + + /** {@code CupertinoLocalizations.delegate} -- a static getter, hence a field. Loads the table for a locale. */ + public static final LocalizationsDelegate delegate = new LocalizationsDelegate() { + @Override + public dart.async.Future load(Locale locale) { + return dart.async.Future.value(forLocale(locale)); + } + }; + + /** Index into the tables: 0 en, 1 es, 2 he. */ + final int lang; + + CupertinoLocalizations(int lang) { + this.lang = lang; + } + + /** + * The strings of the ambient locale's language + * ({@code Localizations.localeOf(context)}); English for any language + * without a table here. + */ + public static CupertinoLocalizations of(BuildContext context) { + return forLocale(com.codename1.flutter.widgets.Localizations.localeOf(context)); + } + + /** The instance for {@code locale}'s language code; English when there is no table for it. */ + public static CupertinoLocalizations forLocale(Locale locale) { + String lc = locale == null ? null : locale.languageCode(); + if ("es".equals(lc)) { + return INSTANCES[1]; + } + if ("he".equals(lc) || "iw".equals(lc)) { + return INSTANCES[2]; + } + return INSTANCES[0]; + } + + private String s(int key) { + return TABLES[lang][key]; + } + + public String alertDialogLabel() { + return s(K_ALERTDIALOGLABEL); + } + + public String anteMeridiemAbbreviation() { + return s(K_ANTEMERIDIEMABBREVIATION); + } + + public String postMeridiemAbbreviation() { + return s(K_POSTMERIDIEMABBREVIATION); + } + + public String copyButtonLabel() { + return s(K_COPYBUTTONLABEL); + } + + public String cutButtonLabel() { + return s(K_CUTBUTTONLABEL); + } + + public String pasteButtonLabel() { + return s(K_PASTEBUTTONLABEL); + } + + public String selectAllButtonLabel() { + return s(K_SELECTALLBUTTONLABEL); + } + + public String searchTextFieldPlaceholderLabel() { + return s(K_SEARCHTEXTFIELDPLACEHOLDERLABEL); + } + + public String modalBarrierDismissLabel() { + return s(K_MODALBARRIERDISMISSLABEL); + } + + public String todayLabel() { + return s(K_TODAYLABEL); + } + + public String cancelButtonLabel() { + return s(K_CANCELBUTTONLABEL); + } + + public String backButtonLabel() { + return s(K_BACKBUTTONLABEL); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalCupertinoLocalizations.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalCupertinoLocalizations.java index 13823cfca0d..f11b24cb6a5 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalCupertinoLocalizations.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalCupertinoLocalizations.java @@ -30,7 +30,7 @@ public abstract class GlobalCupertinoLocalizations { /** Static getter -> static field. */ - public static final LocalizationsDelegate delegate = new LocalizationsDelegate(); + public static final LocalizationsDelegate delegate = CupertinoLocalizations.delegate; private GlobalCupertinoLocalizations() { } diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalMaterialLocalizations.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalMaterialLocalizations.java index 6b4d0b1389f..df6e31c3ca5 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalMaterialLocalizations.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalMaterialLocalizations.java @@ -30,7 +30,7 @@ public abstract class GlobalMaterialLocalizations { /** Static getter -> static field. */ - public static final LocalizationsDelegate delegate = new LocalizationsDelegate(); + public static final LocalizationsDelegate delegate = MaterialLocalizations.delegate; private GlobalMaterialLocalizations() { } diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/MaterialLocalizations.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/MaterialLocalizations.java index 4283e145869..52b3c27110d 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/MaterialLocalizations.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/MaterialLocalizations.java @@ -24,48 +24,360 @@ package com.codename1.flutter.l10n; import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Locale; /** * Localized strings for Material widgets, mirroring Flutter's - * {@code MaterialLocalizations}. English defaults; a single shared instance is - * returned by {@link #of(BuildContext)}. + * {@code MaterialLocalizations}. Tables for English, Spanish and Hebrew; + * {@link #of(BuildContext)} picks one by the ambient locale's language code and + * falls back to English. The date formats are fixed per language rather than + * driven by CLDR patterns. */ public class MaterialLocalizations { - private static final MaterialLocalizations INSTANCE = new MaterialLocalizations(); + private static final int K_OKBUTTONLABEL = 0; + private static final int K_CANCELBUTTONLABEL = 1; + private static final int K_CLOSEBUTTONLABEL = 2; + private static final int K_CONTINUEBUTTONLABEL = 3; + private static final int K_COPYBUTTONLABEL = 4; + private static final int K_CUTBUTTONLABEL = 5; + private static final int K_PASTEBUTTONLABEL = 6; + private static final int K_SELECTALLBUTTONLABEL = 7; + private static final int K_SEARCHFIELDLABEL = 8; + private static final int K_BACKBUTTONTOOLTIP = 9; + private static final int K_CLOSEBUTTONTOOLTIP = 10; + private static final int K_DELETEBUTTONTOOLTIP = 11; + private static final int K_MOREBUTTONTOOLTIP = 12; + private static final int K_NEXTPAGETOOLTIP = 13; + private static final int K_PREVIOUSPAGETOOLTIP = 14; + private static final int K_OPENAPPDRAWERTOOLTIP = 15; + private static final int K_SHOWMENUTOOLTIP = 16; + private static final int K_DRAWERLABEL = 17; + private static final int K_POPUPMENULABEL = 18; + private static final int K_DIALOGLABEL = 19; + private static final int K_ALERTDIALOGLABEL = 20; + private static final int K_DATEPICKERHELPTEXT = 21; + private static final int K_DATEHELPTEXT = 22; + private static final int K_TIMEPICKERDIALHELPTEXT = 23; + private static final int K_REFRESHINDICATORSEMANTICLABEL = 24; + private static final int K_MODALBARRIERDISMISSLABEL = 25; + private static final int K_VIEWLICENSESBUTTONLABEL = 26; + private static final int K_LICENSESPAGETITLE = 27; - /** {@code MaterialLocalizations.delegate} — a static getter, hence a field. */ - public static final LocalizationsDelegate delegate = new LocalizationsDelegate(); + private static final String[][] TABLES = { + // en + { + "OK", + "Cancel", + "Close", + "Continue", + "Copy", + "Cut", + "Paste", + "Select all", + "Search", + "Back", + "Close", + "Delete", + "More", + "Next page", + "Previous page", + "Open navigation menu", + "Show menu", + "Navigation menu", + "Popup menu", + "Dialog", + "Alert", + "Select date", + "mm/dd/yyyy", + "Select time", + "Refresh", + "Dismiss", + "View licenses", + "Licenses" + }, + // es + { + "ACEPTAR", + "Cancelar", + "Cerrar", + "Continuar", + "Copiar", + "Cortar", + "Pegar", + "Seleccionar todo", + "Buscar", + "Atr\u00e1s", + "Cerrar", + "Eliminar", + "M\u00e1s", + "P\u00e1gina siguiente", + "P\u00e1gina anterior", + "Abrir el men\u00fa de navegaci\u00f3n", + "Mostrar men\u00fa", + "Men\u00fa de navegaci\u00f3n", + "Men\u00fa emergente", + "Cuadro de di\u00e1logo", + "Alerta", + "Seleccionar fecha", + "dd/mm/aaaa", + "Seleccionar hora", + "Actualizar", + "Cerrar", + "Ver licencias", + "Licencias" + }, + // he + { + "\u05d0\u05d9\u05e9\u05d5\u05e8", + "\u05d1\u05d9\u05d8\u05d5\u05dc", + "\u05e1\u05d2\u05d9\u05e8\u05d4", + "\u05d4\u05de\u05e9\u05da", + "\u05d4\u05e2\u05ea\u05e7\u05d4", + "\u05d2\u05d6\u05d9\u05e8\u05d4", + "\u05d4\u05d3\u05d1\u05e7\u05d4", + "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05db\u05d5\u05dc", + "\u05d7\u05d9\u05e4\u05d5\u05e9", + "\u05d4\u05e7\u05d5\u05d3\u05dd", + "\u05e1\u05d2\u05d9\u05e8\u05d4", + "\u05de\u05d7\u05d9\u05e7\u05d4", + "\u05e2\u05d5\u05d3", + "\u05d4\u05d3\u05e3 \u05d4\u05d1\u05d0", + "\u05d4\u05d3\u05e3 \u05d4\u05e7\u05d5\u05d3\u05dd", + "\u05e4\u05ea\u05d9\u05d7\u05d4 \u05e9\u05dc \u05ea\u05e4\u05e8\u05d9\u05d8 \u05d4\u05e0\u05d9\u05d5\u05d5\u05d8", + "\u05d4\u05e6\u05d2\u05ea \u05d4\u05ea\u05e4\u05e8\u05d9\u05d8", + "\u05ea\u05e4\u05e8\u05d9\u05d8 \u05e0\u05d9\u05d5\u05d5\u05d8", + "\u05ea\u05e4\u05e8\u05d9\u05d8 \u05e7\u05d5\u05e4\u05e5", + "\u05ea\u05d9\u05d1\u05ea \u05d3\u05d5-\u05e9\u05d9\u05d7", + "\u05d4\u05ea\u05e8\u05d0\u05d4", + "\u05d1\u05d7\u05d9\u05e8\u05ea \u05ea\u05d0\u05e8\u05d9\u05da", + "dd.mm.yyyy", + "\u05d1\u05d7\u05d9\u05e8\u05ea \u05e9\u05e2\u05d4", + "\u05e8\u05e2\u05e0\u05d5\u05df", + "\u05e1\u05d2\u05d9\u05e8\u05d4", + "\u05d4\u05e6\u05d2\u05ea \u05d4\u05e8\u05d9\u05e9\u05d9\u05d5\u05e0\u05d5\u05ea", + "\u05e8\u05d9\u05e9\u05d9\u05d5\u05e0\u05d5\u05ea" + }, + }; + private static final MaterialLocalizations[] INSTANCES = { + new MaterialLocalizations(0), new MaterialLocalizations(1), new MaterialLocalizations(2) + }; + + /** {@code MaterialLocalizations.delegate} -- a static getter, hence a field. Loads the table for a locale. */ + public static final LocalizationsDelegate delegate = new LocalizationsDelegate() { + @Override + public dart.async.Future load(Locale locale) { + return dart.async.Future.value(forLocale(locale)); + } + }; + + /** Index into the tables: 0 en, 1 es, 2 he. */ + final int lang; + + MaterialLocalizations(int lang) { + this.lang = lang; + } + + /** + * The strings of the ambient locale's language + * ({@code Localizations.localeOf(context)}); English for any language + * without a table here. + */ public static MaterialLocalizations of(BuildContext context) { - return INSTANCE; + return forLocale(com.codename1.flutter.widgets.Localizations.localeOf(context)); + } + + /** The instance for {@code locale}'s language code; English when there is no table for it. */ + public static MaterialLocalizations forLocale(Locale locale) { + String lc = locale == null ? null : locale.languageCode(); + if ("es".equals(lc)) { + return INSTANCES[1]; + } + if ("he".equals(lc) || "iw".equals(lc)) { + return INSTANCES[2]; + } + return INSTANCES[0]; + } + + private String s(int key) { + return TABLES[lang][key]; + } + + public String okButtonLabel() { + return s(K_OKBUTTONLABEL); + } + + public String cancelButtonLabel() { + return s(K_CANCELBUTTONLABEL); + } + + public String closeButtonLabel() { + return s(K_CLOSEBUTTONLABEL); + } + + public String continueButtonLabel() { + return s(K_CONTINUEBUTTONLABEL); + } + + public String copyButtonLabel() { + return s(K_COPYBUTTONLABEL); + } + + public String cutButtonLabel() { + return s(K_CUTBUTTONLABEL); + } + + public String pasteButtonLabel() { + return s(K_PASTEBUTTONLABEL); + } + + public String selectAllButtonLabel() { + return s(K_SELECTALLBUTTONLABEL); + } + + public String searchFieldLabel() { + return s(K_SEARCHFIELDLABEL); } public String backButtonTooltip() { - return "Back"; + return s(K_BACKBUTTONTOOLTIP); } public String closeButtonTooltip() { - return "Close"; + return s(K_CLOSEBUTTONTOOLTIP); } - public String closeButtonLabel() { - return "CLOSE"; + public String deleteButtonTooltip() { + return s(K_DELETEBUTTONTOOLTIP); } - public String viewLicensesButtonLabel() { - return "VIEW LICENSES"; + public String moreButtonTooltip() { + return s(K_MOREBUTTONTOOLTIP); } public String nextPageTooltip() { - return "Next page"; + return s(K_NEXTPAGETOOLTIP); } public String previousPageTooltip() { - return "Previous page"; + return s(K_PREVIOUSPAGETOOLTIP); } public String openAppDrawerTooltip() { - return "Open navigation menu"; + return s(K_OPENAPPDRAWERTOOLTIP); + } + + public String showMenuTooltip() { + return s(K_SHOWMENUTOOLTIP); + } + + public String drawerLabel() { + return s(K_DRAWERLABEL); + } + + public String popupMenuLabel() { + return s(K_POPUPMENULABEL); + } + + public String dialogLabel() { + return s(K_DIALOGLABEL); + } + + public String alertDialogLabel() { + return s(K_ALERTDIALOGLABEL); + } + + public String datePickerHelpText() { + return s(K_DATEPICKERHELPTEXT); + } + + public String dateHelpText() { + return s(K_DATEHELPTEXT); + } + + public String timePickerDialHelpText() { + return s(K_TIMEPICKERDIALHELPTEXT); + } + + public String refreshIndicatorSemanticLabel() { + return s(K_REFRESHINDICATORSEMANTICLABEL); + } + + public String modalBarrierDismissLabel() { + return s(K_MODALBARRIERDISMISSLABEL); + } + + public String viewLicensesButtonLabel() { + return s(K_VIEWLICENSESBUTTONLABEL); + } + + public String licensesPageTitle() { + return s(K_LICENSESPAGETITLE); + } + + private static final String[][] MONTHS = {{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}, {"enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"}, {"\u05d9\u05e0\u05d5\u05d0\u05e8", "\u05e4\u05d1\u05e8\u05d5\u05d0\u05e8", "\u05de\u05e8\u05e5", "\u05d0\u05e4\u05e8\u05d9\u05dc", "\u05de\u05d0\u05d9", "\u05d9\u05d5\u05e0\u05d9", "\u05d9\u05d5\u05dc\u05d9", "\u05d0\u05d5\u05d2\u05d5\u05e1\u05d8", "\u05e1\u05e4\u05d8\u05de\u05d1\u05e8", "\u05d0\u05d5\u05e7\u05d8\u05d5\u05d1\u05e8", "\u05e0\u05d5\u05d1\u05de\u05d1\u05e8", "\u05d3\u05e6\u05de\u05d1\u05e8"}}; + private static final String[][] SHORT_MONTHS = {{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}, {"ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sept", "oct", "nov", "dic"}, {"\u05d9\u05e0\u05d5\u05d0\u05e8", "\u05e4\u05d1\u05e8\u05d5\u05d0\u05e8", "\u05de\u05e8\u05e5", "\u05d0\u05e4\u05e8\u05d9\u05dc", "\u05de\u05d0\u05d9", "\u05d9\u05d5\u05e0\u05d9", "\u05d9\u05d5\u05dc\u05d9", "\u05d0\u05d5\u05d2\u05d5\u05e1\u05d8", "\u05e1\u05e4\u05d8\u05de\u05d1\u05e8", "\u05d0\u05d5\u05e7\u05d8\u05d5\u05d1\u05e8", "\u05e0\u05d5\u05d1\u05de\u05d1\u05e8", "\u05d3\u05e6\u05de\u05d1\u05e8"}}; + /** Sunday first. */ + private static final String[][] WEEKDAYS = {{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, {"domingo", "lunes", "martes", "mi\u00e9rcoles", "jueves", "viernes", "s\u00e1bado"}, {"\u05d9\u05d5\u05dd \u05e8\u05d0\u05e9\u05d5\u05df", "\u05d9\u05d5\u05dd \u05e9\u05e0\u05d9", "\u05d9\u05d5\u05dd \u05e9\u05dc\u05d9\u05e9\u05d9", "\u05d9\u05d5\u05dd \u05e8\u05d1\u05d9\u05e2\u05d9", "\u05d9\u05d5\u05dd \u05d7\u05de\u05d9\u05e9\u05d9", "\u05d9\u05d5\u05dd \u05e9\u05d9\u05e9\u05d9", "\u05d9\u05d5\u05dd \u05e9\u05d1\u05ea"}}; + private static final String[][] SHORT_WEEKDAYS = {{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}, {"dom", "lun", "mar", "mi\u00e9", "jue", "vie", "s\u00e1b"}, {"\u05d9\u05d5\u05dd \u05d0\u05f3", "\u05d9\u05d5\u05dd \u05d1\u05f3", "\u05d9\u05d5\u05dd \u05d2\u05f3", "\u05d9\u05d5\u05dd \u05d3\u05f3", "\u05d9\u05d5\u05dd \u05d4\u05f3", "\u05d9\u05d5\u05dd \u05d5\u05f3", "\u05e9\u05d1\u05ea"}}; + private static final String[][] NARROW_WEEKDAYS = {{"S", "M", "T", "W", "T", "F", "S"}, {"D", "L", "M", "X", "J", "V", "S"}, {"\u05d0", "\u05d1", "\u05d2", "\u05d3", "\u05d4", "\u05d5", "\u05e9"}}; + private static final long[] FIRST_DAY = {0, 1, 0}; + + /** Dart's weekday runs 1 (Monday) to 7 (Sunday); the tables start on Sunday. */ + private static int sundayFirst(dart.core.DateTime d) { + return (int) (d.weekday() % 7); + } + + private static int month0(dart.core.DateTime d) { + return (int) d.month() - 1; + } + + /** Flutter's {@code formatMediumDate}: weekday, month and day without the year. */ + public String formatMediumDate(dart.core.DateTime d) { + String wd = SHORT_WEEKDAYS[lang][sundayFirst(d)]; + String m = SHORT_MONTHS[lang][month0(d)]; + switch (lang) { + case 1: + return wd + ", " + d.day() + " " + m; + case 2: + return wd + ", " + d.day() + " \u05d1" + m; + default: + return wd + ", " + m + " " + d.day(); + } + } + + /** Flutter's {@code formatFullDate}: weekday, day, month and year in full. */ + public String formatFullDate(dart.core.DateTime d) { + String wd = WEEKDAYS[lang][sundayFirst(d)]; + String m = MONTHS[lang][month0(d)]; + switch (lang) { + case 1: + return wd + ", " + d.day() + " de " + m + " de " + d.year(); + case 2: + return wd + ", " + d.day() + " \u05d1" + m + " " + d.year(); + default: + return wd + ", " + m + " " + d.day() + ", " + d.year(); + } + } + + /** Flutter's {@code formatMonthYear}. */ + public String formatMonthYear(dart.core.DateTime d) { + String m = MONTHS[lang][month0(d)]; + return lang == 1 ? m + " de " + d.year() : m + " " + d.year(); + } + + /** Flutter's {@code narrowWeekdays}: one-letter names, Sunday first whatever the locale. */ + public dart.core.DartList narrowWeekdays() { + dart.core.DartList out = new dart.core.DartList(); + for (String n : NARROW_WEEKDAYS[lang]) { + out.add(n); + } + return out; + } + + /** Flutter's {@code firstDayOfWeekIndex}: 0 is Sunday. */ + public long firstDayOfWeekIndex() { + return FIRST_DAY[lang]; } } diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AppBarRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AppBarRenderElement.java index 188d2c27a2f..395a963f12f 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AppBarRenderElement.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AppBarRenderElement.java @@ -574,6 +574,15 @@ protected Size performLayout(BoxConstraints constraints) { } setChildOffset(title, tx, topInset + centreY(titleSize, barHeight)); } + if (isRtl()) { + // Leading at the right, the actions flush to the left, the title beside the + // leading -- the whole row mirrored, as Flutter's NavigationToolbar does. + mirrorChildX(leading, width); + for (int i = 0; i < actions.size(); i++) { + mirrorChildX(actions.get(i), width); + } + mirrorChildX(title, width); + } return constraints.constrain(new Size(width, totalHeight)); } diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/LicensePage.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/LicensePage.java index 93d2c2e8cb2..3870777863a 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/LicensePage.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/LicensePage.java @@ -91,7 +91,8 @@ public Widget build(BuildContext context) { padded.child(body); AppBar bar = new AppBar(); - bar.title(new com.codename1.flutter.widgets.Text("Licenses")); + bar.title(new com.codename1.flutter.widgets.Text( + com.codename1.flutter.l10n.MaterialLocalizations.of(context).licensesPageTitle())); Scaffold page = new Scaffold(); page.appBar(bar); page.body(padded); diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ListTileRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ListTileRenderElement.java index 9a25e45ab2f..52ce0df1aca 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ListTileRenderElement.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ListTileRenderElement.java @@ -194,6 +194,13 @@ protected Size performLayout(BoxConstraints constraints) { setChildOffset(trailing, width - hpad - trailingSize.width(), (height - trailingSize.height()) / 2); } + if (isRtl()) { + // Leading at the right edge, trailing at the left, text right of the middle. + mirrorChildX(leading, width); + mirrorChildX(title, width); + mirrorChildX(subtitle, width); + mirrorChildX(trailing, width); + } if (overlay != null) { overlay.layout(BoxConstraints.tight(width, height)); setChildOffset(overlay, 0, 0); diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/LaunchMode.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/LaunchMode.java new file mode 100644 index 00000000000..ff85a7ea97f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/LaunchMode.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +/** url_launcher's {@code LaunchMode}. */ +public enum LaunchMode { + platformDefault, inAppWebView, inAppBrowserView, externalApplication, externalNonBrowserApplication +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/UrlLauncher.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/UrlLauncher.java index 8855ad8a39d..5559f9f9713 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/UrlLauncher.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/UrlLauncher.java @@ -36,8 +36,16 @@ * *

Each returns an already-completed {@link Future} so a non-awaited call * transpiles and runs to completion; {@code launchUrl} additionally fires the - * native open. The {@code mode}/{@code webOnlyWindowName} options are accepted - * for API shape.

+ * native open.

+ * + *

{@code LaunchMode.inAppWebView} and {@code inAppBrowserView} open an http(s) + * URL in a {@link com.codename1.ui.BrowserComponent} on a form of its own, whose + * back command returns to the previous form; Codename One has no in-app browser + * tab, so both get the same web view. Any other scheme in those modes, and + * {@code platformDefault}, {@code externalApplication} and + * {@code externalNonBrowserApplication}, go to {@link Display#execute(String)}: + * the core cannot force the OS to skip the browser for the last one. + * {@code webOnlyWindowName} is accepted and ignored.

*/ public final class UrlLauncher { @@ -60,6 +68,10 @@ public static Future launchUrlString(String urlString, Object mode, Obj return Future.value(false); } final Display d = Display.getInstance(); + if ((mode == LaunchMode.inAppWebView || mode == LaunchMode.inAppBrowserView) && isWeb(urlString)) { + openInApp(urlString); + return Future.value(true); + } return Future.value(launch(urlString, new Funcs.Func1() { @Override public Boolean call(String u) { @@ -73,6 +85,37 @@ public void call(String u) { })); } + static boolean isWeb(String u) { + return u.regionMatches(true, 0, "http:", 0, 5) || u.regionMatches(true, 0, "https:", 0, 6); + } + + private static void openInApp(final String url) { + Runnable r = new Runnable() { + @Override + public void run() { + final com.codename1.ui.Form back = Display.getInstance().getCurrent(); + com.codename1.ui.Form f = new com.codename1.ui.Form(new com.codename1.ui.layouts.BorderLayout()); + com.codename1.ui.BrowserComponent b = new com.codename1.ui.BrowserComponent(); + b.setURL(url); + f.add(com.codename1.ui.layouts.BorderLayout.CENTER, b); + if (back != null) { + f.setBackCommand(new com.codename1.ui.Command("Back") { + @Override + public void actionPerformed(com.codename1.ui.events.ActionEvent evt) { + back.showBack(); + } + }); + } + f.show(); + } + }; + if (Display.getInstance().isEdt()) { + r.run(); + } else { + Display.getInstance().callSerially(r); + } + } + /** * {@code launchUrl} answers true only when the URL was handed off. Display.execute * returns nothing and a port may decline a URL silently (an unhandled custom diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AlignRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AlignRenderElement.java index d05ad06d556..3c40e47c640 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AlignRenderElement.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AlignRenderElement.java @@ -85,7 +85,8 @@ protected Size performLayout(BoxConstraints constraints) { Size self = constraints.constrain(new Size(w, h)); Alignment a = alignment(); setChildOffset(child, - Alignment.along(a.x(), self.width(), cs.width()), + Alignment.along(com.codename1.flutter.AlignmentDirectional.xFor(a, isRtl()), + self.width(), cs.width()), Alignment.along(a.y(), self.height(), cs.height())); return self; } diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FlexRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FlexRenderElement.java index c15b01a948d..b926d039c2f 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FlexRenderElement.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FlexRenderElement.java @@ -207,7 +207,9 @@ protected Size performLayout(BoxConstraints constraints) { break; } - // Position children. + // Position children. Under right-to-left text a Row runs from the right edge (so + // start and end swap too), and a Column's cross-axis start is its right edge. + boolean rtl = isRtl(); double mainPos = leading; for (RenderElement child : renderChildren) { Size cs = child.size(); @@ -227,9 +229,9 @@ protected Size performLayout(BoxConstraints constraints) { break; } if (vertical) { - setChildOffset(child, crossPos, mainPos); + setChildOffset(child, rtl ? crossSize - childCross - crossPos : crossPos, mainPos); } else { - setChildOffset(child, mainPos, crossPos); + setChildOffset(child, rtl ? mainSize - mainPos - mainOf(cs, false) : mainPos, crossPos); } mainPos += mainOf(cs, vertical) + between; } diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IconRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IconRenderElement.java index 22416332ad3..0003f9c401f 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IconRenderElement.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IconRenderElement.java @@ -133,11 +133,11 @@ private void applyIcon(Label l) { s.setBgTransparency(0); try { long g0 = System.currentTimeMillis(); - l.setIcon(FontImage.createMaterial(icon().getIcon().codePoint(), s, + l.setIcon(FontImage.createMaterial(glyph(), s, Dp.mm(sizeLp(themed)))); glyphMs += System.currentTimeMillis() - g0; glyphCount++; - GLYPHS.add(icon().getIcon().codePoint() + "/" + (int) sizeLp(themed) + GLYPHS.add(glyph() + "/" + (int) sizeLp(themed) + "/" + (fg == null ? -1 : fg.rgb())); } catch (Exception err) { // headless or missing icon font: layout still reserves the box @@ -149,4 +149,45 @@ protected Size performLayout(BoxConstraints constraints) { double px = Dp.px(sizeLp()); return constraints.constrain(new Size(px, px)); } + + /** + * The glyph to draw. Under right-to-left text a directional arrow or chevron is drawn + * as its mirror image -- Flutter's {@code matchTextDirection}, which the Material + * icons that point along the reading direction set -- by swapping in the glyph that + * points the other way. + */ + private char glyph() { + char c = icon().getIcon().codePoint(); + if (!isRtl()) { + return c; + } + char[][] pairs = mirroredGlyphs(); + for (char[] p : pairs) { + if (p[0] == c) { + return p[1]; + } + if (p[1] == c) { + return p[0]; + } + } + return c; + } + + private static char[][] mirrored; + + private static char[][] mirroredGlyphs() { + if (mirrored == null) { + mirrored = new char[][] { + {com.codename1.flutter.Icons.arrow_back().codePoint(), + com.codename1.flutter.Icons.arrow_forward().codePoint()}, + {com.codename1.flutter.Icons.arrow_back_ios().codePoint(), + com.codename1.flutter.Icons.arrow_forward_ios().codePoint()}, + {com.codename1.flutter.Icons.chevron_left().codePoint(), + com.codename1.flutter.Icons.chevron_right().codePoint()}, + {com.codename1.flutter.Icons.keyboard_arrow_left().codePoint(), + com.codename1.flutter.Icons.keyboard_arrow_right().codePoint()}, + }; + } + return mirrored; + } } diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Image.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Image.java index 60bd9ce0b1c..1df74eae7d9 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Image.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Image.java @@ -282,6 +282,17 @@ private String decodeHintSuffix() { return w == null && h == null ? "" : "@" + w + "x" + h; } + /// Sets the decode-width hint (raw pixels), for Java-side wrappers such as + /// CachedNetworkImage(memCacheWidth:). + public void cacheWidth(Long v) { + cacheWidth = v; + } + + /// Sets the decode-height hint (raw pixels). + public void cacheHeight(Long v) { + cacheHeight = v; + } + /// Request headers for a network image, or null. public java.util.Map getHeaders() { return headers; diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RichTextRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RichTextRenderElement.java index e34e145a14f..c1245ff2e20 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RichTextRenderElement.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RichTextRenderElement.java @@ -92,13 +92,17 @@ private void applyContent(RichTextComponent c) { private int cn1Align() { TextAlign a = richText().getTextAlign(); + boolean rtl = isRtl(); if (a == null) { - return Component.LEFT; + a = TextAlign.start; } switch (a) { case right: - case end: return Component.RIGHT; + case end: + return rtl ? Component.LEFT : Component.RIGHT; + case start: + return rtl ? Component.RIGHT : Component.LEFT; case center: return Component.CENTER; default: diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/StackRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/StackRenderElement.java index d63a2418461..62395ab7eb5 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/StackRenderElement.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/StackRenderElement.java @@ -70,7 +70,8 @@ private Stack stack() { private Alignment alignment() { Alignment a = stack().getAlignment(); - return a == null ? Alignment.topLeft : a; + // Flutter's default is AlignmentDirectional.topStart: top-right under RTL. + return a == null ? com.codename1.flutter.AlignmentDirectional.topStart : a; } @Override @@ -130,7 +131,7 @@ protected Size performLayout(BoxConstraints constraints) { } else { Size cs = kid.size(); setChildOffset(kid, - Alignment.along(a.x(), self.width(), cs.width()), + Alignment.along(com.codename1.flutter.AlignmentDirectional.xFor(a, isRtl()), self.width(), cs.width()), Alignment.along(a.y(), self.height(), cs.height())); } } @@ -189,7 +190,7 @@ private void placePositioned(PositionedRenderElement kid, Size self, Alignment a } else if (right != null) { x = self.width() - right - cs.width(); } else { - x = Alignment.along(a.x(), self.width(), cs.width()); + x = Alignment.along(com.codename1.flutter.AlignmentDirectional.xFor(a, isRtl()), self.width(), cs.width()); } double y; if (top != null) { diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TextRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TextRenderElement.java index 5657a3d1099..6ddfd89429e 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TextRenderElement.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TextRenderElement.java @@ -191,13 +191,23 @@ private void applyStyle(Label l) { } } TextAlign a = text().getTextAlign(); + // Flutter's default is TextAlign.start, the right edge under right-to-left text. + boolean rtl = isRtl(); + if (a == null && rtl) { + a = TextAlign.start; + } if (a != null) { int cn1Align; switch (a) { case right: - case end: cn1Align = Component.RIGHT; break; + case end: + cn1Align = rtl ? Component.LEFT : Component.RIGHT; + break; + case start: + cn1Align = rtl ? Component.RIGHT : Component.LEFT; + break; case center: cn1Align = Component.CENTER; break; diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WrapRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WrapRenderElement.java index 5a87da4dba9..a3084b83006 100644 --- a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WrapRenderElement.java +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WrapRenderElement.java @@ -160,6 +160,13 @@ protected Size performLayout(BoxConstraints constraints) { } crossPos += runCr + runSpacing; } + if (isRtl()) { + // A horizontal wrap fills each run from the right; a vertical one stacks its + // runs from the right. Both are the left-to-right placement mirrored. + for (RenderElement kid : renderChildren()) { + mirrorChildX(kid, self.width()); + } + } return self; } diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/cachednetworkimage/CachedNetworkImage.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/cachednetworkimage/CachedNetworkImage.java index f2e229f55fd..612ea706f1e 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/cachednetworkimage/CachedNetworkImage.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/cachednetworkimage/CachedNetworkImage.java @@ -23,25 +23,56 @@ */ package com.codename1.flutter.plugins.cachednetworkimage; +import java.util.HashMap; +import java.util.Map; + import com.codename1.flutter.BoxFit; import com.codename1.flutter.BuildContext; -import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.MemoryImage; +import com.codename1.flutter.State; +import com.codename1.flutter.StatefulWidget; import com.codename1.flutter.Widget; import com.codename1.flutter.widgets.Image; +import com.codename1.flutter.widgets.SizedBox; +import com.codename1.io.ConnectionRequest; +import com.codename1.io.NetworkEvent; +import com.codename1.io.NetworkManager; +import com.codename1.ui.CN; +import com.codename1.ui.Display; +import com.codename1.ui.events.ActionListener; +import dart.io.SocketException; import dart.runtime.Funcs; +import dart.typed_data.Uint8List; /** - * package:cached_network_image's {@code CachedNetworkImage}. {@code Image.network} - * already caches what it downloads, so this is that widget under the plugin's - * parameter names. The placeholder and error builders are accepted; the network - * image shows its own placeholder while loading. + * package:cached_network_image's {@code CachedNetworkImage}. The widget fetches the + * bytes itself (a {@code ConnectionRequest} on the network manager, with + * {@code httpHeaders}) so it can build what the plugin builds: while loading, the + * {@code progressIndicatorBuilder} (with a {@link DownloadProgress}) or else the + * {@code placeholder}; on a failed connection, an HTTP error status, an empty body or + * no display to load on, the {@code errorWidget} (with the error); once the bytes are + * here, the picture, decoded as {@code Image.memory} with width, height, fit and the + * {@code memCacheWidth}/{@code memCacheHeight} decode-size hints. Downloaded bytes are + * kept in an in-memory map keyed by {@code cacheKey} (else the URL), so a repeat is + * built straight away; nothing is written to disk. {@code fadeInDuration} is accepted + * and not applied. With no builder given, the loading and error states are empty boxes. */ -public class CachedNetworkImage extends StatelessWidget { - private String imageUrl; - private Double width; - private Double height; - private BoxFit fit; +public class CachedNetworkImage extends StatefulWidget { + /** Bytes of finished downloads, by cache key. */ + private static final Map CACHE = new HashMap(); + + String imageUrl; + Double width; + Double height; + BoxFit fit; + Map httpHeaders; + Long memCacheWidth; + Long memCacheHeight; + String cacheKey; + Funcs.Func2 placeholder; + Funcs.Func3 errorWidget; + Funcs.Func3 progressIndicatorBuilder; public CachedNetworkImage() { } @@ -54,8 +85,6 @@ public void width(Double v) { this.width = v; } - private java.util.Map httpHeaders; - public void height(Double v) { this.height = v; } @@ -64,38 +93,266 @@ public void fit(BoxFit v) { this.fit = v; } - /** Accepted for source compatibility; see the class comment. */ + /// Built while the image loads, when there is no progressIndicatorBuilder. public void placeholder(Funcs.Func2 v) { + this.placeholder = v; } - /** Accepted for source compatibility; see the class comment. */ + /// Built when the load fails, with the error. public void errorWidget(Funcs.Func3 v) { + this.errorWidget = v; } /// Sent with the image request, so an endpoint behind Authorization or a cookie /// serves the image instead of refusing a bare request. public void httpHeaders(Object v) { - this.httpHeaders = v instanceof java.util.Map ? (java.util.Map) v : null; + this.httpHeaders = v instanceof Map ? (Map) v : null; } - /** Accepted for source compatibility. */ public void memCacheWidth(Object v) { + memCacheWidth = v instanceof Number ? Long.valueOf(((Number) v).longValue()) : null; } - /** Accepted for source compatibility. */ public void memCacheHeight(Object v) { + memCacheHeight = v instanceof Number ? Long.valueOf(((Number) v).longValue()) : null; + } + + /// Built while the image loads, in preference to the placeholder. + public void progressIndicatorBuilder(Funcs.Func3 v) { + this.progressIndicatorBuilder = v; } - /** Accepted for source compatibility. */ + /** Accepted for source compatibility; see the class comment. */ public void fadeInDuration(Object v) { } - /** Accepted for source compatibility. */ + /// Keys the in-memory bytes instead of the URL. public void cacheKey(String v) { + this.cacheKey = v; + } + + String key() { + return cacheKey != null ? cacheKey : (imageUrl == null ? "" : imageUrl); } @Override - public Widget build(BuildContext context) { - return Image.networkWithHeaders(imageUrl == null ? "" : imageUrl, null, width, height, fit, httpHeaders); + public State createState() { + return new Loader(); + } + + /// What can be answered without the network: the cached bytes (as byte[]), or the + /// error a load that cannot start fails with; null when a download is needed. + private Object immediate() { + byte[] cached = CACHE.get(key()); + if (cached != null) { + return cached; + } + if (imageUrl == null || imageUrl.length() == 0) { + return new IllegalArgumentException("CachedNetworkImage has no imageUrl"); + } + if (!Display.isInitialized()) { + // nothing to download with; the error path is the answer a caller gets + return new SocketException("No network: Codename One is not initialized"); + } + return null; + } + + /// The widget for one state: bytes, else error, else the loading builders. + private Widget content(BuildContext context, byte[] bytes, Object error, long received, Long total) { + String url = imageUrl == null ? "" : imageUrl; + if (bytes != null) { + Image i = new Image(); + i.image(new MemoryImage(Uint8List.fromBytes(bytes))); + i.width(width); + i.height(height); + i.fit(fit); + i.cacheWidth(memCacheWidth); + i.cacheHeight(memCacheHeight); + return i; + } + Widget built = null; + if (error != null) { + if (errorWidget != null) { + built = errorWidget.call(context, url, error); + } + } else if (progressIndicatorBuilder != null) { + built = progressIndicatorBuilder.call(context, url, new DownloadProgress(url, total, received)); + } else if (placeholder != null) { + built = placeholder.call(context, url); + } + return built != null ? built : SizedBox.shrink(null, null); + } + + /// What the widget builds the moment it is mounted, without mounting it: the + /// cached picture, the error widget when the load cannot start (no display, no + /// URL), else the loading widget. + public Widget buildOnce(BuildContext context) { + Object now = immediate(); + if (now instanceof byte[]) { + return content(context, (byte[]) now, null, 0, null); + } + return content(context, null, now, 0, null); + } + + /// The widget built while a download is {@code downloaded} bytes into {@code total}. + public Widget buildLoading(BuildContext context, long downloaded, Long total) { + return content(context, null, null, downloaded, total); + } + + /// Stores bytes as a finished download of this widget's key. + public void seedCache(dart.typed_data.Uint8List data) { + CACHE.put(key(), data.toBytes()); + } + + private static final class Loader extends State { + private String loadedKey; + private byte[] bytes; + private Object error; + private long received; + private Long total; + private ConnectionRequest request; + private ActionListener progressListener; + + @Override + public void initState() { + start(); + } + + @Override + public void didUpdateWidget(CachedNetworkImage old) { + if (!widget().key().equals(loadedKey)) { + stop(); + start(); + } + } + + @Override + public void dispose() { + stop(); + } + + private void stop() { + if (progressListener != null) { + NetworkManager.getInstance().removeProgressListener(progressListener); + progressListener = null; + } + if (request != null) { + request.kill(); + request = null; + } + } + + private void start() { + final CachedNetworkImage w = widget(); + final String key = w.key(); + loadedKey = key; + bytes = null; + error = null; + received = 0; + total = null; + final String url = w.imageUrl; + Object now = w.immediate(); + if (now instanceof byte[]) { + bytes = (byte[]) now; + return; + } + if (now != null) { + error = now; + return; + } + final ConnectionRequest req = new ConnectionRequest(url, false); + req.setDuplicateSupported(true); + req.setFailSilently(true); + if (w.httpHeaders != null) { + for (Map.Entry e : w.httpHeaders.entrySet()) { + if (e.getKey() != null && e.getValue() != null) { + req.addRequestHeader(String.valueOf(e.getKey()), String.valueOf(e.getValue())); + } + } + } + request = req; + progressListener = new ActionListener() { + @Override + public void actionPerformed(NetworkEvent evt) { + if (evt.getConnectionRequest() != req + || evt.getProgressType() != NetworkEvent.PROGRESS_TYPE_INPUT) { + return; + } + final long got = evt.getSentReceived(); + final long len = evt.getLength(); + CN.callSerially(new Runnable() { + @Override + public void run() { + if (request != req || !mounted()) { + return; + } + setState(new Funcs.VoidFunc0() { + @Override + public void call() { + received = got; + total = len > 0 ? Long.valueOf(len) : null; + } + }); + } + }); + } + }; + NetworkManager.getInstance().addProgressListener(progressListener); + req.addResponseListener(new ActionListener() { + @Override + public void actionPerformed(NetworkEvent evt) { + byte[] data = req.getResponseData(); + if (data == null || data.length == 0) { + finish(req, key, null, new SocketException("Empty response from " + url)); + } else { + finish(req, key, data, null); + } + } + }); + ActionListener failed = new ActionListener() { + @Override + public void actionPerformed(NetworkEvent evt) { + Object cause = evt.getError(); + String msg = evt.getResponseCode() > 0 + ? "HTTP request failed, statusCode: " + evt.getResponseCode() + ", " + url + : "Connection failed: " + url; + finish(req, key, null, cause != null ? cause : new SocketException(msg)); + } + }; + req.addExceptionListener(failed); + req.addResponseCodeListener(failed); + NetworkManager.getInstance().addToQueue(req); + } + + private void finish(final ConnectionRequest req, final String key, final byte[] data, final Object err) { + CN.callSerially(new Runnable() { + @Override + public void run() { + if (request != req || !mounted()) { + return; + } + if (progressListener != null) { + NetworkManager.getInstance().removeProgressListener(progressListener); + progressListener = null; + } + request = null; + if (data != null) { + CACHE.put(key, data); + } + setState(new Funcs.VoidFunc0() { + @Override + public void call() { + bytes = data; + error = err; + } + }); + } + }); + } + + @Override + public Widget build(BuildContext context) { + return widget().content(context, bytes, error, received, total); + } } } diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/cachednetworkimage/DownloadProgress.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/cachednetworkimage/DownloadProgress.java new file mode 100644 index 00000000000..480e0e7c574 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/cachednetworkimage/DownloadProgress.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.cachednetworkimage; + +/** + * cached_network_image's {@code DownloadProgress}, handed to + * {@code progressIndicatorBuilder}: the URL, the bytes received so far and the total + * when the server announced one. + */ +public class DownloadProgress { + private final String originalUrl; + private final Long totalSize; + private final long downloaded; + + public DownloadProgress(String originalUrl, Long totalSize, long downloaded) { + this.originalUrl = originalUrl; + this.totalSize = totalSize; + this.downloaded = downloaded; + } + + public String originalUrl() { + return originalUrl; + } + + public Long totalSize() { + return totalSize; + } + + public long downloaded() { + return downloaded; + } + + /** Received over total, or null while the total is unknown. */ + public Double progress() { + if (totalSize == null || totalSize.longValue() <= 0) { + return null; + } + return Double.valueOf(Math.min(1.0, (double) downloaded / totalSize.longValue())); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/CancelToken.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/CancelToken.java index a3107ff5532..a818b14d061 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/CancelToken.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/CancelToken.java @@ -38,6 +38,11 @@ public class CancelToken { public CancelToken() { } + /// `CancelToken.isCancel(e)`: whether a DioException is a cancellation. + public static boolean isCancel(Object e) { + return e instanceof DioException && ((DioException) e).type() == DioExceptionType.cancel; + } + public boolean isCancelled() { return cancelled; } diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/Dio.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/Dio.java index deeeb022c67..942b736ec04 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/Dio.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/Dio.java @@ -49,7 +49,9 @@ /// completes with what comes out of the last of them. A JSON response (by content /// type, for the default `ResponseType.json`) is decoded into Dart maps and lists. /// With no Display (headless) there is no network, and every request fails with a -/// `connectionError`, so an app's offline path is what runs. +/// `connectionError`, so an app's offline path is what runs. `onSendProgress` is +/// called per 8 KB chunk of the encoded body and `onReceiveProgress` per chunk read, +/// with the Content-Length as total (-1 when absent), on the EDT. public class Dio { private BaseOptions options; @@ -104,39 +106,81 @@ public void close(Boolean force) { } public Future> get(String path, Object data, Map queryParameters, - Options options, CancelToken cancelToken, Object onReceiveProgress) { - return send("GET", path, data, queryParameters, options, cancelToken); + Options options, CancelToken cancelToken, dart.runtime.Funcs.VoidFunc2 onReceiveProgress) { + return send("GET", path, data, queryParameters, options, cancelToken, null, onReceiveProgress, null); } public Future> post(String path, Object data, Map queryParameters, - Options options, CancelToken cancelToken, Object onSendProgress, Object onReceiveProgress) { - return send("POST", path, data, queryParameters, options, cancelToken); + Options options, CancelToken cancelToken, dart.runtime.Funcs.VoidFunc2 onSendProgress, dart.runtime.Funcs.VoidFunc2 onReceiveProgress) { + return send("POST", path, data, queryParameters, options, cancelToken, onSendProgress, onReceiveProgress, null); } public Future> put(String path, Object data, Map queryParameters, - Options options, CancelToken cancelToken, Object onSendProgress, Object onReceiveProgress) { - return send("PUT", path, data, queryParameters, options, cancelToken); + Options options, CancelToken cancelToken, dart.runtime.Funcs.VoidFunc2 onSendProgress, dart.runtime.Funcs.VoidFunc2 onReceiveProgress) { + return send("PUT", path, data, queryParameters, options, cancelToken, onSendProgress, onReceiveProgress, null); } public Future> patch(String path, Object data, Map queryParameters, - Options options, CancelToken cancelToken, Object onSendProgress, Object onReceiveProgress) { - return send("PATCH", path, data, queryParameters, options, cancelToken); + Options options, CancelToken cancelToken, dart.runtime.Funcs.VoidFunc2 onSendProgress, dart.runtime.Funcs.VoidFunc2 onReceiveProgress) { + return send("PATCH", path, data, queryParameters, options, cancelToken, onSendProgress, onReceiveProgress, null); } public Future> delete(String path, Object data, Map queryParameters, Options options, CancelToken cancelToken) { - return send("DELETE", path, data, queryParameters, options, cancelToken); + return send("DELETE", path, data, queryParameters, options, cancelToken, null, null, null); } public Future> head(String path, Object data, Map queryParameters, Options options, CancelToken cancelToken) { - return send("HEAD", path, data, queryParameters, options, cancelToken); + return send("HEAD", path, data, queryParameters, options, cancelToken, null, null, null); } public Future> request(String path, Object data, Map queryParameters, - CancelToken cancelToken, Options options, Object onSendProgress, Object onReceiveProgress) { + CancelToken cancelToken, Options options, dart.runtime.Funcs.VoidFunc2 onSendProgress, dart.runtime.Funcs.VoidFunc2 onReceiveProgress) { String method = options != null && options.method != null ? options.method : null; - return send(method, path, data, queryParameters, options, cancelToken); + return send(method, path, data, queryParameters, options, cancelToken, onSendProgress, onReceiveProgress, null); + } + + /// `dio.download(urlPath, savePath, ...)`: a GET whose body is written to + /// `savePath` (a String path, through dart:io's File) instead of decoded; the + /// response's data is null. A function `savePath` (dio's `(headers) => path`) + /// is not supported and fails the request. `lengthHeader` is accepted; the + /// total passed to `onReceiveProgress` is the response's Content-Length, or -1. + @SuppressWarnings({"unchecked", "rawtypes"}) + public Future> download(String urlPath, Object savePath, dart.runtime.Funcs.VoidFunc2 onReceiveProgress, + Map queryParameters, CancelToken cancelToken, Boolean deleteOnError, + Object fileAccessMode, String lengthHeader, Object data, Options options) { + if (!(savePath instanceof String)) { + RequestOptions ro = compose("GET", urlPath, data, queryParameters, options); + return Future.error(DioException.of(ro, DioExceptionType.unknown, + "download: only a String savePath is supported", null, null)); + } + return send(options != null && options.method != null ? options.method : "GET", urlPath, data, + queryParameters, options, cancelToken, null, onReceiveProgress, (String) savePath); + } + + /// Calls a Dart `ProgressCallback(count, total)`, whichever arity class the + /// closure compiled to; on the EDT, where Dart code runs. + @SuppressWarnings({"unchecked", "rawtypes"}) + static void progress(final Object cb, final long count, final long total) { + if (cb == null) { + return; + } + Runnable r = new Runnable() { + @Override + public void run() { + if (cb instanceof dart.runtime.Funcs.VoidFunc2) { + ((dart.runtime.Funcs.VoidFunc2) cb).call(Long.valueOf(count), Long.valueOf(total)); + } else if (cb instanceof dart.runtime.Funcs.Func2) { + ((dart.runtime.Funcs.Func2) cb).call(Long.valueOf(count), Long.valueOf(total)); + } + } + }; + if (Display.isInitialized() && !Display.getInstance().isEdt()) { + Display.getInstance().callSerially(r); + } else { + r.run(); + } } /// `dio.fetch(requestOptions)`: re-runs a fully built request -- the usual way @@ -155,9 +199,16 @@ public Future> fetch(RequestOptions ro) { @SuppressWarnings({"unchecked", "rawtypes"}) private Future> send(String method, String path, Object data, - Map query, Options opts, CancelToken cancelToken) { + Map query, Options opts, CancelToken cancelToken, + dart.runtime.Funcs.VoidFunc2 onSendProgress, dart.runtime.Funcs.VoidFunc2 onReceiveProgress, String savePath) { RequestOptions ro = compose(method, path, data, query, opts); ro.cancelToken = cancelToken; + ro.onSendProgress = onSendProgress; + ro.onReceiveProgress = onReceiveProgress; + ro.savePath = savePath; + if (savePath != null) { + ro.responseType = ResponseType.bytes; + } return fetch(ro); } @@ -380,7 +431,13 @@ protected void readHeaders(Object connection) throws IOException { @Override protected void buildRequestBody(OutputStream os) throws IOException { if (payload != null) { - os.write(payload); + int off = 0; + while (off < payload.length) { + int n = Math.min(8192, payload.length - off); + os.write(payload, off, n); + off += n; + progress(ro.onSendProgress, off, payload.length); + } } } @@ -388,8 +445,18 @@ protected void buildRequestBody(OutputStream os) throws IOException { protected void readResponse(InputStream input) throws IOException { byte[] buf = new byte[8192]; int n; + long total = -1; + String len = responseHeaders.value("content-length"); + if (len != null) { + try { + total = Long.parseLong(len.trim()); + } catch (NumberFormatException e) { + total = -1; + } + } while ((n = input.read(buf)) > 0) { received.write(buf, 0, n); + progress(ro.onReceiveProgress, received.size(), total); } } @@ -454,6 +521,15 @@ protected void postResponse() { + "to throw for this status code.", null, r)); return; } + if (ro.savePath != null) { + try { + new dart.io.File(ro.savePath).writeAsBytesSync(Uint8List.fromBytes(received.toByteArray())); + } catch (RuntimeException e) { + call.error(0, DioException.of(ro, DioExceptionType.unknown, e.getMessage(), e, r)); + return; + } + r.data = null; + } call.response(0, r); } }; diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/FormData.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/FormData.java index 7ede938ac28..f0ccb6e4365 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/FormData.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/FormData.java @@ -66,8 +66,58 @@ private void addValue(String key, Object v) { } } - /// `formData.fields.add(MapEntry(k, v))` and `formData.files.add(MapEntry(k, file))` - /// go through these. + private dart.core.DartList> liveFields; + private dart.core.DartList> liveFiles; + + /// `formData.fields`: the text fields, as a live list -- an app's + /// `formData.fields.add(MapEntry(k, v))` is read back by encode and length. + public dart.core.DartList> fields() { + if (liveFields == null) { + liveFields = new dart.core.DartList>(); + for (String[] f : fields) { + liveFields.add(new MapEntry(f[0], f[1])); + } + } + return liveFields; + } + + /// Folds the live lists, when an app took them, back into the parts. + private void sync() { + if (liveFields != null) { + fields.clear(); + for (MapEntry e : liveFields) { + if (liveFields != null) { + liveFields.add(e); + } else { + fields.add(new String[] {e.key(), e.value()}); + } + } + } + if (liveFiles != null) { + files.clear(); + for (MapEntry e : liveFiles) { + if (liveFiles != null) { + liveFiles.add(e); + } else { + files.add(new Object[] {e.key(), e.value()}); + } + } + } + } + + /// `formData.files`: the file parts, as a live list (see {@link #fields}). + public dart.core.DartList> files() { + if (liveFiles == null) { + liveFiles = new dart.core.DartList>(); + for (Object[] f : files) { + if (f[1] instanceof MultipartFile && f[0] instanceof String) { + liveFiles.add(new MapEntry((String) f[0], (MultipartFile) f[1])); + } + } + } + return liveFiles; + } + public void addField(MapEntry e) { fields.add(new String[] {e.key(), e.value()}); } @@ -81,6 +131,7 @@ String contentType() { } byte[] encode() { + sync(); try { ByteArrayOutputStream out = new ByteArrayOutputStream(); for (String[] f : fields) { @@ -107,6 +158,7 @@ private static void write(ByteArrayOutputStream out, String s) throws IOExceptio } public long length() { + sync(); return encode().length; } } diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/RequestOptions.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/RequestOptions.java index 88f6b2a0e37..962755a6029 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/RequestOptions.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/dio/RequestOptions.java @@ -41,6 +41,10 @@ public class RequestOptions extends OptionsBase { Object data; Duration connectTimeout; CancelToken cancelToken; + /// The Dart `ProgressCallback`s and `download`'s target; set by Dio, not part of dio's API. + Object onSendProgress; + Object onReceiveProgress; + String savePath; public String path() { return path; diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/AndroidProvider.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/AndroidProvider.java new file mode 100644 index 00000000000..009f522941e --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/AndroidProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebase; + +/** firebase_app_check's AndroidProvider; accepted by activate, none attests here. */ +public enum AndroidProvider { + debug, playIntegrity, safetyNet +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/AppleProvider.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/AppleProvider.java new file mode 100644 index 00000000000..1c500ad652b --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/AppleProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebase; + +/** firebase_app_check's AppleProvider; accepted by activate, none attests here. */ +public enum AppleProvider { + debug, deviceCheck, appAttest, appAttestWithDeviceCheckFallback +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/EmailAuthProvider.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/EmailAuthProvider.java index c6a7c4ec511..6447615701e 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/EmailAuthProvider.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/EmailAuthProvider.java @@ -23,12 +23,15 @@ */ package com.codename1.flutter.plugins.firebase; -/** firebase_auth's {@code EmailAuthProvider}. */ +/** + * firebase_auth's {@code EmailAuthProvider}, and firebase_ui_auth's of the same name: an + * instance in a {@code SignInScreen}'s providers asks for the email form. + */ public final class EmailAuthProvider { public static final String PROVIDER_ID = "password"; public static final String EMAIL_PASSWORD_SIGN_IN_METHOD = "password"; - private EmailAuthProvider() { + public EmailAuthProvider() { } public static AuthCredential credential(String email, String password) { diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FirebaseAppCheck.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FirebaseAppCheck.java new file mode 100644 index 00000000000..500b4d57d7f --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FirebaseAppCheck.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebase; + +import com.codename1.io.Log; +import com.codename1.ui.Display; + +import dart.async.Future; +import dart.async.Stream; +import dart.async.StreamController; + +/** + * firebase_app_check. Codename One has no device attestation (Play Integrity, + * DeviceCheck, App Attest, reCAPTCHA), so {@code activate} accepts every provider and + * applies none of them; the debug provider is what yields a token. {@code getToken} + * answers the Display property {@code firebase.appCheck.debugToken} -- a debug token + * registered for the app in the Firebase console -- and null, with a logged warning, + * when it is unset (and always headless). {@code onTokenChange} reports the token when + * {@code activate} first finds one and when a forced refresh reads a different one. + * {@code setTokenAutoRefreshEnabled} is recorded; there is no expiring token to refresh. + */ +public class FirebaseAppCheck { + /** {@code FirebaseAppCheck.instance}. */ + public static final FirebaseAppCheck instance = new FirebaseAppCheck(); + + /** The Display property the debug token is read from. */ + public static final String DEBUG_TOKEN_PROPERTY = "firebase.appCheck.debugToken"; + + private final StreamController tokens = StreamController.broadcast(); + private boolean activated; + private boolean autoRefresh = true; + private boolean warned; + private String last; + + FirebaseAppCheck() { + } + + public static FirebaseAppCheck instanceFor(Object app) { + return instance; + } + + public Future activate(Object webProvider, Object providerWeb, AndroidProvider androidProvider, + AppleProvider appleProvider, Object providerAndroid, Object providerApple) { + activated = true; + String t = debugToken(); + if (t != null && !t.equals(last)) { + last = t; + tokens.add(t); + } + return Future.value(null); + } + + /** Whether activate has run. */ + public boolean isActivated() { + return activated; + } + + public Future getToken(Boolean forceRefresh) { + String t = debugToken(); + if (t == null) { + // Log needs a running implementation, so headless the warning is skipped. + if (!warned && Display.isInitialized()) { + warned = true; + Log.p("firebase_app_check: no attestation provider on this platform; set the Display property " + + DEBUG_TOKEN_PROPERTY + " to a registered debug token"); + } + return Future.value(null); + } + if (Boolean.TRUE.equals(forceRefresh) && !t.equals(last)) { + last = t; + tokens.add(t); + } + return Future.value(t); + } + + public Future getLimitedUseToken() { + String t = debugToken(); + if (t == null) { + return Future.error(new FirebaseException("app-check", "no-token", + "No App Check token: set " + DEBUG_TOKEN_PROPERTY)); + } + return Future.value(t); + } + + public Future setTokenAutoRefreshEnabled(boolean enabled) { + autoRefresh = enabled; + return Future.value(null); + } + + public boolean isTokenAutoRefreshEnabled() { + return autoRefresh; + } + + public Stream onTokenChange() { + return tokens.stream(); + } + + private static String debugToken() { + if (!Display.isInitialized()) { + return null; + } + String t = Display.getInstance().getProperty(DEBUG_TOKEN_PROPERTY, null); + return t == null || t.length() == 0 ? null : t; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FirebaseMessaging.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FirebaseMessaging.java index b58e10dfb88..94435c3bb2f 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FirebaseMessaging.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/FirebaseMessaging.java @@ -36,6 +36,17 @@ * push key once {@code Display.registerPush} has run. Messages reach the Dart streams * through {@link #deliver}, which the app's {@code PushCallback.push} forwards to; * a message that arrives while the app is not running is not replayed. + * + *

A payload that is a JSON object is a data message: its entries are the message's + * {@code data} (a {@code notification} object, or {@code title}/{@code body} entries, + * make its {@code notification}; without them it is data-only and {@code notification} + * is null). Any other payload is the notification body, with the text under + * {@code data['payload']}. The {@code onBackgroundMessage} handler is called for a push + * that arrives while the app is minimized or not yet showing a form; there is no + * background isolate, so it runs on the app's thread before the message is delivered. + * {@code requestPermission} answers {@code provisional} when provisional was asked for + * and {@code authorized} otherwise (Codename One reports no denial); the other + * permission flags are accepted and not applied. */ public class FirebaseMessaging { /** {@code FirebaseMessaging.instance}. */ @@ -105,7 +116,24 @@ public static void pushReceived(String payload) { } } } - deliver(payload, cold || com.codename1.ui.Display.getInstance().isMinimized()); + boolean opened = cold || com.codename1.ui.Display.getInstance().isMinimized(); + if (opened) { + background(payload); + } + deliver(payload, opened); + } + + private static Funcs.Func1 backgroundHandler; + + /// Runs the onBackgroundMessage handler, if one is set, for a push received while + /// the app was in the background. Answers whether a handler ran. + public static boolean background(String payload) { + Funcs.Func1 h = backgroundHandler; + if (h == null) { + return false; + } + h.call(message(payload)); + return true; } private static final java.util.List HELD = new java.util.ArrayList(); @@ -115,6 +143,39 @@ public static void pushReceived(String payload) { private static RemoteMessage message(String payload) { received++; DartMap data = new DartMap(); + String trimmed = payload == null ? "" : payload.trim(); + if (trimmed.startsWith("{")) { + java.util.Map json = null; + try { + json = new com.codename1.io.JSONParser().parseJSON(new java.io.StringReader(trimmed)); + } catch (java.io.IOException e) { + json = null; + } + if (json != null && !json.isEmpty()) { + String title = null; + String body = null; + Object n = json.get("notification"); + if (n instanceof java.util.Map) { + Object t = ((java.util.Map) n).get("title"); + Object b = ((java.util.Map) n).get("body"); + title = t == null ? null : t.toString(); + body = b == null ? null : b.toString(); + } else { + Object t = json.get("title"); + Object b = json.get("body"); + title = t == null ? null : t.toString(); + body = b == null ? null : b.toString(); + } + Object id = json.get("messageId"); + for (java.util.Map.Entry e : json.entrySet()) { + if (!"notification".equals(e.getKey())) { + data.put(e.getKey(), e.getValue()); + } + } + RemoteNotification rn = title == null && body == null ? null : new RemoteNotification(title, body); + return new RemoteMessage(id instanceof String ? (String) id : "cn1-" + received, data, rn); + } + } data.put("payload", payload); return new RemoteMessage("cn1-" + received, data, new RemoteNotification(null, payload)); } @@ -157,7 +218,8 @@ private static void settlePending(String token, String error) { } public static void onBackgroundMessage(Funcs.Func1 handler) { - // background isolates do not exist here; foreground delivery uses onMessage + // No background isolate: the handler runs on the app's thread (see background). + backgroundHandler = handler; } /// Where getToken reads the push key from, installed by the import-generated entry @@ -209,11 +271,16 @@ public Future requestPermission(Boolean alert, Boolean ann if (Display.isInitialized()) { Display.getInstance().registerPush(); } - return Future.value(new NotificationSettings(AuthorizationStatus.authorized)); + lastStatus = Boolean.TRUE.equals(provisional) ? AuthorizationStatus.provisional + : AuthorizationStatus.authorized; + return Future.value(new NotificationSettings(lastStatus)); } + private static AuthorizationStatus lastStatus; + public Future getNotificationSettings() { - return Future.value(new NotificationSettings(AuthorizationStatus.authorized)); + AuthorizationStatus st = lastStatus; + return Future.value(new NotificationSettings(st == null ? AuthorizationStatus.notDetermined : st)); } /** The message whose tap launched the app, once; null afterwards, as on Flutter. */ diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/ReCaptchaV3Provider.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/ReCaptchaV3Provider.java new file mode 100644 index 00000000000..35a71eb354f --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebase/ReCaptchaV3Provider.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebase; + +/** firebase_app_check's web providers (v3 and Enterprise); the site key is kept, unused. */ +public class ReCaptchaV3Provider { + private final String siteKey; + + public ReCaptchaV3Provider(String siteKey) { + this.siteKey = siteKey; + } + + public String siteKey() { + return siteKey; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/AiTransport.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/AiTransport.java new file mode 100644 index 00000000000..19f0cb24f1d --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/AiTransport.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.Map; + +import com.codename1.io.ConnectionRequest; +import com.codename1.io.NetworkManager; +import com.codename1.ui.Display; + +import dart.async.Completer; +import dart.async.Future; + +/** One JSON POST over ConnectionRequest; the reply is decoded, an error reply becomes a FirebaseAIException. */ +final class AiTransport { + private AiTransport() { + } + + static Future> post(String url, final List headers, String json) { + final Completer> done = new Completer>(); + if (!Display.isInitialized()) { + done.completeError(new FirebaseAIException( + "A network error has occurred: Codename One is not initialized")); + return done.future(); + } + final byte[] payload; + try { + payload = json.getBytes("UTF-8"); + } catch (java.io.UnsupportedEncodingException e) { + done.completeError(new FirebaseAIException(String.valueOf(e))); + return done.future(); + } + final ByteArrayOutputStream received = new ByteArrayOutputStream(); + ConnectionRequest req = new ConnectionRequest() { + private Exception failure; + private boolean settled; + + @Override + protected void buildRequestBody(OutputStream os) throws IOException { + os.write(payload); + } + + @Override + protected void readResponse(InputStream input) throws IOException { + byte[] buf = new byte[4096]; + int n; + while ((n = input.read(buf)) > 0) { + received.write(buf, 0, n); + } + } + + @Override + protected void handleErrorResponseCode(int code, String message) { + // the error body carries the server's message; read in postResponse + } + + /// Transport failures reach only this, so they settle here, on the EDT. + @Override + protected void handleException(Exception err) { + failure = err; + if (Display.isInitialized() && !Display.getInstance().isEdt()) { + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + postResponse(); + } + }); + } else { + postResponse(); + } + } + + @Override + protected void postResponse() { + if (settled) { + return; + } + settled = true; + if (failure != null) { + done.completeError(new FirebaseAIException( + "A network error has occurred: " + failure.getMessage())); + return; + } + String body; + try { + body = new String(received.toByteArray(), "UTF-8"); + } catch (java.io.UnsupportedEncodingException e) { + done.completeError(new FirebaseAIException(String.valueOf(e))); + return; + } + try { + done.complete(GenerateContentResponse.parse(body)); + } catch (FirebaseAIException e) { + if (getResponseCode() >= 400 && body.length() == 0) { + done.completeError(new FirebaseAIException("HTTP " + getResponseCode())); + } else { + done.completeError(e); + } + } + } + }; + req.setUrl(url); + req.setPost(true); + req.setHttpMethod("POST"); + req.setContentType("application/json"); + for (String[] h : headers) { + req.addRequestHeader(h[0], h[1]); + } + req.setReadResponseForErrors(true); + req.setDuplicateSupported(true); + NetworkManager.getInstance().addToQueue(req); + return done.future(); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Candidate.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Candidate.java new file mode 100644 index 00000000000..bf5873e04cb --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Candidate.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +/** firebase_ai's {@code Candidate}: one answer the model proposed. */ +public class Candidate { + private final Content content; + private final FinishReason finishReason; + private final String finishMessage; + + Candidate(Content content, FinishReason finishReason, String finishMessage) { + this.content = content; + this.finishReason = finishReason; + this.finishMessage = finishMessage; + } + + public Content content() { + return content; + } + + public FinishReason finishReason() { + return finishReason; + } + + public String finishMessage() { + return finishMessage; + } + + /** The text parts of the content joined, or null when there are none. */ + public String text() { + StringBuilder sb = null; + for (Part p : content.parts()) { + if (p instanceof TextPart) { + if (sb == null) { + sb = new StringBuilder(); + } + sb.append(((TextPart) p).text()); + } + } + return sb == null ? null : sb.toString(); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/ChatSession.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/ChatSession.java new file mode 100644 index 00000000000..8a12c8b944e --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/ChatSession.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +import java.util.ArrayList; +import java.util.List; + +import dart.async.Completer; +import dart.async.Future; +import dart.async.Stream; +import dart.core.DartList; +import dart.runtime.Funcs; + +/** firebase_ai's {@code ChatSession}: the history grows by each message and the reply that answered it. */ +public class ChatSession { + private final GenerativeModel model; + private final DartList history = new DartList(); + private final Iterable safetySettings; + private final GenerationConfig generationConfig; + + ChatSession(GenerativeModel model, List history, Iterable safetySettings, + GenerationConfig generationConfig) { + this.model = model; + this.history.addAll(history); + this.safetySettings = safetySettings; + this.generationConfig = generationConfig; + } + + public DartList history() { + return history; + } + + public Future sendMessage(final Content message) { + List prompt = new ArrayList(history); + prompt.add(message); + final Completer done = new Completer(); + model.generateContent(prompt, safetySettings, generationConfig, null, null).then( + new Funcs.VoidFunc1() { + @Override + public void call(GenerateContentResponse r) { + history.add(message); + if (!r.candidates().isEmpty()) { + history.add(r.candidates().get(0).content()); + } + done.complete(r); + } + }, new Funcs.VoidFunc1() { + @Override + public void call(Object err) { + done.completeError(err); + } + }); + return done.future(); + } + + /** One event, the whole reply (see {@link GenerativeModel#generateContentStream}). */ + public Stream sendMessageStream(Content message) { + return Stream.fromFuture(sendMessage(message)); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Content.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Content.java new file mode 100644 index 00000000000..ed272553fd5 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Content.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.codename1.util.Base64; + +import dart.core.DartList; +import dart.core.DartMap; +import dart.typed_data.Uint8List; + +/** firebase_ai's {@code Content}: a role and its parts. */ +public class Content { + private final String role; + private final DartList parts = new DartList(); + + public Content(String role, Iterable parts) { + this.role = role; + if (parts != null) { + for (Object o : parts) { + if (o instanceof Part) { + this.parts.add((Part) o); + } + } + } + } + + public static Content text(String text) { + return new Content("user", DartList.of(new TextPart(text))); + } + + public static Content multi(Iterable parts) { + return new Content("user", parts); + } + + public static Content model(Iterable parts) { + return new Content("model", parts); + } + + public static Content data(String mimeType, Uint8List bytes) { + return new Content("user", DartList.of(new InlineDataPart(mimeType, bytes))); + } + + public String role() { + return role; + } + + public DartList parts() { + return parts; + } + + public DartMap toJson() { + DartMap m = new DartMap(); + if (role != null) { + m.put("role", role); + } + DartList ps = new DartList(); + for (Part p : parts) { + ps.add(p.toJson()); + } + m.put("parts", ps); + return m; + } + + /** Rebuilds a Content from the REST API's JSON object; parts of a kind not modelled here are dropped. */ + static Content fromMap(Map json) { + List out = new ArrayList(); + Object ps = json.get("parts"); + if (ps instanceof List) { + for (Object o : (List) ps) { + if (!(o instanceof Map)) { + continue; + } + Map p = (Map) o; + Object t = p.get("text"); + Object inline = p.get("inlineData"); + if (t instanceof String) { + out.add(new TextPart((String) t)); + } else if (inline instanceof Map) { + Map d = (Map) inline; + Object mime = d.get("mimeType"); + Object data = d.get("data"); + if (data instanceof String) { + out.add(new InlineDataPart(mime instanceof String ? (String) mime : null, + Uint8List.fromBytes(Base64.decode(((String) data).getBytes())))); + } + } + } + } + Object r = json.get("role"); + return new Content(r instanceof String ? (String) r : null, out); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/CountTokensResponse.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/CountTokensResponse.java new file mode 100644 index 00000000000..fd674b162b9 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/CountTokensResponse.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +/** firebase_ai's {@code CountTokensResponse}. */ +public class CountTokensResponse { + private final long totalTokens; + private final Long totalBillableCharacters; + + CountTokensResponse(long totalTokens, Long totalBillableCharacters) { + this.totalTokens = totalTokens; + this.totalBillableCharacters = totalBillableCharacters; + } + + public long totalTokens() { + return totalTokens; + } + + public Long totalBillableCharacters() { + return totalBillableCharacters; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/FinishReason.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/FinishReason.java new file mode 100644 index 00000000000..fd325cde954 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/FinishReason.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +/** firebase_ai's FinishReason. */ +public enum FinishReason { + unknown, stop, maxTokens, safety, recitation, other; + + /** Maps the REST API's STOP, MAX_TOKENS, SAFETY, RECITATION and OTHER; anything else is unknown. */ + static FinishReason parse(String s) { + if ("STOP".equals(s)) { + return stop; + } + if ("MAX_TOKENS".equals(s)) { + return maxTokens; + } + if ("SAFETY".equals(s)) { + return safety; + } + if ("RECITATION".equals(s)) { + return recitation; + } + if ("OTHER".equals(s)) { + return other; + } + return unknown; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/FirebaseAI.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/FirebaseAI.java new file mode 100644 index 00000000000..d03d3938d6a --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/FirebaseAI.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +import com.codename1.flutter.plugins.firebase.Firebase; +import com.codename1.flutter.plugins.firebase.FirebaseApp; +import com.codename1.flutter.plugins.firebase.FirebaseAppCheck; +import com.codename1.flutter.plugins.firebase.FirebaseAuth; + +/** + * firebase_ai (Firebase AI Logic, Gemini) over the REST API at firebasevertexai.googleapis.com, + * authenticated with the apiKey of the FirebaseApp from the firebase pack. An App Check token + * (the firebase pack's debug token) and the signed-in user's ID token are sent when present. + * {@code googleAI} addresses the Gemini Developer API, {@code vertexAI} Vertex AI in + * {@code location} (default us-central1). Not applied: function calling ({@code Tool.functionDeclarations}), + * {@code toolConfig}, response schemas, the block method of a SafetySetting, and live/imagen models. + * With Codename One not initialized (headless) every request fails at once with a FirebaseAIException. + */ +public class FirebaseAI { + private final FirebaseApp app; + private final FirebaseAppCheck appCheck; + private final FirebaseAuth auth; + private final String location; + private final boolean vertex; + + private FirebaseAI(FirebaseApp app, FirebaseAppCheck appCheck, FirebaseAuth auth, String location, + boolean vertex) { + this.app = app == null ? Firebase.app(null) : app; + this.appCheck = appCheck; + this.auth = auth; + this.location = location; + this.vertex = vertex; + } + + public static FirebaseAI googleAI(FirebaseApp app, FirebaseAppCheck appCheck, FirebaseAuth auth) { + return new FirebaseAI(app, appCheck, auth, null, false); + } + + public static FirebaseAI vertexAI(FirebaseApp app, FirebaseAppCheck appCheck, FirebaseAuth auth, + String location) { + return new FirebaseAI(app, appCheck, auth, location == null ? "us-central1" : location, true); + } + + public FirebaseApp app() { + return app; + } + + public String location() { + return location; + } + + FirebaseAppCheck appCheck() { + return appCheck == null ? FirebaseAppCheck.instance : appCheck; + } + + FirebaseAuth auth() { + return auth == null ? FirebaseAuth.instanceFor(app, null) : auth; + } + + boolean isVertex() { + return vertex; + } + + public GenerativeModel generativeModel(String model, GenerationConfig generationConfig, + Iterable safetySettings, Iterable tools, Object toolConfig, Content systemInstruction) { + return new GenerativeModel(this, model, generationConfig, safetySettings, tools, systemInstruction); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/FirebaseAIException.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/FirebaseAIException.java new file mode 100644 index 00000000000..37a4b0e5173 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/FirebaseAIException.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +/** firebase_ai's {@code FirebaseAIException}: a message. Also raised for transport and server errors. */ +public class FirebaseAIException extends RuntimeException { + public FirebaseAIException(String message) { + super(message); + } + + public String message() { + return getMessage(); + } + + @Override + public String toString() { + return "FirebaseAIException: " + getMessage(); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/GenerateContentResponse.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/GenerateContentResponse.java new file mode 100644 index 00000000000..81125f4621d --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/GenerateContentResponse.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +import java.io.IOException; +import java.util.Map; +import java.util.List; + +import com.codename1.io.JSONParser; + +import dart.core.DartList; + +/** + * firebase_ai's {@code GenerateContentResponse}. {@code text} joins the first candidate's text + * parts and, like the real class, throws when the prompt itself was blocked. + */ +public class GenerateContentResponse { + private final DartList candidates = new DartList(); + private final UsageMetadata usageMetadata; + private final String blockReason; + + GenerateContentResponse(Iterable candidates, UsageMetadata usage, String blockReason) { + for (Candidate c : candidates) { + this.candidates.add(c); + } + this.usageMetadata = usage; + this.blockReason = blockReason; + } + + public DartList candidates() { + return candidates; + } + + public UsageMetadata usageMetadata() { + return usageMetadata; + } + + public String text() { + if (candidates.isEmpty()) { + if (blockReason != null) { + throw new FirebaseAIException("Response was blocked due to " + blockReason); + } + return null; + } + return candidates.get(0).text(); + } + + /** Parses a generateContent reply; an {@code error} object raises FirebaseAIException. */ + public static GenerateContentResponse cn1FromJson(String json) { + return fromMap(parse(json)); + } + + static Map parse(String json) { + Map m; + try { + m = JSONParser.parseJSON(json); + } catch (IOException e) { + throw new FirebaseAIException("Unhandled format for response: " + e.getMessage()); + } + if (m == null) { + throw new FirebaseAIException("Unhandled format for response: empty body"); + } + Object err = m.get("error"); + if (err instanceof Map) { + Object msg = ((Map) err).get("message"); + throw new FirebaseAIException(msg instanceof String ? (String) msg : "Unknown server error"); + } + return m; + } + + static GenerateContentResponse fromMap(Map m) { + List cs = new java.util.ArrayList(); + Object arr = m.get("candidates"); + if (arr instanceof List) { + for (Object o : (List) arr) { + if (!(o instanceof Map)) { + continue; + } + Map c = (Map) o; + Object content = c.get("content"); + Content ct = content instanceof Map ? Content.fromMap((Map) content) : new Content("model", null); + cs.add(new Candidate(ct, FinishReason.parse(str(c.get("finishReason"))), str(c.get("finishMessage")))); + } + } + UsageMetadata usage = null; + Object u = m.get("usageMetadata"); + if (u instanceof Map) { + Map um = (Map) u; + usage = new UsageMetadata(num(um.get("promptTokenCount")), num(um.get("candidatesTokenCount")), + num(um.get("totalTokenCount"))); + } + String block = null; + Object pf = m.get("promptFeedback"); + if (pf instanceof Map) { + block = str(((Map) pf).get("blockReason")); + } + return new GenerateContentResponse(cs, usage, block); + } + + static String str(Object o) { + return o instanceof String ? (String) o : null; + } + + static Long num(Object o) { + return o instanceof Number ? Long.valueOf(((Number) o).longValue()) : null; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/GenerationConfig.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/GenerationConfig.java new file mode 100644 index 00000000000..8b76539e3a0 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/GenerationConfig.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +import dart.core.DartList; +import dart.core.DartMap; + +/** firebase_ai's {@code GenerationConfig}; {@code responseSchema} is accepted and not sent. */ +public class GenerationConfig { + private Long candidateCount; + private DartList stopSequences; + private Long maxOutputTokens; + private Double temperature; + private Double topP; + private Long topK; + private String responseMimeType; + + /// Named arguments of the Dart constructor arrive as the setters below. + public GenerationConfig() { + } + + public void candidateCount(Long v) { + candidateCount = v; + } + + public void stopSequences(Iterable v) { + DartList stops = null; + if (v != null) { + stops = new DartList(); + for (Object o : v) { + stops.add(String.valueOf(o)); + } + } + stopSequences = stops; + } + + public void maxOutputTokens(Long v) { + maxOutputTokens = v; + } + + public void temperature(Double v) { + temperature = v; + } + + public void topP(Double v) { + topP = v; + } + + public void topK(Long v) { + topK = v; + } + + public void responseMimeType(String v) { + responseMimeType = v; + } + + public void responseSchema(Object v) { + // accepted, not sent + } + + public DartMap toJson() { + DartMap m = new DartMap(); + put(m, "candidateCount", candidateCount); + put(m, "maxOutputTokens", maxOutputTokens); + put(m, "temperature", temperature); + put(m, "topP", topP); + put(m, "topK", topK); + put(m, "stopSequences", stopSequences); + put(m, "responseMimeType", responseMimeType); + return m; + } + + private static void put(DartMap m, String k, Object v) { + if (v != null) { + m.put(k, v); + } + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/GenerativeModel.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/GenerativeModel.java new file mode 100644 index 00000000000..936c7ec0d4c --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/GenerativeModel.java @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.codename1.flutter.plugins.firebase.FirebaseOptions; +import com.codename1.flutter.plugins.firebase.User; +import com.codename1.io.JSONParser; +import com.codename1.ui.Display; + +import dart.async.Completer; +import dart.async.Future; +import dart.async.Stream; +import dart.runtime.Funcs; + +/** firebase_ai's {@code GenerativeModel}; see {@link FirebaseAI} for the transport and what is not applied. */ +public class GenerativeModel { + private static final String BASE = "https://firebasevertexai.googleapis.com/v1beta/projects/"; + + private final FirebaseAI ai; + private final String name; + private final GenerationConfig generationConfig; + private final List safetySettings = new ArrayList(); + private final List tools = new ArrayList(); + private final Content systemInstruction; + + GenerativeModel(FirebaseAI ai, String model, GenerationConfig generationConfig, Iterable safety, + Iterable tools, Content systemInstruction) { + this.ai = ai; + String m = model == null ? "" : model; + int idx = m.lastIndexOf("models/"); + this.name = idx >= 0 ? m.substring(idx + 7) : m; + this.generationConfig = generationConfig; + addSafety(this.safetySettings, safety); + addTools(this.tools, tools); + this.systemInstruction = systemInstruction; + } + + private static void addSafety(List out, Iterable in) { + if (in != null) { + for (Object o : in) { + if (o instanceof SafetySetting) { + out.add((SafetySetting) o); + } + } + } + } + + private static void addTools(List out, Iterable in) { + if (in != null) { + for (Object o : in) { + if (o instanceof Tool) { + out.add((Tool) o); + } + } + } + } + + public Future generateContent(Iterable prompt, Iterable safetySettings, + GenerationConfig generationConfig, Iterable tools, Object toolConfig) { + List contents = contents(prompt); + Map body = body(contents, safetySettings, generationConfig, tools); + final Completer done = new Completer(); + call("generateContent", body).then(new Funcs.VoidFunc1>() { + @Override + public void call(Map r) { + try { + done.complete(GenerateContentResponse.fromMap(r)); + } catch (RuntimeException e) { + done.completeError(e); + } + } + }, new Funcs.VoidFunc1() { + @Override + public void call(Object err) { + done.completeError(err); + } + }); + return done.future(); + } + + /** + * {@code generateContentStream}: the REST streaming endpoint is not used, so the stream is + * one event, the whole reply of {@link #generateContent}. + */ + public Stream generateContentStream(Iterable prompt, Iterable safetySettings, + GenerationConfig generationConfig, Iterable tools, Object toolConfig) { + return Stream.fromFuture(generateContent(prompt, safetySettings, generationConfig, tools, toolConfig)); + } + + public Future countTokens(Iterable contents) { + Map body = new LinkedHashMap(); + List cs = new ArrayList(); + for (Content c : contents(contents)) { + cs.add(c.toJson()); + } + body.put("contents", cs); + final Completer done = new Completer(); + call("countTokens", body).then(new Funcs.VoidFunc1>() { + @Override + public void call(Map r) { + Long total = GenerateContentResponse.num(r.get("totalTokens")); + done.complete(new CountTokensResponse(total == null ? 0L : total.longValue(), + GenerateContentResponse.num(r.get("totalBillableCharacters")))); + } + }, new Funcs.VoidFunc1() { + @Override + public void call(Object err) { + done.completeError(err); + } + }); + return done.future(); + } + + public ChatSession startChat(Iterable history, Iterable safetySettings, GenerationConfig generationConfig) { + return new ChatSession(this, contents(history), safetySettings, generationConfig); + } + + private static List contents(Iterable in) { + List out = new ArrayList(); + if (in != null) { + for (Object o : in) { + if (o instanceof Content) { + out.add((Content) o); + } + } + } + return out; + } + + private Map body(List contents, Iterable safetyOverride, + GenerationConfig configOverride, Iterable toolsOverride) { + Map body = new LinkedHashMap(); + List cs = new ArrayList(); + for (Content c : contents) { + cs.add(c.toJson()); + } + body.put("contents", cs); + List safety = this.safetySettings; + if (safetyOverride != null) { + safety = new ArrayList(); + addSafety(safety, safetyOverride); + } + if (!safety.isEmpty()) { + List ss = new ArrayList(); + for (SafetySetting s : safety) { + ss.add(s.toJson()); + } + body.put("safetySettings", ss); + } + GenerationConfig cfg = configOverride == null ? generationConfig : configOverride; + if (cfg != null) { + body.put("generationConfig", cfg.toJson()); + } + List ts = this.tools; + if (toolsOverride != null) { + ts = new ArrayList(); + addTools(ts, toolsOverride); + } + List tj = new ArrayList(); + for (Tool t : ts) { + Map e = t.toJson(); + if (e != null) { + tj.add(e); + } + } + if (!tj.isEmpty()) { + body.put("tools", tj); + } + if (systemInstruction != null) { + body.put("systemInstruction", systemInstruction.toJson()); + } + return body; + } + + /** The endpoint of {@code method}. */ + String url(String method) { + FirebaseOptions o = ai.app().options(); + StringBuilder sb = new StringBuilder(BASE).append(o.projectId()); + if (ai.isVertex()) { + sb.append("/locations/").append(ai.location()).append("/publishers/google/models/"); + } else { + sb.append("/models/"); + } + return sb.append(name).append(':').append(method).toString(); + } + + Future> call(final String method, final Map body) { + final Completer> done = new Completer>(); + FirebaseOptions o = ai.app().options(); + if (o.apiKey() == null || o.projectId() == null) { + done.completeError(new FirebaseAIException( + "Call Firebase.initializeApp with FirebaseOptions that carry an apiKey and a projectId first")); + return done.future(); + } + if (!Display.isInitialized()) { + done.completeError(new FirebaseAIException( + "A network error has occurred: Codename One is not initialized")); + return done.future(); + } + final List headers = new ArrayList(); + headers.add(new String[] {"x-goog-api-key", o.apiKey()}); + final Runnable send = new Runnable() { + @Override + public void run() { + AiTransport.post(url(method), headers, JSONParser.toJson(body)).then( + new Funcs.VoidFunc1>() { + @Override + public void call(Map r) { + done.complete(r); + } + }, new Funcs.VoidFunc1() { + @Override + public void call(Object err) { + done.completeError(err); + } + }); + } + }; + final Runnable withAuth = new Runnable() { + @Override + public void run() { + User u = ai.auth().currentUser(); + if (u == null) { + send.run(); + return; + } + u.getIdToken(Boolean.FALSE).then(new Funcs.VoidFunc1() { + @Override + public void call(String t) { + if (t != null) { + headers.add(new String[] {"Authorization", "Firebase " + t}); + } + send.run(); + } + }, new Funcs.VoidFunc1() { + @Override + public void call(Object err) { + send.run(); + } + }); + } + }; + ai.appCheck().getToken(Boolean.FALSE).then(new Funcs.VoidFunc1() { + @Override + public void call(String t) { + if (t != null) { + headers.add(new String[] {"X-Firebase-AppCheck", t}); + } + withAuth.run(); + } + }, new Funcs.VoidFunc1() { + @Override + public void call(Object err) { + withAuth.run(); + } + }); + return done.future(); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/HarmBlockThreshold.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/HarmBlockThreshold.java new file mode 100644 index 00000000000..66698a420ce --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/HarmBlockThreshold.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +/** firebase_ai's HarmBlockThreshold. */ +public enum HarmBlockThreshold { + low("BLOCK_LOW_AND_ABOVE"), medium("BLOCK_MEDIUM_AND_ABOVE"), high("BLOCK_ONLY_HIGH"), + none("BLOCK_NONE"), off("OFF"); + + private final String wire; + + HarmBlockThreshold(String wire) { + this.wire = wire; + } + + /** The REST API's spelling. */ + public String wire() { + return wire; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/HarmCategory.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/HarmCategory.java new file mode 100644 index 00000000000..94cb738cead --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/HarmCategory.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +/** firebase_ai's HarmCategory. */ +public enum HarmCategory { + unknown("HARM_CATEGORY_UNSPECIFIED"), harassment("HARM_CATEGORY_HARASSMENT"), + hateSpeech("HARM_CATEGORY_HATE_SPEECH"), sexuallyExplicit("HARM_CATEGORY_SEXUALLY_EXPLICIT"), + dangerousContent("HARM_CATEGORY_DANGEROUS_CONTENT"); + + private final String wire; + + HarmCategory(String wire) { + this.wire = wire; + } + + /** The REST API's spelling. */ + public String wire() { + return wire; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/InlineDataPart.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/InlineDataPart.java new file mode 100644 index 00000000000..c40eaf1c3fb --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/InlineDataPart.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +import java.util.LinkedHashMap; +import java.util.Map; + +import com.codename1.util.Base64; + +import dart.typed_data.Uint8List; + +/** firebase_ai's {@code InlineDataPart}: bytes and their MIME type, sent base64 encoded. */ +public class InlineDataPart extends Part { + private final String mimeType; + private final Uint8List bytes; + + public InlineDataPart(String mimeType, Uint8List bytes) { + this.mimeType = mimeType; + this.bytes = bytes; + } + + /** {@code willContinue}: accepted, not sent. */ + public void willContinue(Boolean v) { + } + + public String mimeType() { + return mimeType; + } + + public Uint8List bytes() { + return bytes; + } + + @Override + public Object toJson() { + Map data = new LinkedHashMap(); + data.put("mimeType", mimeType); + data.put("data", bytes == null ? "" : Base64.encodeNoNewline(bytes.toBytes())); + Map m = new LinkedHashMap(); + m.put("inlineData", data); + return m; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Part.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Part.java new file mode 100644 index 00000000000..287f066b609 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Part.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +/** firebase_ai's {@code Part}: one piece of a Content. */ +public abstract class Part { + Part() { + } + + /** The REST API's JSON object for this part. */ + public abstract Object toJson(); +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/SafetySetting.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/SafetySetting.java new file mode 100644 index 00000000000..fb2090d6fa4 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/SafetySetting.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** firebase_ai's {@code SafetySetting}; the optional block method is accepted and not sent. */ +public class SafetySetting { + private final HarmCategory category; + private final HarmBlockThreshold threshold; + + public SafetySetting(HarmCategory category, HarmBlockThreshold threshold, Object method) { + this.category = category == null ? HarmCategory.unknown : category; + this.threshold = threshold == null ? HarmBlockThreshold.none : threshold; + } + + public HarmCategory category() { + return category; + } + + public HarmBlockThreshold threshold() { + return threshold; + } + + public Map toJson() { + Map m = new LinkedHashMap(); + m.put("category", category.wire()); + m.put("threshold", threshold.wire()); + return m; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/TextPart.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/TextPart.java new file mode 100644 index 00000000000..b63c469d55d --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/TextPart.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** firebase_ai's {@code TextPart}. */ +public class TextPart extends Part { + private final String text; + + public TextPart(String text) { + this.text = text == null ? "" : text; + } + + public String text() { + return text; + } + + @Override + public Object toJson() { + Map m = new LinkedHashMap(); + m.put("text", text); + return m; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Tool.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Tool.java new file mode 100644 index 00000000000..e7140e888d5 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/Tool.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * firebase_ai's {@code Tool}. {@code googleSearch} and {@code codeExecution} are sent as the + * REST API's tool entries; {@code functionDeclarations} is accepted and not sent (there is no + * function-calling loop here), so the model never answers with a function call. + */ +public class Tool { + private final String kind; + + private Tool(String kind) { + this.kind = kind; + } + + public static Tool functionDeclarations(Iterable declarations) { + return new Tool(null); + } + + public static Tool googleSearch(Object googleSearch) { + return new Tool("googleSearch"); + } + + public static Tool codeExecution() { + return new Tool("codeExecution"); + } + + /** The tool entry, or null when this kind is not sent. */ + public Map toJson() { + if (kind == null) { + return null; + } + Map m = new LinkedHashMap(); + m.put(kind, new LinkedHashMap()); + return m; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/UsageMetadata.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/UsageMetadata.java new file mode 100644 index 00000000000..fa587fea132 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseai/UsageMetadata.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseai; + +/** firebase_ai's {@code UsageMetadata}: token counts of a response. */ +public class UsageMetadata { + private final Long promptTokenCount; + private final Long candidatesTokenCount; + private final Long totalTokenCount; + + UsageMetadata(Long prompt, Long candidates, Long total) { + this.promptTokenCount = prompt; + this.candidatesTokenCount = candidates; + this.totalTokenCount = total; + } + + public Long promptTokenCount() { + return promptTokenCount; + } + + public Long candidatesTokenCount() { + return candidatesTokenCount; + } + + public Long totalTokenCount() { + return totalTokenCount; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebasegoogle/GoogleAuthProvider.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebasegoogle/GoogleAuthProvider.java index 3d64e682183..3c2ba3e9565 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebasegoogle/GoogleAuthProvider.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebasegoogle/GoogleAuthProvider.java @@ -53,6 +53,14 @@ public class GoogleAuthProvider implements FederatedSignIn { public static final String GOOGLE_SIGN_IN_METHOD = "google.com"; private final DartList scopes = new DartList(); + private String clientId; + + /// The OAuth client to sign in with, ahead of the Display property and the app's + /// options; FirebaseUI's GoogleProvider names one. Not firebase_auth API. + public GoogleAuthProvider clientId(String v) { + clientId = v; + return this; + } public static OAuthCredential credential(String idToken, String accessToken) { return new OAuthCredential(PROVIDER_ID, GOOGLE_SIGN_IN_METHOD, idToken, accessToken, null, null); @@ -81,7 +89,7 @@ public Future obtainCredential(FirebaseApp app) { final Completer done = new Completer(); GoogleSignIn g = new GoogleSignIn(); g.scopes(scopes); - g.clientId(clientId(app)); + g.clientId(clientId != null ? clientId : clientId(app)); g.signIn().then(new Funcs.VoidFunc1() { @Override public void call(GoogleSignInAccount a) { diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthAction.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthAction.java new file mode 100644 index 00000000000..6c451263062 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthAction.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseui; + +/** firebase_ui_auth's {@code AuthAction}. */ +public enum AuthAction { + signIn, signUp, link, none +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthFailed.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthFailed.java new file mode 100644 index 00000000000..924ec12d68d --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthFailed.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseui; + +/** firebase_ui_auth's {@code AuthFailed}. */ +public class AuthFailed extends AuthState { + private final Object exception; + + public AuthFailed(Object exception) { + this.exception = exception; + } + + public Object exception() { + return exception; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthState.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthState.java new file mode 100644 index 00000000000..9d6603d0a23 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthState.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseui; + +/** firebase_ui_auth's {@code AuthState}: what an {@link AuthStateChangeAction} is called with. */ +public class AuthState { +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthStateChangeAction.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthStateChangeAction.java new file mode 100644 index 00000000000..d83d0bff1ba --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/AuthStateChangeAction.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseui; + +import com.codename1.flutter.BuildContext; + +import dart.runtime.Funcs; + +/** + * firebase_ui_auth's {@code AuthStateChangeAction}. Generics are erased, so the + * transpiler hands the action its {@code T} ({@link #providedType}) and the action runs only + * for a state of that type: an action for {@code SignedIn} is never called with a + * {@code UserCreated}, whose callback would read it as the wrong class. Written without a + * type argument it runs for every state. + */ +public class AuthStateChangeAction { + private final Funcs.VoidFunc2 callback; + private Class type; + + public AuthStateChangeAction(Funcs.VoidFunc2 callback) { + this.callback = callback; + } + + public AuthStateChangeAction providedType(Class t) { + type = t; + return this; + } + + @SuppressWarnings("unchecked") + void fire(BuildContext context, AuthState state) { + if (callback != null && (type == null || type.isInstance(state))) { + callback.call(context, (T) state); + } + } + + static void fireAll(Iterable actions, BuildContext context, AuthState state) { + if (actions == null) { + return; + } + for (Object a : actions) { + if (a instanceof AuthStateChangeAction) { + ((AuthStateChangeAction) a).fire(context, state); + } + } + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/FirebaseUIAuth.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/FirebaseUIAuth.java new file mode 100644 index 00000000000..709dc62d9eb --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/FirebaseUIAuth.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseui; + +import com.codename1.flutter.plugins.firebase.FirebaseApp; + +import dart.core.DartList; + +/** firebase_ui_auth's {@code FirebaseUIAuth}: the providers a screen offers when it names none. */ +public final class FirebaseUIAuth { + private static DartList providers = new DartList(); + + private FirebaseUIAuth() { + } + + public static void configureProviders(DartList list, FirebaseApp app) { + providers = list == null ? new DartList() : list; + } + + public static DartList providersFor(FirebaseApp app) { + return providers; + } + + static DartList orConfigured(DartList list) { + return list == null || list.isEmpty() ? providers : list; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/OAuthProviderButton.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/OAuthProviderButton.java new file mode 100644 index 00000000000..76f85a7c3b4 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/OAuthProviderButton.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseui; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.plugins.firebase.FirebaseAuth; +import com.codename1.flutter.plugins.firebase.UserCredential; + +import dart.runtime.Funcs; + +/** + * firebase_ui_oauth's {@code OAuthProviderButton}: one provider's sign-in button, which runs + * {@code signInWithProvider}. Inside a {@link SignInScreen} the screen's actions run on the + * outcome; a button placed on its own runs none, because the runtime has no + * {@code FirebaseUIActions} scope to find them in. {@code variant} and {@code action} are + * accepted and not applied. + */ +public class OAuthProviderButton extends StatelessWidget { + private Object provider; + private FirebaseAuth auth; + Funcs.VoidFunc1 onSignedIn; + Funcs.VoidFunc1 onError; + + public void provider(Object v) { + provider = v; + } + + public void auth(FirebaseAuth v) { + auth = v; + } + + public void action(Object v) { + } + + public void variant(Object v) { + } + + @Override + public Widget build(BuildContext context) { + final UiOAuthProvider p = provider instanceof UiOAuthProvider ? (UiOAuthProvider) provider : null; + return Ui.button(p == null ? "Sign in" : p.label(), false, new Funcs.VoidFunc0() { + @Override + public void call() { + if (p == null) { + return; + } + FirebaseAuth a = auth == null ? FirebaseAuth.instance : auth; + a.signInWithProvider(p).then(new Funcs.VoidFunc1() { + @Override + public void call(UserCredential c) { + if (onSignedIn != null) { + onSignedIn.call(c); + } + } + }, new Funcs.VoidFunc1() { + @Override + public void call(Object err) { + if (onError != null) { + onError.call(err); + } + } + }); + } + }); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/ProfileScreen.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/ProfileScreen.java new file mode 100644 index 00000000000..f1bcf105e8a --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/ProfileScreen.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseui; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.Scaffold; +import com.codename1.flutter.plugins.firebase.FirebaseAuth; +import com.codename1.flutter.plugins.firebase.User; +import com.codename1.flutter.widgets.Column; +import com.codename1.flutter.widgets.Padding; +import com.codename1.flutter.widgets.Text; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * firebase_ui_auth's {@code ProfileScreen}: the signed-in user's name and email, the + * {@code children} below them, and a sign-out button that fires the + * {@link SignedOutAction}s. Account linking, deletion and editing the name are not offered; + * {@code providers}, {@code avatar*} and {@code showDeleteConfirmationDialog} are accepted and + * not applied. + */ +public class ProfileScreen extends StatelessWidget { + DartList actions; + DartList children; + Widget appBar; + FirebaseAuth auth; + + public void actions(DartList v) { + actions = v; + } + + public void children(DartList v) { + children = v; + } + + public void appBar(Widget v) { + appBar = v; + } + + public void auth(FirebaseAuth v) { + auth = v; + } + + public void providers(Object v) { + } + + public void avatar(Object v) { + } + + public void avatarSize(Object v) { + } + + public void showDeleteConfirmationDialog(Object v) { + } + + @Override + public Widget build(final BuildContext context) { + final FirebaseAuth a = auth == null ? FirebaseAuth.instance : auth; + User u = a.currentUser(); + DartList col = new DartList(); + if (u != null) { + if (u.displayName() != null) { + col.add(Ui.padded(new Text(u.displayName()))); + } + if (u.email() != null) { + col.add(Ui.padded(new Text(u.email()))); + } + } + if (children != null) { + col.addAll(children); + } + col.add(Ui.button("Sign out", false, new Funcs.VoidFunc0() { + @Override + public void call() { + a.signOut(); + SignedOutAction.fireAll(actions, context); + } + })); + Column c = new Column(); + c.children(col); + Padding p = new Padding(); + p.padding(com.codename1.flutter.EdgeInsets.all(16)); + p.child(c); + Scaffold s = new Scaffold(); + if (appBar != null) { + s.appBar(appBar); + } + s.body(p); + return s; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/SignInScreen.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/SignInScreen.java new file mode 100644 index 00000000000..829c5ebd936 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/SignInScreen.java @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseui; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.State; +import com.codename1.flutter.StatefulWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.InputDecoration; +import com.codename1.flutter.material.Scaffold; +import com.codename1.flutter.material.TextEditingController; +import com.codename1.flutter.material.TextField; +import com.codename1.flutter.plugins.firebase.EmailAuthProvider; +import com.codename1.flutter.plugins.firebase.FirebaseAuth; +import com.codename1.flutter.plugins.firebase.UserCredential; +import com.codename1.flutter.widgets.Column; +import com.codename1.flutter.widgets.Padding; +import com.codename1.flutter.widgets.SingleChildScrollView; +import com.codename1.flutter.widgets.Text; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * firebase_ui_auth's {@code SignInScreen}, built from the runtime's Material widgets so it + * follows the app's theme and text direction: the email form when an + * {@code EmailAuthProvider} is among the providers (sign in, or create an account with the + * switch), then one button per Google or Apple provider. A success fires the + * {@code AuthStateChangeAction}s for {@link SignedIn}, or {@link UserCreated} for a new + * account; a failure shows the message and fires those for {@link AuthFailed}. + * + *

{@code headerBuilder} is called with the screen's width as a loose constraint and a + * shrink offset of 0, since the header does not collapse; {@code subtitleBuilder} and + * {@code footerBuilder} are called with the current {@link AuthAction}. Accepted and not + * applied: {@code sideBuilder}, {@code breakpoint}, {@code styles}, {@code headerMaxExtent}, + * {@code oauthButtonVariant}, {@code showPasswordVisibilityToggle} and {@code email}. + */ +public class SignInScreen extends StatefulWidget { + DartList providers; + DartList actions; + FirebaseAuth auth; + Funcs.Func3 headerBuilder; + Funcs.Func2 subtitleBuilder; + Funcs.Func2 footerBuilder; + boolean showAuthActionSwitch = true; + AuthAction initialAction = AuthAction.signIn; + + public void providers(DartList v) { + providers = v; + } + + public void actions(DartList v) { + actions = v; + } + + public void auth(FirebaseAuth v) { + auth = v; + } + + public void headerBuilder(Funcs.Func3 v) { + headerBuilder = v; + } + + public void subtitleBuilder(Funcs.Func2 v) { + subtitleBuilder = v; + } + + public void footerBuilder(Funcs.Func2 v) { + footerBuilder = v; + } + + public void showAuthActionSwitch(Boolean v) { + showAuthActionSwitch = v == null || v.booleanValue(); + } + + public void initialAuthAction(AuthAction v) { + if (v != null) { + initialAction = v; + } + } + + public void sideBuilder(Object v) { + } + + public void breakpoint(Object v) { + } + + public void styles(Object v) { + } + + public void headerMaxExtent(Object v) { + } + + public void oauthButtonVariant(Object v) { + } + + public void showPasswordVisibilityToggle(Object v) { + } + + public void email(String v) { + } + + public void resizeToAvoidBottomInset(Object v) { + } + + @Override + public State createState() { + return new SignInState(); + } + + static final class SignInState extends State { + private final TextEditingController email = new TextEditingController(); + private final TextEditingController password = new TextEditingController(); + private AuthAction action; + private String error; + private boolean busy; + + @Override + public void initState() { + action = widget().initialAction; + } + + private FirebaseAuth auth() { + return widget().auth == null ? FirebaseAuth.instance : widget().auth; + } + + private void signedIn(final UserCredential c, final boolean created) { + busy = false; + error = null; + if (mounted()) { + setState(new Funcs.VoidFunc0() { + @Override + public void call() { + } + }); + } + AuthStateChangeAction.fireAll(widget().actions, context(), + created ? new UserCreated(c) : new SignedIn(c.user())); + } + + private void failed(final Object err) { + busy = false; + error = Ui.message(err); + if (mounted()) { + setState(new Funcs.VoidFunc0() { + @Override + public void call() { + } + }); + } + AuthStateChangeAction.fireAll(widget().actions, context(), new AuthFailed(err)); + } + + private void submit() { + if (busy) { + return; + } + busy = true; + final boolean create = action == AuthAction.signUp; + (create ? auth().createUserWithEmailAndPassword(email.text(), password.text()) + : auth().signInWithEmailAndPassword(email.text(), password.text())) + .then(new Funcs.VoidFunc1() { + @Override + public void call(UserCredential c) { + signedIn(c, create); + } + }, new Funcs.VoidFunc1() { + @Override + public void call(Object err) { + failed(err); + } + }); + } + + @Override + public Widget build(BuildContext context) { + final SignInScreen w = widget(); + DartList children = new DartList(); + if (w.headerBuilder != null) { + Widget h = w.headerBuilder.call(context, + com.codename1.flutter.rendering.BoxConstraints.loose(Double.POSITIVE_INFINITY, 200), + Double.valueOf(0)); + if (h != null) { + children.add(h); + } + } + Text title = new Text(action == AuthAction.signUp ? "Register" : "Sign in"); + children.add(Ui.padded(title)); + if (w.subtitleBuilder != null) { + Widget s = w.subtitleBuilder.call(context, action); + if (s != null) { + children.add(s); + } + } + boolean hasEmail = false; + DartList providers = FirebaseUIAuth.orConfigured(w.providers); + for (int i = 0; i < providers.size(); i++) { + hasEmail |= providers.get(i) instanceof EmailAuthProvider; + } + if (hasEmail) { + children.add(Ui.padded(field("Email", email, false))); + children.add(Ui.padded(field("Password", password, true))); + if (error != null) { + children.add(Ui.padded(new Text(error))); + } + children.add(Ui.button(action == AuthAction.signUp ? "Register" : "Sign in", true, + new Funcs.VoidFunc0() { + @Override + public void call() { + submit(); + } + })); + if (w.showAuthActionSwitch) { + children.add(Ui.button(action == AuthAction.signUp ? "Already have an account? Sign in" + : "Don't have an account? Register", false, new Funcs.VoidFunc0() { + @Override + public void call() { + setState(new Funcs.VoidFunc0() { + @Override + public void call() { + action = action == AuthAction.signUp ? AuthAction.signIn + : AuthAction.signUp; + error = null; + } + }); + } + })); + } + } else if (error != null) { + children.add(Ui.padded(new Text(error))); + } + for (int i = 0; i < providers.size(); i++) { + Object p = providers.get(i); + if (p instanceof UiOAuthProvider) { + OAuthProviderButton b = new OAuthProviderButton(); + b.provider(p); + b.auth(w.auth); + b.onSignedIn = new Funcs.VoidFunc1() { + @Override + public void call(UserCredential c) { + signedIn(c, false); + } + }; + b.onError = new Funcs.VoidFunc1() { + @Override + public void call(Object err) { + failed(err); + } + }; + children.add(b); + } + } + if (w.footerBuilder != null) { + Widget f = w.footerBuilder.call(context, action); + if (f != null) { + children.add(f); + } + } + Column col = new Column(); + col.children(children); + Padding pad = new Padding(); + pad.padding(com.codename1.flutter.EdgeInsets.all(16)); + pad.child(col); + SingleChildScrollView scroll = new SingleChildScrollView(); + scroll.child(pad); + Scaffold s = new Scaffold(); + s.body(scroll); + return s; + } + + private static Widget field(String label, TextEditingController c, boolean obscure) { + InputDecoration d = new InputDecoration(); + d.labelText(label); + TextField f = new TextField(); + f.controller(c); + f.decoration(d); + f.obscureText(obscure); + return f; + } + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/SignedIn.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/SignedIn.java new file mode 100644 index 00000000000..b48b6153566 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/SignedIn.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseui; + +import com.codename1.flutter.plugins.firebase.User; + +/** firebase_ui_auth's {@code SignedIn}. */ +public class SignedIn extends AuthState { + private final User user; + + public SignedIn(User user) { + this.user = user; + } + + public User user() { + return user; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/SignedOutAction.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/SignedOutAction.java new file mode 100644 index 00000000000..66480d8c395 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/SignedOutAction.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseui; + +import com.codename1.flutter.BuildContext; + +import dart.runtime.Funcs; + +/** firebase_ui_auth's {@code SignedOutAction}, run by {@link ProfileScreen}'s sign-out. */ +public class SignedOutAction { + private final Funcs.VoidFunc1 callback; + + public SignedOutAction(Funcs.VoidFunc1 callback) { + this.callback = callback; + } + + static void fireAll(Iterable actions, BuildContext context) { + if (actions == null) { + return; + } + for (Object a : actions) { + if (a instanceof SignedOutAction && ((SignedOutAction) a).callback != null) { + ((SignedOutAction) a).callback.call(context); + } + } + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/Ui.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/Ui.java new file mode 100644 index 00000000000..8f719093813 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/Ui.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseui; + +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.ElevatedButton; +import com.codename1.flutter.material.OutlinedButton; +import com.codename1.flutter.widgets.Padding; +import com.codename1.flutter.widgets.Text; + +import dart.runtime.Funcs; + +/** The widgets the FirebaseUI screens are built from, made the way generated code makes them. */ +final class Ui { + private Ui() { + } + + static Widget padded(Widget child) { + Padding p = new Padding(); + p.padding(EdgeInsets.symmetric(0, 6)); + p.child(child); + return p; + } + + static Widget button(String label, boolean primary, Funcs.VoidFunc0 onPressed) { + if (primary) { + ElevatedButton b = new ElevatedButton(); + b.child(new Text(label)); + b.onPressed(onPressed); + return padded(b); + } + OutlinedButton b = new OutlinedButton(); + b.child(new Text(label)); + b.onPressed(onPressed); + return padded(b); + } + + static String message(Object err) { + if (err instanceof com.codename1.flutter.plugins.firebase.FirebaseException) { + String m = ((com.codename1.flutter.plugins.firebase.FirebaseException) err).message(); + if (m != null) { + return m; + } + } + return String.valueOf(err); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/UiOAuthProvider.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/UiOAuthProvider.java new file mode 100644 index 00000000000..f66ef4a6223 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/UiOAuthProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseui; + +import com.codename1.flutter.plugins.firebase.FederatedSignIn; + +/** + * A FirebaseUI provider with a button of its own (Google's, Apple's). The providers live in + * packs of their own, firebaseuigoogle and firebaseuiapple, so only an app that offers one + * ships its sign-in. + */ +public interface UiOAuthProvider extends FederatedSignIn { + /** The button's label, "Sign in with Google". */ + String label(); +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/UserCreated.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/UserCreated.java new file mode 100644 index 00000000000..e059f448a44 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseui/UserCreated.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseui; + +import com.codename1.flutter.plugins.firebase.UserCredential; + +/** firebase_ui_auth's {@code UserCreated}: a new account from the email form's sign-up. */ +public class UserCreated extends AuthState { + private final UserCredential credential; + + public UserCreated(UserCredential credential) { + this.credential = credential; + } + + public UserCredential credential() { + return credential; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseuiapple/AppleProvider.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseuiapple/AppleProvider.java new file mode 100644 index 00000000000..3ff119bd537 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseuiapple/AppleProvider.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseuiapple; + +import com.codename1.flutter.plugins.firebase.FirebaseApp; +import com.codename1.flutter.plugins.firebase.OAuthCredential; +import com.codename1.flutter.plugins.firebaseapple.AppleAuthProvider; +import com.codename1.flutter.plugins.firebaseui.UiOAuthProvider; + +import dart.async.Future; +import dart.core.DartList; + +/** firebase_ui_oauth_apple's {@code AppleProvider}: signs in through {@link AppleAuthProvider}. */ +public class AppleProvider implements UiOAuthProvider { + private DartList scopes; + + public void scopes(Object v) { + if (v instanceof DartList) { + @SuppressWarnings("unchecked") + DartList l = (DartList) v; + scopes = l; + } + } + + public String providerId() { + return AppleAuthProvider.PROVIDER_ID; + } + + @Override + public String label() { + return "Sign in with Apple"; + } + + @Override + public Future obtainCredential(FirebaseApp app) { + AppleAuthProvider p = new AppleAuthProvider(); + if (scopes != null) { + for (int i = 0; i < scopes.size(); i++) { + p.addScope(String.valueOf(scopes.get(i))); + } + } + return p.obtainCredential(app); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseuigoogle/GoogleProvider.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseuigoogle/GoogleProvider.java new file mode 100644 index 00000000000..a0f030d99b9 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/firebaseuigoogle/GoogleProvider.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.firebaseuigoogle; + +import com.codename1.flutter.plugins.firebase.FirebaseApp; +import com.codename1.flutter.plugins.firebase.OAuthCredential; +import com.codename1.flutter.plugins.firebasegoogle.GoogleAuthProvider; +import com.codename1.flutter.plugins.firebaseui.UiOAuthProvider; + +import dart.async.Future; +import dart.core.DartList; + +/** + * firebase_ui_oauth_google's {@code GoogleProvider}: signs in through + * {@link GoogleAuthProvider} with this provider's {@code clientId}. {@code redirectUri} is + * the Display property {@code google.redirectUri}'s job here; {@code iOSPreferPlist} is + * accepted and not applied. + */ +public class GoogleProvider implements UiOAuthProvider { + private String clientId; + private DartList scopes; + + public void clientId(String v) { + clientId = v; + } + + public void redirectUri(String v) { + } + + public void scopes(DartList v) { + scopes = v; + } + + public void iOSPreferPlist(Boolean v) { + } + + public String providerId() { + return GoogleAuthProvider.PROVIDER_ID; + } + + @Override + public String label() { + return "Sign in with Google"; + } + + @Override + public Future obtainCredential(FirebaseApp app) { + GoogleAuthProvider p = new GoogleAuthProvider().clientId(clientId); + if (scopes != null) { + for (int i = 0; i < scopes.size(); i++) { + p.addScope(scopes.get(i)); + } + } + return p.obtainCredential(app); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/BitmapDescriptor.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/BitmapDescriptor.java index 3c134b6c6c1..2c1e1ed0429 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/BitmapDescriptor.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/BitmapDescriptor.java @@ -23,7 +23,21 @@ */ package com.codename1.flutter.plugins.googlemaps; -/** google_maps_flutter's BitmapDescriptor; markers keep the default pin. */ +import com.codename1.ui.Display; +import com.codename1.ui.EncodedImage; +import com.codename1.ui.Graphics; +import com.codename1.ui.Image; + +import dart.async.Future; +import dart.typed_data.Uint8List; + +/** + * google_maps_flutter's BitmapDescriptor, applied as the Codename One marker icon. + * {@code defaultMarker} keeps the platform pin; {@code defaultMarkerWithHue} draws a pin + * of that hue; {@code fromBytes} / {@code bytes} use the encoded image (PNG or JPEG) as + * it is. {@code fromAssetImage} / {@code asset} read the named bundled resource. Image + * configuration, scaling and anchors are accepted and not applied. + */ public final class BitmapDescriptor { public static final double hueRed = 0; public static final double hueOrange = 30; @@ -35,12 +49,101 @@ public final class BitmapDescriptor { public static final double hueViolet = 270; public static final double hueMagenta = 300; public static final double hueRose = 330; - public static final BitmapDescriptor defaultMarker = new BitmapDescriptor(); + public static final BitmapDescriptor defaultMarker = new BitmapDescriptor(-1, null, null); - private BitmapDescriptor() { + private final double hue; + private final byte[] bytes; + private final String asset; + + private BitmapDescriptor(double hue, byte[] bytes, String asset) { + this.hue = hue; + this.bytes = bytes; + this.asset = asset; } public static BitmapDescriptor defaultMarkerWithHue(double hue) { - return defaultMarker; + double h = hue % 360; + return new BitmapDescriptor(h < 0 ? h + 360 : h, null, null); + } + + public static BitmapDescriptor fromBytes(Uint8List data, Object size) { + return new BitmapDescriptor(-1, data == null ? null : data.toBytes(), null); + } + + public static BitmapDescriptor bytes(Uint8List data, Double imagePixelRatio, Double width, Double height, + Object bitmapScaling) { + return fromBytes(data, null); + } + + public static Future fromAssetImage(Object configuration, String assetName) { + return Future.value(new BitmapDescriptor(-1, null, assetName)); + } + + public static Future asset(Object configuration, String assetName, Double width, + Double height, Object bitmapScaling) { + return fromAssetImage(configuration, assetName); + } + + /** The hue of a {@code defaultMarkerWithHue} pin; -1 for any other descriptor. */ + public double hue() { + return hue; + } + + /** True for an icon given as image bytes. */ + public boolean hasBytes() { + return bytes != null; + } + + /** The marker icon, or null for the platform's default pin (and always headless). */ + EncodedImage toIcon() { + if (!Display.isInitialized()) { + return null; + } + if (bytes != null) { + return EncodedImage.create(bytes); + } + if (asset != null) { + try { + String path = asset.startsWith("/") ? asset : "/" + asset; + java.io.InputStream in = Display.getInstance().getResourceAsStream(getClass(), path); + return in == null ? null : EncodedImage.create(in); + } catch (java.io.IOException e) { + return null; + } + } + if (hue < 0) { + return null; + } + int size = Display.getInstance().convertToPixels(6); + Image img = Image.createImage(size, size + size / 2, 0); + Graphics g = img.getGraphics(); + g.setAntiAliased(true); + g.setColor((int) hueToRgb(hue)); + g.fillArc(0, 0, size, size, 0, 360); + g.fillTriangle(size / 6, size * 2 / 3, size - size / 6, size * 2 / 3, size / 2, size + size / 2 - 1); + g.setColor(0xffffff); + g.fillArc(size / 3, size / 3, size / 3, size / 3, 0, 360); + return EncodedImage.createFromImage(img, false); + } + + /** The RGB of a fully saturated, full-value color of this hue (degrees). */ + public static long hueToRgb(double hue) { + double h = (hue % 360 + 360) % 360 / 60.0; + int sector = (int) Math.floor(h); + double f = h - sector; + int q = (int) Math.round(255 * (1 - f)); + int t = (int) Math.round(255 * f); + int r; + int g; + int b; + switch (sector) { + case 0: r = 255; g = t; b = 0; break; + case 1: r = q; g = 255; b = 0; break; + case 2: r = 0; g = 255; b = t; break; + case 3: r = 0; g = q; b = 255; break; + case 4: r = t; g = 0; b = 255; break; + default: r = 255; g = 0; b = q; break; + } + return (r << 16) | (g << 8) | b; } } diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/Circle.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/Circle.java new file mode 100644 index 00000000000..1084be1eb4c --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/Circle.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.googlemaps; + +import com.codename1.flutter.Color; + +import dart.runtime.Funcs; + +/** + * google_maps_flutter's {@code Circle}, drawn as a Codename One + * {@code com.codename1.maps.Circle}: center, radius, fill and stroke colors, stroke width + * and visibility are applied. zIndex is accepted and not applied. The core map reports + * a tap as a position only, so the map element tests it against the radius itself. As + * in the plugin, {@code onTap} is called only for a circle with {@code consumeTapEvents} + * set, and such a tap does not reach the map's own onTap. + */ +public class Circle { + CircleId circleId; + LatLng center; + double radius; + Color fillColor; + Color strokeColor; + long strokeWidth = 10; + boolean visible = true; + Funcs.VoidFunc0 onTap; + boolean consumeTapEvents; + + public Circle() { + } + + public void circleId(CircleId v) { + circleId = v; + } + + public void center(LatLng v) { + center = v; + } + + public void radius(double v) { + radius = v; + } + + public void fillColor(Color v) { + fillColor = v; + } + + public void strokeColor(Color v) { + strokeColor = v; + } + + public void strokeWidth(long v) { + strokeWidth = v; + } + + public void visible(boolean v) { + visible = v; + } + + public void zIndex(long v) { + } + + public void consumeTapEvents(boolean v) { + consumeTapEvents = v; + } + + public void onTap(Funcs.VoidFunc0 v) { + onTap = v; + } + + public CircleId circleId() { + return circleId; + } + + public LatLng center() { + return center; + } + + public double radius() { + return radius; + } + + public boolean visible() { + return visible; + } + + /** The core circle this one draws as; null without a center. */ + com.codename1.maps.Circle toCn1() { + if (center == null) { + return null; + } + com.codename1.maps.Circle c = new com.codename1.maps.Circle(center.toCn1(), radius); + if (fillColor != null) { + c.setFillColor(fillColor.argb()); + } + if (strokeColor != null) { + c.setStrokeColor(strokeColor.argb() & 0xffffff); + } + c.setStrokeWidth((int) Math.max(0, strokeWidth)); + return c; + } + + /// Whether {@code at} lies within the circle. + public boolean hitTest(LatLng at) { + return at != null && center != null && MapHit.distance(center, at) <= radius; + } + + /// Delivers a tap at {@code at}: answers whether this circle consumed it, calling + /// onTap when it did. + public boolean tapAt(LatLng at) { + if (!consumeTapEvents || !visible || !hitTest(at)) { + return false; + } + if (onTap != null) { + onTap.call(); + } + return true; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/CircleId.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/CircleId.java new file mode 100644 index 00000000000..ad3a5cc1de1 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/CircleId.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.googlemaps; + +/** google_maps_flutter's {@code CircleId}. */ +public class CircleId { + private final String value; + + public CircleId(String value) { + this.value = value; + } + + public String value() { + return value; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/GoogleMap.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/GoogleMap.java index 5bf374a279e..30869646581 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/GoogleMap.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/GoogleMap.java @@ -31,7 +31,10 @@ /** * google_maps_flutter's GoogleMap, hosting Codename One's {@code MapView} -- native * maps where the platform has them, vector tiles elsewhere. Markers, the camera, - * taps and camera moves are supported; polylines, polygons, circles and map styles + * taps and camera moves are supported, and so are polylines, circles and marker icons + * (BitmapDescriptor); myLocationEnabled, myLocationButtonEnabled, zoomControlsEnabled, + * mapToolbarEnabled and gestureRecognizers are accepted and not applied (the core map + * view has no such controls, and its own gestures always run). Polygons and map styles * are accepted and not drawn. */ public class GoogleMap extends Widget { @@ -79,14 +82,18 @@ public void onCameraIdle(Funcs.VoidFunc0 v) { onCameraIdle = v; } - /** Accepted and ignored. */ + Iterable polylines; + Iterable circles; + public void polylines(Object v) { + polylines = v instanceof Iterable ? (Iterable) v : null; } public void polygons(Object v) { } public void circles(Object v) { + circles = v instanceof Iterable ? (Iterable) v : null; } public void myLocationEnabled(boolean v) { diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/GoogleMapRenderElement.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/GoogleMapRenderElement.java index e473180dfb0..971749cebe2 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/GoogleMapRenderElement.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/GoogleMapRenderElement.java @@ -52,6 +52,8 @@ public class GoogleMapRenderElement extends RenderElement { private MapView view; private CameraPosition camera; private final List placed = new ArrayList(); + private final List placedLines = new ArrayList(); + private final List placedCircles = new ArrayList(); private boolean created; public GoogleMapRenderElement(GoogleMap widget) { @@ -89,6 +91,9 @@ protected Component createComponent() { view.addTapListener(new MapTapListener() { @Override public void mapTapped(MapSurface m, com.codename1.maps.LatLng at, int x, int y) { + if (overlayConsumed(LatLng.of(at))) { + return; + } Funcs.VoidFunc1 h = map().onTap; if (h != null) { h.call(LatLng.of(at)); @@ -132,6 +137,44 @@ public void cameraChanged(MapSurface m, com.codename1.maps.CameraPosition p) { return view == null ? null : view.asComponent(); } + /// Hit-tests a tap against the widget's circles and polylines (topmost, that is + /// last, first) with a tolerance of about 4 mm of screen; answers whether one + /// consumed it. + private boolean overlayConsumed(LatLng at) { + if (at == null) { + return false; + } + double mpp = MapHit.metersPerPixel(at.latitude(), camera().zoom()); + double tol = mpp * Display.getInstance().convertToPixels(4f); + if (map().circles != null) { + List l = new ArrayList(); + for (Object o : map().circles) { + l.add(o); + } + for (int i = l.size() - 1; i >= 0; i--) { + if (l.get(i) instanceof Circle && ((Circle) l.get(i)).tapAt(at)) { + return true; + } + } + } + if (map().polylines != null) { + List l = new ArrayList(); + for (Object o : map().polylines) { + l.add(o); + } + for (int i = l.size() - 1; i >= 0; i--) { + if (l.get(i) instanceof Polyline) { + Polyline p = (Polyline) l.get(i); + // a stroke wider than the tolerance is hit across its whole width + if (p.tapAt(at, Math.max(tol, mpp * p.width() / 2))) { + return true; + } + } + } + } + return false; + } + private com.codename1.ui.util.UITimer moveTimer; private com.codename1.maps.CameraPosition lastMove; private int stillTicks; @@ -237,6 +280,7 @@ private void placeMarkers() { view.removeMarker(m); } placed.clear(); + placeOverlays(); if (map().markers == null) { return; } @@ -252,6 +296,10 @@ private void placeMarkers() { if (mk.infoWindow() != null) { opts.title(mk.infoWindow().title()).snippet(mk.infoWindow().snippet()); } + com.codename1.ui.EncodedImage icon = mk.icon() == null ? null : mk.icon().toIcon(); + if (icon != null) { + opts.icon(icon); + } if (mk.tapHandler() != null) { opts.onClick(new ActionListener() { @Override @@ -264,6 +312,35 @@ public void actionPerformed(ActionEvent evt) { } } + /// The polylines and circles of the widget, replacing those placed before. + private void placeOverlays() { + for (com.codename1.maps.Polyline p : placedLines) { + view.removePolyline(p); + } + placedLines.clear(); + for (com.codename1.maps.Circle c : placedCircles) { + view.removeCircle(c); + } + placedCircles.clear(); + if (map().polylines != null) { + for (Object o : map().polylines) { + if (o instanceof Polyline && ((Polyline) o).visible()) { + placedLines.add(view.addPolyline(((Polyline) o).toCn1())); + } + } + } + if (map().circles != null) { + for (Object o : map().circles) { + if (o instanceof Circle && ((Circle) o).visible()) { + com.codename1.maps.Circle c = ((Circle) o).toCn1(); + if (c != null) { + placedCircles.add(view.addCircle(c)); + } + } + } + } + } + @Override protected Size performLayout(BoxConstraints constraints) { double w = constraints.hasBoundedWidth() ? constraints.maxWidth() : Dp.px(DEFAULT_SIZE_LP); diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/MapHit.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/MapHit.java new file mode 100644 index 00000000000..d7c935066fc --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/MapHit.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.googlemaps; + +/** + * Hit testing of taps against map overlays, in map coordinates. The core map reports + * where a tap landed as a latitude and longitude and has no tap for a line or shape, + * so the element tests the point itself: positions are projected to meters around the + * tap (equirectangular, exact enough over the few hundred meters a finger covers). + */ +final class MapHit { + private static final double METERS_PER_DEGREE_LAT = 111320.0; + private static final double EQUATOR_METERS_PER_PIXEL_AT_ZOOM_0 = 156543.03392; + + private MapHit() { + } + + /// Meters one device pixel covers at this latitude and zoom (256 px tiles). + static double metersPerPixel(double latitude, double zoom) { + return EQUATOR_METERS_PER_PIXEL_AT_ZOOM_0 * Math.cos(Math.toRadians(latitude)) / Math.pow(2, zoom); + } + + /// East offset in meters of {@code lng} from {@code lng0}, at latitude {@code lat0}. + private static double east(double lng0, double lng, double lat0) { + double d = lng - lng0; + while (d > 180) { + d -= 360; + } + while (d < -180) { + d += 360; + } + return d * METERS_PER_DEGREE_LAT * Math.cos(Math.toRadians(lat0)); + } + + private static double north(double lat0, double lat) { + return (lat - lat0) * METERS_PER_DEGREE_LAT; + } + + /// Distance in meters between two positions. + static double distance(LatLng a, LatLng b) { + double dx = east(a.longitude(), b.longitude(), a.latitude()); + double dy = north(a.latitude(), b.latitude()); + return Math.sqrt(dx * dx + dy * dy); + } + + /// Distance in meters from {@code p} to the segment {@code a}-{@code b}. + static double distanceToSegment(LatLng p, LatLng a, LatLng b) { + double ax = east(p.longitude(), a.longitude(), p.latitude()); + double ay = north(p.latitude(), a.latitude()); + double bx = east(p.longitude(), b.longitude(), p.latitude()); + double by = north(p.latitude(), b.latitude()); + double dx = bx - ax; + double dy = by - ay; + double len2 = dx * dx + dy * dy; + double t = len2 == 0 ? 0 : Math.max(0, Math.min(1, -(ax * dx + ay * dy) / len2)); + double cx = ax + t * dx; + double cy = ay + t * dy; + return Math.sqrt(cx * cx + cy * cy); + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/Marker.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/Marker.java index bfddf08079a..74a262781b4 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/Marker.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/Marker.java @@ -61,8 +61,15 @@ public void onTap(Funcs.VoidFunc0 v) { onTap = v; } - /** Accepted and ignored: markers use the platform's default pin. */ + /** The pin: a {@link BitmapDescriptor}, applied as the Codename One marker icon. */ public void icon(Object v) { + icon = v instanceof BitmapDescriptor ? (BitmapDescriptor) v : null; + } + + BitmapDescriptor icon; + + public BitmapDescriptor icon() { + return icon; } public void alpha(double v) { diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/Polyline.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/Polyline.java new file mode 100644 index 00000000000..f6825833148 --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/Polyline.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.googlemaps; + +import com.codename1.flutter.Color; + +import dart.runtime.Funcs; + +/** + * google_maps_flutter's {@code Polyline}, drawn as a Codename One + * {@code com.codename1.maps.Polyline}. Points, color, width and visibility are applied; + * geodesic, patterns, caps, joints and zIndex are accepted and not applied. The core + * map reports a tap as a position only, so the map element hit-tests it against the + * segments itself (within a few millimeters, or half the stroke width if wider). As in + * the plugin, {@code onTap} is called only for a polyline with + * {@code consumeTapEvents} set, and such a tap does not reach the map's own onTap. + */ +public class Polyline { + PolylineId polylineId; + Iterable points; + Color color; + long width = 10; + boolean visible = true; + Funcs.VoidFunc0 onTap; + boolean consumeTapEvents; + + public Polyline() { + } + + public void polylineId(PolylineId v) { + polylineId = v; + } + + public void points(Iterable v) { + points = v; + } + + public void color(Color v) { + color = v; + } + + public void width(long v) { + width = v; + } + + public void visible(boolean v) { + visible = v; + } + + public void geodesic(boolean v) { + } + + public void zIndex(long v) { + } + + public void consumeTapEvents(boolean v) { + consumeTapEvents = v; + } + + public void patterns(Object v) { + } + + public void startCap(Object v) { + } + + public void endCap(Object v) { + } + + public void jointType(Object v) { + } + + public void onTap(Funcs.VoidFunc0 v) { + onTap = v; + } + + public PolylineId polylineId() { + return polylineId; + } + + public Iterable points() { + return points; + } + + public long width() { + return width; + } + + public boolean visible() { + return visible; + } + + /** The core polyline this one draws as. */ + com.codename1.maps.Polyline toCn1() { + com.codename1.maps.Polyline p = new com.codename1.maps.Polyline(); + if (points != null) { + for (Object o : points) { + if (o instanceof LatLng) { + p.addPoint(((LatLng) o).toCn1()); + } + } + } + int argb = color == null ? 0xff000000 : color.argb(); + p.setStrokeColor(argb & 0xffffff); + p.setStrokeAlpha((argb >>> 24) & 0xff); + p.setStrokeWidth((int) Math.max(1, width)); + return p; + } + + /// Whether {@code at} lies within {@code toleranceMeters} of the line. + public boolean hitTest(LatLng at, double toleranceMeters) { + if (at == null || points == null) { + return false; + } + LatLng prev = null; + for (Object o : points) { + if (!(o instanceof LatLng)) { + continue; + } + LatLng cur = (LatLng) o; + double d = prev == null ? MapHit.distance(at, cur) : MapHit.distanceToSegment(at, prev, cur); + if (d <= toleranceMeters) { + return true; + } + prev = cur; + } + return false; + } + + /// Delivers a tap at {@code at}: answers whether this polyline consumed it, calling + /// onTap when it did. + public boolean tapAt(LatLng at, double toleranceMeters) { + if (!consumeTapEvents || !visible || !hitTest(at, toleranceMeters)) { + return false; + } + if (onTap != null) { + onTap.call(); + } + return true; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/PolylineId.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/PolylineId.java new file mode 100644 index 00000000000..4dd8549269e --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/googlemaps/PolylineId.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.googlemaps; + +/** google_maps_flutter's {@code PolylineId}. */ +public class PolylineId { + private final String value; + + public PolylineId(String value) { + this.value = value; + } + + public String value() { + return value; + } +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagepicker/ImagePicker.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagepicker/ImagePicker.java index 030f8f14881..d2d723204e1 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagepicker/ImagePicker.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/imagepicker/ImagePicker.java @@ -24,11 +24,20 @@ package com.codename1.flutter.plugins.imagepicker; import com.codename1.capture.Capture; +import com.codename1.capture.VideoCaptureConstraints; +import com.codename1.io.FileSystemStorage; +import com.codename1.io.Util; +import com.codename1.ui.Image; +import com.codename1.ui.util.ImageIO; import com.codename1.flutter.plugins.crossfile.XFile; import com.codename1.ui.Display; import com.codename1.ui.events.ActionEvent; import com.codename1.ui.events.ActionListener; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + import dart.async.Completer; import dart.async.Future; import dart.core.DartList; @@ -36,8 +45,16 @@ /** * image_picker's {@code ImagePicker}: the camera through Capture, the gallery through * {@code Display.openGallery}. A cancelled pick, or one with no display to pick on, - * answers null (an empty list for the multi-pickers), as image_picker does. The size - * and quality options are accepted; the picked file is returned as captured. + * answers null (an empty list for the multi-pickers), as image_picker does. + * + *

{@code maxWidth}/{@code maxHeight} scale a picked image down (aspect kept, never + * up) and {@code imageQuality} re-encodes it as JPEG at that quality, both through + * {@code ImageIO}, into a new file in the app home; with none of them set, or for a + * file that does not decode as an image (a video from pickMedia), the file is + * returned as picked. {@code pickVideo}'s {@code maxDuration} is passed to the camera + * as a {@code VideoCaptureConstraints} length, which a port may not honour; it cannot + * limit a video chosen from the gallery. {@code preferredCameraDevice} and + * {@code requestFullMetadata} are accepted and not applied.

*/ public class ImagePicker { @@ -46,26 +63,32 @@ public ImagePicker() { public Future pickImage(ImageSource source, Double maxWidth, Double maxHeight, Long imageQuality, Object preferredCameraDevice, Boolean requestFullMetadata) { - return pick(source, Display.GALLERY_IMAGE); + return pick(source, Display.GALLERY_IMAGE, new Shrink(maxWidth, maxHeight, imageQuality), 0); } public Future pickVideo(ImageSource source, Object preferredCameraDevice, Object maxDuration) { - return pick(source, Display.GALLERY_VIDEO); + int seconds = 0; + if (maxDuration instanceof dart.core.Duration) { + seconds = (int) (((dart.core.Duration) maxDuration).inMilliseconds() / 1000); + } + return pick(source, Display.GALLERY_VIDEO, null, seconds); } public Future pickMedia(Double maxWidth, Double maxHeight, Long imageQuality, Boolean requestFullMetadata) { - return pick(ImageSource.gallery, Display.GALLERY_ALL); + return pick(ImageSource.gallery, Display.GALLERY_ALL, new Shrink(maxWidth, maxHeight, imageQuality), 0); } public Future> pickMultiImage(Double maxWidth, Double maxHeight, Long imageQuality, Long limit, Boolean requestFullMetadata) { - return pickMany(Display.GALLERY_IMAGE_MULTI, Display.GALLERY_IMAGE, limit); + return pickMany(Display.GALLERY_IMAGE_MULTI, Display.GALLERY_IMAGE, limit, + new Shrink(maxWidth, maxHeight, imageQuality)); } public Future> pickMultipleMedia(Double maxWidth, Double maxHeight, Long imageQuality, Long limit, Boolean requestFullMetadata) { - return pickMany(Display.GALLERY_ALL_MULTI, Display.GALLERY_ALL, limit); + return pickMany(Display.GALLERY_ALL_MULTI, Display.GALLERY_ALL, limit, + new Shrink(maxWidth, maxHeight, imageQuality)); } private static Future> asList(Future one) { @@ -96,7 +119,8 @@ public void call(Object e) { /// `limit` is the plugin's maximum selection. Codename One's gallery cannot be told /// a maximum, so the picker may let the user choose more; the result is capped to /// the first `limit` files, so the app never receives more than it allowed. - private static Future> pickMany(int multiType, int singleType, Long limit) { + private static Future> pickMany(int multiType, int singleType, Long limit, + final Shrink shrink) { if (limit != null && limit.longValue() < 2) { return Future.error(new dart.core.ArgumentError("cannot be lower than 2: limit")); } @@ -105,7 +129,7 @@ private static Future> pickMany(int multiType, int singleType, L return Future.value(new DartList()); } if (!Display.getInstance().isGalleryTypeSupported(multiType)) { - return asList(pick(ImageSource.gallery, singleType)); + return asList(pick(ImageSource.gallery, singleType, shrink, 0)); } final Completer> done = new Completer>(); Display.getInstance().openGallery(new ActionListener() { @@ -118,10 +142,10 @@ public void actionPerformed(ActionEvent evt) { if (out.size() >= max) { break; } - out.add(new XFile(path)); + out.add(new XFile(shrink.apply(path))); } } else if (src instanceof String) { - out.add(new XFile((String) src)); + out.add(new XFile(shrink.apply((String) src))); } done.complete(out); } @@ -129,7 +153,73 @@ public void actionPerformed(ActionEvent evt) { return done.future(); } - private static Future pick(ImageSource source, int galleryType) { + /// The size and quality options of one pick. + static final class Shrink { + final Double maxWidth; + final Double maxHeight; + final Long quality; + + Shrink(Double maxWidth, Double maxHeight, Long quality) { + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + this.quality = quality; + } + + /// Target size for a w x h image: scaled down to fit both maxima, aspect kept. + static int[] fit(int w, int h, Double maxW, Double maxH) { + double s = 1; + if (maxW != null && maxW.doubleValue() > 0 && w > maxW.doubleValue()) { + s = Math.min(s, maxW.doubleValue() / w); + } + if (maxH != null && maxH.doubleValue() > 0 && h > maxH.doubleValue()) { + s = Math.min(s, maxH.doubleValue() / h); + } + return new int[] {Math.max(1, (int) Math.round(w * s)), Math.max(1, (int) Math.round(h * s))}; + } + + String apply(String path) { + if (path == null || (maxWidth == null && maxHeight == null && quality == null)) { + return path; + } + ImageIO io = ImageIO.getImageIO(); + if (io == null) { + return path; + } + FileSystemStorage fs = FileSystemStorage.getInstance(); + InputStream in = null; + OutputStream out = null; + try { + in = fs.openInputStream(path); + Image img = Image.createImage(in); + if (img == null) { + return path; + } + int[] size = fit(img.getWidth(), img.getHeight(), maxWidth, maxHeight); + if (size[0] != img.getWidth() || size[1] != img.getHeight()) { + img = img.scaled(size[0], size[1]); + } + long q = quality == null ? 100 : Math.max(0, Math.min(100, quality.longValue())); + String target = fs.getAppHomePath() + "image_picker_" + System.currentTimeMillis() + + "_" + (counter++) + ".jpg"; + out = fs.openOutputStream(target); + io.save(img, out, ImageIO.FORMAT_JPEG, q / 100f); + return target; + } catch (IOException e) { + return path; + } catch (RuntimeException e) { + // not an image (a picked video): hand back the file as it was + return path; + } finally { + Util.cleanup(in); + Util.cleanup(out); + } + } + } + + private static int counter; + + private static Future pick(ImageSource source, int galleryType, final Shrink shrink, + int maxSeconds) { if (!Display.isInitialized()) { return Future.value(null); } @@ -138,12 +228,17 @@ private static Future pick(ImageSource source, int galleryType) { @Override public void actionPerformed(ActionEvent evt) { Object src = evt == null ? null : evt.getSource(); - done.complete(src instanceof String ? new XFile((String) src) : null); + done.complete(src instanceof String + ? new XFile(shrink == null ? (String) src : shrink.apply((String) src)) : null); } }; if (source == ImageSource.camera) { if (galleryType == Display.GALLERY_VIDEO) { - Capture.captureVideo(onPicked); + if (maxSeconds > 0) { + Capture.captureVideo(new VideoCaptureConstraints(0, 0, maxSeconds), onPicked); + } else { + Capture.captureVideo(onPicked); + } } else { Capture.capturePhoto(onPicked); } diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/notifications/FlutterLocalNotificationsPlugin.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/notifications/FlutterLocalNotificationsPlugin.java index e12688a8350..ef450eb1adf 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/notifications/FlutterLocalNotificationsPlugin.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/notifications/FlutterLocalNotificationsPlugin.java @@ -39,11 +39,22 @@ * {@code onDidReceiveNotificationResponse} callback through {@link #notificationTapped}, * which the imported app's entry point calls from its LocalNotificationCallback. * Notifications are identified to the platform as {@code "cn1flutter-" + id}. + * + *

{@code onDidReceiveBackgroundNotificationResponse} is called too, for a tap that + * reaches the app while it is minimized or before its first form is shown (Codename One + * has no background isolate, so it runs on the app's own thread, before the foreground + * callback). {@code resolvePlatformSpecificImplementation} answers this object for every + * platform type; {@code createNotificationChannel} records the channel's sound + * ({@link RawResourceAndroidNotificationSound}) and a notification shown on that channel + * plays it. Channel importance, lights and vibration are accepted and not applied: + * LocalNotification has no channel of its own. */ public class FlutterLocalNotificationsPlugin { private static final String PREFIX = "cn1flutter-"; private static final Map PAYLOADS = new HashMap(); private static Funcs.VoidFunc1 onTap; + private static Funcs.VoidFunc1 onBackgroundTap; + private static final Map CHANNEL_SOUNDS = new HashMap(); private static NotificationResponse launchResponse; private static int shown; @@ -52,8 +63,9 @@ public FlutterLocalNotificationsPlugin() { public Future initialize(NotificationOptions settings, Funcs.VoidFunc1 onDidReceiveNotificationResponse, - Object onDidReceiveBackgroundNotificationResponse) { + Funcs.VoidFunc1 onDidReceiveBackgroundNotificationResponse) { onTap = onDidReceiveNotificationResponse; + onBackgroundTap = onDidReceiveBackgroundNotificationResponse; return Future.value(Boolean.TRUE); } @@ -70,6 +82,12 @@ public static void notificationTapped(String notificationId) { } NotificationResponse response = new NotificationResponse(id, payload(notificationId)); Funcs.VoidFunc1 cb = onTap; + Funcs.VoidFunc1 bg = onBackgroundTap; + boolean background = !Display.isInitialized() || Display.getInstance().getCurrent() == null + || Display.getInstance().isMinimized(); + if (background && bg != null) { + bg.call(response); + } if (cb != null) { cb.call(response); } else { @@ -145,9 +163,9 @@ private static void schedule(long id, String title, String body, NotificationOpt n.setId(key); n.setAlertTitle(title == null ? "" : title); n.setAlertBody(body == null ? "" : body); - Object sound = details == null ? null : details.find("sound"); - if (sound instanceof String) { - n.setAlertSound((String) sound); + String sound = soundOf(details); + if (sound != null) { + n.setAlertSound(sound); } Object badge = details == null ? null : details.find("badgeNumber"); if (badge instanceof Number) { @@ -157,6 +175,33 @@ private static void schedule(long id, String title, String body, NotificationOpt Display.getInstance().scheduleLocalNotification(n, at, repeat); } + /// The sound a notification plays: its own, else its Android channel's. + static String soundOf(NotificationOptions details) { + if (details == null) { + return null; + } + String own = soundPath(details.find("sound")); + if (own != null) { + return own; + } + Object android = details.get("android"); + Object channel = android instanceof NotificationOptions + ? ((NotificationOptions) android).get("arg0") : details.get("arg0"); + return channel instanceof String ? CHANNEL_SOUNDS.get(channel) : null; + } + + private static String soundPath(Object sound) { + if (sound instanceof RawResourceAndroidNotificationSound) { + return ((RawResourceAndroidNotificationSound) sound).alertSoundPath(); + } + return sound instanceof String ? (String) sound : null; + } + + /** The sound a notification with these details would play; null when none. */ + public static String alertSoundFor(NotificationOptions details) { + return soundOf(details); + } + public Future cancel(long id, String tag) { String key = PREFIX + id; removePayload(key); @@ -316,17 +361,20 @@ private static void removeMeta(String key) { /** * The platform-specific plugin object ({@code AndroidFlutterLocalNotificationsPlugin} - * and so on). There is none here; callers use {@code ?.}, which skips the call. + * and so on): this object, which implements all of them. */ + @SuppressWarnings("unchecked") public T resolvePlatformSpecificImplementation(Class witness) { - return null; + // Every platform type of the stub names this class, so this is the right object + // whichever one the app asked for; no cast happens here (T is erased). + return (T) this; } /** * The platform-specific permission requests. Reached only through the stub's - * {@code Android/IOSFlutterLocalNotificationsPlugin} types, which map to this class; - * {@link #resolvePlatformSpecificImplementation} returns null, so they normally never - * run. Codename One asks for notification permission when it first shows one. + * {@code Android/IOSFlutterLocalNotificationsPlugin} types, which map to this class. + * Codename One asks for notification permission when it first shows one, so they + * answer true. */ public Future requestNotificationsPermission() { return Future.value(Boolean.TRUE); @@ -342,6 +390,14 @@ public Future requestExactAlarmsPermission() { } public Future createNotificationChannel(Object channel) { + if (channel instanceof NotificationOptions) { + NotificationOptions c = (NotificationOptions) channel; + Object id = c.get("arg0"); + String sound = soundPath(c.get("sound")); + if (id instanceof String && sound != null) { + CHANNEL_SOUNDS.put((String) id, sound); + } + } return Future.value(null); } } diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/notifications/RawResourceAndroidNotificationSound.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/notifications/RawResourceAndroidNotificationSound.java new file mode 100644 index 00000000000..0717aacce0f --- /dev/null +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/notifications/RawResourceAndroidNotificationSound.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.plugins.notifications; + +/** + * {@code RawResourceAndroidNotificationSound('name')}: an Android {@code res/raw} + * sound. Codename One's LocalNotification plays a file bundled in the app's default + * package whose name starts with {@code notification_sound}, so {@code 'bells'} maps to + * {@code /notification_sound_bells.mp3} (a name that already has the prefix keeps it, and + * a name with an extension keeps that extension). The file must be bundled under that + * name; Android's raw resource folder is not read. + */ +public class RawResourceAndroidNotificationSound { + private final String sound; + + public RawResourceAndroidNotificationSound(String sound) { + this.sound = sound; + } + + /** The resource name as the app wrote it. */ + public String sound() { + return sound; + } + + /** The path handed to {@code LocalNotification.setAlertSound}. */ + public String alertSoundPath() { + if (sound == null || sound.length() == 0) { + return null; + } + String name = sound.startsWith("/") ? sound.substring(1) : sound; + if (!name.startsWith("notification_sound")) { + name = "notification_sound_" + name; + } + if (name.indexOf('.') < 0) { + name = name + ".mp3"; + } + return "/" + name; + } +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/dart_io.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/dart_io.dart index 3a0e3c52073..47e1cc51dcb 100644 --- a/maven/flutter-runtime/src/main/resources/META-INF/dart/dart_io.dart +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/dart_io.dart @@ -334,5 +334,6 @@ abstract class WebSocket extends Stream { external String? get closeReason; external String? get protocol; external String get extensions; + external Duration? get pingInterval; external set pingInterval(Duration? interval); } diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/dart_ui_picture.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/dart_ui_picture.dart new file mode 100644 index 00000000000..21e9300c177 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/dart_ui_picture.dart @@ -0,0 +1,33 @@ +// dart:ui offscreen drawing: PictureRecorder -> Canvas(recorder) -> endRecording() +// -> Picture.toImage(w, h) -> Image.toByteData(format: ImageByteFormat.png). +// The picture is replayed onto a mutable Codename One image; PNG bytes come from the +// port's ImageIO, or a built-in encoder headless. dart:ui's Image is UiImage here, +// because `Image` is the widget's name in these stubs. + +@JavaName('com.codename1.flutter.ImageByteFormat') +enum ImageByteFormat { rawRgba, rawStraightRgba, rawUnmodified, rawExtendedRgba128, png } + +@JavaName('com.codename1.flutter.PictureRecorder') +class PictureRecorder { + external PictureRecorder(); + external bool get isRecording; + external Picture endRecording(); +} + +@JavaName('com.codename1.flutter.Picture') +class Picture { + external Future toImage(int width, int height); + external UiImage toImageSync(int width, int height); + external int get approximateBytesUsed; + external void dispose(); + external bool get debugDisposed; +} + +@JavaName('com.codename1.flutter.UiImage') +class UiImage { + external int get width; + external int get height; + external Future toByteData({ImageByteFormat format}); + external void dispose(); + external bool get debugDisposed; +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_dartCore.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_dartCore.dart index 3da04d96a41..e168bb7cade 100644 --- a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_dartCore.dart +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_dartCore.dart @@ -195,6 +195,14 @@ class ByteData { external void setUint8(int byteOffset, int value); external int getInt32(int byteOffset); external void setInt32(int byteOffset, int value); + external ByteBuffer get buffer; +} + +@JavaName('dart.typed_data.ByteBuffer') +class ByteBuffer { + external int get lengthInBytes; + external Uint8List asUint8List(); + external ByteData asByteData(); } // --- dart:math ------------------------------------------------------------- @@ -404,30 +412,75 @@ external bool debugCheckHasDirectionality(BuildContext context); @JavaName('com.codename1.flutter.l10n.LocalizationsDelegate') class LocalizationsDelegate { + external Future load(Locale locale); external bool shouldReload(LocalizationsDelegate old); } @JavaName('com.codename1.flutter.l10n.MaterialLocalizations') abstract class MaterialLocalizations { external static MaterialLocalizations of(BuildContext context); - external static LocalizationsDelegate get delegate; + external static LocalizationsDelegate get delegate; + external String get okButtonLabel; + external String get cancelButtonLabel; + external String get closeButtonLabel; + external String get continueButtonLabel; + external String get copyButtonLabel; + external String get cutButtonLabel; + external String get pasteButtonLabel; + external String get selectAllButtonLabel; + external String get searchFieldLabel; external String get backButtonTooltip; external String get closeButtonTooltip; - external String get closeButtonLabel; - external String get viewLicensesButtonLabel; + external String get deleteButtonTooltip; + external String get moreButtonTooltip; external String get nextPageTooltip; external String get previousPageTooltip; external String get openAppDrawerTooltip; + external String get showMenuTooltip; + external String get drawerLabel; + external String get popupMenuLabel; + external String get dialogLabel; + external String get alertDialogLabel; + external String get datePickerHelpText; + external String get dateHelpText; + external String get timePickerDialHelpText; + external String get refreshIndicatorSemanticLabel; + external String get modalBarrierDismissLabel; + external String get viewLicensesButtonLabel; + external String get licensesPageTitle; + external String formatMediumDate(DateTime date); + external String formatFullDate(DateTime date); + external String formatMonthYear(DateTime date); + external List get narrowWeekdays; + external int get firstDayOfWeekIndex; +} + +@JavaName('com.codename1.flutter.l10n.CupertinoLocalizations') +abstract class CupertinoLocalizations { + external static CupertinoLocalizations of(BuildContext context); + external static LocalizationsDelegate get delegate; + external String get alertDialogLabel; + external String get anteMeridiemAbbreviation; + external String get postMeridiemAbbreviation; + external String get copyButtonLabel; + external String get cutButtonLabel; + external String get pasteButtonLabel; + external String get selectAllButtonLabel; + external String get searchTextFieldPlaceholderLabel; + external String get modalBarrierDismissLabel; + external String get todayLabel; + external String get cancelButtonLabel; + external String get backButtonLabel; } @JavaName('com.codename1.flutter.l10n.GlobalMaterialLocalizations') abstract class GlobalMaterialLocalizations { - external static LocalizationsDelegate get delegate; + external static LocalizationsDelegate get delegate; } @JavaName('com.codename1.flutter.l10n.GlobalCupertinoLocalizations') abstract class GlobalCupertinoLocalizations { - external static LocalizationsDelegate get delegate; + external static LocalizationsDelegate get delegate; } @JavaName('com.codename1.flutter.l10n.GlobalWidgetsLocalizations') diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_geometryPaint.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_geometryPaint.dart index cef3356c1fd..8fc91917719 100644 --- a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_geometryPaint.dart +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_geometryPaint.dart @@ -216,6 +216,7 @@ class RRect { @JavaName('com.codename1.flutter.Canvas') class Canvas { + external Canvas(PictureRecorder recorder, [Rect? cullRect]); external void drawPath(Path path, Paint paint); external void drawRect(Rect rect, Paint paint); external void drawRRect(RRect rrect, Paint paint); diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p4_apitail.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p4_apitail.dart index 15a60a8ca8d..7e36219048f 100644 --- a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p4_apitail.dart +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p4_apitail.dart @@ -303,6 +303,9 @@ class SchedulerBinding { // url_launcher package // ====================================================================== +@JavaName('com.codename1.flutter.services.LaunchMode') +enum LaunchMode { platformDefault, inAppWebView, inAppBrowserView, externalApplication, externalNonBrowserApplication } + @JavaName('com.codename1.flutter.services.UrlLauncher.launchUrl') external Future launchUrl(Uri url, {Object? mode, Object? webOnlyWindowName}); diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_cached_network_image.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_cached_network_image.dart index a045338f613..9213e37a057 100644 --- a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_cached_network_image.dart +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_cached_network_image.dart @@ -1,13 +1,26 @@ -// package:cached_network_image, over Image.network, which already caches what it -// downloads. +// package:cached_network_image, fetching the bytes itself so the placeholder, progress and error +// builders run. @JavaName('com.codename1.flutter.plugins.cachednetworkimage.CachedNetworkImage') class CachedNetworkImage extends Widget { external CachedNetworkImage({Key? key, required String imageUrl, double? width, double? height, BoxFit? fit, Widget Function(BuildContext context, String url)? placeholder, Widget Function(BuildContext context, String url, Object error)? errorWidget, + Widget Function(BuildContext context, String url, DownloadProgress progress)? progressIndicatorBuilder, Map? httpHeaders, int? memCacheWidth, int? memCacheHeight, Duration? fadeInDuration, String? cacheKey}); + external Widget buildOnce(BuildContext? context); + external Widget buildLoading(BuildContext? context, int downloaded, int? total); + external void seedCache(Uint8List data); +} + +@JavaName('com.codename1.flutter.plugins.cachednetworkimage.DownloadProgress') +class DownloadProgress { + external DownloadProgress(String originalUrl, int? totalSize, int downloaded); + external String get originalUrl; + external int? get totalSize; + external int get downloaded; + external double? get progress; } @JavaName('com.codename1.flutter.NetworkImage') diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_dio.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_dio.dart index d10a1febed6..77ebfddb418 100644 --- a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_dio.dart +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_dio.dart @@ -132,6 +132,7 @@ class DioError extends DioException { class CancelToken { external CancelToken(); external bool get isCancelled; + external static bool isCancel(DioException e); external void cancel([Object? reason]); } @@ -202,6 +203,8 @@ class FormData { external FormData(); external static FormData fromMap(Map map, [Object? collectionFormat, bool camelCaseContentDisposition = false]); external int get length; + external List> get fields; + external List> get files; } @JavaName('com.codename1.flutter.plugins.dio.Dio') @@ -212,18 +215,21 @@ class Dio { external Interceptors get interceptors; external void close({bool force}); external Future> get(String path, {Object? data, Map? queryParameters, - Options? options, CancelToken? cancelToken, Object? onReceiveProgress}); + Options? options, CancelToken? cancelToken, void Function(int count, int total)? onReceiveProgress}); external Future> post(String path, {Object? data, Map? queryParameters, - Options? options, CancelToken? cancelToken, Object? onSendProgress, Object? onReceiveProgress}); + Options? options, CancelToken? cancelToken, void Function(int count, int total)? onSendProgress, void Function(int count, int total)? onReceiveProgress}); external Future> put(String path, {Object? data, Map? queryParameters, - Options? options, CancelToken? cancelToken, Object? onSendProgress, Object? onReceiveProgress}); + Options? options, CancelToken? cancelToken, void Function(int count, int total)? onSendProgress, void Function(int count, int total)? onReceiveProgress}); external Future> patch(String path, {Object? data, Map? queryParameters, - Options? options, CancelToken? cancelToken, Object? onSendProgress, Object? onReceiveProgress}); + Options? options, CancelToken? cancelToken, void Function(int count, int total)? onSendProgress, void Function(int count, int total)? onReceiveProgress}); external Future> delete(String path, {Object? data, Map? queryParameters, Options? options, CancelToken? cancelToken}); external Future> head(String path, {Object? data, Map? queryParameters, Options? options, CancelToken? cancelToken}); external Future> request(String path, {Object? data, Map? queryParameters, - CancelToken? cancelToken, Options? options, Object? onSendProgress, Object? onReceiveProgress}); + CancelToken? cancelToken, Options? options, void Function(int count, int total)? onSendProgress, void Function(int count, int total)? onReceiveProgress}); + external Future> download(String urlPath, dynamic savePath, {void Function(int count, int total)? onReceiveProgress, + Map? queryParameters, CancelToken? cancelToken, bool? deleteOnError, + Object? fileAccessMode, String? lengthHeader, Object? data, Options? options}); external Future> fetch(RequestOptions requestOptions); } diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_emoji_picker_flutter.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_emoji_picker_flutter.dart index 75dda30acdd..76f2bbd9d62 100644 --- a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_emoji_picker_flutter.dart +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_emoji_picker_flutter.dart @@ -1,3 +1,4 @@ +// cn1:package emoji_picker_flutter // package:emoji_picker_flutter, drawn with Codename One components from a // built-in emoji set. Config is accepted; recents persist in Preferences. diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase.dart index bd464f203fd..2da0931561e 100644 --- a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase.dart +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase.dart @@ -86,6 +86,7 @@ class OAuthCredential extends AuthCredential { @JavaName('com.codename1.flutter.plugins.firebase.EmailAuthProvider') class EmailAuthProvider { + external EmailAuthProvider(); external static String get PROVIDER_ID; external static String get EMAIL_PASSWORD_SIGN_IN_METHOD; external static AuthCredential credential({required String email, required String password}); diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase_ai.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase_ai.dart new file mode 100644 index 00000000000..5c76de45e65 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase_ai.dart @@ -0,0 +1,132 @@ +// cn1:package firebase_ai +// firebase_ai (Firebase AI Logic, Gemini) over the REST API at firebasevertexai.googleapis.com, +// authenticated with the apiKey of the firebase pack's FirebaseApp. generateContentStream is +// one event: the whole generateContent reply. Function calling, toolConfig and response +// schemas are accepted and not applied. + +@JavaName('com.codename1.flutter.plugins.firebaseai.FirebaseAIException') +class FirebaseAIException implements Exception { + external String get message; +} + +@JavaName('com.codename1.flutter.plugins.firebaseai.HarmCategory') +enum HarmCategory { unknown, harassment, hateSpeech, sexuallyExplicit, dangerousContent } + +@JavaName('com.codename1.flutter.plugins.firebaseai.HarmBlockThreshold') +enum HarmBlockThreshold { low, medium, high, none, off } + +@JavaName('com.codename1.flutter.plugins.firebaseai.FinishReason') +enum FinishReason { unknown, stop, maxTokens, safety, recitation, other } + +@JavaName('com.codename1.flutter.plugins.firebaseai.SafetySetting') +class SafetySetting { + external SafetySetting(HarmCategory category, HarmBlockThreshold threshold, [Object? method]); + external HarmCategory get category; + external HarmBlockThreshold get threshold; + external Map toJson(); +} + +@JavaName('com.codename1.flutter.plugins.firebaseai.Tool') +class Tool { + external static Tool functionDeclarations(List functionDeclarations); + external static Tool googleSearch([Object? googleSearch]); + external static Tool codeExecution(); +} + +@JavaName('com.codename1.flutter.plugins.firebaseai.GenerationConfig') +class GenerationConfig { + external GenerationConfig({int? candidateCount, List? stopSequences, int? maxOutputTokens, + double? temperature, double? topP, int? topK, String? responseMimeType, Object? responseSchema}); + external Map toJson(); +} + +@JavaName('com.codename1.flutter.plugins.firebaseai.Part') +abstract class Part { + external Object toJson(); +} + +@JavaName('com.codename1.flutter.plugins.firebaseai.TextPart') +class TextPart extends Part { + external TextPart(String text); + external String get text; +} + +@JavaName('com.codename1.flutter.plugins.firebaseai.InlineDataPart') +class InlineDataPart extends Part { + external InlineDataPart(String mimeType, Uint8List bytes, {bool? willContinue}); + external String get mimeType; + external Uint8List get bytes; +} + +@JavaName('com.codename1.flutter.plugins.firebaseai.Content') +class Content { + external Content(String? role, Iterable parts); + external static Content text(String text); + external static Content multi(Iterable parts); + external static Content model(Iterable parts); + external static Content data(String mimeType, Uint8List bytes); + external String? get role; + external List get parts; + external Map toJson(); +} + +@JavaName('com.codename1.flutter.plugins.firebaseai.UsageMetadata') +class UsageMetadata { + external int? get promptTokenCount; + external int? get candidatesTokenCount; + external int? get totalTokenCount; +} + +@JavaName('com.codename1.flutter.plugins.firebaseai.CountTokensResponse') +class CountTokensResponse { + external int get totalTokens; + external int? get totalBillableCharacters; +} + +@JavaName('com.codename1.flutter.plugins.firebaseai.Candidate') +class Candidate { + external Content get content; + external FinishReason? get finishReason; + external String? get finishMessage; + external String? get text; +} + +@JavaName('com.codename1.flutter.plugins.firebaseai.GenerateContentResponse') +class GenerateContentResponse { + external List get candidates; + external UsageMetadata? get usageMetadata; + external String? get text; + // Codename One only: parses a generateContent reply (an `error` object throws FirebaseAIException). + external static GenerateContentResponse cn1FromJson(String json); +} + +@JavaName('com.codename1.flutter.plugins.firebaseai.ChatSession') +class ChatSession { + external Iterable get history; + external Future sendMessage(Content message); + external Stream sendMessageStream(Content message); +} + +@JavaName('com.codename1.flutter.plugins.firebaseai.GenerativeModel') +class GenerativeModel { + external Future generateContent(Iterable prompt, + {List? safetySettings, GenerationConfig? generationConfig, List? tools, + Object? toolConfig}); + external Stream generateContentStream(Iterable prompt, + {List? safetySettings, GenerationConfig? generationConfig, List? tools, + Object? toolConfig}); + external Future countTokens(Iterable contents); + external ChatSession startChat({List? history, List? safetySettings, + GenerationConfig? generationConfig}); +} + +@JavaName('com.codename1.flutter.plugins.firebaseai.FirebaseAI') +class FirebaseAI { + external static FirebaseAI googleAI({FirebaseApp? app, FirebaseAppCheck? appCheck, FirebaseAuth? auth}); + external static FirebaseAI vertexAI({FirebaseApp? app, FirebaseAppCheck? appCheck, FirebaseAuth? auth, + String? location}); + external FirebaseApp get app; + external String? get location; + external GenerativeModel generativeModel({required String model, GenerationConfig? generationConfig, + List? safetySettings, List? tools, Object? toolConfig, Content? systemInstruction}); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase_app_check.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase_app_check.dart new file mode 100644 index 00000000000..85423bbb5ee --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase_app_check.dart @@ -0,0 +1,34 @@ +// cn1:package firebase_app_check +// firebase_app_check over the firebase pack. Codename One has no Play Integrity, +// DeviceCheck or App Attest attestation, so the providers are accepted and only the +// debug provider yields a token: the Display property `firebase.appCheck.debugToken` +// (a debug token registered in the Firebase console). Without it getToken answers null. + +@JavaName('com.codename1.flutter.plugins.firebase.AndroidProvider') +enum AndroidProvider { debug, playIntegrity, safetyNet } + +@JavaName('com.codename1.flutter.plugins.firebase.AppleProvider') +enum AppleProvider { debug, deviceCheck, appAttest, appAttestWithDeviceCheckFallback } + +@JavaName('com.codename1.flutter.plugins.firebase.ReCaptchaV3Provider') +class ReCaptchaV3Provider { + external ReCaptchaV3Provider(String siteKey); +} + +@JavaName('com.codename1.flutter.plugins.firebase.ReCaptchaV3Provider') +class ReCaptchaEnterpriseProvider { + external ReCaptchaEnterpriseProvider(String siteKey); +} + +@JavaName('com.codename1.flutter.plugins.firebase.FirebaseAppCheck') +class FirebaseAppCheck { + external static FirebaseAppCheck get instance; + external static FirebaseAppCheck instanceFor({required Object app}); + external Future activate({Object? webProvider, Object? providerWeb, AndroidProvider? androidProvider, + AppleProvider? appleProvider, Object? providerAndroid, Object? providerApple}); + external Future getToken([bool? forceRefresh]); + external Future getLimitedUseToken(); + external Future setTokenAutoRefreshEnabled(bool isTokenAutoRefreshEnabled); + external Stream get onTokenChange; + external bool get isTokenAutoRefreshEnabled; +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase_ui.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase_ui.dart new file mode 100644 index 00000000000..f51a15d7857 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_firebase_ui.dart @@ -0,0 +1,85 @@ +// cn1:package firebase_ui_oauth_apple +// package:firebase_ui_auth, firebase_ui_oauth, firebase_ui_oauth_google and +// firebase_ui_oauth_apple: SignInScreen and ProfileScreen built from the runtime's +// Material widgets, over firebase_auth. EmailAuthProvider is firebase_auth's own class, +// which also stands for firebase_ui_auth's (an app imports one and hides the other). + +@JavaName('com.codename1.flutter.plugins.firebaseui.AuthAction') +enum AuthAction { signIn, signUp, link, none } + +@JavaName('com.codename1.flutter.plugins.firebaseui.AuthState') +class AuthState { +} + +@JavaName('com.codename1.flutter.plugins.firebaseui.SignedIn') +class SignedIn extends AuthState { + external User? get user; +} + +@JavaName('com.codename1.flutter.plugins.firebaseui.UserCreated') +class UserCreated extends AuthState { + external UserCredential get credential; +} + +@JavaName('com.codename1.flutter.plugins.firebaseui.AuthFailed') +class AuthFailed extends AuthState { + external Exception get exception; +} + +@JavaName('com.codename1.flutter.plugins.firebaseui.AuthStateChangeAction') +class AuthStateChangeAction { + external AuthStateChangeAction(void Function(BuildContext context, T state) callback); +} + +@JavaName('com.codename1.flutter.plugins.firebaseui.SignedOutAction') +class SignedOutAction { + external SignedOutAction(void Function(BuildContext context) callback); +} + +@JavaName('com.codename1.flutter.plugins.firebaseui.FirebaseUIAuth') +class FirebaseUIAuth { + external static void configureProviders(List configs, {FirebaseApp? app}); + external static List providersFor(FirebaseApp app); +} + +@JavaName('com.codename1.flutter.plugins.firebaseui.SignInScreen') +class SignInScreen extends StatefulWidget { + @Unapplied('sideBuilder, breakpoint, styles, headerMaxExtent, oauthButtonVariant, showPasswordVisibilityToggle, email') + external SignInScreen({Key? key, List? providers, List actions, FirebaseAuth? auth, + Widget Function(BuildContext context, BoxConstraints constraints, double shrinkOffset)? headerBuilder, + Widget Function(BuildContext context, AuthAction action)? subtitleBuilder, + Widget Function(BuildContext context, AuthAction action)? footerBuilder, + Widget Function(BuildContext context, BoxConstraints constraints)? sideBuilder, + bool showAuthActionSwitch, AuthAction? initialAuthAction, double breakpoint, Object? styles, + double? headerMaxExtent, Object? oauthButtonVariant, bool showPasswordVisibilityToggle, String? email, + bool? resizeToAvoidBottomInset}); +} + +@JavaName('com.codename1.flutter.plugins.firebaseui.ProfileScreen') +class ProfileScreen extends StatelessWidget { + @Unapplied('providers, avatar, avatarSize, showDeleteConfirmationDialog') + external ProfileScreen({Key? key, List actions, List children, PreferredSizeWidget? appBar, + FirebaseAuth? auth, List? providers, Widget? avatar, double? avatarSize, + bool showDeleteConfirmationDialog}); +} + +@JavaName('com.codename1.flutter.plugins.firebaseui.OAuthProviderButton') +class OAuthProviderButton extends StatelessWidget { + @Unapplied('action, variant') + external OAuthProviderButton({Key? key, required Object provider, FirebaseAuth? auth, AuthAction? action, + Object? variant}); +} + +@JavaName('com.codename1.flutter.plugins.firebaseuigoogle.GoogleProvider') +class GoogleProvider { + @Unapplied('redirectUri, iOSPreferPlist') + external GoogleProvider({required String clientId, String? redirectUri, List scopes, + bool iOSPreferPlist}); + external String get providerId; +} + +@JavaName('com.codename1.flutter.plugins.firebaseuiapple.AppleProvider') +class AppleProvider { + external AppleProvider({Object? scopes}); + external String get providerId; +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_local_notifications.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_local_notifications.dart index 3b013dc7cfa..d73ac50f986 100644 --- a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_local_notifications.dart +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_local_notifications.dart @@ -94,12 +94,19 @@ class AndroidNotificationChannel { external AndroidNotificationChannel(String id, String name, {Object? android, Object? iOS, Object? macOS, Object? linux, Object? windows, Object? importance, Object? priority, Object? channelDescription, Object? icon, Object? playSound, Object? enableVibration, Object? sound, Object? ticker, Object? styleInformation, Object? color, Object? largeIcon, Object? showWhen, Object? autoCancel, Object? ongoing, Object? category, Object? groupKey, Object? channelShowBadge, Object? visibility, Object? fullScreenIntent, Object? presentAlert, Object? presentBadge, Object? presentSound, Object? presentBanner, Object? presentList, Object? badgeNumber, Object? subtitle, Object? threadIdentifier, Object? attachments, Object? requestAlertPermission, Object? requestBadgePermission, Object? requestSoundPermission, Object? defaultPresentAlert, Object? defaultPresentBadge, Object? defaultPresentSound, Object? defaultPresentBanner, Object? defaultPresentList, Object? notificationCategories, Object? enableLights, Object? ledColor, Object? timeoutAfter, Object? onlyAlertOnce, Object? actions, Object? number, Object? tag}); } +@JavaName('com.codename1.flutter.plugins.notifications.RawResourceAndroidNotificationSound') +class RawResourceAndroidNotificationSound { + external RawResourceAndroidNotificationSound(String sound); + external String get sound; + external String? alertSoundPath(); +} + @JavaName('com.codename1.flutter.plugins.notifications.FlutterLocalNotificationsPlugin') class FlutterLocalNotificationsPlugin { external FlutterLocalNotificationsPlugin(); external Future initialize(InitializationSettings initializationSettings, {void Function(NotificationResponse details)? onDidReceiveNotificationResponse, - Object? onDidReceiveBackgroundNotificationResponse}); + void Function(NotificationResponse details)? onDidReceiveBackgroundNotificationResponse}); external Future show(int id, String? title, String? body, NotificationDetails? notificationDetails, {String? payload}); external Future periodicallyShow(int id, String? title, String? body, RepeatInterval repeatInterval, @@ -115,8 +122,8 @@ class FlutterLocalNotificationsPlugin { external T? resolvePlatformSpecificImplementation([Type? witness]); } -// What resolvePlatformSpecificImplementation would return; it returns null here, -// so the usual `?.requestNotificationsPermission()` is skipped. +// What resolvePlatformSpecificImplementation returns: the plugin itself, for every +// platform type. @JavaName('com.codename1.flutter.plugins.notifications.FlutterLocalNotificationsPlugin') class AndroidFlutterLocalNotificationsPlugin { external Future requestNotificationsPermission(); diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_svg.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_svg.dart index e67f5222b30..47a3c5230c8 100644 --- a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_svg.dart +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_flutter_svg.dart @@ -8,22 +8,27 @@ class ColorFilter { @JavaName('com.codename1.flutter.plugins.svg.SvgPicture') class SvgPicture extends Widget { + @Unapplied('fit, alignment, colorFilter, placeholderBuilder, matchTextDirection, theme') external static SvgPicture asset(String assetName, {Key? key, double? width, double? height, BoxFit fit, dynamic alignment, String? package, ColorFilter? colorFilter, String? semanticsLabel, Widget Function(BuildContext context)? placeholderBuilder, bool matchTextDirection, bool excludeFromSemantics, dynamic clipBehavior, dynamic theme}); + @Unapplied('fit, alignment, colorFilter, placeholderBuilder, matchTextDirection, theme') external static SvgPicture network(String url, {Key? key, double? width, double? height, BoxFit fit, dynamic alignment, Map? headers, ColorFilter? colorFilter, String? semanticsLabel, Widget Function(BuildContext context)? placeholderBuilder, bool matchTextDirection, bool excludeFromSemantics, dynamic clipBehavior, dynamic theme}); + @Unapplied('fit, alignment, colorFilter, placeholderBuilder, matchTextDirection, theme') external static SvgPicture string(String string, {Key? key, double? width, double? height, BoxFit fit, dynamic alignment, ColorFilter? colorFilter, String? semanticsLabel, Widget Function(BuildContext context)? placeholderBuilder, bool matchTextDirection, bool excludeFromSemantics, dynamic clipBehavior, dynamic theme}); + @Unapplied('fit, alignment, colorFilter, placeholderBuilder, matchTextDirection, theme') external static SvgPicture file(File file, {Key? key, double? width, double? height, BoxFit fit, dynamic alignment, ColorFilter? colorFilter, String? semanticsLabel, Widget Function(BuildContext context)? placeholderBuilder, bool matchTextDirection, bool excludeFromSemantics, dynamic clipBehavior, dynamic theme}); + @Unapplied('fit, alignment, colorFilter, placeholderBuilder, matchTextDirection, theme') external static SvgPicture memory(Uint8List bytes, {Key? key, double? width, double? height, BoxFit fit, dynamic alignment, ColorFilter? colorFilter, String? semanticsLabel, Widget Function(BuildContext context)? placeholderBuilder, diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_google_maps_flutter.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_google_maps_flutter.dart index 5bc80c7409b..aa501173a85 100644 --- a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_google_maps_flutter.dart +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_google_maps_flutter.dart @@ -47,6 +47,15 @@ class InfoWindow { class BitmapDescriptor { external static BitmapDescriptor get defaultMarker; external static BitmapDescriptor defaultMarkerWithHue(double hue); + external static BitmapDescriptor fromBytes(Uint8List byteData, {Object? size}); + external static BitmapDescriptor bytes(Uint8List byteData, {double? imagePixelRatio, double? width, + double? height, Object? bitmapScaling}); + external static Future fromAssetImage(Object configuration, String assetName); + external static Future asset(Object configuration, String assetName, {double? width, + double? height, Object? bitmapScaling}); + external double hue(); + external bool hasBytes(); + external static int hueToRgb(double hue); external static double get hueRed; external static double get hueOrange; external static double get hueYellow; @@ -69,6 +78,44 @@ class Marker { external InfoWindow get infoWindow; external bool get draggable; external bool get visible; + external BitmapDescriptor? get icon; +} + +@JavaName('com.codename1.flutter.plugins.googlemaps.PolylineId') +class PolylineId { + external PolylineId(String value); + external String get value; +} + +@JavaName('com.codename1.flutter.plugins.googlemaps.Polyline') +class Polyline { + external Polyline({required PolylineId polylineId, List points, Color color, int width, + bool visible, bool geodesic, int zIndex, bool consumeTapEvents, Object? patterns, + Object? startCap, Object? endCap, Object? jointType, void Function()? onTap}); + external PolylineId get polylineId; + external int get width; + external bool get visible; + external bool hitTest(LatLng at, double toleranceMeters); + external bool tapAt(LatLng at, double toleranceMeters); +} + +@JavaName('com.codename1.flutter.plugins.googlemaps.CircleId') +class CircleId { + external CircleId(String value); + external String get value; +} + +@JavaName('com.codename1.flutter.plugins.googlemaps.Circle') +class Circle { + external Circle({required CircleId circleId, LatLng center, double radius, Color fillColor, + Color strokeColor, int strokeWidth, bool visible, int zIndex, bool consumeTapEvents, + void Function()? onTap}); + external CircleId get circleId; + external LatLng get center; + external double get radius; + external bool get visible; + external bool hitTest(LatLng at); + external bool tapAt(LatLng at); } @JavaName('com.codename1.flutter.plugins.googlemaps.MapType') diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_player.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_player.dart index 8a950691f17..2d3af9b087d 100644 --- a/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_player.dart +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/plugin_video_player.dart @@ -26,10 +26,13 @@ class VideoPlayerValue { @JavaName('com.codename1.flutter.plugins.videoplayer.VideoPlayerController') class VideoPlayerController extends ValueNotifier { + @Unapplied('httpHeaders, closedCaptionFile, formatHint') external static VideoPlayerController networkUrl(Uri url, {VideoPlayerOptions? videoPlayerOptions, Map httpHeaders, dynamic formatHint, dynamic closedCaptionFile}); + @Unapplied('httpHeaders, closedCaptionFile, formatHint') external static VideoPlayerController network(String dataSource, {VideoPlayerOptions? videoPlayerOptions, Map httpHeaders, dynamic formatHint, dynamic closedCaptionFile}); + @Unapplied('httpHeaders, closedCaptionFile, formatHint') external static VideoPlayerController file(File file, {VideoPlayerOptions? videoPlayerOptions, Map httpHeaders, dynamic closedCaptionFile}); external static VideoPlayerController asset(String dataSource, {String? package, diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/RtlMirroringTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/RtlMirroringTest.java new file mode 100644 index 00000000000..fb2d1eb7829 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/RtlMirroringTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +package com.codename1.flutter; + +import com.codename1.flutter.l10n.MaterialLocalizations; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.widgets.Align; +import com.codename1.flutter.widgets.Column; +import com.codename1.flutter.widgets.Directionality; +import com.codename1.flutter.widgets.Row; +import com.codename1.flutter.widgets.Wrap; + +import dart.core.DartList; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/// Layouts that run along the reading direction mirror under right-to-left text, as +/// Flutter's do, and the framework strings follow the locale's language. +class RtlMirroringTest { + + private static RenderElement layoutUnder(TextDirection d, Widget child) { + Directionality dir = new Directionality(); + dir.textDirection(d); + dir.child(child); + RenderHost host = new RenderHost(); + FlutterUI.mount(dir, host, new BuildOwner()); + RenderElement r = host.rootRenderElement(); + r.layout(BoxConstraints.tight(400, 600)); + r.position(0, 0); + return r; + } + + private static Row row() { + Row row = new Row(); + row.children(DartList.of(new ProbeBox(50, 20), new ProbeBox(30, 20))); + return row; + } + + @Test + void rowRunsFromTheRightUnderRtl() { + RenderElement ltr = layoutUnder(TextDirection.ltr, row()); + assertEquals(0, ltr.renderChildren().get(0).x()); + assertEquals(50, ltr.renderChildren().get(1).x()); + RenderElement rtl = layoutUnder(TextDirection.rtl, row()); + assertEquals(350, rtl.renderChildren().get(0).x()); + assertEquals(320, rtl.renderChildren().get(1).x()); + } + + @Test + void rowEndIsTheLeftEdgeUnderRtl() { + Row r = row(); + r.mainAxisAlignment(MainAxisAlignment.end); + RenderElement rtl = layoutUnder(TextDirection.rtl, r); + assertEquals(30, rtl.renderChildren().get(0).x()); + assertEquals(0, rtl.renderChildren().get(1).x()); + } + + @Test + void columnCrossStartIsTheRightEdgeUnderRtl() { + Column c = new Column(); + c.crossAxisAlignment(CrossAxisAlignment.start); + c.children(DartList.of(new ProbeBox(50, 20), new Row())); + Column outer = c; + RenderElement rtl = layoutUnder(TextDirection.rtl, outer); + RenderElement first = rtl.renderChildren().get(0); + assertEquals(rtl.size().width() - 50, first.x(), 0.5); + } + + @Test + void alignmentDirectionalStartIsTheRightEdgeUnderRtl() { + Align a = new Align(); + a.alignment(AlignmentDirectional.centerStart); + a.child(new ProbeBox(40, 20)); + assertEquals(360, layoutUnder(TextDirection.rtl, a).renderChildren().get(0).x()); + Align b = new Align(); + b.alignment(Alignment.centerLeft); + b.child(new ProbeBox(40, 20)); + assertEquals(0, layoutUnder(TextDirection.rtl, b).renderChildren().get(0).x(), + "a plain Alignment never mirrors"); + } + + @Test + void wrapFillsFromTheRightUnderRtl() { + Wrap w = new Wrap(); + w.children(DartList.of(new ProbeBox(50, 20), new ProbeBox(30, 20))); + RenderElement rtl = layoutUnder(TextDirection.rtl, w); + assertEquals(rtl.size().width() - 50, rtl.renderChildren().get(0).x(), 0.5); + } + + @Test + void materialStringsFollowTheLanguage() { + assertEquals("Cancel", MaterialLocalizations.forLocale(new Locale("en")).cancelButtonLabel()); + assertEquals("Cancelar", MaterialLocalizations.forLocale(new Locale("es")).cancelButtonLabel()); + assertEquals("\u05d1\u05d9\u05d8\u05d5\u05dc", + MaterialLocalizations.forLocale(new Locale("he")).cancelButtonLabel()); + assertEquals("OK", MaterialLocalizations.forLocale(new Locale("fr")).okButtonLabel()); + assertEquals(1, MaterialLocalizations.forLocale(new Locale("es")).firstDayOfWeekIndex()); + } +} From b6b9b43e3950e54d231cac173beaf2f407b25f51 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:25:12 +0200 Subject: [PATCH 4/8] Flutter: per-library stub views rebuilt each time, bounded image cache, header reloads Co-Authored-By: Claude Opus 5.5 --- .../dart/transpiler/analyze/StubRegistry.java | 40 +++++------ .../dart/transpiler/codegen/JavaEmitter.java | 2 + .../behavior/stub_scope_libraries/expect.txt | 3 + .../behavior/stub_scope_libraries/main.dart | 11 ++++ .../stub_scope_libraries/settings.dart | 6 ++ .../stub_scope_libraries/zz_picker.dart | 6 ++ .../plugins/audioplayers/AudioPlayer.java | 7 +- .../CachedNetworkImage.java | 66 +++++++++++++++++-- .../plugins/emojipicker/EmojiData.java | 2 +- 9 files changed, 110 insertions(+), 33 deletions(-) create mode 100644 maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/expect.txt create mode 100644 maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/main.dart create mode 100644 maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/settings.dart create mode 100644 maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/zz_picker.dart diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/StubRegistry.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/StubRegistry.java index c1c51b57f61..aa18defa712 100644 --- a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/StubRegistry.java +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/StubRegistry.java @@ -213,7 +213,7 @@ private void variant(String name, String pkg, Object decl) { /// Makes every type name more than one package declares resolve to the declaration of /// the package `importedPackages` contains, for the library about to be emitted. A - /// library importing neither keeps whichever was loaded last. + /// library importing neither sees the one loaded last. public void viewFrom(java.util.Collection importedPackages) { viewFrom(importedPackages, java.util.Collections.emptySet()); } @@ -222,34 +222,30 @@ public void viewFrom(java.util.Collection importedPackages) { /// `programTypes` hides a stub of a package the library does not import: emoji_picker's /// `Config` must not shadow an application's own `Config`. public void viewFrom(java.util.Collection importedPackages, java.util.Set programTypes) { + // Rebuilt from the loaded declarations on every call, never from what the previous + // library's view left behind: a choice or a removal made for one library must not + // carry over to the next. for (Map.Entry> e : variants.entrySet()) { List l = e.getValue(); - if (programTypes.contains(e.getKey())) { - boolean imported = false; - for (Object[] v : l) { - imported |= v[0] == null || importedPackages.contains(v[0]); - } - if (!imported) { - classes.remove(e.getKey()); - enums.remove(e.getKey()); - continue; - } - } - if (l.size() < 2) { - continue; - } + Object[] chosen = l.get(l.size() - 1); for (Object[] v : l) { if (v[0] != null && importedPackages.contains(v[0])) { - classes.remove(e.getKey()); - enums.remove(e.getKey()); - if (v[1] instanceof Ast.ClassDecl) { - classes.put(e.getKey(), (Ast.ClassDecl) v[1]); - } else { - enums.put(e.getKey(), (Ast.EnumDecl) v[1]); - } + chosen = v; break; } } + String name = e.getKey(); + classes.remove(name); + enums.remove(name); + boolean visible = chosen[0] == null || importedPackages.contains(chosen[0]); + if (!visible && programTypes.contains(name)) { + continue; + } + if (chosen[1] instanceof Ast.ClassDecl) { + classes.put(name, (Ast.ClassDecl) chosen[1]); + } else { + enums.put(name, (Ast.EnumDecl) chosen[1]); + } } } diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java index 3b3de6d670d..2b8d493dc4e 100644 --- a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java @@ -223,6 +223,8 @@ private List emitAll() { } } } + // What is emitted from here on belongs to no one library: the loaded-last view. + stubs.viewFrom(java.util.Collections.emptySet(), java.util.Collections.emptySet()); String mainLib = resolveMainLib(mainCandidates); if (mainLib != null) { out.add(emitRegistry(mainLib)); diff --git a/maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/expect.txt new file mode 100644 index 00000000000..fb59a49331c --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/expect.txt @@ -0,0 +1,3 @@ +app +picker true +settings s diff --git a/maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/main.dart b/maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/main.dart new file mode 100644 index 00000000000..232837b2cd9 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/main.dart @@ -0,0 +1,11 @@ +// A stub type is scoped to the libraries that import its package: main and settings +// use the application's own Config, picker uses emoji_picker's, in whichever order +// the libraries are emitted. +import 'settings.dart'; +import 'zz_picker.dart'; + +void main() { + print(Config('app').name); + print(pickerConfigHeight()); + print(describeSettings()); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/settings.dart b/maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/settings.dart new file mode 100644 index 00000000000..dcca6f48e2a --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/settings.dart @@ -0,0 +1,6 @@ +class Config { + final String name; + Config(this.name); +} + +String describeSettings() => 'settings ${Config('s').name}'; diff --git a/maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/zz_picker.dart b/maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/zz_picker.dart new file mode 100644 index 00000000000..e6bcc89e507 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/stub_scope_libraries/zz_picker.dart @@ -0,0 +1,6 @@ +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; + +String pickerConfigHeight() { + final c = Config(height: 256); + return 'picker ${c is Object}'; +} diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/AudioPlayer.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/AudioPlayer.java index 4b42cf720eb..d4abef40f97 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/AudioPlayer.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/audioplayers/AudioPlayer.java @@ -58,10 +58,9 @@ public class AudioPlayer { private final StreamController states = StreamController.broadcast(); public AudioPlayer() { - synchronized (AudioPlayer.class) { - ids++; - this.playerId = "cn1-player-" + ids; - } + // Players are made on the EDT, so the counter needs no lock. + ids++; + this.playerId = "cn1-player-" + ids; } /** The {@code playerId} constructor argument. */ diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/cachednetworkimage/CachedNetworkImage.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/cachednetworkimage/CachedNetworkImage.java index 612ea706f1e..38e08c48135 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/cachednetworkimage/CachedNetworkImage.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/cachednetworkimage/CachedNetworkImage.java @@ -23,7 +23,6 @@ */ package com.codename1.flutter.plugins.cachednetworkimage; -import java.util.HashMap; import java.util.Map; import com.codename1.flutter.BoxFit; @@ -60,7 +59,40 @@ */ public class CachedNetworkImage extends StatefulWidget { /** Bytes of finished downloads, by cache key. */ - private static final Map CACHE = new HashMap(); + /// The encoded bytes of recent downloads, least recently used first, held to + /// [#CACHE_BUDGET] bytes so a long scrolling feed cannot exhaust the heap. + private static final java.util.LinkedHashMap CACHE = + new java.util.LinkedHashMap(); + + /// The most encoded bytes [#CACHE] keeps. + static final long CACHE_BUDGET = 16L * 1024 * 1024; + + private static long cachedBytes; + + static byte[] cacheGet(String key) { + byte[] b = CACHE.remove(key); + if (b != null) { + CACHE.put(key, b); + } + return b; + } + + static void cachePut(String key, byte[] data) { + byte[] old = CACHE.remove(key); + if (old != null) { + cachedBytes -= old.length; + } + if (data.length > CACHE_BUDGET) { + return; + } + CACHE.put(key, data); + cachedBytes += data.length; + while (cachedBytes > CACHE_BUDGET && !CACHE.isEmpty()) { + // The eldest entry, removed through the map rather than an iterator. + String eldest = CACHE.keySet().iterator().next(); + cachedBytes -= CACHE.remove(eldest).length; + } + } String imageUrl; Double width; @@ -143,7 +175,7 @@ public State createState() { /// What can be answered without the network: the cached bytes (as byte[]), or the /// error a load that cannot start fails with; null when a download is needed. private Object immediate() { - byte[] cached = CACHE.get(key()); + byte[] cached = cacheGet(key()); if (cached != null) { return cached; } @@ -201,7 +233,7 @@ public Widget buildLoading(BuildContext context, long downloaded, Long total) { /// Stores bytes as a finished download of this widget's key. public void seedCache(dart.typed_data.Uint8List data) { - CACHE.put(key(), data.toBytes()); + cachePut(key(), data.toBytes()); } private static final class Loader extends State { @@ -220,12 +252,34 @@ public void initState() { @Override public void didUpdateWidget(CachedNetworkImage old) { - if (!widget().key().equals(loadedKey)) { + // A header change (a rotated auth token) reloads too, so a failure the old + // headers caused is not kept. + if (!widget().key().equals(loadedKey) || !sameHeaders(old.httpHeaders, widget().httpHeaders)) { stop(); start(); } } + /// Entry by entry: a Dart map literal is a new object on every build, and DartMap's + /// equality is identity, as Dart's is. + private static boolean sameHeaders(Map a, Map b) { + int na = a == null ? 0 : a.size(); + int nb = b == null ? 0 : b.size(); + if (na != nb) { + return false; + } + if (na == 0) { + return true; + } + for (Map.Entry e : a.entrySet()) { + Object v = b.get(e.getKey()); + if (v == null ? e.getValue() != null || !b.containsKey(e.getKey()) : !v.equals(e.getValue())) { + return false; + } + } + return true; + } + @Override public void dispose() { stop(); @@ -337,7 +391,7 @@ public void run() { } request = null; if (data != null) { - CACHE.put(key, data); + cachePut(key, data); } setState(new Funcs.VoidFunc0() { @Override diff --git a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiData.java b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiData.java index 61156882b69..2b9099e3648 100644 --- a/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiData.java +++ b/maven/flutter-runtime/src/main/plugins/com/codename1/flutter/plugins/emojipicker/EmojiData.java @@ -552,7 +552,7 @@ final class EmojiData { private EmojiData() { } - static synchronized DartList all() { + static DartList all() { if (all == null) { all = new DartList(); add(Category.SMILEYS, SMILEYS); From e0e7f9c04e7d5dbe3f71962e16231fd2f7d70396 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:20:49 +0200 Subject: [PATCH 5/8] Flutter asset tests follow the asset code into build-engine The Gradle support moved copyAssets/removeStaleAssets from TranscodeFlutterMojo into FlutterProjectBuilder; the plugin's test still called the mojo and failed test compilation in every job that builds the plugin. Co-Authored-By: Claude Opus 5.5 --- .../maven/FlutterProjectBuilder.java | 4 +- .../maven/FlutterProjectAssetsTest.java} | 51 ++++++------------- 2 files changed, 17 insertions(+), 38 deletions(-) rename maven/{codenameone-maven-plugin/src/test/java/com/codename1/maven/TranscodeFlutterAssetsTest.java => build-engine/src/test/java/com/codename1/maven/FlutterProjectAssetsTest.java} (82%) diff --git a/maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectBuilder.java b/maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectBuilder.java index 95cceb6c980..e388b54233a 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectBuilder.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/FlutterProjectBuilder.java @@ -221,7 +221,7 @@ private static void checkJdk() throws BuildException { * name produced by {@link #flatAssetName}, which the Flutter runtime's * {@code FlutterAssets} recomputes when resolving {@code Image.asset(...)}.

*/ - private void copyAssets() throws BuildException { + void copyAssets() throws BuildException { File outDir = assetsDir; Set written = new HashSet(); List keys = new ArrayList(); @@ -325,7 +325,7 @@ private int flattenDeclaredAsset(String key, File outDir, Set written, L * the application's own resources, and a name this plugin did not write is not * one it should remove.

*/ - private void removeStaleAssets(Set written) throws BuildException { + void removeStaleAssets(Set written) throws BuildException { File outDir = assetsDir; File manifest = assetManifest; try { diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/TranscodeFlutterAssetsTest.java b/maven/build-engine/src/test/java/com/codename1/maven/FlutterProjectAssetsTest.java similarity index 82% rename from maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/TranscodeFlutterAssetsTest.java rename to maven/build-engine/src/test/java/com/codename1/maven/FlutterProjectAssetsTest.java index 238b880a624..68ddf506fb6 100644 --- a/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/TranscodeFlutterAssetsTest.java +++ b/maven/build-engine/src/test/java/com/codename1/maven/FlutterProjectAssetsTest.java @@ -23,15 +23,13 @@ */ package com.codename1.maven; -import org.apache.maven.model.Build; -import org.apache.maven.project.MavenProject; +import com.codename1.build.SystemStreamLog; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import java.io.File; -import java.lang.reflect.Field; import java.nio.file.Files; import java.util.HashSet; @@ -44,12 +42,12 @@ * what disappears from src/main/flutter disappears from the output, and nothing * the plugin did not write is ever removed. */ -public class TranscodeFlutterAssetsTest { +public class FlutterProjectAssetsTest { @Rule public TemporaryFolder tmp = new TemporaryFolder(); - private TranscodeFlutterMojo mojo; + private FlutterProjectBuilder mojo; private File flutter; private File classes; @@ -58,14 +56,9 @@ public void setUp() throws Exception { File target = tmp.newFolder("target"); classes = new File(target, "classes"); flutter = tmp.newFolder("flutter"); - MavenProject project = new MavenProject(); - Build build = new Build(); - build.setDirectory(target.getAbsolutePath()); - build.setOutputDirectory(classes.getAbsolutePath()); - project.setBuild(build); - mojo = new TranscodeFlutterMojo(); - set("project", project); - set("flutterSourceDir", flutter); + mojo = new FlutterProjectBuilder(flutter, new File(target, "generated-sources/flutter"), classes, + new File(target, "cn1-flutter-assets.lst"), new File(target, "dart2java-state.txt"), null, + new SystemStreamLog()); } @Test @@ -91,7 +84,7 @@ public void theManifestListsEveryBundledAssetKey() throws Exception { asset("assets/2.5x/a.png"); asset("packages/lib/assets/3x/b.png"); mojo.copyAssets(); - File manifest = new File(classes, TranscodeFlutterMojo.ASSET_MANIFEST); + File manifest = new File(classes, FlutterProjectBuilder.ASSET_MANIFEST); assertTrue(manifest.isFile()); assertEquals(java.util.Arrays.asList("assets/2.5x/a.png", "assets/a.png", "packages/lib/assets/3x/b.png"), Files.readAllLines(manifest.toPath(), java.nio.charset.StandardCharsets.UTF_8)); @@ -198,10 +191,10 @@ public void aRootAssetNamedLikeTheManifestIsKeptBesideTheManifest() throws Excep mojo.copyAssets(); - File asset = new File(classes, TranscodeFlutterMojo.flatAssetName("AssetManifest.txt")); + File asset = new File(classes, FlutterProjectBuilder.flatAssetName("AssetManifest.txt")); assertEquals("this is the developer's own asset", new String(Files.readAllBytes(asset.toPath()), java.nio.charset.StandardCharsets.UTF_8)); - File manifestFile = new File(classes, TranscodeFlutterMojo.ASSET_MANIFEST); + File manifestFile = new File(classes, FlutterProjectBuilder.ASSET_MANIFEST); assertTrue("the generated manifest is written under its own name", manifestFile.isFile()); assertFalse(asset.getName().equals(manifestFile.getName())); } @@ -214,11 +207,11 @@ public void aRootAssetNamedLikeTheManifestIsKeptBesideTheManifest() throws Excep @Test public void encodingMatchesTheRuntimeAndIsPrefixFree() { assertEquals("cn1f_assets_sstudies_sreply__card.png", - TranscodeFlutterMojo.flatAssetName("assets/studies/reply_card.png")); - assertEquals("cn1f_a___sb.png", TranscodeFlutterMojo.flatAssetName("a_/b.png")); - assertEquals("cn1f_a_s__b.png", TranscodeFlutterMojo.flatAssetName("a/_b.png")); - assertFalse(TranscodeFlutterMojo.flatAssetName("a_/b.png") - .equals(TranscodeFlutterMojo.flatAssetName("a/_b.png"))); + FlutterProjectBuilder.flatAssetName("assets/studies/reply_card.png")); + assertEquals("cn1f_a___sb.png", FlutterProjectBuilder.flatAssetName("a_/b.png")); + assertEquals("cn1f_a_s__b.png", FlutterProjectBuilder.flatAssetName("a/_b.png")); + assertFalse(FlutterProjectBuilder.flatAssetName("a_/b.png") + .equals(FlutterProjectBuilder.flatAssetName("a/_b.png"))); } private File asset(String key) throws Exception { @@ -229,20 +222,6 @@ private File asset(String key) throws Exception { } private File flat(String key) { - return new File(classes, TranscodeFlutterMojo.flatAssetName(key)); - } - - private void set(String name, Object value) throws Exception { - for (Class c = mojo.getClass(); c != null; c = c.getSuperclass()) { - try { - Field f = c.getDeclaredField(name); - f.setAccessible(true); - f.set(mojo, value); - return; - } catch (NoSuchFieldException keepLooking) { - // up the chain - } - } - throw new NoSuchFieldException(name); + return new File(classes, FlutterProjectBuilder.flatAssetName(key)); } } From 966cee8e9a57be5b3330023dd929c48513204126 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:05:41 +0200 Subject: [PATCH 6/8] Guide snippets: Flutter examples included from docs/demos, UIManager snippet self-contained The import-report JSON, the stub-pack Dart and the stub-pack Java were inline blocks; they now live in docs/demos (the Java as a compiled class) and are included by tag. The UIManager javadoc snippet named an undefined resourceFile and was baselined under an old hash since #6106 changed it; it now compiles in the Playground and its baseline entry is gone. Co-Authored-By: Claude Opus 5.5 --- .../src/com/codename1/ui/plaf/UIManager.java | 2 +- .../developerguide/flutter/Lottie.java | 49 +++++++++++++++++++ .../flutter-import-report.json | 9 ++++ .../developer-guide/flutter-stub-pack.dart | 12 +++++ docs/developer-guide/Flutter-Interop.asciidoc | 25 ++-------- scripts/doc-snippet-baseline.txt | 1 - 6 files changed, 75 insertions(+), 23 deletions(-) create mode 100644 docs/demos/common/src/main/java/com/codenameone/developerguide/flutter/Lottie.java create mode 100644 docs/demos/common/src/main/snippets/developer-guide/flutter-import-report.json create mode 100644 docs/demos/common/src/main/snippets/developer-guide/flutter-stub-pack.dart diff --git a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java index 541f9d89d2d..d81594613ae 100644 --- a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java +++ b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java @@ -565,7 +565,7 @@ private static Font parseFont(String fontStr) { /// ```java /// Resources theme = null; /// try { - /// theme = Resources.openLayered(resourceFile); + /// theme = Resources.openLayered("/theme"); /// UIManager.getInstance().setThemeMap(theme.getThemeMap(theme.getThemeResourceNames()[0])); /// } catch (IOException e) { /// Log.e(e); diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/flutter/Lottie.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/flutter/Lottie.java new file mode 100644 index 00000000000..53930c41f60 --- /dev/null +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/flutter/Lottie.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.flutter; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Text; + +/// The Java half of the guide's example stub pack: the class `plugin_lottie.dart` +/// names with `@JavaName`. +// tag::flutter-stub-pack-java-001[] +public class Lottie extends StatelessWidget { + private String name; + + public static Lottie asset(String name, Double width, Double height, + Boolean repeat, Object fit) { + Lottie l = new Lottie(); + l.name = name; + return l; + } + + @Override + public Widget build(BuildContext context) { + // Play the animation; a label stands in for it here. + return new Text(name); + } +} +// end::flutter-stub-pack-java-001[] diff --git a/docs/demos/common/src/main/snippets/developer-guide/flutter-import-report.json b/docs/demos/common/src/main/snippets/developer-guide/flutter-import-report.json new file mode 100644 index 00000000000..87e589541dc --- /dev/null +++ b/docs/demos/common/src/main/snippets/developer-guide/flutter-import-report.json @@ -0,0 +1,9 @@ +// Generated from docs/developer-guide source blocks. Edit the guide snippets here, not inline. + +// tag::flutter-import-report-json-001[] +{"package": "ui_kit", "status": "unsupported", + "note": "depends on vector_graphics_compiler, which needs dart:ffi", + "cause": "needs dart:ffi", + "chain": ["ui_kit", "vector_graphics_compiler"], + "reachedFrom": ["lib/login/login_screen.dart"]} +// end::flutter-import-report-json-001[] diff --git a/docs/demos/common/src/main/snippets/developer-guide/flutter-stub-pack.dart b/docs/demos/common/src/main/snippets/developer-guide/flutter-stub-pack.dart new file mode 100644 index 00000000000..babee695c5c --- /dev/null +++ b/docs/demos/common/src/main/snippets/developer-guide/flutter-stub-pack.dart @@ -0,0 +1,12 @@ +// Generated from docs/developer-guide source blocks. Edit the guide snippets here, not inline. + +// tag::flutter-stub-pack-dart-001[] +// cn1:package lottie +// cn1:covers lottie Lottie.asset over a Codename One animation +@JavaName('com.codenameone.developerguide.flutter.Lottie') +class Lottie extends Widget { + @Unapplied('fit') + external static Lottie asset(String name, {double? width, double? height, + bool repeat, BoxFit? fit}); +} +// end::flutter-stub-pack-dart-001[] diff --git a/docs/developer-guide/Flutter-Interop.asciidoc b/docs/developer-guide/Flutter-Interop.asciidoc index 2ba277c0fd0..7fea2617f18 100644 --- a/docs/developer-guide/Flutter-Interop.asciidoc +++ b/docs/developer-guide/Flutter-Interop.asciidoc @@ -128,11 +128,7 @@ package that is unsupported itself: [source,json] ---- -{"package": "ui_kit", "status": "unsupported", - "note": "depends on vector_graphics_compiler, which needs dart:ffi", - "cause": "needs dart:ffi", - "chain": ["ui_kit", "vector_graphics_compiler"], - "reachedFrom": ["lib/login/login_screen.dart"]} +include::../demos/common/src/main/snippets/developer-guide/flutter-import-report.json[tag=flutter-import-report-json-001,indent=0] ---- The report also lists, under `googleFonts`, every Google Fonts family the @@ -544,13 +540,7 @@ stands in for the package: [source,dart] ---- -// cn1:covers lottie Lottie.asset over a Codename One animation -@JavaName('com.example.lottie.Lottie') -class Lottie extends Widget { - @Unapplied('fit') - external static Lottie asset(String name, {double? width, double? height, - bool repeat, BoxFit? fit}); -} +include::../demos/common/src/main/snippets/developer-guide/flutter-stub-pack.dart[tag=flutter-stub-pack-dart-001,indent=0] ---- Stub types share one namespace. When two packages export the same name -- the @@ -561,16 +551,9 @@ name is never shadowed by a stub of a package it does not import. The Java half is an ordinary class with the members the declarations map to: -[source,java] +[source,java,role=cn1-no-try] ---- -package com.example.lottie; - -public class Lottie extends com.codename1.flutter.Widget { - public static Lottie asset(String name, Double width, Double height, - Boolean repeat, Object fit) { - // ... - } -} +include::../demos/common/src/main/java/com/codenameone/developerguide/flutter/Lottie.java[tag=flutter-stub-pack-java-001,indent=0] ---- The mapping rules: diff --git a/scripts/doc-snippet-baseline.txt b/scripts/doc-snippet-baseline.txt index 0693af2cadc..8543a681e35 100644 --- a/scripts/doc-snippet-baseline.txt +++ b/scripts/doc-snippet-baseline.txt @@ -321,7 +321,6 @@ CodenameOne/src/com/codename1/ui/list/CellRenderer.java#5eca0098701e CodenameOne/src/com/codename1/ui/list/ListCellRenderer.java#500e2416fe67 CodenameOne/src/com/codename1/ui/list/ListModel.java#8d4d9b014972 CodenameOne/src/com/codename1/ui/list/ListModel.java#d0795f2aa202 -CodenameOne/src/com/codename1/ui/plaf/UIManager.java#6aaadca1b2ce CodenameOne/src/com/codename1/util/regex/RE.java#32627ff643a4 CodenameOne/src/com/codename1/vpn/profile/Vpn.java#aaa2e660926c CodenameOne/src/com/codename1/vpn/profile/VpnProfile.java#a1cc6689b4e5 From 316197efc1e30ffb0d6605030b8638e72b6fb308 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:39:42 +0200 Subject: [PATCH 7/8] Flutter guide: contractions for the Vale gate Co-Authored-By: Claude Opus 5.5 --- docs/developer-guide/Flutter-Interop.asciidoc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/developer-guide/Flutter-Interop.asciidoc b/docs/developer-guide/Flutter-Interop.asciidoc index 7fea2617f18..94d350dff8d 100644 --- a/docs/developer-guide/Flutter-Interop.asciidoc +++ b/docs/developer-guide/Flutter-Interop.asciidoc @@ -122,9 +122,9 @@ covered is the first thing to look at if a screen misbehaves. The same report is written as JSON to `common/target/flutter-import-report.json`, for a script or a CI step to read. Each package has a status (`covered` or `unsupported`), the note or the reason, and the application's own files that -reach it. An unsupported package that is only unsupported because of something +reach it. An unsupported package that's only unsupported because of something it depends on carries the chain that verdict came through, ending with the -package that is unsupported itself: +package that's unsupported itself: [source,json] ---- @@ -401,7 +401,7 @@ never mirrors, as in Flutter. [[flutter-plugin-coverage]] === Plugin coverage -These are the pub packages the runtime provides, and how. A package that is not +These are the pub packages the runtime provides, and how. A package that's not listed and is pure Dart is transpiled from its own sources. [cols="2,4,3", options="header"] @@ -547,7 +547,7 @@ Stub types share one namespace. When two packages export the same name -- the `AppleProvider` of `firebase_app_check` and of `firebase_ui_oauth_apple` -- give each file a `// cn1:package ` line: a library then sees the declaration of the package it imports, and a class of the application's own with the same -name is never shadowed by a stub of a package it does not import. +name is never shadowed by a stub of a package it doesn't import. The Java half is an ordinary class with the members the declarations map to: @@ -558,7 +558,7 @@ include::../demos/common/src/main/java/com/codenameone/developerguide/flutter/Lo The mapping rules: -* Named parameters become positional Java parameters, in the order they are +* Named parameters become positional Java parameters, in the order they're declared. A parameter the call leaves out is passed as `null`. * `int` is `Long` (or `long` when it can't be null), `double` is `Double`, `bool` is `Boolean`, `List` and `Map` are `dart.core.DartList` and From 6def869707a69a88ab26f175afe18b06e2794ba8 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 30 Oct 2026 01:36:42 +0200 Subject: [PATCH 8/8] Flutter benchmark: rebaseline gallery code size for the new runtime surface dart:io RandomAccessFile, UiImage/ByteBuffer, the Material/Cupertino l10n tables and RTL mirroring are linked into every Flutter app, so the gallery's executable grows 3-11% across linux, windows and javascript. Co-Authored-By: Claude Opus 5.5 --- scripts/flutter-bench/baseline/pr/6132.json | 41 +++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 scripts/flutter-bench/baseline/pr/6132.json diff --git a/scripts/flutter-bench/baseline/pr/6132.json b/scripts/flutter-bench/baseline/pr/6132.json new file mode 100644 index 00000000000..7a19c0b3342 --- /dev/null +++ b/scripts/flutter-bench/baseline/pr/6132.json @@ -0,0 +1,41 @@ +{ + "pr": 6132, + "rebaseline": { + "javascript": { + "gallery": { + "code_bytes": { + "from": { + "value": 13533625 + }, + "reason": "PR 6132 adds dart:io RandomAccessFile, UiImage/ByteBuffer, Material/Cupertino l10n tables and RTL mirroring to the always-linked runtime; the gallery binary grows with them", + "runs": 1, + "value": 13971409 + } + } + }, + "linux": { + "gallery": { + "code_bytes": { + "from": { + "value": 14934770 + }, + "reason": "PR 6132 adds dart:io RandomAccessFile, UiImage/ByteBuffer, Material/Cupertino l10n tables and RTL mirroring to the always-linked runtime; the gallery binary grows with them", + "runs": 1, + "value": 16559170 + } + } + }, + "windows": { + "gallery": { + "code_bytes": { + "from": { + "value": 11908096 + }, + "reason": "PR 6132 adds dart:io RandomAccessFile, UiImage/ByteBuffer, Material/Cupertino l10n tables and RTL mirroring to the always-linked runtime; the gallery binary grows with them", + "runs": 1, + "value": 12348928 + } + } + } + } +}