From d7de206dbd459e8c8bf121f73755d12569c6cc55 Mon Sep 17 00:00:00 2001 From: Mukilan Thiyagarajan Date: Mon, 22 Jan 2024 18:30:15 +0530 Subject: [PATCH] Preliminary Android build support (#31086) * Android build * Fixes * More fixes - Still failing in the linking step * More work on getting linking working Signed-off-by: Mukilan Thiyagarajan * android: use mozjs with ndk r25c. loads servo.org more android build fixes. * fix ./mach run for android and make it follow logs Signed-off-by: Mukilan Thiyagarajan * add experimental logic for compositor pause/resume Signed-off-by: Mukilan Thiyagarajan * pass DPI from android to simpleservo Signed-off-by: Mukilan Thiyagarajan * ci: add android workflow Signed-off-by: Mukilan Thiyagarajan * switch to ANDROID_SDK_ROOT and ANDROID_NDK_ROOT vars Signed-off-by: Mukilan Thiyagarajan * upgrade gradle to 4.10.1 Signed-off-by: Mukilan Thiyagarajan * upgrade to gradle 5.1.1 Signed-off-by: Mukilan Thiyagarajan * upgrade to gradle 8 and agp 8 Signed-off-by: Mukilan Thiyagarajan * make compositing work again with external present Signed-off-by: Mukilan Thiyagarajan * android: improve mach support for non-NixOS and CI Signed-off-by: Mukilan Thiyagarajan * fix sampler compilation bug introduced in #30490 Signed-off-by: Mukilan Thiyagarajan * ci: add android build to main workflow Signed-off-by: Mukilan Thiyagarajan * gradle: set MinSdk = targetSdk = 30 NDK requires we compile against the minSdk API level which is 30 in our case. Signed-off-by: Mukilan Thiyagarajan * add instructions for android in README.md Signed-off-by: Mukilan Thiyagarajan * apk: move servosurface to servoview Signed-off-by: Mukilan Thiyagarajan * apk: uncomment the mediasession callbacks on MainActivity Signed-off-by: Mukilan Thiyagarajan * apk: fix crash on MainAtivity.onDestroy Signed-off-by: Mukilan Thiyagarajan * apk: drop VR, arm 5 and unused code This commit drops: * support for google, oculusvr * support for arm5 architecture and also removes * fakeld scripts * unused java code Signed-off-by: Mukilan Thiyagarajan * cleanup shell.nix Signed-off-by: Mukilan Thiyagarajan * android: add FIXMEs for gstreamer code Signed-off-by: Mukilan Thiyagarajan * apk: remove commented code and debug logs Signed-off-by: Mukilan Thiyagarajan * cleanup ServoView.java Signed-off-by: Mukilan Thiyagarajan * mach: comment call to download gstreamer deps for android Signed-off-by: Mukilan Thiyagarajan * disable bluetooth for jniapi as blurdroid is broken Signed-off-by: Mukilan Thiyagarajan * fixup! README.md * fixup! remove change in Cargo.toml * fixup! move shell variables together * fixup! cleanup jniapi/Cargo.toml comments * delete commented gstreamer related android code Signed-off-by: Mukilan Thiyagarajan * remove unused config variable in servbuild Signed-off-by: Mukilan Thiyagarajan * android: more cleanup Signed-off-by: Mukilan Thiyagarajan * force no_static_freetype only for android * use actions to manage sdk, ndk and java Signed-off-by: Mukilan Thiyagarajan * rename embedder event names to be more clear. Signed-off-by: Mukilan Thiyagarajan * link to startup crash issue Signed-off-by: Mukilan Thiyagarajan * fix lint issues Signed-off-by: Mukilan Thiyagarajan * upgrade env_logger to 0.10 with duplicate exception libservo and android_logger can use env_logger 0.10 but quickcheck is still stuck on 0.8 and has not seen any activity in the last 2 years. This commit adds a duplicate exception until the quickcheck dependency can be upgraded (or replaced) Signed-off-by: Mukilan Thiyagarajan * android: fix comments Signed-off-by: Mukilan Thiyagarajan * disable jemalloc on android Signed-off-by: Mukilan Thiyagarajan * fixup! replace linux with android in cfg --------- Signed-off-by: Mukilan Thiyagarajan Co-authored-by: Martin Robinson --- .cargo/config.toml | 25 +- .github/workflows/android.yml | 107 ++++++ .github/workflows/main.yml | 13 +- Cargo.lock | 48 ++- Cargo.toml | 2 +- README.md | 27 ++ components/allocator/Cargo.toml | 5 +- components/allocator/lib.rs | 17 +- .../background_hang_monitor.rs | 6 +- components/bluetooth/Cargo.toml | 1 + components/compositing/compositor.rs | 27 ++ components/compositing/windowing.rs | 11 + .../platform/freetype/android/font_list.rs | 3 +- components/profile/Cargo.toml | 2 +- components/profile/mem.rs | 14 +- components/servo/lib.rs | 12 +- components/webrender_surfman/lib.rs | 24 ++ etc/run_in_headless_android_emulator.py | 4 +- etc/shell.nix | 36 +- ports/libsimpleservo/api/src/lib.rs | 30 +- ports/libsimpleservo/jniapi/Cargo.toml | 12 +- ports/libsimpleservo/jniapi/build.rs | 33 -- ports/libsimpleservo/jniapi/src/lib.rs | 180 ++++----- python/servo/bootstrap_commands.py | 147 +------- python/servo/build_commands.py | 1 - python/servo/command_base.py | 193 ++++------ python/servo/package_commands.py | 5 +- python/servo/post_build_commands.py | 3 +- servo-tidy.toml | 4 + servobuild.example | 4 +- support/android/apk/build.gradle | 66 ++-- support/android/apk/gradle.properties | 21 ++ .../apk/gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 55190 bytes .../gradle/wrapper/gradle-wrapper.properties | 3 +- support/android/apk/gradlew | 100 ++--- support/android/apk/gradlew.bat | 14 +- support/android/apk/jni/Android.mk | 8 +- support/android/apk/jni/Application.mk | 8 +- support/android/apk/servoapp/build.gradle | 34 +- .../servoapp/src/googlevr/AndroidManifest.xml | 19 - .../apk/servoapp/src/main/AndroidManifest.xml | 3 +- .../java/org/mozilla/servo/MainActivity.java | 47 +-- .../src/main/res/layout/activity_main.xml | 4 +- support/android/apk/servoview/build.gradle | 164 +++------ .../src/googlevr/AndroidManifest.xml | 19 - .../servoview/src/main/AndroidManifest.xml | 3 +- .../java/org/mozilla/servoview/JNIServo.java | 8 +- .../java/org/mozilla/servoview/Servo.java | 19 +- .../org/mozilla/servoview/ServoSurface.java | 302 --------------- .../java/org/mozilla/servoview/ServoView.java | 344 ++++++++++-------- .../src/oculusvr/AndroidManifest.xml | 10 - support/android/apk/settings.gradle | 16 + support/android/fakeld/fake-ld-arm.sh | 14 - support/android/fakeld/fake-ld-arm64.sh | 14 - support/android/fakeld/fake-ld-armv7.sh | 14 - support/android/fakeld/fake-ld-x86.sh | 14 - support/android/fakeld/fake-ld.cmd | 2 - support/android/fakeld/fake-ld.sh | 39 -- 58 files changed, 923 insertions(+), 1382 deletions(-) create mode 100644 .github/workflows/android.yml delete mode 100644 ports/libsimpleservo/jniapi/build.rs create mode 100644 support/android/apk/gradle.properties delete mode 100644 support/android/apk/servoapp/src/googlevr/AndroidManifest.xml delete mode 100644 support/android/apk/servoview/src/googlevr/AndroidManifest.xml delete mode 100644 support/android/apk/servoview/src/main/java/org/mozilla/servoview/ServoSurface.java delete mode 100644 support/android/apk/servoview/src/oculusvr/AndroidManifest.xml delete mode 100755 support/android/fakeld/fake-ld-arm.sh delete mode 100755 support/android/fakeld/fake-ld-arm64.sh delete mode 100755 support/android/fakeld/fake-ld-armv7.sh delete mode 100755 support/android/fakeld/fake-ld-x86.sh delete mode 100644 support/android/fakeld/fake-ld.cmd delete mode 100755 support/android/fakeld/fake-ld.sh diff --git a/.cargo/config.toml b/.cargo/config.toml index 836726e6d8e..e1bdd061199 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,26 +1,17 @@ -[target.arm-linux-androideabi] -linker = "./support/android/fakeld/fake-ld-arm.sh" -ar = "arm-linux-androideabi-ar" +[target.aarch64-linux-android] +linker = "aarch64-linux-android30-clang" [target.armv7-linux-androideabi] -linker = "./support/android/fakeld/fake-ld-armv7.sh" -ar = "arm-linux-androideabi-ar" +linker = "armv7a-linux-androideabi30-clang" -[target.aarch64-linux-android] -linker = "./support/android/fakeld/fake-ld-arm64.sh" -ar = "aarch64-linux-android-ar" +[target.armv-linux-androideabi] +linker = "armv7a-linux-androideabi30-clang" [target.i686-linux-android] -linker = "./support/android/fakeld/fake-ld-x86.sh" -ar = "i686-linux-android-ar" +linker = "i686-linux-android30-clang" -[target.arm-unknown-linux-gnueabihf] -linker = "arm-linux-gnueabihf-gcc" -ar = "arm-linux-gnueabihf-ar" - -[target.aarch64-unknown-linux-gnu] -linker = "aarch64-linux-gnu-gcc" -ar = "aarch64-linux-gnu-ar" +[target.x86_64-linux-android] +linker = "x86_64-linux-android30-clang" [target.x86_64-pc-windows-msvc] linker = "lld-link.exe" diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 00000000000..db4cbde755d --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,107 @@ +name: Android +on: + workflow_call: + inputs: + profile: + required: false + default: "release" + type: string + workflow_dispatch: + inputs: + profile: + required: false + default: "release" + type: string + options: ["release", "debug", "production"] + push: + branches: ["try-android"] + +env: + RUST_BACKTRACE: 1 + SHELL: /bin/bash + SCCACHE_GHA_ENABLED: "true" + CCACHE: "sccache" + CARGO_INCREMENTAL: 0 + +jobs: + build: + name: Android Build + runs-on: ubuntu-22.04 + strategy: + matrix: + arch: ['armv7-linux-androideabi', 'i686-linux-android'] + steps: + - uses: actions/checkout@v3 + if: github.event_name != 'issue_comment' && github.event_name != 'pull_request_target' + with: + fetch-depth: 2 + # This is necessary to checkout the pull request if this run was triggered + # via an `issue_comment` action on a pull request. + - uses: actions/checkout@v3 + if: github.event_name == 'issue_comment' || github.event_name == 'pull_request_target' + with: + ref: refs/pull/${{ github.event.issue.number || github.event.number }}/head + fetch-depth: 2 + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.3 + - name: Install taplo + uses: baptiste0928/cargo-install@v2 + with: + crate: taplo-cli + locked: true + - name: Bootstrap Python + run: python3 -m pip install --upgrade pip virtualenv + - name: Bootstrap dependencies + run: sudo apt update && python3 ./mach bootstrap + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + with: + packages: 'tools platform-tools platforms;android-33' + - name: Install Android NDK + uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r25c + - name: Build (arch ${{ matrix.arch }} profile ${{ inputs.profile }}) + env: + ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} + run: | + python3 ./mach build --android --target ${{ matrix.arch }} --${{ inputs.profile }} + # TODO: This is disabled since APK crashes during startup. + # See https://github.com/servo/servo/issues/31134 + # - name: Script tests + # run: ./mach test-android-startup + - name: Rename build timing + run: cp -r target/cargo-timings target/cargo-timings-android-${{ matrix.arch }} + - name: Archive build timing + uses: actions/upload-artifact@v3 + with: + name: cargo-timings + # Using a wildcard here ensures that the archive includes the path. + path: target/cargo-timings-* + - name: Upload APK artifact for mach package + uses: actions/upload-artifact@v3 + with: + name: android-${{ matrix.arch }}-${{ inputs.profile }} + path: target/android/${{ matrix.arch }}/${{ inputs.profile }}/servoapp.apk + + result: + name: Result + runs-on: ubuntu-latest + if: always() + # `needs: 'build'` is necessary to detect cancellation + needs: + - "build" + steps: + - name: Mark the job as successful + run: exit 0 + if: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} + - name: Mark the job as unsuccessful + run: exit 1 + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 82defcc2f6c..649f66a4d82 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -76,7 +76,7 @@ jobs: let platforms = []; if (platform == "all") { - platforms = [ "linux", "windows", "macos" ]; + platforms = [ "linux", "windows", "macos", "android" ]; } else { platforms = [ platform ]; } @@ -125,6 +125,15 @@ jobs: unit-tests: ${{ fromJson(needs.decision.outputs.configuration).unit_tests }} secrets: inherit + build-android: + name: Android + needs: ["decision"] + if: ${{ contains(fromJson(needs.decision.outputs.configuration).platforms, 'android') }} + uses: ./.github/workflows/linux.yml + with: + profile: "release" + secrets: inherit + build-result: name: Result runs-on: ubuntu-latest @@ -135,7 +144,7 @@ jobs: - "build-win" - "build-mac" - "build-linux" - + - "build-android" steps: - name: Mark skipped jobs as successful if: ${{ fromJson(needs.decision.outputs.configuration).platforms[0] != null }} diff --git a/Cargo.lock b/Cargo.lock index 18a1040e12b..0a787cd600a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,20 +128,20 @@ checksum = "80b9e34fcbf29c0563547cb2ecce9b49504597cad6166769b1e4efb45c6c2951" [[package]] name = "android_log-sys" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e" +checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" [[package]] name = "android_logger" -version = "0.10.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9ed09b18365ed295d722d0b5ed59c01b79a826ff2d2a8f73d5ecca8e6fb2f66" +checksum = "c494134f746c14dc653a35a4ea5aca24ac368529da5370ecf41fe0341c35772f" dependencies = [ "android_log-sys", - "env_logger", - "lazy_static", + "env_logger 0.10.1", "log", + "once_cell", ] [[package]] @@ -1604,8 +1604,18 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" dependencies = [ - "atty", + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +dependencies = [ "humantime", + "is-terminal", "log", "regex", "termcolor", @@ -2940,6 +2950,17 @@ dependencies = [ "windows", ] +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.2", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -3379,7 +3400,7 @@ dependencies = [ "devtools", "devtools_traits", "embedder_traits", - "env_logger", + "env_logger 0.10.1", "euclid", "gaol", "gfx", @@ -4568,7 +4589,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ - "env_logger", + "env_logger 0.8.4", "log", "rand", ] @@ -5379,6 +5400,7 @@ version = "0.0.1" dependencies = [ "jemalloc-sys", "jemallocator", + "libc", "winapi", ] @@ -5592,7 +5614,7 @@ version = "0.0.1" dependencies = [ "backtrace", "cbindgen", - "env_logger", + "env_logger 0.10.1", "keyboard-types", "lazy_static", "libc", @@ -5606,10 +5628,8 @@ dependencies = [ name = "simpleservo_jniapi" version = "0.0.1" dependencies = [ - "android_injected_glue", "android_logger", "cc", - "gstreamer", "jni", "libc", "log", @@ -5906,9 +5926,9 @@ dependencies = [ [[package]] name = "surfman" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca39befaf946247c5d3323465a9ec86c4a05523bb87a0d3eb07e71c15181a338" +checksum = "db2e4280229411d6eb8a8f873152dece1904df2682003bdc748adc181e003568" dependencies = [ "bitflags 1.3.2", "cfg_aliases", diff --git a/Cargo.toml b/Cargo.toml index 5495afa005a..c5f7aa60a9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ data-url = "0.1.0" devtools_traits = { path = "components/shared/devtools" } embedder_traits = { path = "components/shared/embedder" } encoding_rs = "0.8" -env_logger = "0.8" +env_logger = "0.10" euclid = "0.22" fnv = "1.0" fxhash = "0.2" diff --git a/README.md b/README.md index f935df82b4a..2c3a56144f6 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,27 @@ though of course it doesn’t produce a binary you can run. ### Building for Android target +Prerequisites: +Servo's build system assumes that both the Android SDK & NDK are +already installed and expects the paths to be specified via the +environment variables `ANDROID_SDK_ROOT` and `ANDROID_NDK_ROOT`. + +Servo also expects the following components are installed via +sdkmanager: + +For building: + +``` sh +sdkmanager install platform-tools platforms;android-33 +``` + +To run in emulator, also install the related components: + +``` sh +sdkmanager install emulator system-images;android-33;google_apis;x86 +``` + +Build commands: For ARM (`armv7-linux-androideabi`, most phones): ``` sh @@ -131,6 +152,12 @@ For x86 (typically for the emulator): ./mach package --release --target i686-linux-android ``` +Install the APK to the device or emulator: + +``` sh +./mach install --release --android +``` + ## Running Run Servo with the command: diff --git a/components/allocator/Cargo.toml b/components/allocator/Cargo.toml index 3d29fca5cf8..7a017e4cf92 100644 --- a/components/allocator/Cargo.toml +++ b/components/allocator/Cargo.toml @@ -9,9 +9,12 @@ publish = false [lib] path = "lib.rs" -[target.'cfg(not(windows))'.dependencies] +[target.'cfg(not(any(windows, target_os = "android")))'.dependencies] jemallocator = { workspace = true } jemalloc-sys = { workspace = true } [target.'cfg(windows)'.dependencies] winapi = { workspace = true, features = ["heapapi"] } + +[target.'cfg(target_os = "android")'.dependencies] +libc = { workspace = true } diff --git a/components/allocator/lib.rs b/components/allocator/lib.rs index d4e8c73b318..9d7c0b466f0 100644 --- a/components/allocator/lib.rs +++ b/components/allocator/lib.rs @@ -9,7 +9,7 @@ static ALLOC: Allocator = Allocator; pub use crate::platform::*; -#[cfg(not(windows))] +#[cfg(not(any(windows, target_os = "android")))] mod platform { use std::os::raw::c_void; @@ -28,6 +28,21 @@ mod platform { } } +#[cfg(target_os = "android")] +mod platform { + pub use std::alloc::System as Allocator; + use std::os::raw::c_void; + + /// Get the size of a heap block. + pub unsafe extern "C" fn usable_size(ptr: *const c_void) -> usize { + libc::malloc_usable_size(ptr) + } + + pub mod libc_compat { + pub use libc::{free, malloc, realloc}; + } +} + #[cfg(windows)] mod platform { pub use std::alloc::System as Allocator; diff --git a/components/background_hang_monitor/background_hang_monitor.rs b/components/background_hang_monitor/background_hang_monitor.rs index f8e26972a0f..1aacadfa19a 100644 --- a/components/background_hang_monitor/background_hang_monitor.rs +++ b/components/background_hang_monitor/background_hang_monitor.rs @@ -100,9 +100,9 @@ impl BackgroundHangMonitorRegister for HangMonitorRegister { not(any(target_arch = "arm", target_arch = "aarch64")) ))] let sampler = crate::sampler_linux::LinuxSampler::new(); - #[cfg(all( - any(target_os = "android", target_os = "linux"), - any(target_arch = "arm", target_arch = "aarch64") + #[cfg(any( + target_os = "android", + all(target_os = "linux", any(target_arch = "arm", target_arch = "aarch64")) ))] let sampler = crate::sampler::DummySampler::new(); diff --git a/components/bluetooth/Cargo.toml b/components/bluetooth/Cargo.toml index 4046b256a1b..536a29e583f 100644 --- a/components/bluetooth/Cargo.toml +++ b/components/bluetooth/Cargo.toml @@ -22,6 +22,7 @@ servo_rand = { path = "../rand" } uuid = { workspace = true } [features] +default = ["bluetooth-test"] native-bluetooth = ["blurz", "blurdroid", "blurmac", "bluetooth-test"] bluetooth-test = ["blurmock"] diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index d0af82067fc..5ef8ffc20a6 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -501,6 +501,33 @@ impl IOCompositor { self.shutdown_state = ShutdownState::FinishedShuttingDown; } + /// The underlying native surface can be lost during servo's lifetime. + /// On Android, for example, this happens when the app is sent to background. + /// We need to unbind the surface so that we don't try to use it again. + pub fn invalidate_native_surface(&mut self) { + debug!("Invalidating native surface in compositor"); + if let Err(e) = self.webrender_surfman.unbind_native_surface_from_context() { + warn!("Unbinding native surface from context failed ({:?})", e); + } + } + + /// On Android, this function will be called when the app moves to foreground + /// and the system creates a new native surface that needs to bound to the current + /// context. + #[allow(unsafe_code)] + pub fn replace_native_surface(&mut self, native_widget: *mut c_void, coords: DeviceIntSize) { + debug!("Replacing native surface in compositor: {native_widget:?}"); + let connection = self.webrender_surfman.connection(); + let native_widget = + unsafe { connection.create_native_widget_from_ptr(native_widget, coords.to_untyped()) }; + if let Err(e) = self + .webrender_surfman + .bind_native_surface_to_context(native_widget) + { + warn!("Binding native surface to context failed ({:?})", e); + } + } + fn handle_browser_message(&mut self, msg: CompositorMsg) -> bool { match (msg, self.shutdown_state) { (_, ShutdownState::FinishedShuttingDown) => { diff --git a/components/compositing/windowing.rs b/components/compositing/windowing.rs index 3aebe8d874c..c78ddb6ba85 100644 --- a/components/compositing/windowing.rs +++ b/components/compositing/windowing.rs @@ -10,6 +10,7 @@ use std::time::Duration; use embedder_traits::{EmbedderProxy, EventLoopWaker}; use euclid::Scale; use keyboard_types::KeyboardEvent; +use libc::c_void; use msg::constellation_msg::{PipelineId, TopLevelBrowsingContextId, TraversalDirection}; use script_traits::{MediaSessionActionType, MouseButton, TouchEventType, TouchId, WheelDelta}; use servo_geometry::DeviceIndependentPixel; @@ -105,6 +106,14 @@ pub enum EmbedderEvent { ChangeBrowserVisibility(TopLevelBrowsingContextId, bool), /// Virtual keyboard was dismissed IMEDismissed, + /// Sent on platforms like Android where the native widget surface can be + /// automatically destroyed by the system, for example when the app + /// is sent to background. + InvalidateNativeSurface, + /// Sent on platforms like Android where system recreates a new surface for + /// the native widget when it is brough back to foreground. This event + /// carries the pointer to the native widget and its new size. + ReplaceNativeSurface(*mut c_void, DeviceIntSize), } impl Debug for EmbedderEvent { @@ -139,6 +148,8 @@ impl Debug for EmbedderEvent { EmbedderEvent::ChangeBrowserVisibility(..) => write!(f, "ChangeBrowserVisibility"), EmbedderEvent::IMEDismissed => write!(f, "IMEDismissed"), EmbedderEvent::ClearCache => write!(f, "ClearCache"), + EmbedderEvent::InvalidateNativeSurface => write!(f, "InvalidateNativeSurface"), + EmbedderEvent::ReplaceNativeSurface(..) => write!(f, "ReplaceNativeSurface"), } } } diff --git a/components/gfx/platform/freetype/android/font_list.rs b/components/gfx/platform/freetype/android/font_list.rs index 8c4b2e38a17..44c6640468c 100644 --- a/components/gfx/platform/freetype/android/font_list.rs +++ b/components/gfx/platform/freetype/android/font_list.rs @@ -4,12 +4,13 @@ use std::path::Path; +use log::warn; use ucd::{Codepoint, UnicodeBlock}; use super::xml::{Attribute, Node}; use crate::text::util::is_cjk; -lazy_static! { +lazy_static::lazy_static! { static ref FONT_LIST: FontList = FontList::new(); } diff --git a/components/profile/Cargo.toml b/components/profile/Cargo.toml index 173fdbcbc2b..da55faa6a00 100644 --- a/components/profile/Cargo.toml +++ b/components/profile/Cargo.toml @@ -23,6 +23,6 @@ task_info = { path = "../../support/rust-task_info" } [target.'cfg(target_os = "linux")'.dependencies] regex = { workspace = true } -[target.'cfg(not(target_os = "windows"))'.dependencies] +[target.'cfg(not(any(target_os = "windows", target_os = "android")))'.dependencies] libc = { workspace = true } jemalloc-sys = { workspace = true } diff --git a/components/profile/mem.rs b/components/profile/mem.rs index 11542045f67..53990d7f05f 100644 --- a/components/profile/mem.rs +++ b/components/profile/mem.rs @@ -387,16 +387,16 @@ impl ReportsForest { //--------------------------------------------------------------------------- mod system_reporter { - #[cfg(not(target_os = "windows"))] + #[cfg(not(any(target_os = "windows", target_os = "android")))] use std::ffi::CString; - #[cfg(not(target_os = "windows"))] + #[cfg(not(any(target_os = "windows", target_os = "android")))] use std::mem::size_of; - #[cfg(not(target_os = "windows"))] + #[cfg(not(any(target_os = "windows", target_os = "android")))] use std::ptr::null_mut; #[cfg(target_os = "linux")] use libc::c_int; - #[cfg(not(target_os = "windows"))] + #[cfg(not(any(target_os = "windows", target_os = "android")))] use libc::{c_void, size_t}; use profile_traits::mem::{Report, ReportKind, ReporterRequest}; use profile_traits::path; @@ -499,10 +499,10 @@ mod system_reporter { None } - #[cfg(not(target_os = "windows"))] + #[cfg(not(any(target_os = "windows", target_os = "android")))] use jemalloc_sys::mallctl; - #[cfg(not(target_os = "windows"))] + #[cfg(not(any(target_os = "windows", target_os = "android")))] fn jemalloc_stat(value_name: &str) -> Option { // Before we request the measurement of interest, we first send an "epoch" // request. Without that jemalloc gives cached statistics(!) which can be @@ -549,7 +549,7 @@ mod system_reporter { Some(value as usize) } - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "android"))] fn jemalloc_stat(_value_name: &str) -> Option { None } diff --git a/components/servo/lib.rs b/components/servo/lib.rs index 424f3fc647e..662a958460c 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -503,7 +503,14 @@ where EmbedderEvent::Resize => { return self.compositor.on_resize_window_event(); }, - + EmbedderEvent::InvalidateNativeSurface => { + self.compositor.invalidate_native_surface(); + }, + EmbedderEvent::ReplaceNativeSurface(native_widget, coords) => { + self.compositor + .replace_native_surface(native_widget, coords); + self.compositor.composite(); + }, EmbedderEvent::AllowNavigationResponse(pipeline_id, allowed) => { let msg = ConstellationMsg::AllowNavigationResponse(pipeline_id, allowed); if let Err(e) = self.constellation_chan.send(msg) { @@ -1091,6 +1098,9 @@ fn default_user_agent_string_for(agent: UserAgent) -> &'static str { const DESKTOP_UA_STRING: &'static str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Servo/1.0 Firefox/111.0"; + #[cfg(target_os = "android")] + const DESKTOP_UA_STRING: &'static str = ""; + match agent { UserAgent::Desktop => DESKTOP_UA_STRING, UserAgent::Android => "Mozilla/5.0 (Android; Mobile; rv:109.0) Servo/1.0 Firefox/111.0", diff --git a/components/webrender_surfman/lib.rs b/components/webrender_surfman/lib.rs index 4d2d4346d2c..8e4dc75cb79 100644 --- a/components/webrender_surfman/lib.rs +++ b/components/webrender_surfman/lib.rs @@ -216,4 +216,28 @@ impl WebrenderSurfman { let ref context = self.0.context.borrow(); device.get_proc_address(context, name) } + + pub fn unbind_native_surface_from_context(&self) -> Result<(), Error> { + let device = self.0.device.borrow_mut(); + let mut context = self.0.context.borrow_mut(); + let mut surface = device.unbind_surface_from_context(&mut context)?.unwrap(); + let _ = device.destroy_surface(&mut context, &mut surface)?; + Ok(()) + } + + pub fn bind_native_surface_to_context(&self, native_widget: NativeWidget) -> Result<(), Error> { + let mut device = self.0.device.borrow_mut(); + let mut context = self.0.context.borrow_mut(); + let surface_access = SurfaceAccess::GPUOnly; + let surface_type = SurfaceType::Widget { native_widget }; + let surface = device.create_surface(&context, surface_access, surface_type)?; + device + .bind_surface_to_context(&mut context, surface) + .map_err(|(err, mut surface)| { + let _ = device.destroy_surface(&mut context, &mut surface); + err + })?; + device.make_context_current(&context)?; + Ok(()) + } } diff --git a/etc/run_in_headless_android_emulator.py b/etc/run_in_headless_android_emulator.py index a3b5d573ef9..758af9b5a5d 100755 --- a/etc/run_in_headless_android_emulator.py +++ b/etc/run_in_headless_android_emulator.py @@ -79,8 +79,8 @@ def main(avd_name, apk_path, *args): def tool_path(directory, bin_name): - if "ANDROID_SDK" in os.environ: - path = os.path.join(os.environ["ANDROID_SDK"], directory, bin_name) + if "ANDROID_SDK_ROOT" in os.environ: + path = os.path.join(os.environ["ANDROID_SDK_ROOT"], directory, bin_name) if os.path.exists(path): return path diff --git a/etc/shell.nix b/etc/shell.nix index 39c8ff2eea9..3c55c1ab491 100644 --- a/etc/shell.nix +++ b/etc/shell.nix @@ -10,6 +10,10 @@ with import (builtins.fetchTarball { url = "https://github.com/oxalica/rust-overlay/archive/a0df72e106322b67e9c6e591fe870380bd0da0d5.tar.gz"; })) ]; + config = { + android_sdk.accept_license = true; + allowUnfree = true; + }; }; let rustToolchain = rust-bin.fromRustupToolchainFile ../rust-toolchain.toml; @@ -27,6 +31,25 @@ let # - glibc 2.38 (#31054) llvmPackages = llvmPackages_14; stdenv = llvmPackages.stdenv; + + buildToolsVersion = "33.0.2"; + androidComposition = androidenv.composeAndroidPackages { + buildToolsVersions = [ buildToolsVersion ]; + includeEmulator = true; + platformVersions = [ "33" ]; + includeSources = false; + includeSystemImages = true; + systemImageTypes = [ "google_apis" ]; + abiVersions = [ "x86" "armeabi-v7a" ]; + includeNDK = true; + ndkVersion = "25.2.9519653"; + useGoogleAPIs = false; + useGoogleTVAddOns = false; + includeExtras = [ + "extras;google;gcm" + ]; + }; + androidSdk = androidComposition.androidsdk; in stdenv.mkDerivation rec { name = "servo-env"; @@ -111,22 +134,33 @@ stdenv.mkDerivation rec { RUSTC_BOOTSTRAP = "crown"; })) + + # for android builds + # TODO: make this optional + openjdk17_headless + androidSdk ] ++ (lib.optionals stdenv.isDarwin [ darwin.apple_sdk.frameworks.AppKit ]); LIBCLANG_PATH = llvmPackages.clang-unwrapped.lib + "/lib/"; + # Required by ./mach build --android + ANDROID_SDK_ROOT = "${androidSdk}/libexec/android-sdk"; + ANDROID_NDK_ROOT = "${ANDROID_SDK_ROOT}/ndk-bundle"; + GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_SDK_ROOT}/build-tools/${buildToolsVersion}/aapt2"; + # Allow cargo to download crates SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt"; # Enable colored cargo and rustc output TERMINFO = "${ncurses.out}/share/terminfo"; + # Provide libraries that aren’t linked against but somehow required LD_LIBRARY_PATH = lib.makeLibraryPath [ # Fixes missing library errors - xorg.libXcursor xorg.libXrandr xorg.libXi libxkbcommon + zlib xorg.libXcursor xorg.libXrandr xorg.libXi libxkbcommon # [WARN script::dom::gpu] Could not get GPUAdapter ("NotFound") # TLA Err: Error: Couldn't request WebGPU adapter. diff --git a/ports/libsimpleservo/api/src/lib.rs b/ports/libsimpleservo/api/src/lib.rs index cd9abc9b511..5a3d1e4cd74 100644 --- a/ports/libsimpleservo/api/src/lib.rs +++ b/ports/libsimpleservo/api/src/lib.rs @@ -18,6 +18,7 @@ use servo::compositing::windowing::{ AnimationState, EmbedderCoordinates, EmbedderEvent, EmbedderMethods, MouseWindowEvent, WindowMethods, }; +use servo::compositing::CompositeTarget; use servo::config::prefs::pref_map; pub use servo::config::prefs::{add_user_prefs, PrefValue}; use servo::embedder_traits::resources::{self, Resource, ResourceReaderMethods}; @@ -298,7 +299,12 @@ pub fn init( gl: gl.clone(), }); - let servo = Servo::new(embedder_callbacks, window_callbacks.clone(), None); + let servo = Servo::new( + embedder_callbacks, + window_callbacks.clone(), + None, + CompositeTarget::Window, + ); SERVO.with(|s| { let mut servo_glue = ServoGlue { @@ -569,6 +575,24 @@ impl ServoGlue { self.process_event(EmbedderEvent::Keyboard(key_event)) } + pub fn pause_compositor(&mut self) -> Result<(), &'static str> { + self.process_event(EmbedderEvent::InvalidateNativeSurface) + } + + pub fn resume_compositor( + &mut self, + native_surface: *mut c_void, + coords: Coordinates, + ) -> Result<(), &'static str> { + if native_surface.is_null() { + panic!("null passed for native_surface"); + } + self.process_event(EmbedderEvent::ReplaceNativeSurface( + native_surface, + coords.framebuffer, + )) + } + pub fn media_session_action( &mut self, action: MediaSessionActionType, @@ -789,6 +813,9 @@ impl ServoGlue { EmbedderMsg::Panic(reason, backtrace) => { self.callbacks.host_callbacks.on_panic(reason, backtrace); }, + EmbedderMsg::ReadyToPresent => { + self.servo.present(); + }, EmbedderMsg::Status(..) | EmbedderMsg::SelectFiles(..) | EmbedderMsg::MoveTo(..) | @@ -798,7 +825,6 @@ impl ServoGlue { EmbedderMsg::NewFavicon(..) | EmbedderMsg::HeadParsed | EmbedderMsg::SetFullscreenState(..) | - EmbedderMsg::ReadyToPresent | EmbedderMsg::ReportProfile(..) | EmbedderMsg::EventDelivered(..) => {}, } diff --git a/ports/libsimpleservo/jniapi/Cargo.toml b/ports/libsimpleservo/jniapi/Cargo.toml index 020767c41be..b74591ff7bf 100644 --- a/ports/libsimpleservo/jniapi/Cargo.toml +++ b/ports/libsimpleservo/jniapi/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "simpleservo_jniapi" version = "0.0.1" -build = "build.rs" authors = ["The Servo Project Developers"] license = "MPL-2.0" edition = "2018" @@ -14,21 +13,21 @@ test = false bench = false [dependencies] -android_injected_glue = "0.2" -android_logger = "0.10" -gstreamer = { workspace = true } +android_logger = "0.13" jni = "0.18.0" libc = { workspace = true } log = { workspace = true } serde_json = { workspace = true } -simpleservo = { path = "../api" } +# TODO: Once the native-bluetooth feature works for +# Android, remove the explicit feature list here. +simpleservo = { path = "../api", default-features = false, features = ["max_log_level", "webdriver"] } [build-dependencies] cc = "1.0" [features] debugmozjs = ["simpleservo/debugmozjs"] -default = ["max_log_level", "native-bluetooth", "webdriver"] +default = ["max_log_level", "webdriver", "no_static_freetype"] googlevr = ["simpleservo/googlevr"] js_backtrace = ["simpleservo/js_backtrace"] max_log_level = ["simpleservo/max_log_level"] @@ -36,3 +35,4 @@ media-gstreamer = ["simpleservo/media-gstreamer"] native-bluetooth = ["simpleservo/native-bluetooth"] webdriver = ["simpleservo/webdriver"] webgl_backtrace = ["simpleservo/webgl_backtrace"] +no_static_freetype = ["simpleservo/no_static_freetype"] diff --git a/ports/libsimpleservo/jniapi/build.rs b/ports/libsimpleservo/jniapi/build.rs deleted file mode 100644 index e97cb310111..00000000000 --- a/ports/libsimpleservo/jniapi/build.rs +++ /dev/null @@ -1,33 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -use std::env; -use std::path::Path; - -fn main() { - // Get the NDK path from NDK_HOME env. - let ndk_path = - env::var_os("ANDROID_NDK").expect("Please set the ANDROID_NDK environment variable"); - let ndk_path = Path::new(&ndk_path); - - // compiling android_native_app_glue.c - let c_file = ndk_path - .join("sources") - .join("android") - .join("native_app_glue") - .join("android_native_app_glue.c"); - cc::Build::new() - .file(c_file) - .warnings(false) - .compile("android_native_app_glue"); - - // Get the output directory. - let out_dir = - env::var("OUT_DIR").expect("Cargo should have set the OUT_DIR environment variable"); - - println!("cargo:rustc-link-lib=static=android_native_app_glue"); - println!("cargo:rustc-link-search=native={}", out_dir); - println!("cargo:rustc-link-lib=log"); - println!("cargo:rustc-link-lib=android"); -} diff --git a/ports/libsimpleservo/jniapi/src/lib.rs b/ports/libsimpleservo/jniapi/src/lib.rs index 53dc59047eb..0293a8332e4 100644 --- a/ports/libsimpleservo/jniapi/src/lib.rs +++ b/ports/libsimpleservo/jniapi/src/lib.rs @@ -5,21 +5,18 @@ #![allow(non_snake_case)] use std::os::raw::{c_char, c_int, c_void}; -use std::ptr::{null, null_mut}; use std::sync::Arc; use std::thread; -use android_logger::{self, Filter}; -use gstreamer::debug_set_threshold_from_string; +use android_logger::{self, Config, FilterBuilder}; use jni::objects::{GlobalRef, JClass, JObject, JString, JValue}; use jni::sys::{jboolean, jfloat, jint, jstring, JNI_TRUE}; -use jni::{errors, JNIEnv, JavaVM}; +use jni::{JNIEnv, JavaVM}; use libc::{dup2, pipe, read}; -use log::Level; +use log::{debug, error, info, warn}; use simpleservo::{ - self, self, deinit, gl_glue, gl_glue, Coordinates, DeviceIntRect, EventLoopWaker, HostTrait, - InitOptions, InputMethodType, MediaSessionPlaybackState, MouseButton, PromptResult, ServoGlue, - ServoGlue, VRInitOptions, SERVO, SERVO, + self, gl_glue, Coordinates, DeviceIntRect, EventLoopWaker, HostTrait, InitOptions, + InputMethodType, MediaSessionPlaybackState, PromptResult, ServoGlue, SERVO, }; struct HostCallbacks { @@ -27,6 +24,19 @@ struct HostCallbacks { jvm: JavaVM, } +extern "C" { + fn ANativeWindow_fromSurface(env: *mut jni::sys::JNIEnv, surface: JObject) -> *mut c_void; +} + +#[no_mangle] +pub fn android_main() { + // FIXME(mukilan): this android_main is only present to stop + // the java side 'System.loadLibrary('simpleservo') call from + // failing due to undefined reference to android_main introduced + // by winit's android-activity crate. There is no way to disable + // this currently. +} + fn call(env: &JNIEnv, f: F) where F: Fn(&mut ServoGlue) -> Result<(), &str>, @@ -51,11 +61,12 @@ pub fn Java_org_mozilla_servoview_JNIServo_version(env: JNIEnv, _class: JClass) pub fn Java_org_mozilla_servoview_JNIServo_init( env: JNIEnv, _: JClass, - activity: JObject, + _activity: JObject, opts: JObject, callbacks_obj: JObject, + surface: JObject, ) { - let (mut opts, log, log_str, gst_debug_str) = match get_options(&env, opts) { + let (mut opts, log, log_str, _gst_debug_str) = match get_options(&env, opts, surface) { Ok((opts, log, log_str, gst_debug_str)) => (opts, log, log_str, gst_debug_str), Err(err) => { throw(&env, &err); @@ -79,26 +90,26 @@ pub fn Java_org_mozilla_servoview_JNIServo_init( "compositing::compositor", "constellation::constellation", ]; - let mut filter = Filter::default().with_min_level(Level::Debug); + let mut filter_builder = FilterBuilder::new(); for &module in &filters { - filter = filter.with_allowed_module_path(module); + filter_builder.filter_module(module, log::LevelFilter::Debug); } if let Some(log_str) = log_str { for module in log_str.split(',') { - filter = filter.with_allowed_module_path(module); + filter_builder.filter_module(module, log::LevelFilter::Debug); } } - if let Some(gst_debug_str) = gst_debug_str { - debug_set_threshold_from_string(&gst_debug_str, true); - } - - android_logger::init_once(filter, Some("simpleservo")); + android_logger::init_once( + Config::default() + .with_max_level(log::LevelFilter::Debug) + .with_filter(filter_builder.build()) + .with_tag("simpleservo"), + ) } info!("init"); - initialize_android_glue(&env, activity); redirect_stdout_to_logcat(); let callbacks_ref = match env.new_global_ref(callbacks_obj) { @@ -329,11 +340,33 @@ pub fn Java_org_mozilla_servoview_JNIServo_pinchZoomEnd( } #[no_mangle] -pub fn Java_org_mozilla_servoview_JNIServo_click(env: JNIEnv, _: JClass, x: jint, y: jint) { +pub fn Java_org_mozilla_servoview_JNIServo_click(env: JNIEnv, _: JClass, x: jfloat, y: jfloat) { debug!("click"); call(&env, |s| s.click(x as f32, y as f32)); } +#[no_mangle] +pub fn Java_org_mozilla_servoview_JNIServo_pauseCompositor(env: JNIEnv, _: JClass) { + debug!("pauseCompositor"); + call(&env, |s| s.pause_compositor()); +} + +#[no_mangle] +pub fn Java_org_mozilla_servoview_JNIServo_resumeCompositor( + env: JNIEnv, + _: JClass, + surface: JObject, + coordinates: JObject, +) { + debug!("resumeCompositor"); + let widget = unsafe { ANativeWindow_fromSurface(env.get_native_interface(), surface) }; + let coords = jni_coords_to_rust_coords(&env, coordinates); + match coords { + Ok(coords) => call(&env, |s| s.resume_compositor(widget, coords.clone())), + Err(error) => throw(&env, &error), + } +} + #[no_mangle] pub fn Java_org_mozilla_servoview_JNIServo_mediaSessionAction( env: JNIEnv, @@ -379,20 +412,6 @@ impl HostCallbacks { } impl HostTrait for HostCallbacks { - fn flush(&self) { - debug!("flush"); - let env = self.jvm.get_env().unwrap(); - env.call_method(self.callbacks.as_obj(), "flush", "()V", &[]) - .unwrap(); - } - - fn make_current(&self) { - debug!("make_current"); - let env = self.jvm.get_env().unwrap(); - env.call_method(self.callbacks.as_obj(), "makeCurrent", "()V", &[]) - .unwrap(); - } - fn prompt_alert(&self, message: String, _trusted: bool) { debug!("prompt_alert"); let env = self.jvm.get_env().unwrap(); @@ -446,9 +465,10 @@ impl HostTrait for HostCallbacks { .unwrap(); } - fn on_title_changed(&self, title: String) { + fn on_title_changed(&self, title: Option) { debug!("on_title_changed"); let env = self.jvm.get_env().unwrap(); + let title = title.unwrap_or_else(String::new); let s = match new_string(&env, &title) { Ok(s) => s, Err(_) => return, @@ -529,7 +549,7 @@ impl HostTrait for HostCallbacks { fn on_ime_show( &self, - _type: InputEncoding, + _input_type: InputMethodType, _text: Option<(String, i32)>, _multiline: bool, _rect: DeviceIntRect, @@ -610,65 +630,16 @@ impl HostTrait for HostCallbacks { .unwrap(); } - fn on_devtools_started(&self, port: Result) { + fn on_devtools_started(&self, port: Result, _token: String) { match port { Ok(p) => info!("Devtools Server running on port {}", p), Err(()) => error!("Error running devtools server"), } } -} -fn initialize_android_glue(env: &JNIEnv, activity: JObject) { - use android_injected_glue::{ffi, ANDROID_APP}; + fn show_context_menu(&self, _title: Option, _items: Vec) {} - // From jni-rs to android_injected_glue - - let clazz = Box::leak(Box::new(env.new_global_ref(activity).unwrap())); - - let activity = Box::into_raw(Box::new(ffi::ANativeActivity { - clazz: clazz.as_obj().into_inner() as *mut c_void, - vm: env.get_java_vm().unwrap().get_java_vm_pointer() as *mut ffi::_JavaVM, - - callbacks: null_mut(), - env: null_mut(), - internalDataPath: null(), - externalDataPath: null(), - sdkVersion: 0, - instance: null_mut(), - assetManager: null_mut(), - obbPath: null(), - })); - - extern "C" fn on_app_cmd(_: *mut ffi::android_app, _: i32) {} - extern "C" fn on_input_event(_: *mut ffi::android_app, _: *const c_void) -> i32 { - 0 - } - - let app = Box::into_raw(Box::new(ffi::android_app { - activity, - onAppCmd: on_app_cmd, - onInputEvent: on_input_event, - - userData: null_mut(), - config: null(), - savedState: null_mut(), - savedStateSize: 0, - looper: null_mut(), - inputQueue: null(), - window: null_mut(), - contentRect: ffi::ARect { - left: 0, - top: 0, - right: 0, - bottom: 0, - }, - activityState: 0, - destroyRequested: 0, - })); - - unsafe { - ANDROID_APP = app; - } + fn on_panic(&self, _reason: String, _backtrace: Option) {} } extern "C" { @@ -803,28 +774,21 @@ fn jni_coords_to_rust_coords(env: &JNIEnv, obj: JObject) -> Result( env: &'a JNIEnv, - obj: JObject, + obj: JObject<'a>, field: &str, type_: &str, ) -> Result>, String> { if env.get_field_id(obj, field, type_).is_err() { - return Err(format!("Can't find `{}` field", &field)); + return Err(format!("Can't find `{}` field", field)); } env.get_field(obj, field, type_) .map(|value| Some(value)) - .or_else(|e| match *e.kind() { - errors::ErrorKind::NullPtr(_) => Ok(None), - _ => Err(format!( - "Can't find `{}` field: {}", - &field, - e.description() - )), - }) + .or_else(|_| Err(format!("Can't find `{}` field", field))) } fn get_non_null_field<'a>( env: &'a JNIEnv, - obj: JObject, + obj: JObject<'a>, field: &str, type_: &str, ) -> Result, String> { @@ -851,6 +815,7 @@ fn get_string(env: &JNIEnv, obj: JObject, field: &str) -> Result, fn get_options( env: &JNIEnv, opts: JObject, + surface: JObject, ) -> Result<(InitOptions, bool, Option, Option), String> { let args = get_string(env, opts, "args")?; let url = get_string(env, opts, "url")?; @@ -862,13 +827,6 @@ fn get_options( let log = get_non_null_field(env, opts, "enableLogs", "Z")? .z() .map_err(|_| "enableLogs not a boolean")?; - let enable_subpixel_text_antialiasing = - get_non_null_field(env, opts, "enableSubpixelTextAntialiasing", "Z")? - .z() - .map_err(|_| "enableSubpixelTextAntialiasing not a boolean")?; - let vr_pointer = get_non_null_field(env, opts, "VRExternalContext", "J")? - .j() - .map_err(|_| "VRExternalContext is not a long")? as *mut c_void; let coordinates = get_non_null_field( env, opts, @@ -885,20 +843,16 @@ fn get_options( None => None, }; + let native_window = unsafe { ANativeWindow_fromSurface(env.get_native_interface(), surface) }; let opts = InitOptions { args: args.unwrap_or(vec![]), - url, coordinates, density, - enable_subpixel_text_antialiasing, - vr_init: if vr_pointer.is_null() { - VRInitOptions::None - } else { - VRInitOptions::VRExternal(vr_pointer) - }, xr_discovery: None, gl_context_pointer: None, native_display_pointer: None, + surfman_integration: simpleservo::SurfmanIntegration::Widget(native_window), + prefs: None, }; Ok((opts, log, log_str, gst_debug_str)) } diff --git a/python/servo/bootstrap_commands.py b/python/servo/bootstrap_commands.py index 1c7afc957d8..33d3b20aa2f 100644 --- a/python/servo/bootstrap_commands.py +++ b/python/servo/bootstrap_commands.py @@ -12,7 +12,6 @@ import glob import json import os import os.path as path -import platform import re import subprocess import sys @@ -30,7 +29,7 @@ from mach.decorators import ( import servo.platform from servo.command_base import CommandBase, cd, check_call -from servo.util import delete, download_bytes, download_file, extract, check_hash +from servo.util import delete, download_bytes @CommandProvider @@ -66,150 +65,6 @@ class MachCommands(CommandBase): return 1 return 0 - @Command('bootstrap-android', - description='Install the Android SDK and NDK.', - category='bootstrap') - @CommandArgument('--build', - action='store_true', - help='Install Android-specific dependencies for building') - @CommandArgument('--emulator-x86', - action='store_true', - help='Install Android x86 emulator and system image') - @CommandArgument('--accept-all-licences', - action='store_true', - help='For non-interactive use') - def bootstrap_android(self, build=False, emulator_x86=False, accept_all_licences=False): - if not (build or emulator_x86): - print("Must specify `--build` or `--emulator-x86` or both.") - - ndk = "android-ndk-r15c-{system}-{arch}" - tools = "sdk-tools-{system}-4333796" - - emulator_platform = "android-28" - emulator_image = "system-images;%s;google_apis;x86" % emulator_platform - - known_sha1 = { - # https://dl.google.com/android/repository/repository2-1.xml - "sdk-tools-darwin-4333796.zip": "ed85ea7b59bc3483ce0af4c198523ba044e083ad", - "sdk-tools-linux-4333796.zip": "8c7c28554a32318461802c1291d76fccfafde054", - "sdk-tools-windows-4333796.zip": "aa298b5346ee0d63940d13609fe6bec621384510", - - # https://developer.android.com/ndk/downloads/older_releases - "android-ndk-r15c-windows-x86.zip": "f2e47121feb73ec34ced5e947cbf1adc6b56246e", - "android-ndk-r15c-windows-x86_64.zip": "970bb2496de0eada74674bb1b06d79165f725696", - "android-ndk-r15c-darwin-x86_64.zip": "ea4b5d76475db84745aa8828000d009625fc1f98", - "android-ndk-r15c-linux-x86_64.zip": "0bf02d4e8b85fd770fd7b9b2cdec57f9441f27a2", - } - - toolchains = path.join(self.context.topdir, "android-toolchains") - if not path.isdir(toolchains): - os.makedirs(toolchains) - - def download(target_dir, name, flatten=False): - final = path.join(toolchains, target_dir) - if path.isdir(final): - return - - base_url = "https://dl.google.com/android/repository/" - filename = name + ".zip" - url = base_url + filename - archive = path.join(toolchains, filename) - - if not path.isfile(archive): - download_file(filename, url, archive) - check_hash(archive, known_sha1[filename], "sha1") - print("Extracting " + filename) - remove = True # Set to False to avoid repeated downloads while debugging this script - if flatten: - extracted = final + "_" - extract(archive, extracted, remove=remove) - contents = os.listdir(extracted) - assert len(contents) == 1 - os.rename(path.join(extracted, contents[0]), final) - os.rmdir(extracted) - else: - extract(archive, final, remove=remove) - - system = platform.system().lower() - machine = platform.machine().lower() - arch = {"i386": "x86"}.get(machine, machine) - if build: - download("ndk", ndk.format(system=system, arch=arch), flatten=True) - download("sdk", tools.format(system=system)) - - components = [] - if emulator_x86: - components += [ - "platform-tools", - "emulator", - "platforms;" + emulator_platform, - emulator_image, - ] - if build: - components += [ - "platform-tools", - "platforms;android-18", - ] - - sdkmanager = [path.join(toolchains, "sdk", "tools", "bin", "sdkmanager")] + components - if accept_all_licences: - yes = subprocess.Popen(["yes"], stdout=subprocess.PIPE) - process = subprocess.Popen( - sdkmanager, stdin=yes.stdout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - ) - # Reduce progress bar spam by removing duplicate lines. - # Printing the same line again with \r is a no-op in a real terminal, - # but each line is shown individually in Taskcluster's log viewer. - previous_line = None - line = b"" - while 1: - # Read one byte at a time because in Python: - # * readline() blocks until "\n", which doesn't come before the prompt - # * read() blocks until EOF, which doesn't come before the prompt - # * read(n) keeps reading until it gets n bytes or EOF, - # but we don't know reliably how many bytes to read until the prompt - byte = process.stdout.read(1) - if len(byte) == 0: - print(line) - break - line += byte - if byte == b'\n' or byte == b'\r': - if line != previous_line: - print(line.decode("utf-8", "replace"), end="") - sys.stdout.flush() - previous_line = line - line = b"" - exit_code = process.wait() - yes.terminate() - if exit_code: - return exit_code - else: - subprocess.check_call(sdkmanager) - - if emulator_x86: - avd_path = path.join(toolchains, "avd", "servo-x86") - process = subprocess.Popen(stdin=subprocess.PIPE, stdout=subprocess.PIPE, args=[ - path.join(toolchains, "sdk", "tools", "bin", "avdmanager"), - "create", "avd", - "--path", avd_path, - "--name", "servo-x86", - "--package", emulator_image, - "--force", - ]) - output = b"" - while 1: - # Read one byte at a time, see comment above. - byte = process.stdout.read(1) - if len(byte) == 0: - break - output += byte - # There seems to be no way to disable this prompt: - if output.endswith(b"Do you wish to create a custom hardware profile? [no]"): - process.stdin.write("no\n") - assert process.wait() == 0 - with open(path.join(avd_path, "config.ini"), "a") as f: - f.write("disk.dataPartition.size=2G\n") - @Command('update-hsts-preload', description='Download the HSTS preload list', category='bootstrap') diff --git a/python/servo/build_commands.py b/python/servo/build_commands.py index f61c82fe5b3..dd63aff54f3 100644 --- a/python/servo/build_commands.py +++ b/python/servo/build_commands.py @@ -98,7 +98,6 @@ class MachCommands(CommandBase): for key in env: print((key, env[key])) - self.download_and_build_android_dependencies_if_needed(env) status = self.run_cargo_build_like_command( "build", opts, env=env, verbose=verbose, libsimpleservo=libsimpleservo, **kwargs diff --git a/python/servo/command_base.py b/python/servo/command_base.py index f6d105e7837..833f0bac417 100644 --- a/python/servo/command_base.py +++ b/python/servo/command_base.py @@ -334,7 +334,7 @@ class CommandBase(object): def get_binary_path(self, build_type: BuildType, target=None, android=False, simpleservo=False): base_path = util.get_target_dir() if android: - base_path = path.join(base_path, "android", self.config["android"]["target"]) + base_path = path.join(base_path, self.config["android"]["target"]) simpleservo = True elif target: base_path = path.join(base_path, target) @@ -529,58 +529,38 @@ class CommandBase(object): # Paths to Android build tools: if self.config["android"]["sdk"]: - env["ANDROID_SDK"] = self.config["android"]["sdk"] + env["ANDROID_SDK_ROOT"] = self.config["android"]["sdk"] if self.config["android"]["ndk"]: - env["ANDROID_NDK"] = self.config["android"]["ndk"] - if self.config["android"]["toolchain"]: - env["ANDROID_TOOLCHAIN"] = self.config["android"]["toolchain"] - if self.config["android"]["platform"]: - env["ANDROID_PLATFORM"] = self.config["android"]["platform"] - - # These are set because they are the variable names that build-apk - # expects. However, other submodules have makefiles that reference - # the env var names above. Once winit is enabled and set as the - # default, we could modify the subproject makefiles to use the names - # below and remove the vars above, to avoid duplication. - if "ANDROID_SDK" in env: - env["ANDROID_HOME"] = env["ANDROID_SDK"] - if "ANDROID_NDK" in env: - env["NDK_HOME"] = env["ANDROID_NDK"] - if "ANDROID_TOOLCHAIN" in env: - env["NDK_STANDALONE"] = env["ANDROID_TOOLCHAIN"] + env["ANDROID_NDK_ROOT"] = self.config["android"]["ndk"] toolchains = path.join(self.context.topdir, "android-toolchains") for kind in ["sdk", "ndk"]: default = os.path.join(toolchains, kind) if os.path.isdir(default): - env.setdefault("ANDROID_" + kind.upper(), default) + env.setdefault(f"ANDROID_{kind.upper()}_ROOT", default) - tools = os.path.join(toolchains, "sdk", "platform-tools") - if os.path.isdir(tools): - env["PATH"] = "%s%s%s" % (tools, os.pathsep, env["PATH"]) - - if "ANDROID_NDK" not in env: - print("Please set the ANDROID_NDK environment variable.") + if "ANDROID_NDK_ROOT" not in env: + print("Please set the ANDROID_NDK_ROOT environment variable.") sys.exit(1) - if "ANDROID_SDK" not in env: - print("Please set the ANDROID_SDK environment variable.") + if "ANDROID_SDK_ROOT" not in env: + print("Please set the ANDROID_SDK_ROOT environment variable.") sys.exit(1) android_platform = self.config["android"]["platform"] android_toolchain_name = self.config["android"]["toolchain_name"] - android_toolchain_prefix = self.config["android"]["toolchain_prefix"] android_lib = self.config["android"]["lib"] - android_arch = self.config["android"]["arch"] - # Check if the NDK version is 15 - if not os.path.isfile(path.join(env["ANDROID_NDK"], 'source.properties')): + android_api = android_platform.replace('android-', '') + + # Check if the NDK version is 25 + if not os.path.isfile(path.join(env["ANDROID_NDK_ROOT"], 'source.properties')): print("ANDROID_NDK should have file `source.properties`.") - print("The environment variable ANDROID_NDK may be set at a wrong path.") + print("The environment variable ANDROID_NDK_ROOT may be set at a wrong path.") sys.exit(1) - with open(path.join(env["ANDROID_NDK"], 'source.properties'), encoding="utf8") as ndk_properties: + with open(path.join(env["ANDROID_NDK_ROOT"], 'source.properties'), encoding="utf8") as ndk_properties: lines = ndk_properties.readlines() - if lines[1].split(' = ')[1].split('.')[0] != '15': - print("Currently only support NDK 15. Please re-run `./mach bootstrap-android`.") + if lines[1].split(' = ')[1].split('.')[0] != '25': + print("Servo currently only supports NDK r25c.") sys.exit(1) # Android builds also require having the gcc bits on the PATH and various INCLUDE @@ -598,48 +578,33 @@ class CommandBase(object): host_suffix = "x86_64" host = os_type + "-" + host_suffix - host_cc = env.get('HOST_CC') or shutil.which(["clang"]) or util.whichget_exec_path(["gcc"]) - host_cxx = env.get('HOST_CXX') or util.whichget_exec_path(["clang++"]) or util.whichget_exec_path(["g++"]) - - llvm_toolchain = path.join(env['ANDROID_NDK'], "toolchains", "llvm", "prebuilt", host) - gcc_toolchain = path.join(env['ANDROID_NDK'], "toolchains", - android_toolchain_prefix + "-4.9", "prebuilt", host) - gcc_libs = path.join(gcc_toolchain, "lib", "gcc", android_toolchain_name, "4.9.x") + host_cc = env.get('HOST_CC') or shutil.which("clang") + host_cxx = env.get('HOST_CXX') or shutil.which("clang++") + llvm_toolchain = path.join(env['ANDROID_NDK_ROOT'], "toolchains", "llvm", "prebuilt", host) env['PATH'] = (path.join(llvm_toolchain, "bin") + ':' + env['PATH']) - env['ANDROID_SYSROOT'] = path.join(env['ANDROID_NDK'], "sysroot") - support_include = path.join(env['ANDROID_NDK'], "sources", "android", "support", "include") - cpufeatures_include = path.join(env['ANDROID_NDK'], "sources", "android", "cpufeatures") - cxx_include = path.join(env['ANDROID_NDK'], "sources", "cxx-stl", - "llvm-libc++", "include") - clang_include = path.join(llvm_toolchain, "lib64", "clang", "3.8", "include") - cxxabi_include = path.join(env['ANDROID_NDK'], "sources", "cxx-stl", - "llvm-libc++abi", "include") - sysroot_include = path.join(env['ANDROID_SYSROOT'], "usr", "include") - arch_include = path.join(sysroot_include, android_toolchain_name) - android_platform_dir = path.join(env['ANDROID_NDK'], "platforms", android_platform, "arch-" + android_arch) - arch_libs = path.join(android_platform_dir, "usr", "lib") - clang_include = path.join(llvm_toolchain, "lib64", "clang", "5.0", "include") - android_api = android_platform.replace('android-', '') + + def to_ndk_bin(prog): + return path.join(llvm_toolchain, "bin", prog) env["RUST_TARGET"] = self.cross_compile_target env['HOST_CC'] = host_cc env['HOST_CXX'] = host_cxx env['HOST_CFLAGS'] = '' env['HOST_CXXFLAGS'] = '' - env['CC'] = path.join(llvm_toolchain, "bin", "clang") - env['CPP'] = path.join(llvm_toolchain, "bin", "clang") + " -E" - env['CXX'] = path.join(llvm_toolchain, "bin", "clang++") - env['ANDROID_TOOLCHAIN'] = gcc_toolchain - env['ANDROID_TOOLCHAIN_DIR'] = gcc_toolchain - env['ANDROID_VERSION'] = android_api - env['ANDROID_PLATFORM_DIR'] = android_platform_dir - env['GCC_TOOLCHAIN'] = gcc_toolchain - gcc_toolchain_bin = path.join(gcc_toolchain, android_toolchain_name, "bin") - env['AR'] = path.join(gcc_toolchain_bin, "ar") - env['RANLIB'] = path.join(gcc_toolchain_bin, "ranlib") - env['OBJCOPY'] = path.join(gcc_toolchain_bin, "objcopy") - env['YASM'] = path.join(env['ANDROID_NDK'], 'prebuilt', host, 'bin', 'yasm') + env['CC'] = to_ndk_bin("clang") + env['CPP'] = to_ndk_bin("clang") + " -E" + env['CXX'] = to_ndk_bin("clang++") + + env['AR'] = to_ndk_bin("llvm-ar") + env['RANLIB'] = to_ndk_bin("llvm-ranlib") + env['OBJCOPY'] = to_ndk_bin("llvm-objcopy") + env['YASM'] = to_ndk_bin("yasm") + env['STRIP'] = to_ndk_bin("llvm-strip") + env['HARFBUZZ_SYS_NO_PKG_CONFIG'] = "true" + env['RUST_FONTCONFIG_DLOPEN'] = "on" + + env["LIBCLANG_PATH"] = path.join(llvm_toolchain, "lib64") # A cheat-sheet for some of the build errors caused by getting the search path wrong... # # fatal error: 'limits' file not found @@ -651,52 +616,28 @@ class CommandBase(object): # # Also worth remembering: autoconf uses C for its configuration, # even for C++ builds, so the C flags need to line up with the C++ flags. - env['CFLAGS'] = ' '.join([ - "--target=" + self.cross_compile_target, - "--sysroot=" + env['ANDROID_SYSROOT'], - "--gcc-toolchain=" + gcc_toolchain, - "-isystem", sysroot_include, - "-I" + arch_include, - "-B" + arch_libs, - "-L" + arch_libs, - "-D__ANDROID_API__=" + android_api, - ]) - env['CXXFLAGS'] = ' '.join([ - "--target=" + self.cross_compile_target, - "--sysroot=" + env['ANDROID_SYSROOT'], - "--gcc-toolchain=" + gcc_toolchain, - "-I" + cpufeatures_include, - "-I" + cxx_include, - "-I" + clang_include, - "-isystem", sysroot_include, - "-I" + cxxabi_include, - "-I" + clang_include, - "-I" + arch_include, - "-I" + support_include, - "-L" + gcc_libs, - "-B" + arch_libs, - "-L" + arch_libs, - "-D__ANDROID_API__=" + android_api, - "-D__STDC_CONSTANT_MACROS", - "-D__NDK_FPABI__=", - ]) - env['CPPFLAGS'] = ' '.join([ - "--target=" + self.cross_compile_target, - "--sysroot=" + env['ANDROID_SYSROOT'], - "-I" + arch_include, - ]) - env["NDK_ANDROID_VERSION"] = android_api + env['CFLAGS'] = "--target=" + android_toolchain_name + env['CXXFLAGS'] = "--target=" + android_toolchain_name + + # These two variables are needed for the mozjs compilation. + env['ANDROID_API_LEVEL'] = android_api + env["ANDROID_NDK_HOME"] = env["ANDROID_NDK_ROOT"] + + # The two variables set below are passed by our custom + # support/android/toolchain.cmake to the NDK's CMake toolchain file env["ANDROID_ABI"] = android_lib env["ANDROID_PLATFORM"] = android_platform - env["NDK_CMAKE_TOOLCHAIN_FILE"] = path.join(env['ANDROID_NDK'], "build", "cmake", "android.toolchain.cmake") - env["CMAKE_TOOLCHAIN_FILE"] = path.join(self.android_support_dir(), "toolchain.cmake") + env["NDK_CMAKE_TOOLCHAIN_FILE"] = path.join( + env['ANDROID_NDK_ROOT'], "build", "cmake", "android.toolchain.cmake") + env["CMAKE_TOOLCHAIN_FILE"] = path.join( + self.context.topdir, "support", "android", "toolchain.cmake") # Set output dir for gradle aar files - env["AAR_OUT_DIR"] = self.android_aar_dir() + env["AAR_OUT_DIR"] = path.join(self.context.topdir, "target", "android", "aar") if not os.path.exists(env['AAR_OUT_DIR']): os.makedirs(env['AAR_OUT_DIR']) - env['PKG_CONFIG_ALLOW_CROSS'] = "1" + env['PKG_CONFIG_SYSROOT_DIR'] = path.join(llvm_toolchain, 'sysroot') @staticmethod def common_command_arguments(build_configuration=False, build_type=False): @@ -863,11 +804,7 @@ class CommandBase(object): if self.config["build"]["media-stack"] != "auto": media_stack = self.config["build"]["media-stack"] assert media_stack - elif ( - not self.cross_compile_target - or ("armv7" in self.cross_compile_target and self.is_android_build) - or "x86_64" in self.cross_compile_target - ): + elif not self.cross_compile_target: media_stack = "gstreamer" else: media_stack = "dummy" @@ -943,22 +880,16 @@ class CommandBase(object): return call(["cargo", command] + args + cargo_args, env=env, verbose=verbose) - def android_support_dir(self): - return path.join(self.context.topdir, "support", "android") - - def android_aar_dir(self): - return path.join(self.context.topdir, "target", "android", "aar") - def android_adb_path(self, env): - if "ANDROID_SDK" in env: - sdk_adb = path.join(env["ANDROID_SDK"], "platform-tools", "adb") + if "ANDROID_SDK_ROOT" in env: + sdk_adb = path.join(env["ANDROID_SDK_ROOT"], "platform-tools", "adb") if path.exists(sdk_adb): return sdk_adb return "adb" def android_emulator_path(self, env): - if "ANDROID_SDK" in env: - sdk_adb = path.join(env["ANDROID_SDK"], "emulator", "emulator") + if "ANDROID_SDK_ROOT" in env: + sdk_adb = path.join(env["ANDROID_SDK_ROOT"], "emulator", "emulator") if path.exists(sdk_adb): return sdk_adb return "emulator" @@ -968,29 +899,29 @@ class CommandBase(object): build by writing the appropriate toolchain configuration values into the stored configuration.""" if target == "armv7-linux-androideabi": - self.config["android"]["platform"] = "android-21" + self.config["android"]["platform"] = "android-30" self.config["android"]["target"] = target self.config["android"]["toolchain_prefix"] = "arm-linux-androideabi" self.config["android"]["arch"] = "arm" self.config["android"]["lib"] = "armeabi-v7a" - self.config["android"]["toolchain_name"] = "arm-linux-androideabi" + self.config["android"]["toolchain_name"] = "armv7a-linux-androideabi30" return True elif target == "aarch64-linux-android": - self.config["android"]["platform"] = "android-21" + self.config["android"]["platform"] = "android-30" self.config["android"]["target"] = target self.config["android"]["toolchain_prefix"] = target self.config["android"]["arch"] = "arm64" self.config["android"]["lib"] = "arm64-v8a" - self.config["android"]["toolchain_name"] = target + self.config["android"]["toolchain_name"] = "aarch64-linux-androideabi30" return True elif target == "i686-linux-android": # https://github.com/jemalloc/jemalloc/issues/1279 - self.config["android"]["platform"] = "android-21" + self.config["android"]["platform"] = "android-30" self.config["android"]["target"] = target - self.config["android"]["toolchain_prefix"] = "x86" + self.config["android"]["toolchain_prefix"] = target self.config["android"]["arch"] = "x86" self.config["android"]["lib"] = "x86" - self.config["android"]["toolchain_name"] = target + self.config["android"]["toolchain_name"] = "i686-linux-android30" return True return False diff --git a/python/servo/package_commands.py b/python/servo/package_commands.py index 62ae60f5fa6..ab8cbacb2cd 100644 --- a/python/servo/package_commands.py +++ b/python/servo/package_commands.py @@ -160,7 +160,7 @@ class PackageCommands(CommandBase): else: raise Exception("TODO what should this be?") - flavor_name = "Main" + flavor_name = "Basic" if flavor is not None: flavor_name = flavor.title() @@ -176,10 +176,7 @@ class PackageCommands(CommandBase): variant = ":assemble" + flavor_name + arch_string + build_type_string apk_task_name = ":servoapp" + variant aar_task_name = ":servoview" + variant - maven_task_name = ":servoview:uploadArchive" argv = ["./gradlew", "--no-daemon", apk_task_name, aar_task_name] - if maven: - argv.append(maven_task_name) try: with cd(path.join("support", "android", "apk")): subprocess.check_call(argv, env=env) diff --git a/python/servo/post_build_commands.py b/python/servo/post_build_commands.py index bd9382b55a1..ebaf5a5e983 100644 --- a/python/servo/post_build_commands.py +++ b/python/servo/post_build_commands.py @@ -118,6 +118,7 @@ class PostBuildCommands(CommandBase): "am start " + extra + " org.mozilla.servo/org.mozilla.servo.MainActivity", "sleep 0.5", "echo Servo PID: $(pidof org.mozilla.servo)", + "logcat --pid=$(pidof org.mozilla.servo)", "exit" ] args = [self.android_adb_path(env)] @@ -129,7 +130,7 @@ class PostBuildCommands(CommandBase): if usb: args += ["-d"] shell = subprocess.Popen(args + ["shell"], stdin=subprocess.PIPE) - shell.communicate("\n".join(script) + "\n") + shell.communicate(bytes("\n".join(script) + "\n", "utf8")) return shell.wait() args = [bin or self.get_nightly_binary_path(nightly) or self.get_binary_path(build_type)] diff --git a/servo-tidy.toml b/servo-tidy.toml index 6e295cac954..bc235ae68c9 100644 --- a/servo-tidy.toml +++ b/servo-tidy.toml @@ -65,6 +65,10 @@ packages = [ # style (0.64) vs. webxr (0.66) vs. mozjs_sys (0.68). "bindgen", + + # quickcheck (required by layout_2020 for tests) is + # stuck on 0.8.4 with no new releases. + "env_logger", ] # Files that are ignored for all tidy and lint checks. files = [ diff --git a/servobuild.example b/servobuild.example index 1c63d91d4b0..d0d930ec202 100644 --- a/servobuild.example +++ b/servobuild.example @@ -56,8 +56,6 @@ media-stack = "auto" # Android information [android] -# Defaults to the value of $ANDROID_SDK, $ANDROID_NDK, $ANDROID_TOOLCHAIN, $ANDROID_PLATFORM respectively +# Defaults to the value of $ANDROID_SDK_ROOT, $ANDROID_NDK_ROOT respectively #sdk = "/opt/android-sdk" #ndk = "/opt/android-ndk" -#toolchain = "/opt/android-toolchain" -#platform = "android-18" diff --git a/support/android/apk/build.gradle b/support/android/apk/build.gradle index aa429b3da22..c2619efb978 100644 --- a/support/android/apk/build.gradle +++ b/support/android/apk/build.gradle @@ -1,42 +1,30 @@ -import org.apache.tools.ant.taskdefs.condition.Os - -buildscript { - repositories { - jcenter() - google() - } - dependencies { - classpath 'com.android.tools.build:gradle:3.1.3' - } -} - -allprojects { - repositories { - jcenter() - flatDir { - dirs rootDir.absolutePath + "/../../../target/android/aar" - } - google() - } +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '8.0.1' apply false + id 'com.android.library' version '8.0.1' apply false } // Utility methods -String getTargetDir(boolean debug, String arch) { +ext.getTargetDir = { boolean debug, String arch -> def basePath = project.rootDir.getParentFile().getParentFile().getParentFile().absolutePath return basePath + '/target/android/' + getSubTargetDir(debug, arch) } -String getSubTargetDir(boolean debug, String arch) { +ext.getNativeTargetDir = { boolean debug, String arch -> + def basePath = project.rootDir.getParentFile().getParentFile().getParentFile().absolutePath + return basePath + '/target/' + getSubTargetDir(debug, arch) +} + +ext.getSubTargetDir = { boolean debug, String arch -> return getRustTarget(arch) + '/' + (debug ? 'debug' : 'release') } -String getJniLibsPath(boolean debug, String arch) { - return getTargetDir(debug, arch) + '/apk/jniLibs' +ext.getJniLibsPath = { boolean debug, String arch -> + return getTargetDir(debug, arch) + '/jniLibs' } -static String getRustTarget(String arch) { +ext.getRustTarget = { String arch -> switch (arch.toLowerCase()) { - case 'arm' : return 'arm-linux-androideabi' case 'armv7' : return 'armv7-linux-androideabi' case 'arm64' : return 'aarch64-linux-android' case 'x86' : return 'i686-linux-android' @@ -44,9 +32,8 @@ static String getRustTarget(String arch) { } } -static String getNDKAbi(String arch) { +ext.getNDKAbi = { String arch -> switch (arch.toLowerCase()) { - case 'arm' : return 'armeabi' case 'armv7' : return 'armeabi-v7a' case 'arm64' : return 'arm64-v8a' case 'x86' : return 'x86' @@ -54,16 +41,10 @@ static String getNDKAbi(String arch) { } } -String getNdkDir() { +ext.getNdkDir = { -> // Read environment variable used in rust build system - String ndkDir = System.getenv('ANDROID_NDK') - if (ndkDir == null) { - ndkDir = System.getenv('ANDROID_NDK_HOME') - } - if (ndkDir == null) { - ndkDir = System.getenv('ANDROID_NDK_ROOT') - } - if (ndkDir == null) { + String ndkRoot = System.getenv('ANDROID_NDK_ROOT') + if (ndkRoot == null) { // Fallback to ndkDir in local.properties def rootDir = project.rootDir def localProperties = new File(rootDir, "local.properties") @@ -72,14 +53,13 @@ String getNdkDir() { properties.load(instr) } - ndkDir = properties.getProperty('ndk.dir') + ndkRoot = properties.getProperty('ndk.dir') } - def cmd = Os.isFamily(Os.FAMILY_WINDOWS) ? 'ndk-build.cmd' : 'ndk-build' - def ndkbuild = new File(ndkDir + '/' + cmd) - if (!ndkbuild.exists()) { - throw new GradleException("Please set a valid NDK_HOME environment variable" + + def ndkDir = ndkRoot != null ? new File(ndkRoot) : null + if (!ndkDir || !ndkDir.exists()) { + throw new GradleException("Please set a valid ANDROID_NDK_ROOT environment variable" + "or ndk.dir path in local.properties file"); } - return ndkbuild.absolutePath + return ndkDir.absolutePath } diff --git a/support/android/apk/gradle.properties b/support/android/apk/gradle.properties new file mode 100644 index 00000000000..a03b3548962 --- /dev/null +++ b/support/android/apk/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true diff --git a/support/android/apk/gradle/wrapper/gradle-wrapper.jar b/support/android/apk/gradle/wrapper/gradle-wrapper.jar index 13372aef5e24af05341d49695ee84e5f9b594659..87b738cbd051603d91cc39de6cb000dd98fe6b02 100644 GIT binary patch literal 55190 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 z ze;^F?mwdU?K+=fBabokSls^6_6At#1Sh7W*y?r6Ss*dmZP{n;VB^LDxM1QWh;@H0J z!4S*_5j_;+@-NpO1KfQd&;C7T`9ak;X8DTRz$hDNcjG}xAfg%gwZSb^zhE~O);NMO zn2$fl7Evn%=Lk!*xsM#(y$mjukN?A&mzEw3W5>_o+6oh62kq=4-`e3B^$rG=XG}Kd zK$blh(%!9;@d@3& zGFO60j1Vf54S}+XD?%*uk7wW$f`4U3F*p7@I4Jg7f`Il}2H<{j5h?$DDe%wG7jZQL zI{mj?t?Hu>$|2UrPr5&QyK2l3mas?zzOk0DV30HgOQ|~xLXDQ8M3o#;CNKO8RK+M; zsOi%)js-MU>9H4%Q)#K_me}8OQC1u;f4!LO%|5toa1|u5Q@#mYy8nE9IXmR}b#sZK z3sD395q}*TDJJA9Er7N`y=w*S&tA;mv-)Sx4(k$fJBxXva0_;$G6!9bGBw13c_Uws zXks4u(8JA@0O9g5f?#V~qR5*u5aIe2HQO^)RW9TTcJk28l`Syl>Q#ZveEE4Em+{?%iz6=V3b>rCm9F zPQQm@-(hfNdo2%n?B)u_&Qh7^^@U>0qMBngH8}H|v+Ejg*Dd(Y#|jgJ-A zQ_bQscil%eY}8oN7ZL+2r|qv+iJY?*l)&3W_55T3GU;?@Om*(M`u0DXAsQ7HSl56> z4P!*(%&wRCb?a4HH&n;lAmr4rS=kMZb74Akha2U~Ktni>>cD$6jpugjULq)D?ea%b zk;UW0pAI~TH59P+o}*c5Ei5L-9OE;OIBt>^(;xw`>cN2`({Rzg71qrNaE=cAH^$wP zNrK9Glp^3a%m+ilQj0SnGq`okjzmE7<3I{JLD6Jn^+oas=h*4>Wvy=KXqVBa;K&ri z4(SVmMXPG}0-UTwa2-MJ=MTfM3K)b~DzSVq8+v-a0&Dsv>4B65{dBhD;(d44CaHSM zb!0ne(*<^Q%|nuaL`Gb3D4AvyO8wyygm=1;9#u5x*k0$UOwx?QxR*6Od8>+ujfyo0 zJ}>2FgW_iv(dBK2OWC-Y=Tw!UwIeOAOUUC;h95&S1hn$G#if+d;*dWL#j#YWswrz_ zMlV=z+zjZJ%SlDhxf)vv@`%~$Afd)T+MS1>ZE7V$Rj#;J*<9Ld=PrK0?qrazRJWx) z(BTLF@Wk279nh|G%ZY7_lK7=&j;x`bMND=zgh_>>-o@6%8_#Bz!FnF*onB@_k|YCF z?vu!s6#h9bL3@tPn$1;#k5=7#s*L;FLK#=M89K^|$3LICYWIbd^qguQp02w5>8p-H z+@J&+pP_^iF4Xu>`D>DcCnl8BUwwOlq6`XkjHNpi@B?OOd`4{dL?kH%lt78(-L}eah8?36zw9d-dI6D{$s{f=M7)1 zRH1M*-82}DoFF^Mi$r}bTB5r6y9>8hjL54%KfyHxn$LkW=AZ(WkHWR;tIWWr@+;^^ zVomjAWT)$+rn%g`LHB6ZSO@M3KBA? z+W7ThSBgpk`jZHZUrp`F;*%6M5kLWy6AW#T{jFHTiKXP9ITrMlEdti7@&AT_a-BA!jc(Kt zWk>IdY-2Zbz?U1)tk#n_Lsl?W;0q`;z|t9*g-xE!(}#$fScX2VkjSiboKWE~afu5d z2B@9mvT=o2fB_>Mnie=TDJB+l`GMKCy%2+NcFsbpv<9jS@$X37K_-Y!cvF5NEY`#p z3sWEc<7$E*X*fp+MqsOyMXO=<2>o8)E(T?#4KVQgt=qa%5FfUG_LE`n)PihCz2=iNUt7im)s@;mOc9SR&{`4s9Q6)U31mn?}Y?$k3kU z#h??JEgH-HGt`~%)1ZBhT9~uRi8br&;a5Y3K_Bl1G)-y(ytx?ok9S*Tz#5Vb=P~xH z^5*t_R2It95=!XDE6X{MjLYn4Eszj9Y91T2SFz@eYlx9Z9*hWaS$^5r7=W5|>sY8}mS(>e9Ez2qI1~wtlA$yv2e-Hjn&K*P z2zWSrC~_8Wrxxf#%QAL&f8iH2%R)E~IrQLgWFg8>`Vnyo?E=uiALoRP&qT{V2{$79 z%9R?*kW-7b#|}*~P#cA@q=V|+RC9=I;aK7Pju$K-n`EoGV^-8Mk=-?@$?O37evGKn z3NEgpo_4{s>=FB}sqx21d3*=gKq-Zk)U+bM%Q_}0`XGkYh*+jRaP+aDnRv#Zz*n$pGp zEU9omuYVXH{AEx>=kk}h2iKt!yqX=EHN)LF}z1j zJx((`CesN1HxTFZ7yrvA2jTPmKYVij>45{ZH2YtsHuGzIRotIFj?(8T@ZWUv{_%AI zgMZlB03C&FtgJqv9%(acqt9N)`4jy4PtYgnhqev!r$GTIOvLF5aZ{tW5MN@9BDGu* zBJzwW3sEJ~Oy8is`l6Ly3an7RPtRr^1Iu(D!B!0O241Xua>Jee;Rc7tWvj!%#yX#m z&pU*?=rTVD7pF6va1D@u@b#V@bShFr3 zMyMbNCZwT)E-%L-{%$3?n}>EN>ai7b$zR_>=l59mW;tfKj^oG)>_TGCJ#HbLBsNy$ zqAqPagZ3uQ(Gsv_-VrZmG&hHaOD#RB#6J8&sL=^iMFB=gH5AIJ+w@sTf7xa&Cnl}@ zxrtzoNq>t?=(+8bS)s2p3>jW}tye0z2aY_Dh@(18-vdfvn;D?sv<>UgL{Ti08$1Q+ zZI3q}yMA^LK=d?YVg({|v?d1|R?5 zL0S3fw)BZazRNNX|7P4rh7!+3tCG~O8l+m?H} z(CB>8(9LtKYIu3ohJ-9ecgk+L&!FX~Wuim&;v$>M4 zUfvn<=Eok(63Ubc>mZrd8d7(>8bG>J?PtOHih_xRYFu1Hg{t;%+hXu2#x%a%qzcab zv$X!ccoj)exoOnaco_jbGw7KryOtuf(SaR-VJ0nAe(1*AA}#QV1lMhGtzD>RoUZ;WA?~!K{8%chYn?ttlz17UpDLlhTkGcVfHY6R<2r4E{mU zq-}D?+*2gAkQYAKrk*rB%4WFC-B!eZZLg4(tR#@kUQHIzEqV48$9=Q(~J_0 zy1%LSCbkoOhRO!J+Oh#;bGuXe;~(bIE*!J@i<%_IcB7wjhB5iF#jBn5+u~fEECN2* z!QFh!m<(>%49H12Y33+?$JxKV3xW{xSs=gxkxW-@Xds^|O1`AmorDKrE8N2-@ospk z=Au%h=f!`_X|G^A;XWL}-_L@D6A~*4Yf!5RTTm$!t8y&fp5_oqvBjW{FufS`!)5m% z2g(=9Ap6Y2y(9OYOWuUVGp-K=6kqQ)kM0P^TQT{X{V$*sN$wbFb-DaUuJF*!?EJPl zJev!UsOB^UHZ2KppYTELh+kqDw+5dPFv&&;;C~=u$Mt+Ywga!8YkL2~@g67}3wAQP zrx^RaXb1(c7vwU8a2se75X(cX^$M{FH4AHS7d2}heqqg4F0!1|Na>UtAdT%3JnS!B)&zelTEj$^b0>Oyfw=P-y-Wd^#dEFRUN*C{!`aJIHi<_YA2?piC%^ zj!p}+ZnBrM?ErAM+D97B*7L8U$K zo(IR-&LF(85p+fuct9~VTSdRjs`d-m|6G;&PoWvC&s8z`TotPSoksp;RsL4VL@CHf z_3|Tn%`ObgRhLmr60<;ya-5wbh&t z#ycN_)3P_KZN5CRyG%LRO4`Ot)3vY#dNX9!f!`_>1%4Q`81E*2BRg~A-VcN7pcX#j zrbl@7`V%n z6J53(m?KRzKb)v?iCuYWbH*l6M77dY4keS!%>}*8n!@ROE4!|7mQ+YS4dff1JJC(t z6Fnuf^=dajqHpH1=|pb(po9Fr8it^;2dEk|Ro=$fxqK$^Yix{G($0m-{RCFQJ~LqUnO7jJcjr zl*N*!6WU;wtF=dLCWzD6kW;y)LEo=4wSXQDIcq5WttgE#%@*m><@H;~Q&GniA-$in z`sjWFLgychS1kIJmPtd-w6%iKkj&dGhtB%0)pyy0M<4HZ@ZY0PWLAd7FCrj&i|NRh?>hZj*&FYnyu%Ur`JdiTu&+n z78d3n)Rl6q&NwVj_jcr#s5G^d?VtV8bkkYco5lV0LiT+t8}98LW>d)|v|V3++zLbHC(NC@X#Hx?21J0M*gP2V`Yd^DYvVIr{C zSc4V)hZKf|OMSm%FVqSRC!phWSyuUAu%0fredf#TDR$|hMZihJ__F!)Nkh6z)d=NC z3q4V*K3JTetxCPgB2_)rhOSWhuXzu+%&>}*ARxUaDeRy{$xK(AC0I=9%X7dmc6?lZNqe-iM(`?Xn3x2Ov>sej6YVQJ9Q42>?4lil?X zew-S>tm{=@QC-zLtg*nh5mQojYnvVzf3!4TpXPuobW_*xYJs;9AokrXcs!Ay z;HK>#;G$*TPN2M!WxdH>oDY6k4A6S>BM0Nimf#LfboKxJXVBC=RBuO&g-=+@O-#0m zh*aPG16zY^tzQLNAF7L(IpGPa+mDsCeAK3k=IL6^LcE8l0o&)k@?dz!79yxUquQIe($zm5DG z5RdXTv)AjHaOPv6z%99mPsa#8OD@9=URvHoJ1hYnV2bG*2XYBgB!-GEoP&8fLmWGg z9NG^xl5D&3L^io&3iYweV*qhc=m+r7C#Jppo$Ygg;jO2yaFU8+F*RmPL` zYxfGKla_--I}YUT353k}nF1zt2NO?+kofR8Efl$Bb^&llgq+HV_UYJUH7M5IoN0sT z4;wDA0gs55ZI|FmJ0}^Pc}{Ji-|#jdR$`!s)Di4^g3b_Qr<*Qu2rz}R6!B^;`Lj3sKWzjMYjexX)-;f5Y+HfkctE{PstO-BZan0zdXPQ=V8 zS8cBhnQyy4oN?J~oK0zl!#S|v6h-nx5to7WkdEk0HKBm;?kcNO*A+u=%f~l&aY*+J z>%^Dz`EQ6!+SEX$>?d(~|MNWU-}JTrk}&`IR|Ske(G^iMdk04)Cxd@}{1=P0U*%L5 zMFH_$R+HUGGv|ju2Z>5x(-aIbVJLcH1S+(E#MNe9g;VZX{5f%_|Kv7|UY-CM(>vf= z!4m?QS+AL+rUyfGJ;~uJGp4{WhOOc%2ybVP68@QTwI(8kDuYf?#^xv zBmOHCZU8O(x)=GVFn%tg@TVW1)qJJ_bU}4e7i>&V?r zh-03>d3DFj&@}6t1y3*yOzllYQ++BO-q!)zsk`D(z||)y&}o%sZ-tUF>0KsiYKFg6 zTONq)P+uL5Vm0w{D5Gms^>H1qa&Z##*X31=58*r%Z@Ko=IMXX{;aiMUp-!$As3{sq z0EEk02MOsgGm7$}E%H1ys2$yftNbB%1rdo@?6~0!a8Ym*1f;jIgfcYEF(I_^+;Xdr z2a>&oc^dF3pm(UNpazXgVzuF<2|zdPGjrNUKpdb$HOgNp*V56XqH`~$c~oSiqx;8_ zEz3fHoU*aJUbFJ&?W)sZB3qOSS;OIZ=n-*#q{?PCXi?Mq4aY@=XvlNQdA;yVC0Vy+ z{Zk6OO!lMYWd`T#bS8FV(`%flEA9El;~WjZKU1YmZpG#49`ku`oV{Bdtvzyz3{k&7 zlG>ik>eL1P93F zd&!aXluU_qV1~sBQf$F%sM4kTfGx5MxO0zJy<#5Z&qzNfull=k1_CZivd-WAuIQf> zBT3&WR|VD|=nKelnp3Q@A~^d_jN3@$x2$f@E~e<$dk$L@06Paw$);l*ewndzL~LuU zq`>vfKb*+=uw`}NsM}~oY}gW%XFwy&A>bi{7s>@(cu4NM;!%ieP$8r6&6jfoq756W z$Y<`J*d7nK4`6t`sZ;l%Oen|+pk|Ry2`p9lri5VD!Gq`U#Ms}pgX3ylAFr8(?1#&dxrtJgB>VqrlWZf61(r`&zMXsV~l{UGjI7R@*NiMJLUoK*kY&gY9kC@^}Fj* zd^l6_t}%Ku<0PY71%zQL`@}L}48M!@=r)Q^Ie5AWhv%#l+Rhu6fRpvv$28TH;N7Cl z%I^4ffBqx@Pxpq|rTJV)$CnxUPOIn`u278s9#ukn>PL25VMv2mff)-RXV&r`Dwid7}TEZxXX1q(h{R6v6X z&x{S_tW%f)BHc!jHNbnrDRjGB@cam{i#zZK*_*xlW@-R3VDmp)<$}S%t*@VmYX;1h zFWmpXt@1xJlc15Yjs2&e%)d`fimRfi?+fS^BoTcrsew%e@T^}wyVv6NGDyMGHSKIQ zC>qFr4GY?#S#pq!%IM_AOf`#}tPoMn7JP8dHXm(v3UTq!aOfEXNRtEJ^4ED@jx%le zvUoUs-d|2(zBsrN0wE(Pj^g5wx{1YPg9FL1)V1JupsVaXNzq4fX+R!oVX+q3tG?L= z>=s38J_!$eSzy0m?om6Wv|ZCbYVHDH*J1_Ndajoh&?L7h&(CVii&rmLu+FcI;1qd_ zHDb3Vk=(`WV?Uq;<0NccEh0s`mBXcEtmwt6oN99RQt7MNER3`{snV$qBTp={Hn!zz z1gkYi#^;P8s!tQl(Y>|lvz{5$uiXsitTD^1YgCp+1%IMIRLiSP`sJru0oY-p!FPbI)!6{XM%)(_Dolh1;$HlghB-&e><;zU&pc=ujpa-(+S&Jj zX1n4T#DJDuG7NP;F5TkoG#qjjZ8NdXxF0l58RK?XO7?faM5*Z17stidTP|a%_N z^e$D?@~q#Pf+708cLSWCK|toT1YSHfXVIs9Dnh5R(}(I;7KhKB7RD>f%;H2X?Z9eR z{lUMuO~ffT!^ew= z7u13>STI4tZpCQ?yb9;tSM-(EGb?iW$a1eBy4-PVejgMXFIV_Ha^XB|F}zK_gzdhM z!)($XfrFHPf&uyFQf$EpcAfk83}91Y`JFJOiQ;v5ca?)a!IxOi36tGkPk4S6EW~eq z>WiK`Vu3D1DaZ}515nl6>;3#xo{GQp1(=uTXl1~ z4gdWxr-8a$L*_G^UVd&bqW_nzMM&SlNW$8|$lAfo@zb+P>2q?=+T^qNwblP*RsN?N zdZE%^Zs;yAwero1qaoqMp~|KL=&npffh981>2om!fseU(CtJ=bW7c6l{U5(07*e0~ zJRbid6?&psp)ilmYYR3ZIg;t;6?*>hoZ3uq7dvyyq-yq$zH$yyImjfhpQb@WKENSP zl;KPCE+KXzU5!)mu12~;2trrLfs&nlEVOndh9&!SAOdeYd}ugwpE-9OF|yQs(w@C9 zoXVX`LP~V>%$<(%~tE*bsq(EFm zU5z{H@Fs^>nm%m%wZs*hRl=KD%4W3|(@j!nJr{Mmkl`e_uR9fZ-E{JY7#s6i()WXB0g-b`R{2r@K{2h3T+a>82>722+$RM*?W5;Bmo6$X3+Ieg9&^TU(*F$Q3 zT572!;vJeBr-)x?cP;^w1zoAM`nWYVz^<6N>SkgG3s4MrNtzQO|A?odKurb6DGZffo>DP_)S0$#gGQ_vw@a9JDXs2}hV&c>$ zUT0;1@cY5kozKOcbN6)n5v)l#>nLFL_x?2NQgurQH(KH@gGe>F|$&@ zq@2A!EXcIsDdzf@cWqElI5~t z4cL9gg7{%~4@`ANXnVAi=JvSsj95-7V& zME3o-%9~2?cvlH#twW~99=-$C=+b5^Yv}Zh4;Mg-!LS zw>gqc=}CzS9>v5C?#re>JsRY!w|Mtv#%O3%Ydn=S9cQarqkZwaM4z(gL~1&oJZ;t; zA5+g3O6itCsu93!G1J_J%Icku>b3O6qBW$1Ej_oUWc@MI)| zQ~eyS-EAAnVZp}CQnvG0N>Kc$h^1DRJkE7xZqJ0>p<>9*apXgBMI-v87E0+PeJ-K& z#(8>P_W^h_kBkI;&e_{~!M+TXt@z8Po*!L^8XBn{of)knd-xp{heZh~@EunB2W)gd zAVTw6ZZasTi>((qpBFh(r4)k zz&@Mc@ZcI-4d639AfcOgHOU+YtpZ)rC%Bc5gw5o~+E-i+bMm(A6!uE>=>1M;V!Wl4 z<#~muol$FsY_qQC{JDc8b=$l6Y_@_!$av^08`czSm!Xan{l$@GO-zPq1s>WF)G=wv zDD8j~Ht1pFj)*-b7h>W)@O&m&VyYci&}K|0_Z*w`L>1jnGfCf@6p}Ef*?wdficVe_ zmPRUZ(C+YJU+hIj@_#IiM7+$4kH#VS5tM!Ksz01siPc-WUe9Y3|pb4u2qnn zRavJiRpa zq?tr&YV?yKt<@-kAFl3s&Kq#jag$hN+Y%%kX_ytvpCsElgFoN3SsZLC>0f|m#&Jhu zp7c1dV$55$+k78FI2q!FT}r|}cIV;zp~#6X2&}22$t6cHx_95FL~T~1XW21VFuatb zpM@6w>c^SJ>Pq6{L&f9()uy)TAWf;6LyHH3BUiJ8A4}od)9sriz~e7}l7Vr0e%(=>KG1Jay zW0azuWC`(|B?<6;R)2}aU`r@mt_#W2VrO{LcX$Hg9f4H#XpOsAOX02x^w9+xnLVAt z^~hv2guE-DElBG+`+`>PwXn5kuP_ZiOO3QuwoEr)ky;o$n7hFoh}Aq0@Ar<8`H!n} zspCC^EB=6>$q*gf&M2wj@zzfBl(w_@0;h^*fC#PW9!-kT-dt*e7^)OIU{Uw%U4d#g zL&o>6`hKQUps|G4F_5AuFU4wI)(%9(av7-u40(IaI|%ir@~w9-rLs&efOR@oQy)}{ z&T#Qf`!|52W0d+>G!h~5A}7VJky`C3^fkJzt3|M&xW~x-8rSi-uz=qBsgODqbl(W#f{Ew#ui(K)(Hr&xqZs` zfrK^2)tF#|U=K|_U@|r=M_Hb;qj1GJG=O=d`~#AFAccecIaq3U`(Ds1*f*TIs=IGL zp_vlaRUtFNK8(k;JEu&|i_m39c(HblQkF8g#l|?hPaUzH2kAAF1>>Yykva0;U@&oRV8w?5yEK??A0SBgh?@Pd zJg{O~4xURt7!a;$rz9%IMHQeEZHR8KgFQixarg+MfmM_OeX#~#&?mx44qe!wt`~dd zqyt^~ML>V>2Do$huU<7}EF2wy9^kJJSm6HoAD*sRz%a|aJWz_n6?bz99h)jNMp}3k ztPVbos1$lC1nX_OK0~h>=F&v^IfgBF{#BIi&HTL}O7H-t4+wwa)kf3AE2-Dx@#mTA z!0f`>vz+d3AF$NH_-JqkuK1C+5>yns0G;r5ApsU|a-w9^j4c+FS{#+7- zH%skr+TJ~W_8CK_j$T1b;$ql_+;q6W|D^BNK*A+W5XQBbJy|)(IDA=L9d>t1`KX2b zOX(Ffv*m?e>! zS3lc>XC@IqPf1g-%^4XyGl*1v0NWnwZTW?z4Y6sncXkaA{?NYna3(n@(+n+#sYm}A zGQS;*Li$4R(Ff{obl3#6pUsA0fKuWurQo$mWXMNPV5K66V!XYOyc})^>889Hg3I<{V^Lj9($B4Zu$xRr=89-lDz9x`+I8q(vEAimx1K{sTbs|5x7S zZ+7o$;9&9>@3K;5-DVzGw=kp7ez%1*kxhGytdLS>Q)=xUWv3k_x(IsS8we39Tijvr z`GKk>gkZTHSht;5q%fh9z?vk%sWO}KR04G9^jleJ^@ovWrob7{1xy7V=;S~dDVt%S za$Q#Th%6g1(hiP>hDe}7lcuI94K-2~Q0R3A1nsb7Y*Z!DtQ(Ic<0;TDKvc6%1kBdJ z$hF!{uALB0pa?B^TC}#N5gZ|CKjy|BnT$7eaKj;f>Alqdb_FA3yjZ4CCvm)D&ibL) zZRi91HC!TIAUl<|`rK_6avGh`!)TKk=j|8*W|!vb9>HLv^E%t$`@r@piI(6V8pqDG zBON7~=cf1ZWF6jc{qkKm;oYBtUpIdau6s+<-o^5qNi-p%L%xAtn9OktFd{@EjVAT% z#?-MJ5}Q9QiK_jYYWs+;I4&!N^(mb!%4zx7qO6oCEDn=8oL6#*9XIJ&iJ30O`0vsFy|fEVkw}*jd&B6!IYi+~Y)qv6QlM&V9g0 zh)@^BVDB|P&#X{31>G*nAT}Mz-j~zd>L{v{9AxrxKFw8j;ccQ$NE0PZCc(7fEt1xd z`(oR2!gX6}R+Z77VkDz^{I)@%&HQT5q+1xlf*3R^U8q%;IT8-B53&}dNA7GW`Ki&= z$lrdH zDCu;j$GxW<&v_4Te7=AE2J0u1NM_7Hl9$u{z(8#%8vvrx2P#R7AwnY|?#LbWmROa; zOJzU_*^+n(+k;Jd{e~So9>OF>fPx$Hb$?~K1ul2xr>>o@**n^6IMu8+o3rDp(X$cC z`wQt9qIS>yjA$K~bg{M%kJ00A)U4L+#*@$8UlS#lN3YA{R{7{-zu#n1>0@(#^eb_% zY|q}2)jOEM8t~9p$X5fpT7BZQ1bND#^Uyaa{mNcFWL|MoYb@>y`d{VwmsF&haoJuS2W7azZU0{tu#Jj_-^QRc35tjW~ae&zhKk!wD}#xR1WHu z_7Fys#bp&R?VXy$WYa$~!dMxt2@*(>@xS}5f-@6eoT%rwH zv_6}M?+piNE;BqaKzm1kK@?fTy$4k5cqYdN8x-<(o6KelwvkTqC3VW5HEnr+WGQlF zs`lcYEm=HPpmM4;Ich7A3a5Mb3YyQs7(Tuz-k4O0*-YGvl+2&V(B&L1F8qfR0@vQM-rF<2h-l9T12eL}3LnNAVyY_z51xVr$%@VQ-lS~wf3mnHc zoM({3Z<3+PpTFCRn_Y6cbxu9v>_>eTN0>hHPl_NQQuaK^Mhrv zX{q#80ot;ptt3#js3>kD&uNs{G0mQp>jyc0GG?=9wb33hm z`y2jL=J)T1JD7eX3xa4h$bG}2ev=?7f>-JmCj6){Upo&$k{2WA=%f;KB;X5e;JF3IjQBa4e-Gp~xv- z|In&Rad7LjJVz*q*+splCj|{7=kvQLw0F@$vPuw4m^z=B^7=A4asK_`%lEf_oIJ-O z{L)zi4bd#&g0w{p1$#I&@bz3QXu%Y)j46HAJKWVfRRB*oXo4lIy7BcVl4hRs<%&iQ zr|)Z^LUJ>qn>{6y`JdabfNNFPX7#3`x|uw+z@h<`x{J4&NlDjnknMf(VW_nKWT!Jh zo1iWBqT6^BR-{T=4Ybe+?6zxP_;A5Uo{}Xel%*=|zRGm1)pR43K39SZ=%{MDCS2d$~}PE-xPw4ZK6)H;Zc&0D5p!vjCn0wCe&rVIhchR9ql!p2`g0b@JsC^J#n_r*4lZ~u0UHKwo(HaHUJDHf^gdJhTdTW z3i7Zp_`xyKC&AI^#~JMVZj^9WsW}UR#nc#o+ifY<4`M+?Y9NTBT~p`ONtAFf8(ltr*ER-Ig!yRs2xke#NN zkyFcaQKYv>L8mQdrL+#rjgVY>Z2_$bIUz(kaqL}cYENh-2S6BQK-a(VNDa_UewSW` zMgHi<3`f!eHsyL6*^e^W7#l?V|42CfAjsgyiJsA`yNfAMB*lAsJj^K3EcCzm1KT zDU2+A5~X%ax-JJ@&7>m`T;;}(-e%gcYQtj}?ic<*gkv)X2-QJI5I0tA2`*zZRX(;6 zJ0dYfMbQ+{9Rn3T@Iu4+imx3Y%bcf2{uT4j-msZ~eO)5Z_T7NC|Nr3)|NWjomhv=E zXaVin)MY)`1QtDyO7mUCjG{5+o1jD_anyKn73uflH*ASA8rm+S=gIfgJ);>Zx*hNG z!)8DDCNOrbR#9M7Ud_1kf6BP)x^p(|_VWCJ+(WGDbYmnMLWc?O4zz#eiP3{NfP1UV z(n3vc-axE&vko^f+4nkF=XK-mnHHQ7>w05$Q}iv(kJc4O3TEvuIDM<=U9@`~WdKN* zp4e4R1ncR_kghW}>aE$@OOc~*aH5OOwB5U*Z)%{LRlhtHuigxH8KuDwvq5{3Zg{Vr zrd@)KPwVKFP2{rXho(>MTZZfkr$*alm_lltPob4N4MmhEkv`J(9NZFzA>q0Ch;!Ut zi@jS_=0%HAlN+$-IZGPi_6$)ap>Z{XQGt&@ZaJ(es!Po5*3}>R4x66WZNsjE4BVgn z>}xm=V?F#tx#e+pimNPH?Md5hV7>0pAg$K!?mpt@pXg6UW9c?gvzlNe0 z3QtIWmw$0raJkjQcbv-7Ri&eX6Ks@@EZ&53N|g7HU<;V1pkc&$3D#8k!coJ=^{=vf z-pCP;vr2#A+i#6VA?!hs6A4P@mN62XYY$#W9;MwNia~89i`=1GoFESI+%Mbrmwg*0 zbBq4^bA^XT#1MAOum)L&ARDXJ6S#G>&*72f50M1r5JAnM1p7GFIv$Kf9eVR(u$KLt z9&hQ{t^i16zL1c(tRa~?qr?lbSN;1k;%;p*#gw_BwHJRjcYPTj6>y-rw*dFTnEs95 z`%-AoPL!P16{=#RI0 zUb6#`KR|v^?6uNnY`zglZ#Wd|{*rZ(x&Hk8N6ob6mpX~e^qu5kxvh$2TLJA$M=rx zc!#ot+sS+-!O<0KR6+Lx&~zgEhCsbFY{i_DQCihspM?e z-V}HemMAvFzXR#fV~a=Xf-;tJ1edd}Mry@^=9BxON;dYr8vDEK<<{ zW~rg(ZspxuC&aJo$GTM!9_sXu(EaQJNkV9AC(ob#uA=b4*!Uf}B*@TK=*dBvKKPAF z%14J$S)s-ws9~qKsf>DseEW(ssVQ9__YNg}r9GGx3AJiZR@w_QBlGP>yYh0lQCBtf zx+G;mP+cMAg&b^7J!`SiBwC81M_r0X9kAr2y$0(Lf1gZK#>i!cbww(hn$;fLIxRf? z!AtkSZc-h76KGSGz%48Oe`8ZBHkSXeVb!TJt_VC>$m<#}(Z}!(3h631ltKb3CDMw^fTRy%Ia!b&at`^g7Ew-%WLT9(#V0OP9CE?uj62s>`GI3NA z!`$U+i<`;IQyNBkou4|-7^9^ylac-Xu!M+V5p5l0Ve?J0wTSV+$gYtoc=+Ve*OJUJ z$+uIGALW?}+M!J9+M&#bT=Hz@{R2o>NtNGu1yS({pyteyb>*sg4N`KAD?`u3F#C1y z2K4FKOAPASGZTep54PqyCG(h3?kqQQAxDSW@>T2d!n;9C8NGS;3A8YMRcL>b=<<%M zMiWf$jY;`Ojq5S{kA!?28o)v$;)5bTL<4eM-_^h4)F#eeC2Dj*S`$jl^yn#NjJOYT zx%yC5Ww@eX*zsM)P(5#wRd=0+3~&3pdIH7CxF_2iZSw@>kCyd z%M}$1p((Bidw4XNtk&`BTkU{-PG)SXIZ)yQ!Iol6u8l*SQ1^%zC72FP zLvG>_Z0SReMvB%)1@+et0S{<3hV@^SY3V~5IY(KUtTR{*^xJ^2NN{sIMD9Mr9$~(C$GLNlSpzS=fsbw-DtHb_T|{s z9OR|sx!{?F``H!gVUltY7l~dx^a(2;OUV^)7 z%@hg`8+r&xIxmzZ;Q&v0X%9P)U0SE@r@(lKP%TO(>6I_iF{?PX(bez6v8Gp!W_nd5 z<8)`1jcT)ImNZp-9rr4_1MQ|!?#8sJQx{`~7)QZ75I=DPAFD9Mt{zqFrcrXCU9MG8 zEuGcy;nZ?J#M3!3DWW?Zqv~dnN6ijlIjPfJx(#S0cs;Z=jDjKY|$w2s4*Xa1Iz953sN2Lt!Vmk|%ZwOOqj`sA--5Hiaq8!C%LV zvWZ=bxeRV(&%BffMJ_F~~*FdcjhRVNUXu)MS(S#67rDe%Ler=GS+WysC1I2=Bmbh3s6wdS}o$0 zz%H08#SPFY9JPdL6blGD$D-AaYi;X!#zqib`(XX*i<*eh+2UEPzU4}V4RlC3{<>-~ zadGA8lSm>b7Z!q;D_f9DT4i)Q_}ByElGl*Cy~zX%IzHp)@g-itZB6xM70psn z;AY8II99e6P2drgtTG5>`^|7qg`9MTp%T~|1N3tBqV}2zgow3TFAH{XPor0%=HrkXnKyxyozHlJ6 zd3}OWkl?H$l#yZqOzZbMI+lDLoH48;s10!m1!K87g;t}^+A3f3e&w{EYhVPR0Km*- zh5-ku$Z|Ss{2?4pGm(Rz!0OQb^_*N`)rW{z)^Cw_`a(_L9j=&HEJl(!4rQy1IS)>- zeTIr>hOii`gc(fgYF(cs$R8l@q{mJzpoB5`5r>|sG zBpsY}RkY(g5`bj~D>(;F8v*DyjX(#nVLSs>)XneWI&%Wo>a0u#4A?N<1SK4D}&V1oN)76 z%S>a2n3n>G`YY1>0Hvn&AMtMuI_?`5?4y3w2Hnq4Qa2YH5 zxKdfM;k467djL31Y$0kd9FCPbU=pHBp@zaIi`Xkd80;%&66zvSqsq6%aY)jZacfvw ztkWE{ZV6V2WL9e}Dvz|!d96KqVkJU@5ryp#rReeWu>mSrOJxY^tWC9wd0)$+lZc%{ zY=c4#%OSyQJvQUuy^u}s8DN8|8T%TajOuaY^)R-&8s@r9D`(Ic4NmEu)fg1f!u`xUb;9t#rM z>}cY=648@d5(9A;J)d{a^*ORdVtJrZ77!g~^lZ9@)|-ojvW#>)Jhe8$7W3mhmQh@S zU=CSO+1gSsQ+Tv=x-BD}*py_Ox@;%#hPb&tqXqyUW9jV+fonnuCyVw=?HR>dAB~Fg z^vl*~y*4|)WUW*9RC%~O1gHW~*tJb^a-j;ae2LRNo|0S2`RX>MYqGKB^_ng7YRc@! zFxg1X!VsvXkNuv^3mI`F2=x6$(pZdw=jfYt1ja3FY7a41T07FPdCqFhU6%o|Yb6Z4 zpBGa=(ao3vvhUv#*S{li|EyujXQPUV;0sa5!0Ut)>tPWyC9e0_9(=v*z`TV5OUCcx zT=w=^8#5u~7<}8Mepqln4lDv*-~g^VoV{(+*4w(q{At6d^E-Usa2`JXty++Oh~on^ z;;WHkJsk2jvh#N|?(2PLl+g!M0#z_A;(#Uy=TzL&{Ei5G9#V{JbhKV$Qmkm%5tn!CMA? z@hM=b@2DZWTQ6>&F6WCq6;~~WALiS#@{|I+ucCmD6|tBf&e;$_)%JL8$oIQ%!|Xih1v4A$=7xNO zZVz$G8;G5)rxyD+M0$20L$4yukA_D+)xmK3DMTH3Q+$N&L%qB)XwYx&s1gkh=%qGCCPwnwhbT4p%*3R)I}S#w7HK3W^E%4w z2+7ctHPx3Q97MFYB48HfD!xKKb(U^K_4)Bz(5dvwyl*R?)k;uHEYVi|{^rvh)w7}t z`tnH{v9nlVHj2ign|1an_wz0vO)*`3RaJc#;(W-Q6!P&>+@#fptCgtUSn4!@b7tW0&pE2Qj@7}f#ugu4*C)8_}AMRuz^WG zc)XDcOPQjRaGptRD^57B83B-2NKRo!j6TBAJntJPHNQG;^Oz}zt5F^kId~miK3J@l ztc-IKp6qL!?u~q?qfGP0I~$5gvq#-0;R(oLU@sYayr*QH95fnrYA*E|n%&FP@Cz`a zSdJ~(c@O^>qaO`m9IQ8sd8!L<+)GPJDrL7{4{ko2gWOZel^3!($Gjt|B&$4dtfTmBmC>V`R&&6$wpgvdmns zxcmfS%9_ZoN>F~azvLFtA(9Q5HYT#A(byGkESnt{$Tu<73$W~reB4&KF^JBsoqJ6b zS?$D7DoUgzLO-?P`V?5_ub$nf1p0mF?I)StvPomT{uYjy!w&z$t~j&en=F~hw|O(1 zlV9$arQmKTc$L)Kupwz_zA~deT+-0WX6NzFPh&d+ly*3$%#?Ca9Z9lOJsGVoQ&1HNg+)tJ_sw)%oo*DK)iU~n zvL``LqTe=r=7SwZ@LB)9|3QB5`0(B9r(iR}0nUwJss-v=dXnwMRQFYSRK1blS#^g(3@z{`=8_CGDm!LESTWig zzm1{?AG&7`uYJ;PoFO$o8RWuYsV26V{>D-iYTnvq7igWx9@w$EC*FV^vpvDl@i9yp zPIqiX@hEZF4VqzI3Y)CHhR`xKN8poL&~ak|wgbE4zR%Dm(a@?bw%(7(!^>CM!^4@J z6Z)KhoQP;WBq_Z_&<@i2t2&xq>N>b;Np2rX?yK|-!14iE2T}E|jC+=wYe~`y38g3J z8QGZquvqBaG!vw&VtdXWX5*i5*% zJP~7h{?&E|<#l{klGPaun`IgAJ4;RlbRqgJz5rmHF>MtJHbfqyyZi53?Lhj=(Ku#& z__ubmZIxzSq3F90Xur!1)Vqe6b@!ueHA!93H~jdHmaS5Q^CULso}^poy)0Op6!{^9 zWyCyyIrdBP4fkliZ%*g+J-A!6VFSRF6Liu6G^^=W>cn81>4&7(c7(6vCGSAJ zQZ|S3mb|^Wf=yJ(h~rq`iiW~|n#$+KcblIR<@|lDtm!&NBzSG-1;7#YaU+-@=xIm4 zE}edTYd~e&_%+`dIqqgFntL-FxL3!m4yTNt<(^Vt9c6F(`?9`u>$oNxoKB29<}9FE zgf)VK!*F}nW?}l95%RRk8N4^Rf8)Xf;drT4<|lUDLPj^NPMrBPL;MX&0oGCsS za3}vWcF(IPx&W6{s%zwX{UxHX2&xLGfT{d9bWP!g;Lg#etpuno$}tHoG<4Kd*=kpU z;4%y(<^yj(UlG%l-7E9z_Kh2KoQ19qT3CR@Ghr>BAgr3Vniz3LmpC4g=g|A3968yD2KD$P7v$ zx9Q8`2&qH3&y-iv0#0+jur@}k`6C%7fKbCr|tHX2&O%r?rBpg`YNy~2m+ z*L7dP$RANzVUsG_Lb>=__``6vA*xpUecuGsL+AW?BeSwyoQfDlXe8R1*R1M{0#M?M zF+m19`3<`gM{+GpgW^=UmuK*yMh3}x)7P738wL8r@(Na6%ULPgbPVTa6gh5Q(SR0f znr6kdRpe^(LVM;6Rt(Z@Lsz3EX*ry6(WZ?w>#ZRelx)N%sE+MN>5G|Z8{%@b&D+Ov zPU{shc9}%;G7l;qbonIb_1m^Qc8ez}gTC-k02G8Rl?7={9zBz8uRX2{XJQ{vZhs67avlRn| zgRtWl0Lhjet&!YC47GIm%1gdq%T24_^@!W3pCywc89X4I5pnBCZDn(%!$lOGvS*`0!AoMtqxNPFgaMR zwoW$p;8l6v%a)vaNsesED3f}$%(>zICnoE|5JwP&+0XI}JxPccd+D^gx`g`=GsUc0 z9Uad|C+_@_0%JmcObGnS@3+J^0P!tg+fUZ_w#4rk#TlJYPXJiO>SBxzs9(J;XV9d{ zmTQE1(K8EYaz9p^XLbdWudyIPJlGPo0U*)fAh-jnbfm@SYD_2+?|DJ-^P+ojG{2{6 z>HJtedEjO@j_tqZ4;Zq1t5*5cWm~W?HGP!@_f6m#btM@46cEMhhK{(yI&jG)fwL1W z^n_?o@G8a-jYt!}$H*;{0#z8lANlo!9b@!c5K8<(#lPlpE!z86Yq#>WT&2} z;;G1$pD%iNoj#Z=&kij5&V1KHIhN-h<;{HC5wD)PvkF>CzlQOEx_0;-TJ*!#&{Wzt zKcvq^SZIdop}y~iouNqtU7K7+?eIz-v_rfNM>t#i+dD$s_`M;sjGubTdP)WI*uL@xPOLHt#~T<@Yz>xt50ZoTw;a(a}lNiDN-J${gOdE zx?8LOA|tv{Mb}=TTR=LcqMqbCJkKj+@;4Mu)Cu0{`~ohix6E$g&tff)aHeUAQQ%M? zIN4uSUTzC1iMEWL*W-in1y)C`E+R8j?4_?X4&2Zv5?QdkNMz(k} zw##^Ikx`#_s>i&CO_mu@vJJ*|3ePRDl5pq$9V^>D;g0R%l>lw;ttyM6Sy`NBF{)Lr zSk)V>mZr96+aHY%vTLLt%vO-+juw6^SO_ zYGJaGeWX6W(TOQx=5oTGXOFqMMU*uZyt>MR-Y`vxW#^&)H zk0!F8f*@v6NO@Z*@Qo)+hlX40EWcj~j9dGrLaq%1;DE_%#lffXCcJ;!ZyyyZTz74Q zb2WSly6sX{`gQeToQsi1-()5EJ1nJ*kXGD`xpXr~?F#V^sxE3qSOwRSaC9x9oa~jJ zTG9`E|q zC5Qs1xh}jzb5UPYF`3N9YuMnI7xsZ41P;?@c|%w zl=OxLr6sMGR+`LStLvh)g?fA5p|xbUD;yFAMQg&!PEDYxVYDfA>oTY;CFt`cg?Li1 z0b})!9Rvw&j#*&+D2))kXLL z0+j=?7?#~_}N-qdEIP>DQaZh#F(#e0WNLzwUAj@r694VJ8?Dr5_io2X49XYsG^ zREt0$HiNI~6VV!ycvao+0v7uT$_ilKCvsC+VDNg7yG1X+eNe^3D^S==F3ByiW0T^F zH6EsH^}Uj^VPIE&m)xlmOScYR(w750>hclqH~~dM2+;%GDXT`u4zG!p((*`Hwx41M z4KB+`hfT(YA%W)Ve(n+Gu9kuXWKzxg{1ff^xNQw>w%L-)RySTk9kAS92(X0Shg^Q? zx1YXg_TLC^?h6!4mBqZ9pKhXByu|u~gF%`%`vdoaGBN3^j4l!4x?Bw4Jd)Z4^di}! zXlG1;hFvc>H?bmmu1E7Vx=%vahd!P1#ZGJOJYNbaek^$DHt`EOE|Hlij+hX>ocQFSLVu|wz`|KVl@Oa;m2k6b*mNK2Vo{~l9>Qa3@B7G7#k?)aLx;w6U ze8bBq%vF?5v>#TspEoaII!N}sRT~>bh-VWJ7Q*1qsz%|G)CFmnttbq$Ogb{~YK_=! z{{0vhlW@g!$>|}$&4E3@k`KPElW6x#tSX&dfle>o!irek$NAbDzdd2pVeNzk4&qgJ zXvNF0$R96~g0x+R1igR=Xu&X_Hc5;!Ze&C)eUTB$9wW&?$&o8Yxhm5s(S`;?{> z*F?9Gr0|!OiKA>Rq-ae=_okB6&yMR?!JDer{@iQgIn=cGxs-u^!8Q$+N&pfg2WM&Z zulHu=Uh~U>fS{=Nm0x>ACvG*4R`Dx^kJ65&Vvfj`rSCV$5>c04N26Rt2S?*kh3JKq z9(3}5T?*x*AP(X2Ukftym0XOvg~r6Ms$2x&R&#}Sz23aMGU&7sU-cFvE3Eq`NBJe84VoftWF#v7PDAp`@V zRFCS24_k~;@~R*L)eCx@Q9EYmM)Sn}HLbVMyxx%{XnMBDc-YZ<(DXDBYUt8$u5Zh} zBK~=M9cG$?_m_M61YG+#|9Vef7LfbH>(C21&aC)x$^Lg}fa#SF){RX|?-xZjSOrn# z2ZAwUF)$VB<&S;R3FhNSQOV~8w%A`V9dWyLiy zgt7G=Z4t|zU3!dh5|s(@XyS|waBr$>@=^Dspmem8)@L`Ns{xl%rGdX!R(BiC5C7Vo zXetb$oC_iXS}2x_Hy}T(hUUNbO47Q@+^4Q`h>(R-;OxCyW#eoOeC51jzxnM1yxBrp zz6}z`(=cngs6X05e79o_B7@3K|Qpe3n38Py_~ zpi?^rj!`pq!7PHGliC$`-8A^Ib?2qgJJCW+(&TfOnFGJ+@-<<~`7BR0f4oSINBq&R z2CM`0%WLg_Duw^1SPwj-{?BUl2Y=M4e+7yL1{C&&f&zjF06#xf>VdLozgNye(BNgSD`=fFbBy0HIosLl@JwCQl^s;eTnc( z3!r8G=K>zb`|bLLI0N|eFJk%s)B>oJ^M@AQzqR;HUjLsOqW<0v>1ksT_#24*U@R3HJu*A^#1o#P3%3_jq>icD@<`tqU6ICEgZrME(xX#?i^Z z%Id$_uyQGlFD-CcaiRtRdGn|K`Lq5L-rx7`vYYGH7I=eLfHRozPiUtSe~Tt;IN2^gCXmf2#D~g2@9bhzK}3nphhG%d?V7+Zq{I2?Gt*!NSn_r~dd$ zqkUOg{U=MI?Ehx@`(X%rQB?LP=CjJ*V!rec{#0W2WshH$X#9zep!K)tzZoge*LYd5 z@g?-j5_mtMp>_WW`p*UNUZTFN{_+#m*bJzt{hvAdkF{W40{#L3w6gzPztnsA_4?&0 z(+>pv!zB16rR-(nm(^c>Z(its{ny677vT8sF564^mlZvJ!h65}OW%Hn|2OXbOQM%b z{6C54Z2v;^hyMQ;UH+HwFD2!F!VlQ}6Z{L0_9g5~CH0@Mqz?ZC`^QkhOU#$Lx<4`B zyZsa9uPF!rZDo8ZVfzzR#raQ>5|)k~_Ef*wDqG^76o)j!C4 zykvT*o$!-MBko@?{b~*Zf2*YMlImrK`cEp|#D7f%Twm<|C|dWDPVt zzd|cWgMz^T0YO0lQ8%TE1O06v|NZl~LH{LLQ58WtNjWhFP#}eWVO&eiP!jmdp!%24 z{&z-MK{-h=QDqf+S+Pgi=_wg$I{F28X*%lJ>A7Yl#$}fMhymMu?R9TEB?#6@|Q^e^AHhxcRL$z1gsc`-Q`3j+eYAd<4@z^{+?JM8bmu zSVlrVZ5-)SzLn&LU9GhXYG{{I+u(+6ES+tAtQUanYC0^6kWkks8cG;C&r1KGs)Cq}WZSd3k1c?lkzwLySimkP5z)T2Ox3pNs;PdQ=8JPDkT7#0L!cV? zzn${PZs;o7UjcCVd&DCDpFJvjI=h(KDmdByJuDYXQ|G@u4^Kf?7YkE67fWM97kj6F z973tGtv!k$k{<>jd~D&c(x5hVbJa`bILdy(00%lY5}HZ2N>)a|))3UZ&fUa5@uB`H z+LrYm@~t?g`9~@dFzW5l>=p0hG%rv0>(S}jEzqQg6-jImG%Pr%HPtqIV_Ym6yRydW z4L+)NhcyYp*g#vLH{1lK-hQQSScfvNiNx|?nSn-?cc8}-9~Z_0oxlr~(b^EiD`Mx< zlOLK)MH?nl4dD|hx!jBCIku-lI(&v~bCU#!L7d0{)h z;k4y^X+=#XarKzK*)lv0d6?kE1< zmCG^yDYrSwrKIn04tG)>>10%+ zEKzs$S*Zrl+GeE55f)QjY$ zD5hi~J17k;4VSF_`{lPFwf^Qroqg%kqM+Pdn%h#oOPIsOIwu?JR717atg~!)*CgXk zERAW?c}(66rnI+LqM^l7BW|9dH~5g1(_w$;+AAzSYlqop*=u5}=g^e0xjlWy0cUIT7{Fs2Xqx*8% zW71JB%hk%aV-wjNE0*$;E-S9hRx5|`L2JXxz4TX3nf8fMAn|523ssV;2&145zh{$V z#4lt)vL2%DCZUgDSq>)ei2I`*aeNXHXL1TB zC8I4!uq=YYVjAdcCjcf4XgK2_$y5mgsCdcn2U!VPljXHco>+%`)6W=gzJk0$e%m$xWUCs&Ju-nUJjyQ04QF_moED2(y6q4l+~fo845xm zE5Esx?~o#$;rzpCUk2^2$c3EBRNY?wO(F3Pb+<;qfq;JhMFuSYSxiMejBQ+l8(C-- zz?Xufw@7{qvh$;QM0*9tiO$nW(L>83egxc=1@=9Z3)G^+*JX-z92F((wYiK>f;6 zkc&L6k4Ua~FFp`x7EF;ef{hb*n8kx#LU|6{5n=A55R4Ik#sX{-nuQ}m7e<{pXq~8#$`~6| zi{+MIgsBRR-o{>)CE8t0Bq$|SF`M0$$7-{JqwFI1)M^!GMwq5RAWMP!o6G~%EG>$S zYDS?ux;VHhRSm*b^^JukYPVb?t0O%^&s(E7Rb#TnsWGS2#FdTRj_SR~YGjkaRFDI=d)+bw$rD;_!7&P2WEmn zIqdERAbL&7`iA^d?8thJ{(=)v>DgTF7rK-rck({PpYY$7uNY$9-Z< ze4=??I#p;$*+-Tm!q8z}k^%-gTm59^3$*ByyroqUe02Dne4?Fc%JlO>*f9Zj{++!^ zBz0FxuS&7X52o6-^CYq>jkXa?EEIfh?xdBPAkgpWpb9Tam^SXoFb3IRfLwanWfskJ zIbfU-rJ1zPmOV)|%;&NSWIEbbwj}5DIuN}!m7v4($I{Rh@<~-sK{fT|Wh?<|;)-Z; zwP{t@{uTsmnO@5ZY82lzwl4jeZ*zsZ7w%a+VtQXkigW$zN$QZnKw4F`RG`=@eWowO zFJ6RC4e>Y7Nu*J?E1*4*U0x^>GK$>O1S~gkA)`wU2isq^0nDb`);Q(FY<8V6^2R%= zDY}j+?mSj{bz2>F;^6S=OLqiHBy~7h4VVscgR#GILP!zkn68S^c04ZL3e$lnSU_(F zZm3e`1~?eu1>ys#R6>Gu$`rWZJG&#dsZ?^)4)v(?{NPt+_^Ak>Ap6828Cv^B84fa4 z_`l$0SSqkBU}`f*H#<14a)khT1Z5Z8;=ga^45{l8y*m|3Z60vgb^3TnuUKaa+zP;m zS`za@C#Y;-LOm&pW||G!wzr+}T~Q9v4U4ufu*fLJC=PajN?zN=?v^8TY}wrEeUygdgwr z7szml+(Bar;w*c^!5txLGKWZftqbZP`o;Kr1)zI}0Kb8yr?p6ZivtYL_KA<+9)XFE z=pLS5U&476PKY2aKEZh}%|Vb%!us(^qf)bKdF7x_v|Qz8lO7Ro>;#mxG0gqMaTudL zi2W!_#3@INslT}1DFJ`TsPvRBBGsODklX0`p-M6Mrgn~6&fF`kdj4K0I$<2Hp(YIA z)fFdgR&=qTl#sEFj6IHzEr1sYM6 zNfi!V!biByA&vAnZd;e_UfGg_={}Tj0MRt3SG%BQYnX$jndLG6>ssgIV{T3#=;RI% zE}b!9z#fek19#&nFgC->@!IJ*Fe8K$ZOLmg|6(g}ccsSBpc`)3;Ar8;3_k`FQ#N9&1tm>c|2mzG!!uWvelm zJj|oDZ6-m(^|dn3em(BF&3n12=hdtlb@%!vGuL*h`CXF?^=IHU%Q8;g8vABm=U!vX zT%Ma6gpKQC2c;@wH+A{)q+?dAuhetSxBDui+Z;S~6%oQq*IwSMu-UhMDy{pP z-#GB-a0`0+cJ%dZ7v0)3zfW$eV>w*mgU4Cma{P$DY3|w364n$B%cf()fZ;`VIiK_O zQ|q|(55+F$H(?opzr%r)BJLy6M&7Oq8KCsh`pA5^ohB@CDlMKoDVo5gO&{0k)R0b(UOfd>-(GZGeF}y?QI_T+GzdY$G{l!l% zHyToqa-x&X4;^(-56Lg$?(KYkgJn9W=w##)&CECqIxLe@+)2RhO*-Inpb7zd8txFG6mY8E?N8JP!kRt_7-&X{5P?$LAbafb$+hkA*_MfarZxf zXLpXmndnV3ubbXe*SYsx=eeuBKcDZI0bg&LL-a8f9>T(?VyrpC6;T{)Z{&|D5a`Aa zjP&lP)D)^YYWHbjYB6ArVs+4xvrUd1@f;;>*l zZH``*BxW+>Dd$be{`<&GN(w+m3B?~3Jjz}gB8^|!>pyZo;#0SOqWem%xeltYZ}KxOp&dS=bg|4 zY-^F~fv8v}u<7kvaZH`M$fBeltAglH@-SQres30fHC%9spF8Ld%4mjZJDeGNJR8+* zl&3Yo$|JYr2zi9deF2jzEC) zl+?io*GUGRp;^z+4?8gOFA>n;h%TJC#-st7#r&-JVeFM57P7rn{&k*z@+Y5 zc2sui8(gFATezp|Te|1-Q*e|Xi+__8bh$>%3|xNc2kAwTM!;;|KF6cS)X3SaO8^z8 zs5jV(s(4_NhWBSSJ}qUzjuYMKlkjbJS!7_)wwVsK^qDzHx1u*sC@C1ERqC#l%a zk>z>m@sZK{#GmsB_NkEM$$q@kBrgq%=NRBhL#hjDQHrI7(XPgFvP&~ZBJ@r58nLme zK4tD}Nz6xrbvbD6DaDC9E_82T{(WRQBpFc+Zb&W~jHf1MiBEqd57}Tpo8tOXj@LcF zwN8L-s}UO8%6piEtTrj@4bLH!mGpl5mH(UJR1r9bBOrSt0tSJDQ9oIjcW#elyMAxl7W^V(>8M~ss0^>OKvf{&oUG@uW{f^PtV#JDOx^APQKm& z{*Ysrz&ugt4PBUX@KERQbycxP%D+ApR%6jCx7%1RG2YpIa0~tqS6Xw6k#UN$b`^l6d$!I z*>%#Eg=n#VqWnW~MurJLK|hOQPTSy7G@29g@|g;mXC%MF1O7IAS8J^Q6D&Ra!h^+L&(IBYg2WWzZjT-rUsJMFh@E)g)YPW_)W9GF3 zMZz4RK;qcjpnat&J;|MShuPc4qAc)A| zVB?h~3TX+k#Cmry90=kdDoPYbhzs#z96}#M=Q0nC{`s{3ZLU)c(mqQQX;l~1$nf^c zFRQ~}0_!cM2;Pr6q_(>VqoW0;9=ZW)KSgV-c_-XdzEapeLySavTs5-PBsl-n3l;1jD z9^$^xR_QKDUYoeqva|O-+8@+e??(pRg@V|=WtkY!_IwTN~ z9Rd&##eWt_1w$7LL1$-ETciKFyHnNPjd9hHzgJh$J(D@3oYz}}jVNPjH!viX0g|Y9 zDD`Zjd6+o+dbAbUA( zEqA9mSoX5p|9sDVaRBFx_8)Ra4HD#xDB(fa4O8_J2`h#j17tSZOd3%}q8*176Y#ak zC?V8Ol<*X{Q?9j{Ys4Bc#sq!H;^HU$&F_`q2%`^=9DP9YV-A!ZeQ@#p=#ArloIgUH%Y-s>G!%V3aoXaY=f<UBrJTN+*8_lMX$yC=Vq+ zrjLn-pO%+VIvb~>k%`$^aJ1SevcPUo;V{CUqF>>+$c(MXxU12mxqyFAP>ki{5#;Q0 zx7Hh2zZdZzoxPY^YqI*Vgr)ip0xnpQJ+~R*UyFi9RbFd?<_l8GH@}gGmdB)~V7vHg z>Cjy78TQTDwh~+$u$|K3if-^4uY^|JQ+rLVX=u7~bLY29{lr>jWV7QCO5D0I>_1?; zx>*PxE4|wC?#;!#cK|6ivMzJ({k3bT_L3dHY#h7M!ChyTT`P#%3b=k}P(;QYTdrbe z+e{f@we?3$66%02q8p3;^th;9@y2vqt@LRz!DO(WMIk?#Pba85D!n=Ao$5NW0QVgS zoW)fa45>RkjU?H2SZ^#``zs6dG@QWj;MO4k6tIp8ZPminF`rY31dzv^e-3W`ZgN#7 z)N^%Rx?jX&?!5v`hb0-$22Fl&UBV?~cV*{hPG6%ml{k;m+a-D^XOF6DxPd$3;2VVY zT)E%m#ZrF=D=84$l}71DK3Vq^?N4``cdWn3 zqV=mX1(s`eCCj~#Nw4XMGW9tK>$?=cd$ule0Ir8UYzhi?%_u0S?c&j7)-~4LdolkgP^CUeE<2`3m)I^b ztV`K0k$OS^-GK0M0cNTLR22Y_eeT{<;G(+51Xx}b6f!kD&E4; z&Op8;?O<4D$t8PB4#=cWV9Q*i4U+8Bjlj!y4`j)^RNU#<5La6|fa4wLD!b6?RrBsF z@R8Nc^aO8ty7qzlOLRL|RUC-Bt-9>-g`2;@jfNhWAYciF{df9$n#a~28+x~@x0IWM zld=J%YjoKm%6Ea>iF){z#|~fo_w#=&&HRogJmXJDjCp&##oVvMn9iB~gyBlNO3B5f zXgp_1I~^`A0z_~oAa_YBbNZbDsnxLTy0@kkH!=(xt8|{$y<+|(wSZW7@)#|fs_?gU5-o%vpsQPRjIxq;AED^oG%4S%`WR}2(*!84Pe8Jw(snJ zq~#T7+m|w#acH1o%e<+f;!C|*&_!lL*^zRS`;E}AHh%cj1yR&3Grv&0I9k9v0*w8^ zXHEyRyCB`pDBRAxl;ockOh6$|7i$kzCBW$}wGUc|2bo3`x*7>B@eI=-7lKvI)P=gQ zf_GuA+36kQb$&{ZH)6o^x}wS}S^d&Xmftj%nIU=>&j@0?z8V3PLb1JXgHLq)^cTvB zFO6(yj1fl1Bap^}?hh<>j?Jv>RJdK{YpGjHxnY%d8x>A{k+(18J|R}%mAqq9Uzm8^Us#Ir_q^w9-S?W07YRD`w%D(n;|8N%_^RO`zp4 z@`zMAs>*x0keyE)$dJ8hR37_&MsSUMlGC*=7|wUehhKO)C85qoU}j>VVklO^TxK?! zO!RG~y4lv#W=Jr%B#sqc;HjhN={wx761vA3_$S>{j+r?{5=n3le|WLJ(2y_r>{)F_ z=v8Eo&xFR~wkw5v-{+9^JQukxf8*CXDWX*ZzjPVDc>S72uxAcY+(jtg3ns_5R zRYl2pz`B)h+e=|7SfiAAP;A zk0tR)3u1qy0{+?bQOa17SpBRZ5LRHz(TQ@L0%n5xJ21ri>^X420II1?5^FN3&bV?( zCeA)d9!3FAhep;p3?wLPs`>b5Cd}N!;}y`Hq3ppDs0+><{2ey0yq8o7m-4|oaMsWf zsLrG*aMh91drd-_QdX6t&I}t2!`-7$DCR`W2yoV%bcugue)@!SXM}fJOfG(bQQh++ zjAtF~zO#pFz})d8h)1=uhigDuFy`n*sbxZ$BA^Bt=Jdm}_KB6sCvY(T!MQnqO;TJs zVD{*F(FW=+v`6t^6{z<3-fx#|Ze~#h+ymBL^^GKS%Ve<)sP^<4*y_Y${06eD zH_n?Ani5Gs4&1z)UCL-uBvq(8)i!E@T_*0Sp5{Ddlpgke^_$gukJc_f9e=0Rfpta@ ze5~~aJBNK&OJSw!(rDRAHV0d+eW#1?PFbr==uG-$_fu8`!DWqQD~ef-Gx*ZmZx33_ zb0+I(0!hIK>r9_S5A*UwgRBKSd6!ieiYJHRigU@cogJ~FvJHY^DSysg)ac=7#wDBf zNLl!E$AiUMZC%%i5@g$WsN+sMSoUADKZ}-Pb`{7{S>3U%ry~?GVX!BDar2dJHLY|g zTJRo#Bs|u#8ke<3ohL2EFI*n6adobnYG?F3-#7eZZQO{#rmM8*PFycBR^UZKJWr(a z8cex$DPOx_PL^TO<%+f^L6#tdB8S^y#+fb|acQfD(9WgA+cb15L+LUdHKv)wE6={i zX^iY3N#U7QahohDP{g`IHS?D00eJC9DIx0V&nq!1T* z4$Bb?trvEG9JixrrNRKcjX)?KWR#Y(dh#re_<y*=5!J+-Wwb*D>jKXgr5L8_b6pvSAn3RIvI5oj!XF^m?otNA=t^dg z#V=L0@W)n?4Y@}49}YxQS=v5GsIF3%Cp#fFYm0Bm<}ey& zOfWB^vS8ye?n;%yD%NF8DvOpZqlB++#4KnUj>3%*S(c#yACIU>TyBG!GQl7{b8j#V z;lS})mrRtT!IRh2B-*T58%9;!X}W^mg;K&fb7?2#JH>JpCZV5jbDfOgOlc@wNLfHN z8O92GeBRjCP6Q9^Euw-*i&Wu=$>$;8Cktx52b{&Y^Ise-R1gTKRB9m0*Gze>$k?$N zua_0Hmbcj8qQy{ZyJ%`6v6F+yBGm>chZxCGpeL@os+v&5LON7;$tb~MQAbSZKG$k z8w`Mzn=cX4Hf~09q8_|3C7KnoM1^ZGU}#=vn1?1^Kc-eWv4x^T<|i9bCu;+lTQKr- zRwbRK!&XrWRoO7Kw!$zNQb#cJ1`iugR(f_vgmu!O)6tFH-0fOSBk6$^y+R07&&B!(V#ZV)CX42( zTC(jF&b@xu40fyb1=_2;Q|uPso&Gv9OSM1HR{iGPi@JUvmYM;rkv#JiJZ5-EFA%Lu zf;wAmbyclUM*D7>^nPatbGr%2aR5j55qSR$hR`c?d+z z`qko8Yn%vg)p=H`1o?=b9K0%Blx62gSy)q*8jWPyFmtA2a+E??&P~mT@cBdCsvFw4 zg{xaEyVZ|laq!sqN}mWq^*89$e6%sb6Thof;ml_G#Q6_0-zwf80?O}D0;La25A0C+ z3)w-xesp6?LlzF4V%yA9Ryl_Kq*wMk4eu&)Tqe#tmQJtwq`gI^7FXpToum5HP3@;N zpe4Y!wv5uMHUu`zbdtLys5)(l^C(hFKJ(T)z*PC>7f6ZRR1C#ao;R&_8&&a3)JLh* zOFKz5#F)hJqVAvcR#1)*AWPGmlEKw$sQd)YWdAs_W-ojA?Lm#wCd}uF0^X=?AA#ki zWG6oDQZJ5Tvifdz4xKWfK&_s`V*bM7SVc^=w7-m}jW6U1lQEv_JsW6W(| zkKf>qn^G!EWn~|7{G-&t0C6C%4)N{WRK_PM>4sW8^dDkFM|p&*aBuN%fg(I z^M-49vnMd%=04N95VO+?d#el>LEo^tvnQsMop70lNqq@%cTlht?e+B5L1L9R4R(_6 z!3dCLeGXb+_LiACNiqa^nOELJj%q&F^S+XbmdP}`KAep%TDop{Pz;UDc#P&LtMPgH zy+)P1jdgZQUuwLhV<89V{3*=Iu?u#v;v)LtxoOwV(}0UD@$NCzd=id{UuDdedeEp| z`%Q|Y<6T?kI)P|8c!K0Za&jxPhMSS!T`wlQNlkE(2B*>m{D#`hYYD>cgvsKrlcOcs7;SnVCeBiK6Wfho@*Ym9 zr0zNfrr}0%aOkHd)d%V^OFMI~MJp+Vg-^1HPru3Wvac@-QjLX9Dx}FL(l>Z;CkSvC zOR1MK%T1Edv2(b9$ttz!E7{x4{+uSVGz`uH&)gG`$)Vv0^E#b&JSZp#V)b6~$RWwe zzC3FzI`&`EDK@aKfeqQ4M(IEzDd~DS>GB$~ip2n!S%6sR&7QQ*=Mr(v*v-&07CO%# zMBTaD8-EgW#C6qFPPG1Ph^|0AFs;I+s|+A@WU}%@WbPI$S0+qFR^$gim+Fejs2f!$ z@Xdlb_K1BI;iiOUj`j+gOD%mjq^S~J0cZZwuqfzNH9}|(vvI6VO+9ZDA_(=EAo;( zKKzm`k!s!_sYCGOm)93Skaz+GF7eY@Ra8J$C)`X)`aPKym?7D^SI}Mnef4C@SgIEB z>nONSFl$qd;0gSZhNcRlq9VVHPkbakHlZ1gJ1y9W+@!V$TLpdsbKR-VwZrsSM^wLr zL9ob&JG)QDTaf&R^cnm5T5#*J3(pSpjM5~S1 z@V#E2syvK6wb?&h?{E)CoI~9uA(hST7hx4_6M(7!|BW3TR_9Q zLS{+uPoNgw(aK^?=1rFcDO?xPEk5Sm=|pW%-G2O>YWS^(RT)5EQ2GSl75`b}vRcD2 z|HX(x0#Qv+07*O|vMIV(0?KGjOny#Wa~C8Q(kF^IR8u|hyyfwD&>4lW=)Pa311caC zUk3aLCkAFkcidp@C%vNVLNUa#1ZnA~ZCLrLNp1b8(ndgB(0zy{Mw2M@QXXC{hTxr7 zbipeHI-U$#Kr>H4}+cu$#2fG6DgyWgq{O#8aa)4PoJ^;1z7b6t&zt zPei^>F1%8pcB#1`z`?f0EAe8A2C|}TRhzs*-vN^jf(XNoPN!tONWG=abD^=Lm9D?4 zbq4b(in{eZehKC0lF}`*7CTzAvu(K!eAwDNC#MlL2~&gyFKkhMIF=32gMFLvKsbLY z1d$)VSzc^K&!k#2Q?(f>pXn){C+g?vhQ0ijV^Z}p5#BGrGb%6n>IH-)SA$O)*z3lJ z1rtFlovL`cC*RaVG!p!4qMB+-f5j^1)ALf4Z;2X&ul&L!?`9Vdp@d(%(>O=7ZBV;l z?bbmyPen>!P{TJhSYPmLs759b1Ni1`d$0?&>OhxxqaU|}-?Z2c+}jgZ&vCSaCivx| z-&1gw2Lr<;U-_xzlg}Fa_3NE?o}R-ZRX->__}L$%2ySyiPegbnM{UuADqwDR{C2oS zPuo88%DNfl4xBogn((9j{;*YGE0>2YoL?LrH=o^SaAcgO39Ew|vZ0tyOXb509#6{7 z0<}CptRX5(Z4*}8CqCgpT@HY3Q)CvRz_YE;nf6ZFwEje^;Hkj0b1ESI*8Z@(RQrW4 z35D5;S73>-W$S@|+M~A(vYvX(yvLN(35THo!yT=vw@d(=q8m+sJyZMB7T&>QJ=jkwQVQ07*Am^T980rldC)j}}zf!gq7_z4dZ zHwHB94%D-EB<-^W@9;u|(=X33c(G>q;Tfq1F~-Lltp|+uwVzg?e$M96ndY{Lcou%w zWRkjeE`G*i)Bm*|_7bi+=MPm8by_};`=pG!DSGBP6y}zvV^+#BYx{<>p0DO{j@)(S zxcE`o+gZf8EPv1g3E1c3LIbw+`rO3N+Auz}vn~)cCm^DlEi#|Az$b z2}Pqf#=rxd!W*6HijC|u-4b~jtuQS>7uu{>wm)PY6^S5eo=?M>;tK`=DKXuArZvaU zHk(G??qjKYS9G6Du)#fn+ob=}C1Hj9d?V$_=J41ljM$CaA^xh^XrV-jzi7TR-{{9V zZZI0;aQ9YNEc`q=Xvz;@q$eqL<}+L(>HR$JA4mB6~g*YRSnpo zTofY;u7F~{1Pl=pdsDQx8Gg#|@BdoWo~J~j%DfVlT~JaC)he>he6`C`&@@#?;e(9( zgKcmoidHU$;pi{;VXyE~4>0{kJ>K3Uy6`s*1S--*mM&NY)*eOyy!7?9&osK*AQ~vi z{4qIQs)s#eN6j&0S()cD&aCtV;r>ykvAzd4O-fG^4Bmx2A2U7-kZR5{Qp-R^i4H2yfwC7?9(r3=?oH(~JR4=QMls>auMv*>^^!$}{}R z;#(gP+O;kn4G|totqZGdB~`9yzShMze{+$$?9%LJi>4YIsaPMwiJ{`gocu0U}$Q$vI5oeyKrgzz>!gI+XFt!#n z7vs9Pn`{{5w-@}FJZn?!%EQV!PdA3hw%Xa2#-;X4*B4?`WM;4@bj`R-yoAs_t4!!` zEaY5OrYi`3u3rXdY$2jZdZvufgFwVna?!>#t#DKAD2;U zqpqktqJ)8EPY*w~yj7r~#bNk|PDM>ZS?5F7T5aPFVZrqeX~5_1*zTQ%;xUHe#li?s zJ*5XZVERVfRjwX^s=0<%nXhULK+MdibMjzt%J7#fuh?NXyJ^pqpfG$PFmG!h*opyi zmMONjJY#%dkdRHm$l!DLeBm#_0YCq|x17c1fYJ#5YMpsjrFKyU=y>g5QcTgbDm28X zYL1RK)sn1@XtkGR;tNb}(kg#9L=jNSbJizqAgV-TtK2#?LZXrCIz({ zO^R|`ZDu(d@E7vE}df5`a zNIQRp&mDFbgyDKtyl@J|GcR9!h+_a$za$fnO5Ai9{)d7m@?@qk(RjHwXD}JbKRn|u z=Hy^z2vZ<1Mf{5ihhi9Y9GEG74Wvka;%G61WB*y7;&L>k99;IEH;d8-IR6KV{~(LZ zN7@V~f)+yg7&K~uLvG9MAY+{o+|JX?yf7h9FT%7ZrW7!RekjwgAA4jU$U#>_!ZC|c zA9%tc9nq|>2N1rg9uw-Qc89V}I5Y`vuJ(y`Ibc_?D>lPF0>d_mB@~pU`~)uWP48cT@fTxkWSw{aR!`K{v)v zpN?vQZZNPgs3ki9h{An4&Cap-c5sJ!LVLtRd=GOZ^bUpyDZHm6T|t#218}ZA zx*=~9PO>5IGaBD^XX-_2t7?7@WN7VfI^^#Csdz9&{1r z9y<9R?BT~-V8+W3kzWWQ^)ZSI+R zt^Lg`iN$Z~a27)sC_03jrD-%@{ArCPY#Pc*u|j7rE%}jF$LvO4vyvAw3bdL_mg&ei zXys_i=Q!UoF^Xp6^2h5o&%cQ@@)$J4l`AG09G6Uj<~A~!xG>KjKSyTX)zH*EdHMK0 zo;AV-D+bqWhtD-!^+`$*P0B`HokilLd1EuuwhJ?%3wJ~VXIjIE3tj653PExvIVhE& zFMYsI(OX-Q&W$}9gad^PUGuKElCvXxU_s*kx%dH)Bi&$*Q(+9j>(Q>7K1A#|8 zY!G!p0kW29rP*BNHe_wH49bF{K7tymi}Q!Vc_Ox2XjwtpM2SYo7n>?_sB=$c8O5^? z6as!fE9B48FcE`(ruNXP%rAZlDXrFTC7^aoXEX41k)tIq)6kJ*(sr$xVqsh_m3^?? zOR#{GJIr6E0Sz{-( z-R?4asj|!GVl0SEagNH-t|{s06Q3eG{kZOoPHL&Hs0gUkPc&SMY=&{C0&HDI)EHx9 zm#ySWluxwp+b~+K#VG%21%F65tyrt9RTPR$eG0afer6D`M zTW=y!@y6yi#I5V#!I|8IqU=@IfZo!@9*P+f{yLxGu$1MZ%xRY(gRQ2qH@9eMK0`Z> zgO`4DHfFEN8@m@dxYuljsmVv}c4SID+8{kr>d_dLzF$g>urGy9g+=`xAfTkVtz56G zrKNsP$yrDyP=kIqPN9~rVmC-wH672NF7xU>~j5M06Xr&>UJBmOV z%7Ie2d=K=u^D`~i3(U7x?n=h!SCSD1`aFe-sY<*oh+=;B>UVFBOHsF=(Xr(Cai{dL z4S7Y>PHdfG9Iav5FtKzx&UCgg)|DRLvq7!0*9VD`e6``Pgc z1O!qSaNeBBZnDXClh(Dq@XAk?Bd6+_rsFt`5(E+V2c)!Mx4X z47X+QCB4B7$B=Fw1Z1vnHg;x9oDV1YQJAR6Q3}_}BXTFg$A$E!oGG%`Rc()-Ysc%w za(yEn0fw~AaEFr}Rxi;if?Gv)&g~21UzXU9osI9{rNfH$gPTTk#^B|irEc<8W+|9$ zc~R${X2)N!npz1DFVa%nEW)cgPq`MSs)_I*Xwo<+ZK-2^hD(Mc8rF1+2v7&qV;5SET-ygMLNFsb~#u+LpD$uLR1o!ha67gPV5Q{v#PZK5X zUT4aZ{o}&*q7rs)v%*fDTl%}VFX?Oi{i+oKVUBqbi8w#FI%_5;6`?(yc&(Fed4Quy8xsswG+o&R zO1#lUiA%!}61s3jR7;+iO$;1YN;_*yUnJK=$PT_}Q%&0T@2i$ zwGC@ZE^A62YeOS9DU9me5#`(wv24fK=C)N$>!!6V#6rX3xiHehfdvwWJ>_fwz9l)o`Vw9yi z0p5BgvIM5o_ zgo-xaAkS_mya8FXo1Ke4;U*7TGSfm0!fb4{E5Ar8T3p!Z@4;FYT8m=d`C@4-LM121 z?6W@9d@52vxUT-6K_;1!SE%FZHcm0U$SsC%QB zxkTrfH;#Y7OYPy!nt|k^Lgz}uYudos9wI^8x>Y{fTzv9gfTVXN2xH`;Er=rTeAO1x znaaJOR-I)qwD4z%&dDjY)@s`LLSd#FoD!?NY~9#wQRTHpD7Vyyq?tKUHKv6^VE93U zt_&ePH+LM-+9w-_9rvc|>B!oT>_L59nipM-@ITy|x=P%Ezu@Y?N!?jpwP%lm;0V5p z?-$)m84(|7vxV<6f%rK3!(R7>^!EuvA&j@jdTI+5S1E{(a*wvsV}_)HDR&8iuc#>+ zMr^2z*@GTnfDW-QS38OJPR3h6U&mA;vA6Pr)MoT7%NvA`%a&JPi|K8NP$b1QY#WdMt8-CDA zyL0UXNpZ?x=tj~LeM0wk<0Dlvn$rtjd$36`+mlf6;Q}K2{%?%EQ+#FJy6v5cS+Q-~ ztk||Iwr$(CZQHi38QZF;lFFBNt+mg2*V_AhzkM<8#>E_S^xj8%T5tXTytD6f)vePG z^B0Ne-*6Pqg+rVW?%FGHLhl^ycQM-dhNCr)tGC|XyES*NK%*4AnZ!V+Zu?x zV2a82fs8?o?X} zjC1`&uo1Ti*gaP@E43NageV^$Xue3%es2pOrLdgznZ!_a{*`tfA+vnUv;^Ebi3cc$?-kh76PqA zMpL!y(V=4BGPQSU)78q~N}_@xY5S>BavY3Sez-+%b*m0v*tOz6zub9%*~%-B)lb}t zy1UgzupFgf?XyMa+j}Yu>102tP$^S9f7;b7N&8?_lYG$okIC`h2QCT_)HxG1V4Uv{xdA4k3-FVY)d}`cmkePsLScG&~@wE?ix2<(G7h zQ7&jBQ}Kx9mm<0frw#BDYR7_HvY7En#z?&*FurzdDNdfF znCL1U3#iO`BnfPyM@>;#m2Lw9cGn;(5*QN9$zd4P68ji$X?^=qHraP~Nk@JX6}S>2 zhJz4MVTib`OlEAqt!UYobU0-0r*`=03)&q7ubQXrt|t?^U^Z#MEZV?VEin3Nv1~?U zuwwSeR10BrNZ@*h7M)aTxG`D(By$(ZP#UmBGf}duX zhx;7y1x@j2t5sS#QjbEPIj95hV8*7uF6c}~NBl5|hgbB(}M3vnt zu_^>@s*Bd>w;{6v53iF5q7Em>8n&m&MXL#ilSzuC6HTzzi-V#lWoX zBOSBYm|ti@bXb9HZ~}=dlV+F?nYo3?YaV2=N@AI5T5LWWZzwvnFa%w%C<$wBkc@&3 zyUE^8xu<=k!KX<}XJYo8L5NLySP)cF392GK97(ylPS+&b}$M$Y+1VDrJa`GG7+%ToAsh z5NEB9oVv>as?i7f^o>0XCd%2wIaNRyejlFws`bXG$Mhmb6S&shdZKo;p&~b4wv$ z?2ZoM$la+_?cynm&~jEi6bnD;zSx<0BuCSDHGSssT7Qctf`0U!GDwG=+^|-a5%8Ty z&Q!%m%geLjBT*#}t zv1wDzuC)_WK1E|H?NZ&-xr5OX(ukXMYM~_2c;K}219agkgBte_#f+b9Al8XjL-p}1 z8deBZFjplH85+Fa5Q$MbL>AfKPxj?6Bib2pevGxIGAG=vr;IuuC%sq9x{g4L$?Bw+ zvoo`E)3#bpJ{Ij>Yn0I>R&&5B$&M|r&zxh+q>*QPaxi2{lp?omkCo~7ibow#@{0P> z&XBocU8KAP3hNPKEMksQ^90zB1&&b1Me>?maT}4xv7QHA@Nbvt-iWy7+yPFa9G0DP zP82ooqy_ku{UPv$YF0kFrrx3L=FI|AjG7*(paRLM0k1J>3oPxU0Zd+4&vIMW>h4O5G zej2N$(e|2Re z@8xQ|uUvbA8QVXGjZ{Uiolxb7c7C^nW`P(m*Jkqn)qdI0xTa#fcK7SLp)<86(c`A3 zFNB4y#NHe$wYc7V)|=uiW8gS{1WMaJhDj4xYhld;zJip&uJ{Jg3R`n+jywDc*=>bW zEqw(_+j%8LMRrH~+M*$V$xn9x9P&zt^evq$P`aSf-51`ZOKm(35OEUMlO^$>%@b?a z>qXny!8eV7cI)cb0lu+dwzGH(Drx1-g+uDX;Oy$cs+gz~?LWif;#!+IvPR6fa&@Gj zwz!Vw9@-Jm1QtYT?I@JQf%`=$^I%0NK9CJ75gA}ff@?I*xUD7!x*qcyTX5X+pS zAVy4{51-dHKs*OroaTy;U?zpFS;bKV7wb}8v+Q#z<^$%NXN(_hG}*9E_DhrRd7Jqp zr}2jKH{avzrpXj?cW{17{kgKql+R(Ew55YiKK7=8nkzp7Sx<956tRa(|yvHlW zNO7|;GvR(1q}GrTY@uC&ow0me|8wE(PzOd}Y=T+Ih8@c2&~6(nzQrK??I7DbOguA9GUoz3ASU%BFCc8LBsslu|nl>q8Ag(jA9vkQ`q2amJ5FfA7GoCdsLW znuok(diRhuN+)A&`rH{$(HXWyG2TLXhVDo4xu?}k2cH7QsoS>sPV)ylb45Zt&_+1& zT)Yzh#FHRZ-z_Q^8~IZ+G~+qSw-D<{0NZ5!J1%rAc`B23T98TMh9ylkzdk^O?W`@C??Z5U9#vi0d<(`?9fQvNN^ji;&r}geU zSbKR5Mv$&u8d|iB^qiLaZQ#@)%kx1N;Og8Js>HQD3W4~pI(l>KiHpAv&-Ev45z(vYK<>p6 z6#pU(@rUu{i9UngMhU&FI5yeRub4#u=9H+N>L@t}djC(Schr;gc90n%)qH{$l0L4T z;=R%r>CuxH!O@+eBR`rBLrT0vnP^sJ^+qE^C8ZY0-@te3SjnJ)d(~HcnQw@`|qAp|Trrs^E*n zY1!(LgVJfL?@N+u{*!Q97N{Uu)ZvaN>hsM~J?*Qvqv;sLnXHjKrtG&x)7tk?8%AHI zo5eI#`qV1{HmUf-Fucg1xn?Kw;(!%pdQ)ai43J3NP4{%x1D zI0#GZh8tjRy+2{m$HyI(iEwK30a4I36cSht3MM85UqccyUq6$j5K>|w$O3>`Ds;`0736+M@q(9$(`C6QZQ-vAKjIXKR(NAH88 zwfM6_nGWlhpy!_o56^BU``%TQ%tD4hs2^<2pLypjAZ;W9xAQRfF_;T9W-uidv{`B z{)0udL1~tMg}a!hzVM0a_$RbuQk|EG&(z*{nZXD3hf;BJe4YxX8pKX7VaIjjDP%sk zU5iOkhzZ&%?A@YfaJ8l&H;it@;u>AIB`TkglVuy>h;vjtq~o`5NfvR!ZfL8qS#LL` zD!nYHGzZ|}BcCf8s>b=5nZRYV{)KK#7$I06s<;RyYC3<~`mob_t2IfR*dkFJyL?FU zvuo-EE4U(-le)zdgtW#AVA~zjx*^80kd3A#?vI63pLnW2{j*=#UG}ISD>=ZGA$H&` z?Nd8&11*4`%MQlM64wfK`{O*ad5}vk4{Gy}F98xIAsmjp*9P=a^yBHBjF2*Iibo2H zGJAMFDjZcVd%6bZ`dz;I@F55VCn{~RKUqD#V_d{gc|Z|`RstPw$>Wu+;SY%yf1rI=>51Oolm>cnjOWHm?ydcgGs_kPUu=?ZKtQS> zKtLS-v$OMWXO>B%Z4LFUgw4MqA?60o{}-^6tf(c0{Y3|yF##+)RoXYVY-lyPhgn{1 z>}yF0Ab}D#1*746QAj5c%66>7CCWs8O7_d&=Ktu!SK(m}StvvBT1$8QP3O2a*^BNA z)HPhmIi*((2`?w}IE6Fo-SwzI_F~OC7OR}guyY!bOQfpNRg3iMvsFPYb9-;dT6T%R zhLwIjgiE^-9_4F3eMHZ3LI%bbOmWVe{SONpujQ;3C+58=Be4@yJK>3&@O>YaSdrevAdCLMe_tL zl8@F}{Oc!aXO5!t!|`I zdC`k$5z9Yf%RYJp2|k*DK1W@AN23W%SD0EdUV^6~6bPp_HZi0@dku_^N--oZv}wZA zH?Bf`knx%oKB36^L;P%|pf#}Tp(icw=0(2N4aL_Ea=9DMtF})2ay68V{*KfE{O=xL zf}tcfCL|D$6g&_R;r~1m{+)sutQPKzVv6Zw(%8w&4aeiy(qct1x38kiqgk!0^^X3IzI2ia zxI|Q)qJNEf{=I$RnS0`SGMVg~>kHQB@~&iT7+eR!Ilo1ZrDc3TVW)CvFFjHK4K}Kh z)dxbw7X%-9Ol&Y4NQE~bX6z+BGOEIIfJ~KfD}f4spk(m62#u%k<+iD^`AqIhWxtKGIm)l$7=L`=VU0Bz3-cLvy&xdHDe-_d3%*C|Q&&_-n;B`87X zDBt3O?Wo-Hg6*i?f`G}5zvM?OzQjkB8uJhzj3N;TM5dSM$C@~gGU7nt-XX_W(p0IA6$~^cP*IAnA<=@HVqNz=Dp#Rcj9_6*8o|*^YseK_4d&mBY*Y&q z8gtl;(5%~3Ehpz)bLX%)7|h4tAwx}1+8CBtu9f5%^SE<&4%~9EVn4*_!r}+{^2;} zwz}#@Iw?&|8F2LdXUIjh@kg3QH69tqxR_FzA;zVpY=E zcHnWh(3j3UXeD=4m_@)Ea4m#r?axC&X%#wC8FpJPDYR~@65T?pXuWdPzEqXP>|L`S zKYFF0I~%I>SFWF|&sDsRdXf$-TVGSoWTx7>7mtCVUrQNVjZ#;Krobgh76tiP*0(5A zs#<7EJ#J`Xhp*IXB+p5{b&X3GXi#b*u~peAD9vr0*Vd&mvMY^zxTD=e(`}ybDt=BC(4q)CIdp>aK z0c?i@vFWjcbK>oH&V_1m_EuZ;KjZSiW^i30U` zGLK{%1o9TGm8@gy+Rl=-5&z`~Un@l*2ne3e9B+>wKyxuoUa1qhf?-Pi= zZLCD-b7*(ybv6uh4b`s&Ol3hX2ZE<}N@iC+h&{J5U|U{u$XK0AJz)!TSX6lrkG?ris;y{s zv`B5Rq(~G58?KlDZ!o9q5t%^E4`+=ku_h@~w**@jHV-+cBW-`H9HS@o?YUUkKJ;AeCMz^f@FgrRi@?NvO3|J zBM^>4Z}}!vzNum!R~o0)rszHG(eeq!#C^wggTgne^2xc9nIanR$pH1*O;V>3&#PNa z7yoo?%T(?m-x_ow+M0Bk!@ow>A=skt&~xK=a(GEGIWo4AW09{U%(;CYLiQIY$bl3M zxC_FGKY%J`&oTS{R8MHVe{vghGEshWi!(EK*DWmoOv|(Ff#(bZ-<~{rc|a%}Q4-;w z{2gca97m~Nj@Nl{d)P`J__#Zgvc@)q_(yfrF2yHs6RU8UXxcU(T257}E#E_A}%2_IW?%O+7v((|iQ{H<|$S7w?;7J;iwD>xbZc$=l*(bzRXc~edIirlU0T&0E_EXfS5%yA zs0y|Sp&i`0zf;VLN=%hmo9!aoLGP<*Z7E8GT}%)cLFs(KHScNBco(uTubbxCOD_%P zD7XlHivrSWLth7jf4QR9`jFNk-7i%v4*4fC*A=;$Dm@Z^OK|rAw>*CI%E z3%14h-)|Q%_$wi9=p!;+cQ*N1(47<49TyB&B*bm_m$rs+*ztWStR~>b zE@V06;x19Y_A85N;R+?e?zMTIqdB1R8>(!4_S!Fh={DGqYvA0e-P~2DaRpCYf4$-Q z*&}6D!N_@s`$W(|!DOv%>R0n;?#(HgaI$KpHYpnbj~I5eeI(u4CS7OJajF%iKz)*V zt@8=9)tD1ML_CrdXQ81bETBeW!IEy7mu4*bnU--kK;KfgZ>oO>f)Sz~UK1AW#ZQ_ic&!ce~@(m2HT@xEh5u%{t}EOn8ET#*U~PfiIh2QgpT z%gJU6!sR2rA94u@xj3%Q`n@d}^iMH#X>&Bax+f4cG7E{g{vlJQ!f9T5wA6T`CgB%6 z-9aRjn$BmH=)}?xWm9bf`Yj-f;%XKRp@&7?L^k?OT_oZXASIqbQ#eztkW=tmRF$~% z6(&9wJuC-BlGrR*(LQKx8}jaE5t`aaz#Xb;(TBK98RJBjiqbZFyRNTOPA;fG$;~e` zsd6SBii3^(1Y`6^#>kJ77xF{PAfDkyevgox`qW`nz1F`&w*DH5Oh1idOTLES>DToi z8Qs4|?%#%>yuQO1#{R!-+2AOFznWo)e3~_D!nhoDgjovB%A8< zt%c^KlBL$cDPu!Cc`NLc_8>f?)!FGV7yudL$bKj!h;eOGkd;P~sr6>r6TlO{Wp1%xep8r1W{`<4am^(U} z+nCDP{Z*I?IGBE&*KjiaR}dpvM{ZFMW%P5Ft)u$FD373r2|cNsz%b0uk1T+mQI@4& zFF*~xDxDRew1Bol-*q>F{Xw8BUO;>|0KXf`lv7IUh%GgeLUzR|_r(TXZTbfXFE0oc zmGMwzNFgkdg><=+3MnncRD^O`m=SxJ6?}NZ8BR)=ag^b4Eiu<_bN&i0wUaCGi60W6 z%iMl&`h8G)y`gfrVw$={cZ)H4KSQO`UV#!@@cDx*hChXJB7zY18EsIo1)tw0k+8u; zg(6qLysbxVbLFbkYqKbEuc3KxTE+%j5&k>zHB8_FuDcOO3}FS|eTxoUh2~|Bh?pD| zsmg(EtMh`@s;`(r!%^xxDt(5wawK+*jLl>_Z3shaB~vdkJ!V3RnShluzmwn7>PHai z3avc`)jZSAvTVC6{2~^CaX49GXMtd|sbi*swkgoyLr=&yp!ASd^mIC^D;a|<=3pSt zM&0u%#%DGzlF4JpMDs~#kU;UCtyW+d3JwNiu`Uc7Yi6%2gfvP_pz8I{Q<#25DjM_D z(>8yI^s@_tG@c=cPoZImW1CO~`>l>rs=i4BFMZT`vq5bMOe!H@8q@sEZX<-kiY&@u3g1YFc zc@)@OF;K-JjI(eLs~hy8qOa9H1zb!3GslI!nH2DhP=p*NLHeh^9WF?4Iakt+b( z-4!;Q-8c|AX>t+5I64EKpDj4l2x*!_REy9L_9F~i{)1?o#Ws{YG#*}lg_zktt#ZlN zmoNsGm7$AXLink`GWtY*TZEH!J9Qv+A1y|@>?&(pb(6XW#ZF*}x*{60%wnt{n8Icp zq-Kb($kh6v_voqvA`8rq!cgyu;GaWZ>C2t6G5wk! zcKTlw=>KX3ldU}a1%XESW71))Z=HW%sMj2znJ;fdN${00DGGO}d+QsTQ=f;BeZ`eC~0-*|gn$9G#`#0YbT(>O(k&!?2jI z&oi9&3n6Vz<4RGR}h*1ggr#&0f%Op(6{h>EEVFNJ0C>I~~SmvqG+{RXDrexBz zw;bR@$Wi`HQ3e*eU@Cr-4Z7g`1R}>3-Qej(#Dmy|CuFc{Pg83Jv(pOMs$t(9vVJQJ zXqn2Ol^MW;DXq!qM$55vZ{JRqg!Q1^Qdn&FIug%O3=PUr~Q`UJuZ zc`_bE6i^Cp_(fka&A)MsPukiMyjG$((zE$!u>wyAe`gf-1Qf}WFfi1Y{^ zdCTTrxqpQE#2BYWEBnTr)u-qGSVRMV7HTC(x zb(0FjYH~nW07F|{@oy)rlK6CCCgyX?cB;19Z(bCP5>lwN0UBF}Ia|L0$oGHl-oSTZ zr;(u7nDjSA03v~XoF@ULya8|dzH<2G=n9A)AIkQKF0mn?!BU(ipengAE}6r`CE!jd z=EcX8exgDZZQ~~fgxR-2yF;l|kAfnjhz|i_o~cYRdhnE~1yZ{s zG!kZJ<-OVnO{s3bOJK<)`O;rk>=^Sj3M76Nqkj<_@Jjw~iOkWUCL+*Z?+_Jvdb!0cUBy=(5W9H-r4I zxAFts>~r)B>KXdQANyaeKvFheZMgoq4EVV0|^NR@>ea* zh%<78{}wsdL|9N1!jCN-)wH4SDhl$MN^f_3&qo?>Bz#?c{ne*P1+1 z!a`(2Bxy`S^(cw^dv{$cT^wEQ5;+MBctgPfM9kIQGFUKI#>ZfW9(8~Ey-8`OR_XoT zflW^mFO?AwFWx9mW2-@LrY~I1{dlX~jBMt!3?5goHeg#o0lKgQ+eZcIheq@A&dD}GY&1c%hsgo?z zH>-hNgF?Jk*F0UOZ*bs+MXO(dLZ|jzKu5xV1v#!RD+jRrHdQ z>>b){U(I@i6~4kZXn$rk?8j(eVKYJ2&k7Uc`u01>B&G@c`P#t#x@>Q$N$1aT514fK zA_H8j)UKen{k^ehe%nbTw}<JV6xN_|| z(bd-%aL}b z3VITE`N~@WlS+cV>C9TU;YfsU3;`+@hJSbG6aGvis{Gs%2K|($)(_VfpHB|DG8Nje+0tCNW%_cu3hk0F)~{-% zW{2xSu@)Xnc`Dc%AOH)+LT97ImFR*WekSnJ3OYIs#ijP4TD`K&7NZKsfZ;76k@VD3py?pSw~~r^VV$Z zuUl9lF4H2(Qga0EP_==vQ@f!FLC+Y74*s`Ogq|^!?RRt&9e9A&?Tdu=8SOva$dqgYU$zkKD3m>I=`nhx-+M;-leZgt z8TeyQFy`jtUg4Ih^JCUcq+g_qs?LXSxF#t+?1Jsr8c1PB#V+f6aOx@;ThTIR4AyF5 z3m$Rq(6R}U2S}~Bn^M0P&Aaux%D@ijl0kCCF48t)+Y`u>g?|ibOAJoQGML@;tn{%3IEMaD(@`{7ByXQ`PmDeK*;W?| zI8%%P8%9)9{9DL-zKbDQ*%@Cl>Q)_M6vCs~5rb(oTD%vH@o?Gk?UoRD=C-M|w~&vb z{n-B9>t0EORXd-VfYC>sNv5vOF_Wo5V)(Oa%<~f|EU7=npanpVX^SxPW;C!hMf#kq z*vGNI-!9&y!|>Zj0V<~)zDu=JqlQu+ii387D-_U>WI_`3pDuHg{%N5yzU zEulPN)%3&{PX|hv*rc&NKe(bJLhH=GPuLk5pSo9J(M9J3v)FxCo65T%9x<)x+&4Rr2#nu2?~Glz|{28OV6 z)H^`XkUL|MG-$XE=M4*fIPmeR2wFWd>5o*)(gG^Y>!P4(f z68RkX0cRBOFc@`W-IA(q@p@m>*2q-`LfujOJ8-h$OgHte;KY4vZKTxO95;wh#2ZDL zKi8aHkz2l54lZd81t`yY$Tq_Q2_JZ1d(65apMg}vqwx=ceNOWjFB)6m3Q!edw2<{O z4J6+Un(E8jxs-L-K_XM_VWahy zE+9fm_ZaxjNi{fI_AqLKqhc4IkqQ4`Ut$=0L)nzlQw^%i?bP~znsbMY3f}*nPWqQZ zz_CQDpZ?Npn_pEr`~SX1`OoSkS;bmzQ69y|W_4bH3&U3F7EBlx+t%2R02VRJ01cfX zo$$^ObDHK%bHQaOcMpCq@@Jp8!OLYVQO+itW1ZxlkmoG#3FmD4b61mZjn4H|pSmYi2YE;I#@jtq8Mhjdgl!6({gUsQA>IRXb#AyWVt7b=(HWGUj;wd!S+q z4S+H|y<$yPrrrTqQHsa}H`#eJFV2H5Dd2FqFMA%mwd`4hMK4722|78d(XV}rz^-GV(k zqsQ>JWy~cg_hbp0=~V3&TnniMQ}t#INg!o2lN#H4_gx8Tn~Gu&*ZF8#kkM*5gvPu^ zw?!M^05{7q&uthxOn?%#%RA_%y~1IWly7&_-sV!D=Kw3DP+W)>YYRiAqw^d7vG_Q%v;tRbE1pOBHc)c&_5=@wo4CJTJ1DeZErEvP5J(kc^GnGYX z|LqQjTkM{^gO2cO#-(g!7^di@$J0ibC(vsnVkHt3osnWL8?-;R1BW40q5Tmu_9L-s z7fNF5fiuS-%B%F$;D97N-I@!~c+J>nv%mzQ5vs?1MgR@XD*Gv`A{s8 z5Cr>z5j?|sb>n=c*xSKHpdy667QZT?$j^Doa%#m4ggM@4t5Oe%iW z@w~j_B>GJJkO+6dVHD#CkbC(=VMN8nDkz%44SK62N(ZM#AsNz1KW~3(i=)O;q5JrK z?vAVuL}Rme)OGQuLn8{3+V352UvEBV^>|-TAAa1l-T)oiYYD&}Kyxw73shz?Bn})7 z_a_CIPYK(zMp(i+tRLjy4dV#CBf3s@bdmwXo`Y)dRq9r9-c@^2S*YoNOmAX%@OYJOXs zT*->in!8Ca_$W8zMBb04@|Y)|>WZ)-QGO&S7Zga1(1#VR&)X+MD{LEPc%EJCXIMtr z1X@}oNU;_(dfQ_|kI-iUSTKiVzcy+zr72kq)TIp(GkgVyd%{8@^)$%G)pA@^Mfj71FG%d?sf(2Vm>k%X^RS`}v0LmwIQ7!_7cy$Q8pT?X1VWecA_W68u==HbrU& z@&L6pM0@8ZHL?k{6+&ewAj%grb6y@0$3oamTvXsjGmPL_$~OpIyIq%b$(uI1VKo zk_@{r>1p84UK3}B>@d?xUZ}dJk>uEd+-QhwFQ`U?rA=jj+$w8sD#{492P}~R#%z%0 z5dlltiAaiPKv9fhjmuy{*m!C22$;>#85EduvdSrFES{QO$bHpa7E@&{bWb@<7VhTF zXCFS_wB>7*MjJ3$_i4^A2XfF2t7`LOr3B@??OOUk=4fKkaHne4RhI~Lm$JrHfUU*h zgD9G66;_F?3>0W{pW2A^DR7Bq`ZUiSc${S8EM>%gFIqAw0du4~kU#vuCb=$I_PQv? zZfEY7X6c{jJZ@nF&T>4oyy(Zr_XqnMq)ZtGPASbr?IhZOnL|JKY()`eo=P5UK9(P-@ zOJKFogtk|pscVD+#$7KZs^K5l4gC}*CTd0neZ8L(^&1*bPrCp23%{VNp`4Ld*)Fly z)b|zb*bCzp?&X3_=qLT&0J+=p01&}9*xbk~^hd^@mV!Ha`1H+M&60QH2c|!Ty`RepK|H|Moc5MquD z=&$Ne3%WX+|7?iiR8=7*LW9O3{O%Z6U6`VekeF8lGr5vd)rsZu@X#5!^G1;nV60cz zW?9%HgD}1G{E(YvcLcIMQR65BP50)a;WI*tjRzL7diqRqh$3>OK{06VyC=pj6OiardshTnYfve5U>Tln@y{DC99f!B4> zCrZa$B;IjDrg}*D5l=CrW|wdzENw{q?oIj!Px^7DnqAsU7_=AzXxoA;4(YvN5^9ag zwEd4-HOlO~R0~zk>!4|_Z&&q}agLD`Nx!%9RLC#7fK=w06e zOK<>|#@|e2zjwZ5aB>DJ%#P>k4s0+xHJs@jROvoDQfSoE84l8{9y%5^POiP+?yq0> z7+Ymbld(s-4p5vykK@g<{X*!DZt1QWXKGmj${`@_R~=a!qPzB357nWW^KmhV!^G3i zsYN{2_@gtzsZH*FY!}}vNDnqq>kc(+7wK}M4V*O!M&GQ|uj>+8!Q8Ja+j3f*MzwcI z^s4FXGC=LZ?il4D+Y^f89wh!d7EU-5dZ}}>_PO}jXRQ@q^CjK-{KVnmFd_f&IDKmx zZ5;PDLF%_O);<4t`WSMN;Ec^;I#wU?Z?_R|Jg`#wbq;UM#50f@7F?b7ySi-$C-N;% zqXowTcT@=|@~*a)dkZ836R=H+m6|fynm#0Y{KVyYU=_*NHO1{=Eo{^L@wWr7 zjz9GOu8Fd&v}a4d+}@J^9=!dJRsCO@=>K6UCM)Xv6};tb)M#{(k!i}_0Rjq z2kb7wPcNgov%%q#(1cLykjrxAg)By+3QueBR>Wsep&rWQHq1wE!JP+L;q+mXts{j@ zOY@t9BFmofApO0k@iBFPeKsV3X=|=_t65QyohXMSfMRr7Jyf8~ogPVmJwbr@`nmml zov*NCf;*mT(5s4K=~xtYy8SzE66W#tW4X#RnN%<8FGCT{z#jRKy@Cy|!yR`7dsJ}R z!eZzPCF+^b0qwg(mE=M#V;Ud9)2QL~ z-r-2%0dbya)%ui_>e6>O3-}4+Q!D+MU-9HL2tH)O`cMC1^=rA=q$Pcc;Zel@@ss|K zH*WMdS^O`5Uv1qNTMhM(=;qjhaJ|ZC41i2!kt4;JGlXQ$tvvF8Oa^C@(q6(&6B^l) zNG{GaX?`qROHwL-F1WZDEF;C6Inuv~1&ZuP3j53547P38tr|iPH#3&hN*g0R^H;#) znft`cw0+^Lwe{!^kQat+xjf_$SZ05OD6~U`6njelvd+4pLZU(0ykS5&S$)u?gm!;} z+gJ8g12b1D4^2HH!?AHFAjDAP^q)Juw|hZfIv{3Ryn%4B^-rqIF2 zeWk^za4fq#@;re{z4_O|Zj&Zn{2WsyI^1%NW=2qA^iMH>u>@;GAYI>Bk~u0wWQrz* zdEf)7_pSYMg;_9^qrCzvv{FZYwgXK}6e6ceOH+i&+O=x&{7aRI(oz3NHc;UAxMJE2 zDb0QeNpm$TDcshGWs!Zy!shR$lC_Yh-PkQ`{V~z!AvUoRr&BAGS#_*ZygwI2-)6+a zq|?A;+-7f0Dk4uuht z6sWPGl&Q$bev1b6%aheld88yMmBp2j=z*egn1aAWd?zN=yEtRDGRW&nmv#%OQwuJ; zqKZ`L4DsqJwU{&2V9f>2`1QP7U}`6)$qxTNEi`4xn!HzIY?hDnnJZw+mFnVSry=bLH7ar+M(e9h?GiwnOM?9ZJcTJ08)T1-+J#cr&uHhXkiJ~}&(}wvzCo33 zLd_<%rRFQ3d5fzKYQy41<`HKk#$yn$Q+Fx-?{3h72XZrr*uN!5QjRon-qZh9-uZ$rWEKZ z!dJMP`hprNS{pzqO`Qhx`oXGd{4Uy0&RDwJ`hqLw4v5k#MOjvyt}IkLW{nNau8~XM z&XKeoVYreO=$E%z^WMd>J%tCdJx5-h+8tiawu2;s& zD7l`HV!v@vcX*qM(}KvZ#%0VBIbd)NClLBu-m2Scx1H`jyLYce;2z;;eo;ckYlU53 z9JcQS+CvCwj*yxM+e*1Vk6}+qIik2VzvUuJyWyO}piM1rEk%IvS;dsXOIR!#9S;G@ zPcz^%QTf9D<2~VA5L@Z@FGQqwyx~Mc-QFzT4Em?7u`OU!PB=MD8jx%J{<`tH$Kcxz zjIvb$x|`s!-^^Zw{hGV>rg&zb;=m?XYAU0LFw+uyp8v@Y)zmjj&Ib7Y1@r4`cfrS%cVxJiw`;*BwIU*6QVsBBL;~nw4`ZFqs z1YSgLVy=rvA&GQB4MDG+j^)X1N=T;Ty2lE-`zrg(dNq?=Q`nCM*o8~A2V~UPArX<| zF;e$5B0hPSo56=ePVy{nah#?e-Yi3g*z6iYJ#BFJ-5f0KlQ-PRiuGwe29fyk1T6>& zeo2lvb%h9Vzi&^QcVNp}J!x&ubtw5fKa|n2XSMlg#=G*6F|;p)%SpN~l8BaMREDQN z-c9O}?%U1p-ej%hzIDB!W_{`9lS}_U==fdYpAil1E3MQOFW^u#B)Cs zTE3|YB0bKpXuDKR9z&{4gNO3VHDLB!xxPES+)yaJxo<|}&bl`F21};xsQnc!*FPZA zSct2IU3gEu@WQKmY-vA5>MV?7W|{$rAEj4<8`*i)<%fj*gDz2=ApqZ&MP&0UmO1?q!GN=di+n(#bB_mHa z(H-rIOJqamMfwB%?di!TrN=x~0jOJtvb0e9uu$ZCVj(gJyK}Fa5F2S?VE30P{#n3eMy!-v7e8viCooW9cfQx%xyPNL*eDKL zB=X@jxulpkLfnar7D2EeP*0L7c9urDz{XdV;@tO;u`7DlN7#~ zAKA~uM2u8_<5FLkd}OzD9K zO5&hbK8yakUXn8r*H9RE zO9Gsipa2()=&x=1mnQtNP#4m%GXThu8Ccqx*qb;S{5}>bU*V5{SY~(Hb={cyTeaTM zMEaKedtJf^NnJrwQ^Bd57vSlJ3l@$^0QpX@_1>h^+js8QVpwOiIMOiSC_>3@dt*&| zV?0jRdlgn|FIYam0s)a@5?0kf7A|GD|dRnP1=B!{ldr;N5s)}MJ=i4XEqlC}w)LEJ}7f9~c!?It(s zu>b=YBlFRi(H-%8A!@Vr{mndRJ z_jx*?BQpK>qh`2+3cBJhx;>yXPjv>dQ0m+nd4nl(L;GmF-?XzlMK zP(Xeyh7mFlP#=J%i~L{o)*sG7H5g~bnL2Hn3y!!r5YiYRzgNTvgL<(*g5IB*gcajK z86X3LoW*5heFmkIQ-I_@I_7b!Xq#O;IzOv(TK#(4gd)rmCbv5YfA4koRfLydaIXUU z8(q?)EWy!sjsn-oyUC&uwJqEXdlM}#tmD~*Ztav=mTQyrw0^F=1I5lj*}GSQTQOW{ z=O12;?fJfXxy`)ItiDB@0sk43AZo_sRn*jc#S|(2*%tH84d|UTYN!O4R(G6-CM}84 zpiyYJ^wl|w@!*t)dwn0XJv2kuHgbfNL$U6)O-k*~7pQ?y=sQJdKk5x`1>PEAxjIWn z{H$)fZH4S}%?xzAy1om0^`Q$^?QEL}*ZVQK)NLgmnJ`(we z21c23X1&=^>k;UF-}7}@nzUf5HSLUcOYW&gsqUrj7%d$)+d8ZWwTZq)tOgc%fz95+ zl%sdl)|l|jXfqIcjKTFrX74Rbq1}osA~fXPSPE?XO=__@`7k4Taa!sHE8v-zfx(AM zXT_(7u;&_?4ZIh%45x>p!(I&xV|IE**qbqCRGD5aqLpCRvrNy@uT?iYo-FPpu`t}J zSTZ}MDrud+`#^14r`A%UoMvN;raizytxMBV$~~y3i0#m}0F}Dj_fBIz+)1RWdnctP z>^O^vd0E+jS+$V~*`mZWER~L^q?i-6RPxxufWdrW=%prbCYT{5>Vgu%vPB)~NN*2L zB?xQg2K@+Xy=sPh$%10LH!39p&SJG+3^i*lFLn=uY8Io6AXRZf;p~v@1(hWsFzeKzx99_{w>r;cypkPVJCKtLGK>?-K0GE zGH>$g?u`)U_%0|f#!;+E>?v>qghuBwYZxZ*Q*EE|P|__G+OzC-Z+}CS(XK^t!TMoT zc+QU|1C_PGiVp&_^wMxfmMAuJDQ%1p4O|x5DljN6+MJiO%8s{^ts8$uh5`N~qK46c`3WY#hRH$QI@*i1OB7qBIN*S2gK#uVd{ zik+wwQ{D)g{XTGjKV1m#kYhmK#?uy)g@idi&^8mX)Ms`^=hQGY)j|LuFr8SJGZjr| zzZf{hxYg)-I^G|*#dT9Jj)+wMfz-l7ixjmwHK9L4aPdXyD-QCW!2|Jn(<3$pq-BM; zs(6}egHAL?8l?f}2FJSkP`N%hdAeBiD{3qVlghzJe5s9ZUMd`;KURm_eFaK?d&+TyC88v zCv2R(Qg~0VS?+p+l1e(aVq`($>|0b{{tPNbi} zaZDffTZ7N|t2D5DBv~aX#X+yGagWs1JRsqbr4L8a`B`m) z1p9?T`|*8ZXHS7YD8{P1Dk`EGM`2Yjsy0=7M&U6^VO30`Gx!ZkUoqmc3oUbd&)V*iD08>dk=#G!*cs~^tOw^s8YQqYJ z!5=-4ZB7rW4mQF&YZw>T_in-c9`0NqQ_5Q}fq|)%HECgBd5KIo`miEcJ>~a1e2B@) zL_rqoQ;1MowD34e6#_U+>D`WcnG5<2Q6cnt4Iv@NC$*M+i3!c?6hqPJLsB|SJ~xo! zm>!N;b0E{RX{d*in3&0w!cmB&TBNEjhxdg!fo+}iGE*BWV%x*46rT@+cXU;leofWy zxst{S8m!_#hIhbV7wfWN#th8OI5EUr3IR_GOIzBgGW1u4J*TQxtT7PXp#U#EagTV* zehVkBFF06`@5bh!t%L)-)`p|d7D|^kED7fsht#SN7*3`MKZX};Jh0~nCREL_BGqNR zxpJ4`V{%>CAqEE#Dt95u=;Un8wLhrac$fao`XlNsOH%&Ey2tK&vAcriS1kXnntDuttcN{%YJz@!$T zD&v6ZQ>zS1`o!qT=JK-Y+^i~bZkVJpN8%<4>HbuG($h9LP;{3DJF_Jcl8CA5M~<3s^!$Sg62zLEnJtZ z0`)jwK75Il6)9XLf(64~`778D6-#Ie1IR2Ffu+_Oty%$8u+bP$?803V5W6%(+iZzp zp5<&sBV&%CJcXUIATUakP1czt$&0x$lyoLH!ueNaIpvtO z*eCijxOv^-D?JaLzH<3yhOfDENi@q#4w(#tl-19(&Yc2K%S8Y&r{3~-)P17sC1{rQ zOy>IZ6%814_UoEi+w9a4XyGXF66{rgE~UT)oT4x zg9oIx@|{KL#VpTyE=6WK@Sbd9RKEEY)5W{-%0F^6(QMuT$RQRZ&yqfyF*Z$f8>{iT zq(;UzB-Ltv;VHvh4y%YvG^UEkvpe9ugiT97ErbY0ErCEOWs4J=kflA!*Q}gMbEP`N zY#L`x9a?E)*~B~t+7c8eR}VY`t}J;EWuJ-6&}SHnNZ8i0PZT^ahA@@HXk?c0{)6rC zP}I}_KK7MjXqn1E19gOwWvJ3i9>FNxN67o?lZy4H?n}%j|Dq$p%TFLUPJBD;R|*0O z3pLw^?*$9Ax!xy<&fO@;E2w$9nMez{5JdFO^q)B0OmGwkxxaDsEU+5C#g+?Ln-Vg@ z-=z4O*#*VJa*nujGnGfK#?`a|xfZsuiO+R}7y(d60@!WUIEUt>K+KTI&I z9YQ6#hVCo}0^*>yr-#Lisq6R?uI=Ms!J7}qm@B}Zu zp%f-~1Cf!-5S0xXl`oqq&fS=tt0`%dDWI&6pW(s zJXtYiY&~t>k5I0RK3sN;#8?#xO+*FeK#=C^%{Y>{k{~bXz%(H;)V5)DZRk~(_d0b6 zV!x54fwkl`1y;%U;n|E#^Vx(RGnuN|T$oJ^R%ZmI{8(9>U-K^QpDcT?Bb@|J0NAfvHtL#wP ziYupr2E5=_KS{U@;kyW7oy*+UTOiF*e+EhYqVcV^wx~5}49tBNSUHLH1=x}6L2Fl^4X4633$k!ZHZTL50Vq+a5+ z<}uglXQ<{x&6ey)-lq6;4KLHbR)_;Oo^FodsYSw3M-)FbLaBcPI=-ao+|))T2ksKb z{c%Fu`HR1dqNw8%>e0>HI2E_zNH1$+4RWfk}p-h(W@)7LC zwVnUO17y+~kw35CxVtokT44iF$l8XxYuetp)1Br${@lb(Q^e|q*5%7JNxp5B{r<09 z-~8o#rI1(Qb9FhW-igcsC6npf5j`-v!nCrAcVx5+S&_V2D>MOWp6cV$~Olhp2`F^Td{WV`2k4J`djb#M>5D#k&5XkMu*FiO(uP{SNX@(=)|Wm`@b> z_D<~{ip6@uyd7e3Rn+qM80@}Cl35~^)7XN?D{=B-4@gO4mY%`z!kMIZizhGtCH-*7 z{a%uB4usaUoJwbkVVj%8o!K^>W=(ZzRDA&kISY?`^0YHKe!()(*w@{w7o5lHd3(Us zUm-K=z&rEbOe$ackQ3XH=An;Qyug2g&vqf;zsRBldxA+=vNGoM$Zo9yT?Bn?`Hkiq z&h@Ss--~+=YOe@~JlC`CdSHy zcO`;bgMASYi6`WSw#Z|A;wQgH@>+I3OT6(*JgZZ_XQ!LrBJfVW2RK%#02|@V|H4&8DqslU6Zj(x!tM{h zRawG+Vy63_8gP#G!Eq>qKf(C&!^G$01~baLLk#)ov-Pqx~Du>%LHMv?=WBx2p2eV zbj5fjTBhwo&zeD=l1*o}Zs%SMxEi9yokhbHhY4N!XV?t8}?!?42E-B^Rh&ABFxovs*HeQ5{{*)SrnJ%e{){Z_#JH+jvwF7>Jo zE+qzWrugBwVOZou~oFa(wc7?`wNde>~HcC@>fA^o>ll?~aj-e|Ju z+iJzZg0y1@eQ4}rm`+@hH(|=gW^;>n>ydn!8%B4t7WL)R-D>mMw<7Wz6>ulFnM7QA ze2HEqaE4O6jpVq&ol3O$46r+DW@%glD8Kp*tFY#8oiSyMi#yEpVIw3#t?pXG?+H>v z$pUwT@0ri)_Bt+H(^uzp6qx!P(AdAI_Q?b`>0J?aAKTPt>73uL2(WXws9+T|%U)Jq zP?Oy;y6?{%J>}?ZmfcnyIQHh_jL;oD$`U#!v@Bf{5%^F`UiOX%)<0DqQ^nqA5Ac!< z1DPO5C>W0%m?MN*x(k>lDT4W3;tPi=&yM#Wjwc5IFNiLkQf`7GN+J*MbB4q~HVePM zeDj8YyA*btY&n!M9$tuOxG0)2um))hsVsY+(p~JnDaT7x(s2If0H_iRSju7!z7p|8 zzI`NV!1hHWX3m)?t68k6yNKvop{Z>kl)f5GV(~1InT4%9IxqhDX-rgj)Y|NYq_NTlZgz-)=Y$=x9L7|k0=m@6WQ<4&r=BX@pW25NtCI+N{e&`RGSpR zeb^`@FHm5?pWseZ6V08{R(ki}--13S2op~9Kzz;#cPgL}Tmrqd+gs(fJLTCM8#&|S z^L+7PbAhltJDyyxAVxqf(2h!RGC3$;hX@YNz@&JRw!m5?Q)|-tZ8u0D$4we+QytG^ zj0U_@+N|OJlBHdWPN!K={a$R1Zi{2%5QD}s&s-Xn1tY1cwh)8VW z$pjq>8sj4)?76EJs6bA0E&pfr^Vq`&Xc;Tl2T!fm+MV%!H|i0o;7A=zE?dl)-Iz#P zSY7QRV`qRc6b&rON`BValC01zSLQpVemH5y%FxK8m^PeNN(Hf1(%C}KPfC*L?Nm!nMW0@J3(J=mYq3DPk;TMs%h`-amWbc%7{1Lg3$ z^e=btuqch-lydbtLvazh+fx?87Q7!YRT(=-Vx;hO)?o@f1($e5B?JB9jcRd;zM;iE zu?3EqyK`@_5Smr#^a`C#M>sRwq2^|ym)X*r;0v6AM`Zz1aK94@9Ti)Lixun2N!e-A z>w#}xPxVd9AfaF$XTTff?+#D(xwOpjZj9-&SU%7Z-E2-VF-n#xnPeQH*67J=j>TL# z<v}>AiTXrQ(fYa%82%qlH=L z6Fg8@r4p+BeTZ!5cZlu$iR?EJpYuTx>cJ~{{B7KODY#o*2seq=p2U0Rh;3mX^9sza zk^R_l7jzL5BXWlrVkhh!+LQ-Nc0I`6l1mWkp~inn)HQWqMTWl4G-TBLglR~n&6J?4 z7J)IO{wkrtT!Csntw3H$Mnj>@;QbrxC&Shqn^VVu$Ls*_c~TTY~fri6fO-=eJsC*8(3(H zSyO>=B;G`qA398OvCHRvf3mabrPZaaLhn*+jeA`qI!gP&i8Zs!*bBqMXDJpSZG$N) zx0rDLvcO>EoqCTR)|n7eOp-jmd>`#w`6`;+9+hihW2WnKVPQ20LR94h+(p)R$Y!Q zj_3ZEY+e@NH0f6VjLND)sh+Cvfo3CpcXw?`$@a^@CyLrAKIpjL8G z`;cDLqvK=ER)$q)+6vMKlxn!!SzWl>Ib9Ys9L)L0IWr*Ox;Rk#(Dpqf;wapY_EYL8 zKFrV)Q8BBKO4$r2hON%g=r@lPE;kBUVYVG`uxx~QI>9>MCXw_5vnmDsm|^KRny929 zeKx>F(LDs#K4FGU*k3~GX`A!)l8&|tyan-rBHBm6XaB5hc5sGKWwibAD7&3M-gh1n z2?eI7E2u{(^z#W~wU~dHSfy|m)%PY454NBxED)y-T3AO`CLQxklcC1I@Y`v4~SEI#Cm> z-cjqK6I?mypZapi$ZK;y&G+|#D=woItrajg69VRD+Fu8*UxG6KdfFmFLE}HvBJ~Y) zC&c-hr~;H2Idnsz7_F~MKpBZldh)>itc1AL0>4knbVy#%pUB&9vqL1Kg*^aU`k#(p z=A%lur(|$GWSqILaWZ#2xj(&lheSiA|N6DOG?A|$!aYM)?oME6ngnfLw0CA79WA+y zhUeLbMw*VB?drVE_D~3DWVaD>8x?_q>f!6;)i3@W<=kBZBSE=uIU60SW)qct?AdM zXgti8&O=}QNd|u%Fpxr172Kc`sX^@fm>Fxl8fbFalJYci_GGoIzU*~U*I!QLz? z4NYk^=JXBS*Uph@51da-v;%?))cB^(ps}y8yChu7CzyC9SX{jAq13zdnqRHRvc{ha zcPmgCUqAJ^1RChMCCz;ZN*ap{JPoE<1#8nNObDbAt6Jr}Crq#xGkK@w2mLhIUecvy z#?s~?J()H*?w9K`_;S+8TNVkHSk}#yvn+|~jcB|he}OY(zH|7%EK%-Tq=)18730)v zM3f|=oFugXq3Lqn={L!wx|u(ycZf(Te11c3?^8~aF; zNMC)gi?nQ#S$s{46yImv_7@4_qu|XXEza~);h&cr*~dO@#$LtKZa@@r$8PD^jz{D6 zk~5;IJBuQjsKk+8i0wzLJ2=toMw4@rw7(|6`7*e|V(5-#ZzRirtkXBO1oshQ&0>z&HAtSF8+871e|ni4gLs#`3v7gnG#^F zDv!w100_HwtU}B2T!+v_YDR@-9VmoGW+a76oo4yy)o`MY(a^GcIvXW+4)t{lK}I-& zl-C=(w_1Z}tsSFjFd z3iZjkO6xnjLV3!EE?ex9rb1Zxm)O-CnWPat4vw08!GtcQ3lHD+ySRB*3zQu-at$rj zzBn`S?5h=JlLXX8)~Jp%1~YS6>M8c-Mv~E%s7_RcvIYjc-ia`3r>dvjxZ6=?6=#OM zfsv}?hGnMMdi9C`J9+g)5`M9+S79ug=!xE_XcHdWnIRr&hq$!X7aX5kJV8Q(6Lq?|AE8N2H z37j{DPDY^Jw!J>~>Mwaja$g%q1sYfH4bUJFOR`x=pZQ@O(-4b#5=_Vm(0xe!LW>YF zO4w`2C|Cu%^C9q9B>NjFD{+qt)cY3~(09ma%mp3%cjFsj0_93oVHC3)AsbBPuQNBO z`+zffU~AgGrE0K{NVR}@oxB4&XWt&pJ-mq!JLhFWbnXf~H%uU?6N zWJ7oa@``Vi$pMWM#7N9=sX1%Y+1qTGnr_G&h3YfnkHPKG}p>i{fAG+(klE z(g~u_rJXF48l1D?;;>e}Ra{P$>{o`jR_!s{hV1Wk`vURz`W2c$-#r9GM7jgs2>um~ zouGlCm92rOiLITzf`jgl`v2qYw^!Lh0YwFHO1|3Krp8ztE}?#2+>c)yQlNw%5e6w5 zIm9BKZN5Q9b!tX`Zo$0RD~B)VscWp(FR|!a!{|Q$={;ZWl%10vBzfgWn}WBe!%cug z^G%;J-L4<6&aCKx@@(Grsf}dh8fuGT+TmhhA)_16uB!t{HIAK!B-7fJLe9fsF)4G- zf>(~ⅅ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 H-+%o-(+5aQ diff --git a/support/android/apk/gradle/wrapper/gradle-wrapper.properties b/support/android/apk/gradle/wrapper/gradle-wrapper.properties index 915f189a922..da1db5f04e8 100644 --- a/support/android/apk/gradle/wrapper/gradle-wrapper.properties +++ b/support/android/apk/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Wed Jul 11 13:23:08 CEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/support/android/apk/gradlew b/support/android/apk/gradlew index 9d82f789151..af6708ff229 100755 --- a/support/android/apk/gradlew +++ b/support/android/apk/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -6,42 +6,6 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -60,6 +24,46 @@ cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -150,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/support/android/apk/gradlew.bat b/support/android/apk/gradlew.bat index aec99730b4e..0f8d5937c4a 100644 --- a/support/android/apk/gradlew.bat +++ b/support/android/apk/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +46,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/support/android/apk/jni/Android.mk b/support/android/apk/jni/Android.mk index 20a4de2d4b2..7ee4ffe58d8 100644 --- a/support/android/apk/jni/Android.mk +++ b/support/android/apk/jni/Android.mk @@ -16,13 +16,7 @@ MY_LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) -LOCAL_PATH:= $(SERVO_TARGET_DIR) +LOCAL_PATH := $(SERVO_TARGET_DIR) LOCAL_MODULE := servojni LOCAL_SRC_FILES := libsimpleservo.so include $(PREBUILT_SHARED_LIBRARY) - -include $(CLEAR_VARS) -LOCAL_PATH:= $(SERVO_TARGET_DIR)/../../gstreamer/gst-build-$(APP_ABI) -LOCAL_MODULE := gstreamer -LOCAL_SRC_FILES := libgstreamer_android.so -include $(PREBUILT_SHARED_LIBRARY) diff --git a/support/android/apk/jni/Application.mk b/support/android/apk/jni/Application.mk index 9944120aebe..5b349f06d44 100644 --- a/support/android/apk/jni/Application.mk +++ b/support/android/apk/jni/Application.mk @@ -1,5 +1,5 @@ NDK_TOOLCHAIN_VERSION := clang -APP_MODULES := c++_shared servojni gstreamer -APP_PLATFORM := android-21 -APP_STL:= c++_shared -APP_ABI:= armeabi-v7a x86 +APP_MODULES := c++_shared servojni +APP_PLATFORM := android-30 +APP_STL := c++_shared +APP_ABI := armeabi-v7a x86 diff --git a/support/android/apk/servoapp/build.gradle b/support/android/apk/servoapp/build.gradle index 6b440e7b2c7..6e1cb514d26 100644 --- a/support/android/apk/servoapp/build.gradle +++ b/support/android/apk/servoapp/build.gradle @@ -1,18 +1,22 @@ -apply plugin: 'com.android.application' +plugins { + id 'com.android.application' +} import java.util.regex.Matcher import java.util.regex.Pattern android { - compileSdkVersion 27 - buildToolsVersion '27.0.3' + compileSdk 33 + buildToolsVersion "33.0.2" + + namespace 'org.mozilla.servo' buildDir = rootDir.absolutePath + "/../../../target/android/gradle/servoapp" defaultConfig { applicationId "org.mozilla.servo" - minSdkVersion 21 - targetSdkVersion 27 + minSdk 30 + targetSdk 30 versionCode 1 versionName "1.0.0" } @@ -26,11 +30,7 @@ android { flavorDimensions "default" productFlavors { - main { - } - googlevr { - } - oculusvr { + basic { } } @@ -61,19 +61,6 @@ android { } // Custom build types - armDebug { - initWith(debug) - ndk { - abiFilters getNDKAbi('arm') - } - } - - armRelease { - initWith(release) - ndk { - abiFilters getNDKAbi('arm') - } - } armv7Debug { initWith(debug) ndk { @@ -144,7 +131,6 @@ android { } dependencies { - implementation 'com.android.support.constraint:constraint-layout:1.1.2' if (findProject(':servoview-local')) { implementation project(':servoview-local') } else { diff --git a/support/android/apk/servoapp/src/googlevr/AndroidManifest.xml b/support/android/apk/servoapp/src/googlevr/AndroidManifest.xml deleted file mode 100644 index a7df4ac14a9..00000000000 --- a/support/android/apk/servoapp/src/googlevr/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/support/android/apk/servoapp/src/main/AndroidManifest.xml b/support/android/apk/servoapp/src/main/AndroidManifest.xml index 71db56c53a1..3bedf50ffbf 100644 --- a/support/android/apk/servoapp/src/main/AndroidManifest.xml +++ b/support/android/apk/servoapp/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ - + diff --git a/support/android/apk/servoapp/src/main/java/org/mozilla/servo/MainActivity.java b/support/android/apk/servoapp/src/main/java/org/mozilla/servo/MainActivity.java index 1e1c9dbabeb..a366e8037d2 100644 --- a/support/android/apk/servoapp/src/main/java/org/mozilla/servo/MainActivity.java +++ b/support/android/apk/servoapp/src/main/java/org/mozilla/servo/MainActivity.java @@ -78,8 +78,7 @@ public class MainActivity extends Activity implements Servo.Client { Intent intent = getIntent(); String args = intent.getStringExtra("servoargs"); String log = intent.getStringExtra("servolog"); - String gstdebug = intent.getStringExtra("gstdebug"); - mServoView.setServoArgs(args, log, gstdebug); + mServoView.setServoArgs(args, log); if (Intent.ACTION_VIEW.equals(intent.getAction())) { mServoView.loadUri(intent.getData()); @@ -89,8 +88,10 @@ public class MainActivity extends Activity implements Servo.Client { @Override protected void onDestroy() { - super.onDestroy(); - mMediaSession.hideMediaSessionControls(); + super.onDestroy(); + if (mMediaSession != null) { + mMediaSession.hideMediaSessionControls(); + } } private void setupUrlField() { @@ -229,31 +230,31 @@ public class MainActivity extends Activity implements Servo.Client { @Override public void onMediaSessionMetadata(String title, String artist, String album) { - if (mMediaSession == null) { - mMediaSession = new MediaSession(mServoView, this, getApplicationContext()); - } - Log.d("onMediaSessionMetadata", title + " " + artist + " " + album); - mMediaSession.updateMetadata(title, artist, album); + if (mMediaSession == null) { + mMediaSession = new MediaSession(mServoView, this, getApplicationContext()); + } + Log.d("onMediaSessionMetadata", title + " " + artist + " " + album); + mMediaSession.updateMetadata(title, artist, album); } @Override public void onMediaSessionPlaybackStateChange(int state) { - Log.d("onMediaSessionPlaybackStateChange", String.valueOf(state)); - if (mMediaSession == null) { - mMediaSession = new MediaSession(mServoView, this, getApplicationContext()); - } + Log.d("onMediaSessionPlaybackStateChange", String.valueOf(state)); + if (mMediaSession == null) { + mMediaSession = new MediaSession(mServoView, this, getApplicationContext()); + } - mMediaSession.setPlaybackState(state); + mMediaSession.setPlaybackState(state); - if (state == MediaSession.PLAYBACK_STATE_NONE) { - mMediaSession.hideMediaSessionControls(); - return; - } - if (state == MediaSession.PLAYBACK_STATE_PLAYING || - state == MediaSession.PLAYBACK_STATE_PAUSED) { - mMediaSession.showMediaSessionControls(); - return; - } + if (state == MediaSession.PLAYBACK_STATE_NONE) { + mMediaSession.hideMediaSessionControls(); + return; + } + if (state == MediaSession.PLAYBACK_STATE_PLAYING || + state == MediaSession.PLAYBACK_STATE_PAUSED) { + mMediaSession.showMediaSessionControls(); + return; + } } @Override diff --git a/support/android/apk/servoapp/src/main/res/layout/activity_main.xml b/support/android/apk/servoapp/src/main/res/layout/activity_main.xml index 2cfb3357a01..42cf7a1a4b9 100644 --- a/support/android/apk/servoapp/src/main/res/layout/activity_main.xml +++ b/support/android/apk/servoapp/src/main/res/layout/activity_main.xml @@ -1,5 +1,5 @@ - - + diff --git a/support/android/apk/servoview/build.gradle b/support/android/apk/servoview/build.gradle index 51bab4b45e2..5898bb7bca2 100644 --- a/support/android/apk/servoview/build.gradle +++ b/support/android/apk/servoview/build.gradle @@ -1,18 +1,23 @@ -apply plugin: 'com.android.library' +plugins { + id 'com.android.library' +} import groovy.io.FileType import java.util.regex.Matcher import java.util.regex.Pattern android { - compileSdkVersion 27 - buildToolsVersion '27.0.3' + compileSdk 33 + buildToolsVersion "33.0.2" + + namespace 'org.mozilla.servoview' buildDir = rootDir.absolutePath + "/../../../target/android/gradle/servoview" + ndkPath = getNdkDir() defaultConfig { - minSdkVersion 18 - targetSdkVersion 27 + minSdk 30 + targetSdk 30 versionCode 1 versionName "1.0" } @@ -26,24 +31,18 @@ android { flavorDimensions "default" productFlavors { - main { - } - googlevr { - minSdkVersion 21 - } - oculusvr { - minSdkVersion 21 + basic { } } - splits { - density { - enable false - } - abi { - enable false - } - } + splits { + density { + enable false + } + abi { + enable false + } + } buildTypes { @@ -59,65 +58,29 @@ android { } // Custom build types - armDebug { - initWith(debug) - ndk { - abiFilters getNDKAbi('arm') - } - } - armRelease { - initWith(release) - ndk { - abiFilters getNDKAbi('arm') - } - } armv7Debug { initWith(debug) - ndk { - abiFilters getNDKAbi('armv7') - } } armv7Release { initWith(release) - ndk { - abiFilters getNDKAbi('armv7') - } } arm64Debug { initWith(debug) - ndk { - abiFilters getNDKAbi('arm64') - } } arm64Release { initWith(release) - ndk { - abiFilters getNDKAbi('arm64') - } } x86Debug { initWith(debug) - ndk { - abiFilters getNDKAbi('x86') - } } x86Release { initWith(release) - ndk { - abiFilters getNDKAbi('x86') - } } } sourceSets { main { } - armDebug { - jniLibs.srcDirs = [getJniLibsPath(true, 'arm')] - } - armRelease { - jniLibs.srcDirs = [getJniLibsPath(false, 'arm')] - } armv7Debug { jniLibs.srcDirs = [getJniLibsPath(true, 'armv7')] } @@ -145,34 +108,49 @@ android { } } - - // Call our custom NDK Build task using flavor parameters + // Call our custom NDK Build task using flavor parameters. + // This step is needed because the Android Gradle Plugin system's + // integration with native C/C++ shared objects (based on the + // `android.externalNativeBuild` dsl object) assumes that we + // actually execute compiler commands to produced the shared + // objects. We already have the libsimpleservo.so produced by rustc. + // We could simply copy the .so to the `sourceSet.jniLibs` folder + // to make AGP bundle it with the APK, but this doesn't copy the STL + // (libc++_shared.so) as well. So we use ndk-build as a glorified + // `cp` command to copy the libsimpleservo.so from target/ + // to target/android and crucially also include libc++_shared.so + // as well. + // + // FIXME(mukilan): According to the AGP docs, we should not be + // relying on task names used by the plugin system to hook into + // the build process, but instead we should use officially supported + // extension points such as `androidComponents.beforeVariants` tasks.all { compileTask -> - Pattern pattern = Pattern.compile(/^compile[A-Z][\w\d]+([A-Z][\w\d]+)(Debug|Release)Ndk/) + // This matches the task `mergeBasicArmv7DebugJniLibFolders`. + Pattern pattern = Pattern.compile(/^merge[A-Z][\w\d]+([A-Z][\w\d]+)(Debug|Release)JniLibFolders/) Matcher matcher = pattern.matcher(compileTask.name) if (!matcher.find()) { - return + return } def taskName = "ndkbuild" + compileTask.name tasks.create(name: taskName, type: Exec) { def debug = compileTask.name.contains("Debug") def arch = matcher.group(1) - commandLine getNdkDir(), - 'APP_BUILD_SCRIPT=../jni/Android.mk', - 'NDK_APPLICATION_MK=../jni/Application.mk', - 'NDK_LIBS_OUT=' + getJniLibsPath(debug, arch), - 'NDK_OUT=' + getTargetDir(debug, arch) + '/apk/obj', - 'NDK_DEBUG=' + (debug ? '1' : '0'), - 'APP_ABI=' + getNDKAbi(arch), - 'SERVO_TARGET_DIR=' + getTargetDir(debug, arch) + commandLine getNdkDir() + "/ndk-build", + 'APP_BUILD_SCRIPT=../jni/Android.mk', + 'NDK_APPLICATION_MK=../jni/Application.mk', + 'NDK_LIBS_OUT=' + getJniLibsPath(debug, arch), + 'NDK_DEBUG=' + (debug ? '1' : '0'), + 'APP_ABI=' + getNDKAbi(arch), + 'NDK_LOG=1', + 'SERVO_TARGET_DIR=' + getNativeTargetDir(debug, arch) } compileTask.dependsOn taskName } - project.afterEvaluate { android.libraryVariants.all { variant -> Pattern pattern = Pattern.compile(/^[\w\d]+([A-Z][\w\d]+)(Debug|Release)/) @@ -205,7 +183,7 @@ dependencies { ] // Iterate all build types and dependencies // For each dependency call the proper implementation command and set the correct dependency path - def list = ['arm', 'armv7', 'arm64', 'x86'] + def list = ['armv7', 'arm64', 'x86'] for (arch in list) { for (debug in [true, false]) { String basePath = getTargetDir(debug, arch) + "/build" @@ -220,10 +198,9 @@ dependencies { } } - googlevrImplementation 'com.google.vr:sdk-base:1.140.0' - googlevrImplementation(name: 'GVRService', ext: 'aar') - oculusvrImplementation(name: 'OVRService', ext: 'aar') - implementation 'com.android.support.constraint:constraint-layout:1.1.2' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.9.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' } // folderFilter can be used to improve search performance @@ -258,42 +235,3 @@ class ServoDependency { public String fileName; public String folderFilter; } - -apply plugin: 'maven' -import org.gradle.api.internal.artifacts.publish.DefaultPublishArtifact - -uploadArchives { - doFirst { - for ( arch in ["arm", "armv7", "arm64", "x86"] ) { - def target = getTargetDir(false, arch) - def aar = new File(target, "servoview.aar") - if (aar.exists()) { - def art = new DefaultPublishArtifact("servoview-" + arch, "aar", "aar", null, new Date(), aar); - project.artifacts.add('archives', art) - } - } - } - repositories.mavenDeployer { - repository(url: "file://localhost/${buildDir}/maven") - def cmd = "git rev-parse --short HEAD" - def proc = cmd.execute() - def commit = proc.text.trim() - def version = "0.0.1." + new Date().format('yyyyMMdd') + "." + commit - for ( arch_ in ["arm", "armv7", "arm64", "x86"] ) { - def arch = arch_ - addFilter(arch) {artifact, file -> artifact.name == "servoview-" + arch} - pom(arch).artifactId = "servoview-" + arch - pom(arch).groupId = 'org.mozilla.servoview' - pom(arch).version = version - pom(arch).project { - licenses { - license { - name 'The Mozilla Public License, v. 2.0' - url 'https://mozilla.org/MPL/2.0/' - distribution 'repo' - } - } - } - } - } -} diff --git a/support/android/apk/servoview/src/googlevr/AndroidManifest.xml b/support/android/apk/servoview/src/googlevr/AndroidManifest.xml deleted file mode 100644 index 7e1c7048634..00000000000 --- a/support/android/apk/servoview/src/googlevr/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/support/android/apk/servoview/src/main/AndroidManifest.xml b/support/android/apk/servoview/src/main/AndroidManifest.xml index 1e916f18c97..94cbbcfc396 100644 --- a/support/android/apk/servoview/src/main/AndroidManifest.xml +++ b/support/android/apk/servoview/src/main/AndroidManifest.xml @@ -1,2 +1 @@ - + diff --git a/support/android/apk/servoview/src/main/java/org/mozilla/servoview/JNIServo.java b/support/android/apk/servoview/src/main/java/org/mozilla/servoview/JNIServo.java index 5fa91fa34f6..29dce9f01b1 100644 --- a/support/android/apk/servoview/src/main/java/org/mozilla/servoview/JNIServo.java +++ b/support/android/apk/servoview/src/main/java/org/mozilla/servoview/JNIServo.java @@ -6,7 +6,7 @@ package org.mozilla.servoview; import android.app.Activity; - +import android.view.Surface; /** * Maps /ports/libsimpleservo API */ @@ -14,13 +14,12 @@ import android.app.Activity; public class JNIServo { JNIServo() { System.loadLibrary("c++_shared"); - System.loadLibrary("gstreamer_android"); System.loadLibrary("simpleservo"); } public native String version(); - public native void init(Activity activity, ServoOptions options, Callbacks callbacks); + public native void init(Activity activity, ServoOptions options, Callbacks callbacks, Surface surface); public native void deinit(); @@ -66,6 +65,9 @@ public class JNIServo { public native void click(float x, float y); + public native void pauseCompositor(); + public native void resumeCompositor(Surface surface, ServoCoordinates coords); + public native void mediaSessionAction(int action); public static class ServoOptions { diff --git a/support/android/apk/servoview/src/main/java/org/mozilla/servoview/Servo.java b/support/android/apk/servoview/src/main/java/org/mozilla/servoview/Servo.java index 7f8bd3fc211..e8dfd332a11 100644 --- a/support/android/apk/servoview/src/main/java/org/mozilla/servoview/Servo.java +++ b/support/android/apk/servoview/src/main/java/org/mozilla/servoview/Servo.java @@ -8,6 +8,7 @@ package org.mozilla.servoview; import android.app.Activity; import android.content.Context; import android.util.Log; +import android.view.Surface; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; @@ -30,21 +31,16 @@ public class Servo { RunCallback runCallback, GfxCallbacks gfxcb, Client client, - Activity activity) { + Activity activity, + Surface surface) { mRunCallback = runCallback; mServoCallbacks = new Callbacks(client, gfxcb); mRunCallback.inGLThread(() -> { - mJNI.init(activity, options, mServoCallbacks); + mJNI.init(activity, options, mServoCallbacks, surface); }); - - try { - GStreamer.init((Context) activity); - } catch (Exception e) { - e.printStackTrace(); - } } public void resetGfxCallbacks(GfxCallbacks gfxcb) { @@ -164,6 +160,13 @@ public class Servo { mRunCallback.inGLThread(() -> mJNI.click(x, y)); } + public void pauseCompositor() { + mRunCallback.inGLThread(() -> mJNI.pauseCompositor()); + } + public void resumeCompositor(Surface surface, ServoCoordinates coords) { + mRunCallback.inGLThread(() -> mJNI.resumeCompositor(surface, coords)); + } + public void suspend(boolean suspended) { mSuspended = suspended; } diff --git a/support/android/apk/servoview/src/main/java/org/mozilla/servoview/ServoSurface.java b/support/android/apk/servoview/src/main/java/org/mozilla/servoview/ServoSurface.java deleted file mode 100644 index 80f897fa4b9..00000000000 --- a/support/android/apk/servoview/src/main/java/org/mozilla/servoview/ServoSurface.java +++ /dev/null @@ -1,302 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -package org.mozilla.servoview; - -import android.app.Activity; -import android.net.Uri; -import android.opengl.EGL14; -import android.opengl.EGLConfig; -import android.opengl.EGLContext; -import android.opengl.EGLDisplay; -import android.opengl.EGLSurface; -import android.opengl.GLUtils; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; -import android.view.Surface; - -import org.mozilla.servoview.JNIServo.ServoCoordinates; -import org.mozilla.servoview.JNIServo.ServoOptions; -import org.mozilla.servoview.Servo.Client; -import org.mozilla.servoview.Servo.GfxCallbacks; -import org.mozilla.servoview.Servo.RunCallback; - -import static android.opengl.EGL14.EGL_CONTEXT_CLIENT_VERSION; -import static android.opengl.EGL14.EGL_NO_CONTEXT; -import static android.opengl.EGL14.EGL_NO_SURFACE; -import static android.opengl.EGL14.EGL_OPENGL_ES2_BIT; - -public class ServoSurface { - private static final String LOGTAG = "ServoSurface"; - private final GLThread mGLThread; - private final Handler mMainLooperHandler; - private Handler mGLLooperHandler; - private Surface mASurface; - private int mPadding; - private int mWidth; - private int mHeight; - private long mVRExternalContext; - private Servo mServo; - private Client mClient = null; - private String mServoArgs; - private String mServoLog; - private String mInitialUri; - private Activity mActivity; - - public ServoSurface(Surface surface, int width, int height, int padding) { - mPadding = padding; - mWidth = width; - mHeight = height; - mASurface = surface; - mMainLooperHandler = new Handler(Looper.getMainLooper()); - mGLThread = new GLThread(); - } - - public void onSurfaceChanged(Surface surface) { - mASurface = surface; - mGLThread.onSurfaceChanged(); - } - - public void setClient(Client client) { - mClient = client; - } - - public void setServoArgs(String args, String log) { - mServoArgs = args; - mServoLog = log; - } - - public void setActivity(Activity activity) { - mActivity = activity; - } - - public void setVRExternalContext(long context) { - mVRExternalContext = context; - } - - public void runLoop() { - mGLThread.start(); - } - - public void shutdown() { - Log.d(LOGTAG, "shutdown"); - mServo.shutdown(); - mServo = null; - mGLThread.shutdown(); - try { - Log.d(LOGTAG, "Waiting for GL thread to shutdown"); - mGLThread.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - public void reload() { - mServo.reload(); - } - - public void goBack() { - mServo.goBack(); - } - - public void goForward() { - mServo.goForward(); - } - - public void stop() { - mServo.stop(); - } - - public void loadUri(String uri) { - if (mServo != null) { - mServo.loadUri(uri); - } else { - mInitialUri = uri; - } - } - - public void loadUri(Uri uri) { - loadUri(uri.toString()); - } - - public void scrollStart(int dx, int dy, int x, int y) { - mServo.scrollStart(dx, dy, x, y); - } - - public void scroll(int dx, int dy, int x, int y) { - mServo.scroll(dx, dy, x, y); - } - - public void scrollEnd(int dx, int dy, int x, int y) { - mServo.scrollEnd(dx, dy, x, y); - } - - public void click(float x, float y) { - mServo.click(x, y); - } - - public void onSurfaceResized(int width, int height) { - mWidth = width; - mHeight = height; - - ServoCoordinates coords = new ServoCoordinates(); - coords.x = mPadding; - coords.y = mPadding; - coords.width = width - 2 * mPadding; - coords.height = height - 2 * mPadding; - coords.fb_width = width; - coords.fb_height = height; - - mServo.resize(coords); - } - - static class GLSurface implements GfxCallbacks { - private EGLConfig[] mEGLConfigs; - private EGLDisplay mEglDisplay; - private EGLContext mEglContext; - private EGLSurface mEglSurface; - - void throwGLError(String function) { - throwGLError(function, EGL14.eglGetError()); - } - - void throwGLError(String function, int error) { - throw new RuntimeException("Error: " + function + "() Failed " + GLUtils.getEGLErrorString(error)); - } - - GLSurface(Surface surface) { - mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); - int[] version = new int[2]; - if (!EGL14.eglInitialize(mEglDisplay, version, 0, version, 1)) { - throwGLError("eglInitialize"); - } - mEGLConfigs = new EGLConfig[1]; - int[] configsCount = new int[1]; - int[] configSpec = new int[]{ - EGL14.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL14.EGL_RED_SIZE, 8, - EGL14.EGL_GREEN_SIZE, 8, - EGL14.EGL_BLUE_SIZE, 8, - EGL14.EGL_ALPHA_SIZE, 8, - EGL14.EGL_DEPTH_SIZE, 24, - EGL14.EGL_STENCIL_SIZE, 0, - EGL14.EGL_NONE - }; - if ((!EGL14.eglChooseConfig(mEglDisplay, configSpec, 0, mEGLConfigs, 0, 1, configsCount, 0)) || (configsCount[0] == 0)) { - throwGLError("eglChooseConfig"); - } - if (mEGLConfigs[0] == null) { - throw new RuntimeException("Error: eglConfig() not Initialized"); - } - int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL14.EGL_NONE}; - mEglContext = EGL14.eglCreateContext(mEglDisplay, mEGLConfigs[0], EGL14.EGL_NO_CONTEXT, attrib_list, 0); - int glError = EGL14.eglGetError(); - if (glError != EGL14.EGL_SUCCESS) { - throwGLError("eglCreateContext", glError); - } - mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEGLConfigs[0], surface, new int[]{EGL14.EGL_NONE}, 0); - if (mEglSurface == null || mEglSurface == EGL14.EGL_NO_SURFACE) { - glError = EGL14.eglGetError(); - if (glError == EGL14.EGL_BAD_NATIVE_WINDOW) { - Log.e(LOGTAG, "Error: createWindowSurface() Returned EGL_BAD_NATIVE_WINDOW."); - return; - } - throwGLError("createWindowSurface", glError); - } - - makeCurrent(); - } - - - public void makeCurrent() { - if (!EGL14.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { - throwGLError("eglMakeCurrent"); - } - } - - public void flushGLBuffers() { - EGL14.eglSwapBuffers(mEglDisplay, mEglSurface); - } - - public void animationStateChanged(boolean animating) { - // FIXME - } - - void destroy() { - Log.d(LOGTAG, "Destroying surface"); - if (!EGL14.eglMakeCurrent(mEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { - throwGLError("eglMakeCurrent"); - } - if (!EGL14.eglDestroyContext(mEglDisplay, mEglContext)) { - throwGLError("eglDestroyContext"); - } - if (!EGL14.eglDestroySurface(mEglDisplay, mEglSurface)) { - throwGLError("eglDestroySurface"); - } - if (!EGL14.eglTerminate(mEglDisplay)) { - throwGLError("eglTerminate"); - } - } - - } - - class GLThread extends Thread implements RunCallback { - private GLSurface mSurface; - - public void inGLThread(Runnable r) { - mGLLooperHandler.post(r); - } - - public void inUIThread(Runnable r) { - mMainLooperHandler.post(r); - } - - public void onSurfaceChanged() { - Log.d(LOGTAG, "GLThread::onSurfaceChanged"); - mSurface.destroy(); - mSurface = new GLSurface(mASurface); - mServo.resetGfxCallbacks(mSurface); - } - - public void shutdown() { - Log.d(LOGTAG, "GLThread::shutdown"); - mSurface.destroy(); - mGLLooperHandler.getLooper().quitSafely(); - } - - public void run() { - Looper.prepare(); - - mSurface = new GLSurface(mASurface); - - mGLLooperHandler = new Handler(); - - inUIThread(() -> { - ServoCoordinates coords = new ServoCoordinates(); - coords.x = mPadding; - coords.y = mPadding; - coords.width = mWidth - 2 * mPadding; - coords.height = mHeight - 2 * mPadding; - coords.fb_width = mWidth; - coords.fb_height = mHeight; - - ServoOptions options = new ServoOptions(); - options.coordinates = coords; - options.args = mServoArgs; - options.density = 1; - options.url = mInitialUri; - options.logStr = mServoLog; - options.enableLogs = true; - options.enableSubpixelTextAntialiasing = false; - options.VRExternalContext = mVRExternalContext; - - mServo = new Servo(options, this, mSurface, mClient, mActivity); - }); - - Looper.loop(); - } - } -} diff --git a/support/android/apk/servoview/src/main/java/org/mozilla/servoview/ServoView.java b/support/android/apk/servoview/src/main/java/org/mozilla/servoview/ServoView.java index 3784bbf58cb..19343d134d7 100644 --- a/support/android/apk/servoview/src/main/java/org/mozilla/servoview/ServoView.java +++ b/support/android/apk/servoview/src/main/java/org/mozilla/servoview/ServoView.java @@ -7,17 +7,15 @@ package org.mozilla.servoview; import android.app.Activity; import android.content.Context; -import android.net.Uri; -import android.opengl.GLES31; -import android.opengl.GLSurfaceView; import android.util.AttributeSet; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; import android.util.DisplayMetrics; import android.util.Log; -import android.view.Choreographer; -import android.view.GestureDetector; -import android.view.MotionEvent; -import android.view.ScaleGestureDetector; -import android.widget.OverScroller; +import android.view.Surface; +import android.view.SurfaceView; +import android.view.SurfaceHolder; import org.mozilla.servoview.JNIServo.ServoCoordinates; import org.mozilla.servoview.JNIServo.ServoOptions; @@ -25,41 +23,45 @@ import org.mozilla.servoview.Servo.Client; import org.mozilla.servoview.Servo.GfxCallbacks; import org.mozilla.servoview.Servo.RunCallback; -import javax.microedition.khronos.egl.EGLConfig; -import javax.microedition.khronos.opengles.GL10; +import android.view.Choreographer; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.widget.OverScroller; -public class ServoView extends GLSurfaceView - implements - GestureDetector.OnGestureListener, - ScaleGestureDetector.OnScaleGestureListener, - Choreographer.FrameCallback, - GfxCallbacks, - RunCallback { +import java.util.ArrayList; +public class ServoView extends SurfaceView + implements + GfxCallbacks, + RunCallback, + Choreographer.FrameCallback, + GestureDetector.OnGestureListener, + ScaleGestureDetector.OnScaleGestureListener { private static final String LOGTAG = "ServoView"; - - private Activity mActivity; - private Servo mServo; + private GLThread mGLThread; + private Handler mGLLooperHandler; + private Surface mASurface; + protected Servo mServo = null; private Client mClient = null; - private Uri mInitialUri = null; - private boolean mAnimating; private String mServoArgs; private String mServoLog; - private String mGstDebug; + private String mInitialUri; + private Activity mActivity; private GestureDetector mGestureDetector; - private ScaleGestureDetector mScaleGestureDetector; - - private OverScroller mScroller; private int mLastX = 0; private int mCurX = 0; private int mLastY = 0; private int mCurY = 0; private boolean mFlinging; + private ScaleGestureDetector mScaleGestureDetector; + private OverScroller mScroller; private boolean mZooming; private float mZoomFactor = 1; - private boolean mRedrawing; + private boolean mAnimating; + private boolean mPaused = false; public ServoView(Context context) { super(context); @@ -71,79 +73,49 @@ public class ServoView extends GLSurfaceView init(context); } - public void onDetachedFromWindow() { - mServo.shutdown(); - mServo = null; - super.onDetachedFromWindow(); - } - private void init(Context context) { mActivity = (Activity) context; setFocusable(true); setFocusableInTouchMode(true); + setClickable(true); + ArrayList view = new ArrayList(); + view.add(this); + addTouchables(view); setWillNotCacheDrawing(false); - setEGLContextClientVersion(3); - setEGLConfigChooser(8, 8, 8, 8, 24, 0); - setPreserveEGLContextOnPause(true); - ServoGLRenderer mRenderer = new ServoGLRenderer(this); - setRenderer(mRenderer); - setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); initGestures(context); + + mGLThread = new GLThread(mActivity, this); + getHolder().addCallback(mGLThread); + mGLThread.start(); } - public void setServoArgs(String args, String log, String gstdebug) { + + public void setClient(Client client) { + mClient = client; + } + + public void setServoArgs(String args, String log) { mServoArgs = args; mServoLog = log; - mGstDebug = gstdebug; } - public void reload() { - mServo.reload(); + + // RunCallback + public void inGLThread(Runnable r) { + mGLLooperHandler.post(r); } - public void goBack() { - mServo.goBack(); + public void inUIThread(Runnable r) { + post(r); } - public void goForward() { - mServo.goForward(); - } - - public void stop() { - mServo.stop(); - } - - public void onSurfaceInvalidated(int width, int height) { - if (mServo != null) { - ServoCoordinates coords = new ServoCoordinates(); - coords.width = width; - coords.height = height; - coords.fb_width = width; - coords.fb_height = height; - - mServo.resize(coords); - mServo.refresh(); - } - } - - public void loadUri(Uri uri) { - if (mServo != null) { - mServo.loadUri(uri.toString()); - } else { - mInitialUri = uri; - } - } - - public void mediaSessionAction(int action) { - mServo.mediaSessionAction(action); - } + // GfxCallbacks public void flushGLBuffers() { - requestRender(); } + // Scroll and click - public void animationStateChanged(boolean animating) { if (!mAnimating && animating) { post(() -> startLooping()); @@ -154,48 +126,6 @@ public class ServoView extends GLSurfaceView public void makeCurrent() { } - public void inGLThread(Runnable f) { - queueEvent(f); - } - - public void inUIThread(Runnable f) { - post(f); - } - - public void onGLReady() { - ServoCoordinates coords = new ServoCoordinates(); - coords.width = getWidth(); - coords.height = getHeight(); - coords.fb_width = getWidth(); - coords.fb_height = getHeight(); - - ServoOptions options = new ServoOptions(); - options.args = mServoArgs; - options.coordinates = coords; - options.enableLogs = true; - options.enableSubpixelTextAntialiasing = true; - - DisplayMetrics metrics = new DisplayMetrics(); - mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics); - options.density = metrics.density; - inGLThread(() -> { - String uri = mInitialUri == null ? null : mInitialUri.toString(); - options.url = uri; - options.logStr = mServoLog; - options.gstDebugStr = mGstDebug; - mServo = new Servo(options, this, this, mClient, mActivity); - }); - } - - public void setClient(Client client) { - mClient = client; - } - - private void initGestures(Context context) { - mGestureDetector = new GestureDetector(context, this); - mScaleGestureDetector = new ScaleGestureDetector(context, this); - mScroller = new OverScroller(context); - } private void startLooping() { // In case we were already drawing. @@ -253,6 +183,63 @@ public class ServoView extends GLSurfaceView } } + // Calls from Activity + public void onPause() { + if (mServo != null) { + mServo.suspend(true); + } + } + + public void onResume() { + if (mServo != null) { + mServo.suspend(false); + } + } + + public void reload() { + mServo.reload(); + } + + public void goBack() { + mServo.goBack(); + } + + public void goForward() { + mServo.goForward(); + } + + public void stop() { + mServo.stop(); + } + + public void loadUri(String uri) { + if (mServo != null) { + mServo.loadUri(uri); + } else { + mInitialUri = uri; + } + } + + public void loadUri(Uri uri) { + loadUri(uri.toString()); + } + + public void scrollStart(int dx, int dy, int x, int y) { + mServo.scrollStart(dx, dy, x, y); + } + + public void scroll(int dx, int dy, int x, int y) { + mServo.scroll(dx, dy, x, y); + } + + public void scrollEnd(int dx, int dy, int x, int y) { + mServo.scrollEnd(dx, dy, x, y); + } + + public void click(float x, float y) { + mServo.click(x, y); + } + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { mFlinging = true; @@ -275,20 +262,22 @@ public class ServoView extends GLSurfaceView return true; } + @Override public boolean onTouchEvent(final MotionEvent e) { mGestureDetector.onTouchEvent(e); mScaleGestureDetector.onTouchEvent(e); int action = e.getActionMasked(); + float x = e.getX(); - float y = e.getY(); - + int pointerIndex = e.getActionIndex(); int pointerId = e.getPointerId(pointerIndex); + switch (action) { case (MotionEvent.ACTION_DOWN): - mServo.touchDown(x, y, pointerId); + case (MotionEvent.ACTION_POINTER_DOWN): mFlinging = false; mScroller.forceFinished(true); mCurX = (int) x; @@ -299,32 +288,35 @@ public class ServoView extends GLSurfaceView case (MotionEvent.ACTION_MOVE): mCurX = (int) x; mCurY = (int) y; - mServo.touchMove(x, y, pointerId); return true; case (MotionEvent.ACTION_UP): - mServo.touchUp(x, y, pointerId); + case (MotionEvent.ACTION_POINTER_UP): + return true; case (MotionEvent.ACTION_CANCEL): - mServo.touchCancel(x, y, pointerId); return true; default: return true; } } - public boolean onSingleTapUp(MotionEvent e) { - return false; - } - + // OnGestureListener public void onLongPress(MotionEvent e) { } public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + mServo.scroll((int) -distanceX, (int) -distanceY, (int) e1.getX(), (int) e1.getY()); return true; } + public boolean onSingleTapUp(MotionEvent e) { + click(e.getX(), e.getY()); + return false; + } + public void onShowPress(MotionEvent e) { } + // OnScaleGestureListener @Override public boolean onScaleBegin(ScaleGestureDetector detector) { if (mScroller.isFinished()) { @@ -351,41 +343,81 @@ public class ServoView extends GLSurfaceView mServo.pinchZoomEnd(mZoomFactor, 0, 0); } - - @Override - public void onPause() { - super.onPause(); - if (mServo != null) { - mServo.suspend(true); - } + private void initGestures(Context context) { + mGestureDetector = new GestureDetector(context, this); + mScaleGestureDetector = new ScaleGestureDetector(context, this); + mScroller = new OverScroller(context); } - @Override - public void onResume() { - super.onResume(); - if (mServo != null) { - mServo.suspend(false); - } + public void mediaSessionAction(int action) { + mServo.mediaSessionAction(action); } - static class ServoGLRenderer implements Renderer { - - private final ServoView mView; - - ServoGLRenderer(ServoView view) { - mView = view; + class GLThread extends Thread implements SurfaceHolder.Callback { + private Activity mActivity; + private ServoView mServoView; + GLThread(Activity activity, ServoView servoView) { + mActivity = activity; + mServoView = servoView; } - public void onSurfaceCreated(GL10 unused, EGLConfig config) { - mView.onGLReady(); + public void surfaceCreated(SurfaceHolder holder) { + Log.d(LOGTAG, "GLThread::surfaceCreated"); + + ServoCoordinates coords = new ServoCoordinates(); + coords.width = mServoView.getWidth(); + coords.height = mServoView.getHeight(); + coords.fb_width = mServoView.getWidth(); + coords.fb_height = mServoView.getHeight(); + + Surface surface = holder.getSurface(); + ServoOptions options = new ServoOptions(); + options.args = mServoView.mServoArgs; + options.coordinates = coords; + options.enableLogs = true; + options.enableSubpixelTextAntialiasing = true; + + DisplayMetrics metrics = new DisplayMetrics(); + mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics); + options.density = metrics.density; + if (mServoView.mServo == null && !mPaused) { + mServoView.mServo = new Servo( + options, mServoView, mServoView, mClient, mActivity, surface); + } else { + mPaused = false; + mServoView.mServo.resumeCompositor(surface, coords); + } + } - public void onDrawFrame(GL10 unused) { + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Log.d(LOGTAG, "GLThread::surfaceChanged"); + ServoCoordinates coords = new ServoCoordinates(); + coords.width = width; + coords.height = height; + coords.fb_width = width; + coords.fb_height = height; + + mServoView.mServo.resize(coords); } - public void onSurfaceChanged(GL10 gl, int width, int height) { - GLES31.glViewport(0, 0, width, height); - mView.onSurfaceInvalidated(width, height); + public void surfaceDestroyed(SurfaceHolder holder) { + Log.d(LOGTAG, "GLThread::surfaceDestroyed"); + mPaused = true; + mServoView.mServo.pauseCompositor(); + } + + public void shutdown() { + Log.d(LOGTAG, "GLThread::shutdown"); + mGLLooperHandler.getLooper().quitSafely(); + } + + public void run() { + Looper.prepare(); + + mGLLooperHandler = new Handler(); + + Looper.loop(); } } } diff --git a/support/android/apk/servoview/src/oculusvr/AndroidManifest.xml b/support/android/apk/servoview/src/oculusvr/AndroidManifest.xml deleted file mode 100644 index 076c3dfa688..00000000000 --- a/support/android/apk/servoview/src/oculusvr/AndroidManifest.xml +++ /dev/null @@ -1,10 +0,0 @@ - - -> - - - - - - - diff --git a/support/android/apk/settings.gradle b/support/android/apk/settings.gradle index 3d36551554c..d8826861853 100644 --- a/support/android/apk/settings.gradle +++ b/support/android/apk/settings.gradle @@ -1,3 +1,19 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + include ':servoapp' def userPropertiesFile = new File('user.properties') diff --git a/support/android/fakeld/fake-ld-arm.sh b/support/android/fakeld/fake-ld-arm.sh deleted file mode 100755 index 0e81b85fb14..00000000000 --- a/support/android/fakeld/fake-ld-arm.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - -set -o errexit -set -o nounset -set -o pipefail - -source ./support/android/fakeld/fake-ld.sh - -export _GCC_PARAMS="${@}" -call_gcc "arch-arm" "armeabi" "arm-linux-androideabi" diff --git a/support/android/fakeld/fake-ld-arm64.sh b/support/android/fakeld/fake-ld-arm64.sh deleted file mode 100755 index eb7b0ada9a2..00000000000 --- a/support/android/fakeld/fake-ld-arm64.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - -set -o errexit -set -o nounset -set -o pipefail - -source ./support/android/fakeld/fake-ld.sh - -export _GCC_PARAMS="${@}" -call_gcc "arch-arm64" "arm64-v8a" "aarch64-linux-android" diff --git a/support/android/fakeld/fake-ld-armv7.sh b/support/android/fakeld/fake-ld-armv7.sh deleted file mode 100755 index b1758a9b07a..00000000000 --- a/support/android/fakeld/fake-ld-armv7.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - -set -o errexit -set -o nounset -set -o pipefail - -source ./support/android/fakeld/fake-ld.sh - -export _GCC_PARAMS="${@}" -call_gcc "arch-arm" "armeabi-v7a" "armv7-linux-androideabi" diff --git a/support/android/fakeld/fake-ld-x86.sh b/support/android/fakeld/fake-ld-x86.sh deleted file mode 100755 index 2df1d5e49e9..00000000000 --- a/support/android/fakeld/fake-ld-x86.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - -set -o errexit -set -o nounset -set -o pipefail - -source ./support/android/fakeld/fake-ld.sh - -export _GCC_PARAMS="${@}" -call_gcc "arch-x86" "x86" "i686-linux-android" diff --git a/support/android/fakeld/fake-ld.cmd b/support/android/fakeld/fake-ld.cmd deleted file mode 100644 index b13c7e818ab..00000000000 --- a/support/android/fakeld/fake-ld.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -gcc -mwindows %* diff --git a/support/android/fakeld/fake-ld.sh b/support/android/fakeld/fake-ld.sh deleted file mode 100755 index 29eef1326ba..00000000000 --- a/support/android/fakeld/fake-ld.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash - -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - -set -o errexit -set -o nounset -set -o pipefail - -call_gcc() -{ - TARGET_DIR="${OUT_DIR}/../../.." - - export _ANDROID_ARCH=$1 - export _ANDROID_TARGET=$3 - export ANDROID_SYSROOT="${ANDROID_NDK}/platforms/${ANDROID_PLATFORM}/${_ANDROID_ARCH}" - ANDROID_TOOLCHAIN="" - for host in "linux-x86_64" "linux-x86" "darwin-x86_64" "darwin-x86"; do - if [[ -d "${ANDROID_NDK}/toolchains/llvm/prebuilt/${host}/bin" ]]; then - ANDROID_TOOLCHAIN="${ANDROID_NDK}/toolchains/llvm/prebuilt/${host}/bin" - break - fi - done - - ANDROID_CPU_ARCH_DIR=$2 - ANDROID_CXX_LIBS="${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/${ANDROID_CPU_ARCH_DIR}" - - echo "toolchain: ${ANDROID_TOOLCHAIN}" - echo "libs dir: ${ANDROID_CXX_LIBS}" - echo "sysroot: ${ANDROID_SYSROOT}" - echo "targetdir: ${TARGET_DIR}" - - "${ANDROID_TOOLCHAIN}/clang" \ - --sysroot="${ANDROID_SYSROOT}" \ - --gcc-toolchain="${GCC_TOOLCHAIN}" \ - --target="${_ANDROID_TARGET}" \ - -L "${ANDROID_CXX_LIBS}" ${_GCC_PARAMS} -lc++ -}