diff options
author | Tomaž Vajngerl <tomaz.vajngerl@collabora.com> | 2014-06-26 11:22:21 +0200 |
---|---|---|
committer | Jan Holesovsky <kendy@collabora.com> | 2014-06-30 14:48:03 +0200 |
commit | a6ecd8b2a9f0f3eaa66388861a6dcc6260ec72b9 (patch) | |
tree | 8e2dedeb6bdb4de787962262c38d7af43c6a6b62 /android/experimental | |
parent | 18d2afbf9a544100c2decd99bee0eb5cf3e8f0e3 (diff) |
LOAndroid3: ant/make for building, Bootstrap project
LOAndroid3 is based of LibreOffice4Android project which uses
ant/make for building. By using LibreOffice4Android as the base,
the project creates a APK archive which has all needed files
to start LibreOffice in Android environment.
Change-Id: I697d5f727bdaf93e774144ad597d7081d2609908
Diffstat (limited to 'android/experimental')
88 files changed, 9496 insertions, 0 deletions
diff --git a/android/experimental/LOAndroid3/AndroidManifest.xml b/android/experimental/LOAndroid3/AndroidManifest.xml new file mode 100644 index 000000000000..81e88226f0c5 --- /dev/null +++ b/android/experimental/LOAndroid3/AndroidManifest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.libreoffice" + android:installLocation="preferExternal" + android:versionCode="1" + android:versionName="1.0"> + + <!-- App requires OpenGL ES 2.0 --> + <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="11"/> + + <uses-feature android:glEsVersion="0x00020000" android:required="true" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + + <application + android:allowBackup="true" + android:icon="@drawable/main" + android:label="@string/app_name" + android:hardwareAccelerated="true" + android:theme="@style/AppTheme" > + <activity + android:name="LibreOfficeMainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/android/experimental/LOAndroid3/Makefile b/android/experimental/LOAndroid3/Makefile new file mode 100644 index 000000000000..4db011f13616 --- /dev/null +++ b/android/experimental/LOAndroid3/Makefile @@ -0,0 +1,128 @@ +ifeq ($(BUILDDIR),) +include ../../../config_host.mk +endif + +# The default target just builds. + +all: build-ant + +# The package of this app +APP_PACKAGE=org.libreoffice + +BOOTSTRAPDIR=../../Bootstrap +include $(BOOTSTRAPDIR)/Makefile.shared + +native-code.cxx: $(SRCDIR)/solenv/bin/native-code.py + $< -g core -g writer > $@ + +copy-stuff: +# Then "assets". Let the directory structure under assets mimic +# that under solver for now. +# +# Please note that I have no idea what all of this is really necessary and for +# much of this stuff being copied, no idea whether it makes any sense at all. +# Much of this is copy-pasted from android/qa/sc/Makefile (where a couple of +# unit tests for sc are built, and those do seem to mostly work) and +# android/qa/desktop/Makefile (mmeeks's desktop demo, also works to some +# extent). +# + mkdir -p assets/gz.unpack/program/ure assets/lib assets/program/services assets/ure/share/misc assets/ComponentTarget/i18npool/util + gzip -9 <$(INSTDIR)/$(LIBO_ETC_FOLDER)/types/offapi.rdb >assets/gz.unpack/program/offapi.rdb + gzip -9 <$(INSTDIR)/$(LIBO_ETC_FOLDER)/types/oovbaapi.rdb >assets/gz.unpack/program/oovbaapi.rdb + gzip -9 <$(INSTDIR)/$(LIBO_URE_SHARE_FOLDER)/misc/types.rdb >assets/gz.unpack/program/udkapi.rdb +# For some reason the vnd.sun.star.expand:$LO_LIB_DIR doesn't seem to work, it expands to empty!? + for F in program/services/services ure/share/misc/services; do \ + sed -e 's!uri="vnd.sun.star.expand:$$LO_LIB_DIR/!uri="file://$$APP_DATA_DIR/lib/!g' <$(INSTDIR)/$$F.rdb >assets/$$F.rdb; \ + done + cp $(SRC_ROOT)/odk/examples/java/DocumentHandling/test/test1.odt \ + assets + cp $(WORKDIR)/ComponentTarget/i18npool/util/i18npool.component assets/ComponentTarget/i18npool/util +# + mkdir -p assets/share/config + cp -R $(INSTDIR)/share/registry assets/share + cp -R $(INSTDIR)/share/config/soffice.cfg assets/share/config +# +# Set up rc, the "inifile". See BootstrapMap::getBaseIni(). As this app +# doesn't use soffice_main() (at least I think it shouldn't), the +# rtl::Bootstrap::setIniFilename() call there that hardcodes +# /assets/program/lofficerc isn't executed. Instead the hardcoding of +# /assets/rc in BootstrapMap::getBaseIni() gets used. + echo '[Bootstrap]' > assets/rc + echo 'Logo=1' >> assets/rc + echo 'NativeProgress=1' >> assets/rc + echo 'URE_BOOTSTRAP=file:///assets/program/fundamentalrc' >> assets/rc +# echo 'RTL_LOGFILE=file:///dev/log/main' >> assets/rc + echo 'HOME=$$APP_DATA_DIR/cache' >> assets/rc + echo 'OSL_SOCKET_PATH=$$APP_DATA_DIR/cache' >> assets/rc +# +# Set up fundamentalrc + echo '[Bootstrap]' > assets/program/fundamentalrc + echo 'LO_LIB_DIR=file://$$APP_DATA_DIR/lib/' >> assets/program/fundamentalrc + echo 'URE_LIB_DIR=file://$$APP_DATA_DIR/lib/' >> assets/program/fundamentalrc # checkme - is this used to find configs ? + echo 'BRAND_BASE_DIR=file:///assets' >> assets/program/fundamentalrc + echo 'CONFIGURATION_LAYERS=xcsxcu:$${BRAND_BASE_DIR}/share/registry res:$${BRAND_BASE_DIR}/share/registry' >> assets/program/fundamentalrc + echo 'URE_BIN_DIR=file:///assets/ure/bin/dir/nothing-here/we-can/exec-anyway' >> assets/program/fundamentalrc +# +# Set up unorc + echo '[Bootstrap]' > assets/program/unorc + echo 'URE_INTERNAL_LIB_DIR=file://$$APP_DATA_DIR/lib/' >> assets/program/unorc + echo 'UNO_TYPES=file://$$APP_DATA_DIR/program/udkapi.rdb file://$$APP_DATA_DIR/program/offapi.rdb file://$$APP_DATA_DIR/program/oovbaapi.rdb' >> assets/program/unorc + echo 'UNO_SERVICES=file:///assets/ure/share/misc/services.rdb file:///assets/program/services/services.rdb' >> assets/program/unorc +# +# Set up bootstraprc + echo '[Bootstrap]' > assets/program/bootstraprc + echo 'InstallMode=<installmode>' >> assets/program/bootstraprc + echo 'ProductKey=LibreOffice $(LIBO_VERSION_MAJOR).$(LIBO_VERSION_MINOR)' >> assets/program/bootstraprc + echo 'UserInstallation=file://$$APP_DATA_DIR' >> assets/program/bootstraprc +# +# Set up versionrc + echo '[Version]' > assets/program/versionrc + echo 'AllLanguages=en-US' >> assets/program/versionrc + echo 'BuildVersion=' >> assets/program/versionrc + echo 'buildid=dead-beef' >> assets/program/versionrc + echo 'ProductMajor=360' >> assets/program/versionrc + echo 'ProductMinor=1' >> assets/program/versionrc + echo 'ReferenceOOoMajorMinor=3.6' >> assets/program/versionrc +# +# .res files + mkdir -p assets/program/resource + cp $(INSTDIR)/$(LIBO_SHARE_RESOURCE_FOLDER)/*en-US.res assets/program/resource +# +# Assets that are unpacked at run-time into the app's data directory. These +# are files read by non-LO code, fontconfig and freetype for now, that doesn't +# understand "/assets" paths. + mkdir -p assets/unpack/etc/fonts + cp fonts.conf assets/unpack/etc/fonts +# $UserInstallation/user/fonts is added to the fontconfig path in +# vcl/generic/fontmanager/helper.cxx: psp::getFontPath(). UserInstallation is +# set to the app's data dir above. + mkdir -p assets/gz.unpack/user/fonts + for F in $(INSTDIR)/share/fonts/truetype/Liberation*.ttf $(INSTDIR)/share/fonts/truetype/Gen*.ttf $(INSTDIR)/share/fonts/truetype/opens___.ttf; do \ + gzip -9 <$$F >assets/gz.unpack/user/fonts/`basename $$F`; \ + done +# +# Then gdbserver and gdb.setup so that we can debug with ndk-gdb. +# + mkdir -p $(SODEST) + cp $(ANDROID_NDK_GDBSERVER) $(SODEST) + echo set solib-search-path ./obj/local/$(ANDROID_APP_ABI) >$(SODEST)/gdb.setup + +build-ant: android_version_setup copy-stuff link-so properties +# +# Copy jar files we need +# + for F in java_uno \ + juh \ + jurt \ + ridl \ + unoloader; do \ + $(call COPYJAR,$(INSTDIR)/$(LIBO_URE_SHARE_JAVA_FOLDER)/$${F}.jar); \ + done + for F in unoil; do \ + $(call COPYJAR,$(INSTDIR)/$(LIBO_SHARE_JAVA_FOLDER)/$${F}.jar); \ + done +# + unset JAVA_HOME && $(ANT) -quiet debug + +run: + adb shell am start -n $(APP_PACKAGE)/.ui.LibreOfficeUIActivity -e input /assets/test1.odt diff --git a/android/experimental/LOAndroid3/build.xml b/android/experimental/LOAndroid3/build.xml new file mode 100644 index 000000000000..52d06eede74c --- /dev/null +++ b/android/experimental/LOAndroid3/build.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project name="LibreOfficeViewer" default="help"> + + <!-- The local.properties file is created and updated by the 'android' tool. + It contains the path to the SDK. It should *NOT* be checked into + Version Control Systems. --> + <loadproperties srcFile="local.properties" /> + + <!-- The ant.properties file can be created by you. It is only edited by the + 'android' tool to add properties to it. + This is the place to change some Ant specific build properties. + Here are some properties you may want to change/update: + + source.dir + The name of the source directory. Default is 'src'. + out.dir + The name of the output directory. Default is 'bin'. + + For other overridable properties, look at the beginning of the rules + files in the SDK, at tools/ant/build.xml + + Properties related to the SDK location or the project target should + be updated using the 'android' tool with the 'update' action. + + This file is an integral part of the build system for your + application and should be checked into Version Control Systems. + + --> + <property file="ant.properties" /> + + <!-- The project.properties file is created and updated by the 'android' + tool, as well as ADT. + + This contains project specific properties such as project target, and library + dependencies. Lower level build properties are stored in ant.properties + (or in .classpath for Eclipse projects). + + This file is an integral part of the build system for your + application and should be checked into Version Control Systems. --> + <loadproperties srcFile="project.properties" /> + + <!-- quick check on sdk.dir --> + <fail + message="sdk.dir is missing. Make sure to generate local.properties using 'android update project'" + unless="sdk.dir" + /> + + +<!-- extension targets. Uncomment the ones where you want to do custom work + in between standard targets --> +<!-- + <target name="-pre-build"> + </target> + <target name="-pre-compile"> + </target> + + /* This is typically used for code obfuscation. + Compiled code location: ${out.classes.absolute.dir} + If this is not done in place, override ${out.dex.input.absolute.dir} */ + <target name="-post-compile"> + </target> +--> + + <!-- Import the actual build file. + + To customize existing targets, there are two options: + - Customize only one target: + - copy/paste the target into this file, *before* the + <import> task. + - customize it to your needs. + - Customize the whole content of build.xml + - copy/paste the content of the rules files (minus the top node) + into this file, replacing the <import> task. + - customize to your needs. + + *********************** + ****** IMPORTANT ****** + *********************** + In all cases you must update the value of version-tag below to read 'custom' instead of an integer, + in order to avoid having your file be overridden by tools such as "android update project" + --> + <!-- version-tag: 1 --> + <import file="${android.library.reference.1}/no-resource-compress.xml" /> +</project> diff --git a/android/experimental/LOAndroid3/dummies.cxx b/android/experimental/LOAndroid3/dummies.cxx new file mode 100644 index 000000000000..5607ecfd72d6 --- /dev/null +++ b/android/experimental/LOAndroid3/dummies.cxx @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * 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 http://mozilla.org/MPL/2.0/. + */ + +// Dummy implementations of the callback functions in the UI layer +// that the LO layer calls. As this experimental Android app doesn't +// handle any of that, these do nothing. + +#include <android/log.h> + +#include <touch/touch.h> + +extern "C" +__attribute__ ((visibility("default"))) +void +touch_ui_selection_start(MLOSelectionKind kind, + const void *documentHandle, + MLORect *rectangles, + int rectangleCount, + void *preview) +{ +} + +extern "C" +__attribute__ ((visibility("default"))) +void +touch_ui_selection_resize_done(bool success, + const void *documentHandle, + MLORect *rectangles, + int rectangleCount) +{ +} + +extern "C" +__attribute__ ((visibility("default"))) +void +touch_ui_selection_none() +{ +} + + +static const char * +dialog_kind_to_string(MLODialogKind kind) +{ + switch (kind) { + case MLODialogMessage: + return "MSG"; + case MLODialogInformation: + return "INF"; + case MLODialogWarning: + return "WRN"; + case MLODialogError: + return "ERR"; + case MLODialogQuery: + return "QRY"; + default: + return "WTF"; + } +} + +extern "C" +__attribute__ ((visibility("default"))) +MLODialogResult +touch_ui_dialog_modal(MLODialogKind kind, const char *message) +{ + __android_log_print(ANDROID_LOG_INFO, "===> %s: %s", dialog_kind_to_string(kind), message); + return MLODialogOK; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/android/experimental/LOAndroid3/fonts.conf b/android/experimental/LOAndroid3/fonts.conf new file mode 100644 index 000000000000..263648a0aa87 --- /dev/null +++ b/android/experimental/LOAndroid3/fonts.conf @@ -0,0 +1,154 @@ +<?xml version="1.0"?> +<!DOCTYPE fontconfig SYSTEM "fonts.dtd"> +<!-- /etc/fonts/fonts.conf file to configure system font access --> +<fontconfig> + +<!-- Font directory list --> + + <dir>/system/fonts</dir> + + <alias> + <family>serif</family> + <prefer> + <family>Droid Serif</family> + </prefer> + </alias> + <alias> + <family>sans-serif</family> + <prefer> + <family>Roboto</family> + <family>Droid Sans Fallback</family> + </prefer> + </alias> + <alias> + <family>monospace</family> + <prefer> + <family>Droid Sans Mono</family> + </prefer> + </alias> + +<!-- + Accept deprecated 'mono' alias, replacing it with 'monospace' +--> + <match target="pattern"> + <test qual="any" name="family"> + <string>mono</string> + </test> + <edit name="family" mode="assign"> + <string>monospace</string> + </edit> + </match> + +<!-- + Accept alternate 'sans serif' spelling, replacing it with 'sans-serif' +--> + <match target="pattern"> + <test qual="any" name="family"> + <string>sans serif</string> + </test> + <edit name="family" mode="assign"> + <string>sans-serif</string> + </edit> + </match> + +<!-- + Accept deprecated 'sans' alias, replacing it with 'sans-serif' +--> + <match target="pattern"> + <test qual="any" name="family"> + <string>sans</string> + </test> + <edit name="family" mode="assign"> + <string>sans-serif</string> + </edit> + </match> + +<!-- + Load local system customization file +--> + <include ignore_missing="yes">conf.d</include> + +<!-- Font cache directory list --> + + <!-- Yeah this hardcoding is wrong of course, will have to fix + later to patch in proper code in fontonfig on Android to + find out a good place. + --> + <cachedir>/data/data/org.libreoffice/fontconfig</cachedir> + + <config> +<!-- + These are the default Unicode chars that are expected to be blank + in fonts. All other blank chars are assumed to be broken and + won't appear in the resulting charsets + --> + <blank> + <int>0x0020</int> <!-- SPACE --> + <int>0x00A0</int> <!-- NO-BREAK SPACE --> + <int>0x00AD</int> <!-- SOFT HYPHEN --> + <int>0x034F</int> <!-- COMBINING GRAPHEME JOINER --> + <int>0x0600</int> <!-- ARABIC NUMBER SIGN --> + <int>0x0601</int> <!-- ARABIC SIGN SANAH --> + <int>0x0602</int> <!-- ARABIC FOOTNOTE MARKER --> + <int>0x0603</int> <!-- ARABIC SIGN SAFHA --> + <int>0x06DD</int> <!-- ARABIC END OF AYAH --> + <int>0x070F</int> <!-- SYRIAC ABBREVIATION MARK --> + <int>0x115F</int> <!-- HANGUL CHOSEONG FILLER --> + <int>0x1160</int> <!-- HANGUL JUNGSEONG FILLER --> + <int>0x1680</int> <!-- OGHAM SPACE MARK --> + <int>0x17B4</int> <!-- KHMER VOWEL INHERENT AQ --> + <int>0x17B5</int> <!-- KHMER VOWEL INHERENT AA --> + <int>0x180E</int> <!-- MONGOLIAN VOWEL SEPARATOR --> + <int>0x2000</int> <!-- EN QUAD --> + <int>0x2001</int> <!-- EM QUAD --> + <int>0x2002</int> <!-- EN SPACE --> + <int>0x2003</int> <!-- EM SPACE --> + <int>0x2004</int> <!-- THREE-PER-EM SPACE --> + <int>0x2005</int> <!-- FOUR-PER-EM SPACE --> + <int>0x2006</int> <!-- SIX-PER-EM SPACE --> + <int>0x2007</int> <!-- FIGURE SPACE --> + <int>0x2008</int> <!-- PUNCTUATION SPACE --> + <int>0x2009</int> <!-- THIN SPACE --> + <int>0x200A</int> <!-- HAIR SPACE --> + <int>0x200B</int> <!-- ZERO WIDTH SPACE --> + <int>0x200C</int> <!-- ZERO WIDTH NON-JOINER --> + <int>0x200D</int> <!-- ZERO WIDTH JOINER --> + <int>0x200E</int> <!-- LEFT-TO-RIGHT MARK --> + <int>0x200F</int> <!-- RIGHT-TO-LEFT MARK --> + <int>0x2028</int> <!-- LINE SEPARATOR --> + <int>0x2029</int> <!-- PARAGRAPH SEPARATOR --> + <int>0x202A</int> <!-- LEFT-TO-RIGHT EMBEDDING --> + <int>0x202B</int> <!-- RIGHT-TO-LEFT EMBEDDING --> + <int>0x202C</int> <!-- POP DIRECTIONAL FORMATTING --> + <int>0x202D</int> <!-- LEFT-TO-RIGHT OVERRIDE --> + <int>0x202E</int> <!-- RIGHT-TO-LEFT OVERRIDE --> + <int>0x202F</int> <!-- NARROW NO-BREAK SPACE --> + <int>0x205F</int> <!-- MEDIUM MATHEMATICAL SPACE --> + <int>0x2060</int> <!-- WORD JOINER --> + <int>0x2061</int> <!-- FUNCTION APPLICATION --> + <int>0x2062</int> <!-- INVISIBLE TIMES --> + <int>0x2063</int> <!-- INVISIBLE SEPARATOR --> + <int>0x206A</int> <!-- INHIBIT SYMMETRIC SWAPPING --> + <int>0x206B</int> <!-- ACTIVATE SYMMETRIC SWAPPING --> + <int>0x206C</int> <!-- INHIBIT ARABIC FORM SHAPING --> + <int>0x206D</int> <!-- ACTIVATE ARABIC FORM SHAPING --> + <int>0x206E</int> <!-- NATIONAL DIGIT SHAPES --> + <int>0x206F</int> <!-- NOMINAL DIGIT SHAPES --> + <int>0x2800</int> <!-- BRAILLE PATTERN BLANK --> + <int>0x3000</int> <!-- IDEOGRAPHIC SPACE --> + <int>0x3164</int> <!-- HANGUL FILLER --> + <int>0xFEFF</int> <!-- ZERO WIDTH NO-BREAK SPACE --> + <int>0xFFA0</int> <!-- HALFWIDTH HANGUL FILLER --> + <int>0xFFF9</int> <!-- INTERLINEAR ANNOTATION ANCHOR --> + <int>0xFFFA</int> <!-- INTERLINEAR ANNOTATION SEPARATOR --> + <int>0xFFFB</int> <!-- INTERLINEAR ANNOTATION TERMINATOR --> + </blank> +<!-- + Rescan configuration every 3600 seconds when FcFontSetList is called + --> + <rescan> + <int>3600</int> + </rescan> + </config> + +</fontconfig> diff --git a/android/experimental/LOAndroid3/jni/Android.mk b/android/experimental/LOAndroid3/jni/Android.mk new file mode 100644 index 000000000000..939a1ea503bb --- /dev/null +++ b/android/experimental/LOAndroid3/jni/Android.mk @@ -0,0 +1,8 @@ +# Needed just to satisfy ndk-gdb for now, but maybe later we will actually add +# some JNI code here + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +include $(BUILD_SHARED_LIBRARY) diff --git a/android/experimental/LOAndroid3/proguard-project.txt b/android/experimental/LOAndroid3/proguard-project.txt new file mode 100644 index 000000000000..f2fe1559a217 --- /dev/null +++ b/android/experimental/LOAndroid3/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/android/experimental/LOAndroid3/project.properties b/android/experimental/LOAndroid3/project.properties new file mode 100644 index 000000000000..772d3c57623c --- /dev/null +++ b/android/experimental/LOAndroid3/project.properties @@ -0,0 +1,14 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "ant.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=android-15 + +# Use the Bootstrap class +android.library.reference.1=../../Bootstrap diff --git a/android/experimental/LOAndroid3/res/drawable-hdpi/base.png b/android/experimental/LOAndroid3/res/drawable-hdpi/base.png Binary files differnew file mode 100644 index 000000000000..729dbcd82ebf --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-hdpi/base.png diff --git a/android/experimental/LOAndroid3/res/drawable-hdpi/calc.png b/android/experimental/LOAndroid3/res/drawable-hdpi/calc.png Binary files differnew file mode 100644 index 000000000000..a3f5fd4d80c0 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-hdpi/calc.png diff --git a/android/experimental/LOAndroid3/res/drawable-hdpi/draw.png b/android/experimental/LOAndroid3/res/drawable-hdpi/draw.png Binary files differnew file mode 100644 index 000000000000..b3ee11426a04 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-hdpi/draw.png diff --git a/android/experimental/LOAndroid3/res/drawable-hdpi/dummy_page.png b/android/experimental/LOAndroid3/res/drawable-hdpi/dummy_page.png Binary files differnew file mode 100644 index 000000000000..c58d276e7085 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-hdpi/dummy_page.png diff --git a/android/experimental/LOAndroid3/res/drawable-hdpi/ic_launcher.png b/android/experimental/LOAndroid3/res/drawable-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..96a442e5b8e9 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-hdpi/ic_launcher.png diff --git a/android/experimental/LOAndroid3/res/drawable-hdpi/ic_status_logo.png b/android/experimental/LOAndroid3/res/drawable-hdpi/ic_status_logo.png Binary files differnew file mode 100644 index 000000000000..d5f16694f342 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-hdpi/ic_status_logo.png diff --git a/android/experimental/LOAndroid3/res/drawable-hdpi/impress.png b/android/experimental/LOAndroid3/res/drawable-hdpi/impress.png Binary files differnew file mode 100644 index 000000000000..5909f05bf089 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-hdpi/impress.png diff --git a/android/experimental/LOAndroid3/res/drawable-hdpi/lo_icon.png b/android/experimental/LOAndroid3/res/drawable-hdpi/lo_icon.png Binary files differnew file mode 100644 index 000000000000..2ef86417e69e --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-hdpi/lo_icon.png diff --git a/android/experimental/LOAndroid3/res/drawable-hdpi/main.png b/android/experimental/LOAndroid3/res/drawable-hdpi/main.png Binary files differnew file mode 100644 index 000000000000..7e8e2a05e2da --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-hdpi/main.png diff --git a/android/experimental/LOAndroid3/res/drawable-hdpi/math.png b/android/experimental/LOAndroid3/res/drawable-hdpi/math.png Binary files differnew file mode 100644 index 000000000000..50b8dc863bff --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-hdpi/math.png diff --git a/android/experimental/LOAndroid3/res/drawable-hdpi/startcenter.png b/android/experimental/LOAndroid3/res/drawable-hdpi/startcenter.png Binary files differnew file mode 100644 index 000000000000..7e8e2a05e2da --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-hdpi/startcenter.png diff --git a/android/experimental/LOAndroid3/res/drawable-mdpi/background.png b/android/experimental/LOAndroid3/res/drawable-mdpi/background.png Binary files differnew file mode 100644 index 000000000000..611592b5167a --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-mdpi/background.png diff --git a/android/experimental/LOAndroid3/res/drawable-mdpi/base.png b/android/experimental/LOAndroid3/res/drawable-mdpi/base.png Binary files differnew file mode 100644 index 000000000000..729dbcd82ebf --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-mdpi/base.png diff --git a/android/experimental/LOAndroid3/res/drawable-mdpi/calc.png b/android/experimental/LOAndroid3/res/drawable-mdpi/calc.png Binary files differnew file mode 100644 index 000000000000..a3f5fd4d80c0 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-mdpi/calc.png diff --git a/android/experimental/LOAndroid3/res/drawable-mdpi/docu.png b/android/experimental/LOAndroid3/res/drawable-mdpi/docu.png Binary files differnew file mode 100644 index 000000000000..ab34ae5638e1 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-mdpi/docu.png diff --git a/android/experimental/LOAndroid3/res/drawable-mdpi/draw.png b/android/experimental/LOAndroid3/res/drawable-mdpi/draw.png Binary files differnew file mode 100644 index 000000000000..b3ee11426a04 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-mdpi/draw.png diff --git a/android/experimental/LOAndroid3/res/drawable-mdpi/ic_launcher.png b/android/experimental/LOAndroid3/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..359047dfa4ed --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-mdpi/ic_launcher.png diff --git a/android/experimental/LOAndroid3/res/drawable-mdpi/ic_status_logo.png b/android/experimental/LOAndroid3/res/drawable-mdpi/ic_status_logo.png Binary files differnew file mode 100644 index 000000000000..835fc9290727 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-mdpi/ic_status_logo.png diff --git a/android/experimental/LOAndroid3/res/drawable-mdpi/impress.png b/android/experimental/LOAndroid3/res/drawable-mdpi/impress.png Binary files differnew file mode 100644 index 000000000000..5909f05bf089 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-mdpi/impress.png diff --git a/android/experimental/LOAndroid3/res/drawable-mdpi/lo_icon.png b/android/experimental/LOAndroid3/res/drawable-mdpi/lo_icon.png Binary files differnew file mode 100644 index 000000000000..4f3f89beadc2 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-mdpi/lo_icon.png diff --git a/android/experimental/LOAndroid3/res/drawable-mdpi/shadow.png b/android/experimental/LOAndroid3/res/drawable-mdpi/shadow.png Binary files differnew file mode 100644 index 000000000000..3ce69155c6b5 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-mdpi/shadow.png diff --git a/android/experimental/LOAndroid3/res/drawable-mdpi/writer.png b/android/experimental/LOAndroid3/res/drawable-mdpi/writer.png Binary files differnew file mode 100644 index 000000000000..2f4abcb280cd --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-mdpi/writer.png diff --git a/android/experimental/LOAndroid3/res/drawable-xhdpi/base.png b/android/experimental/LOAndroid3/res/drawable-xhdpi/base.png Binary files differnew file mode 100644 index 000000000000..729dbcd82ebf --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-xhdpi/base.png diff --git a/android/experimental/LOAndroid3/res/drawable-xhdpi/calc.png b/android/experimental/LOAndroid3/res/drawable-xhdpi/calc.png Binary files differnew file mode 100644 index 000000000000..a3f5fd4d80c0 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-xhdpi/calc.png diff --git a/android/experimental/LOAndroid3/res/drawable-xhdpi/draw.png b/android/experimental/LOAndroid3/res/drawable-xhdpi/draw.png Binary files differnew file mode 100644 index 000000000000..b3ee11426a04 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-xhdpi/draw.png diff --git a/android/experimental/LOAndroid3/res/drawable-xhdpi/ic_launcher.png b/android/experimental/LOAndroid3/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..71c6d760f051 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-xhdpi/ic_launcher.png diff --git a/android/experimental/LOAndroid3/res/drawable-xhdpi/ic_status_logo.png b/android/experimental/LOAndroid3/res/drawable-xhdpi/ic_status_logo.png Binary files differnew file mode 100644 index 000000000000..c8005425416a --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-xhdpi/ic_status_logo.png diff --git a/android/experimental/LOAndroid3/res/drawable-xhdpi/impress.png b/android/experimental/LOAndroid3/res/drawable-xhdpi/impress.png Binary files differnew file mode 100644 index 000000000000..5909f05bf089 --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-xhdpi/impress.png diff --git a/android/experimental/LOAndroid3/res/drawable-xhdpi/writer.png b/android/experimental/LOAndroid3/res/drawable-xhdpi/writer.png Binary files differnew file mode 100644 index 000000000000..2f4abcb280cd --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-xhdpi/writer.png diff --git a/android/experimental/LOAndroid3/res/drawable-xxhdpi/ic_launcher.png b/android/experimental/LOAndroid3/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..4df18946442e --- /dev/null +++ b/android/experimental/LOAndroid3/res/drawable-xxhdpi/ic_launcher.png diff --git a/android/experimental/LOAndroid3/res/layout/activity_main.xml b/android/experimental/LOAndroid3/res/layout/activity_main.xml new file mode 100644 index 000000000000..7b53d58a2b5c --- /dev/null +++ b/android/experimental/LOAndroid3/res/layout/activity_main.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/main_layout" + android:background="#fff" + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + + <RelativeLayout + android:id="@+id/gecko_layout" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:layout_weight="1"/> + +</LinearLayout>
\ No newline at end of file diff --git a/android/experimental/LOAndroid3/res/menu/main.xml b/android/experimental/LOAndroid3/res/menu/main.xml new file mode 100644 index 000000000000..e9709c90d729 --- /dev/null +++ b/android/experimental/LOAndroid3/res/menu/main.xml @@ -0,0 +1,8 @@ +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + tools:context="org.libreoffice.MainActivity" > + <item android:id="@+id/action_settings" + android:title="@string/action_settings" + android:orderInCategory="100" /> +</menu> diff --git a/android/experimental/LOAndroid3/res/values/colors.xml b/android/experimental/LOAndroid3/res/values/colors.xml new file mode 100644 index 000000000000..f8e207d4e00e --- /dev/null +++ b/android/experimental/LOAndroid3/res/values/colors.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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 http://mozilla.org/MPL/2.0/. --> + +<resources> + <color name="background_light">#FFECF0F3</color> + <color name="background_normal">#FFCED7DE</color> + <color name="background_private">#FF292C29</color> + <color name="background_tabs">#FF363B40</color> + <color name="highlight">#33000000</color> + <color name="highlight_focused">#1A000000</color> + <color name="highlight_dark">#33FFFFFF</color> + <color name="highlight_dark_focused">#1AFFFFFF</color> + + <!-- highlight on shaped button: 20% white over background_tabs --> + <color name="highlight_shaped">#FF696D71</color> + + <!-- highlight-focused on shaped button: 10% white over background_tabs --> + <color name="highlight_shaped_focused">#FF565B60</color> + + <!-- highlight on nav button: 20% black over background_normal --> + <color name="highlight_nav">#FFA5ACB2</color> + + <!-- highlight-focused on nav button: 10% black over background_normal --> + <color name="highlight_nav_focused">#FFB9C1C7</color> + + <!-- highlight on private nav button: 20% white over background_private --> + <color name="highlight_nav_pb">#FF545654</color> + + <!-- highlight-focused on private nav button: 10% white over background_private --> + <color name="highlight_nav_focused_pb">#FF3F423F</color> + + <!-- + Application theme colors + --> + <!-- Default colors --> + <color name="text_color_primary">#222222</color> + <color name="text_color_secondary">#777777</color> + <color name="text_color_tertiary">#9198A1</color> + + <!-- Default inverse colors --> + <color name="text_color_primary_inverse">#FFFFFF</color> + <color name="text_color_secondary_inverse">#DDDDDD</color> + <color name="text_color_tertiary_inverse">#A4A7A9</color> + + <!-- Disabled colors --> + <color name="text_color_primary_disable_only">#999999</color> + + <!-- Hint colors --> + <color name="text_color_hint">#666666</color> + <color name="text_color_hint_inverse">#7F828A</color> + + <!-- Highlight colors --> + <color name="text_color_highlight">#FF9500</color> + <color name="text_color_highlight_inverse">#D06BFF</color> + + <!-- Link colors --> + <color name="text_color_link">#22629E</color> + + <color name="splash_background">#000000</color> + <color name="splash_msgfont">#ffffff</color> + <color name="splash_urlfont">#000000</color> + <color name="splash_content">#ffffff</color> + + <color name="doorhanger_text">#FF222222</color> + <color name="doorhanger_link">#FF2AA1FE</color> + <color name="doorhanger_divider_light">#FFD1D5DA</color> + <color name="doorhanger_divider_dark">#FFB3C2CE</color> + <color name="doorhanger_background_dark">#FFDDE4EA</color> + + <color name="validation_message_text">#ffffff</color> + <color name="url_bar_text_highlight">#FFFF9500</color> + <color name="url_bar_text_highlight_pb">#FFD06BFF</color> + <color name="suggestion_primary">#dddddd</color> + <color name="suggestion_pressed">#bbbbbb</color> + <color name="tab_row_pressed">#4D000000</color> + <color name="dialogtitle_textcolor">#ffffff</color> + + <color name="textbox_background">#FFF</color> + <color name="textbox_background_disabled">#DDD</color> + <color name="textbox_stroke">#000</color> + <color name="textbox_stroke_disabled">#666</color> + + <color name="url_bar_urltext">#A6A6A6</color> + <color name="url_bar_domaintext">#000</color> + <color name="url_bar_domaintext_private">#FFF</color> + <color name="url_bar_blockedtext">#b14646</color> + <color name="url_bar_shadow">#12000000</color> + + <color name="home_last_tab_bar_bg">#FFF5F7F9</color> + + <color name="panel_grid_item_image_background">#D1D9E1</color> +</resources> + diff --git a/android/experimental/LOAndroid3/res/values/dimens.xml b/android/experimental/LOAndroid3/res/values/dimens.xml new file mode 100644 index 000000000000..47c82246738c --- /dev/null +++ b/android/experimental/LOAndroid3/res/values/dimens.xml @@ -0,0 +1,5 @@ +<resources> + <!-- Default screen margins, per the Android Design guidelines. --> + <dimen name="activity_horizontal_margin">16dp</dimen> + <dimen name="activity_vertical_margin">16dp</dimen> +</resources> diff --git a/android/experimental/LOAndroid3/res/values/strings.xml b/android/experimental/LOAndroid3/res/values/strings.xml new file mode 100644 index 000000000000..93431ed24788 --- /dev/null +++ b/android/experimental/LOAndroid3/res/values/strings.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">LibreOffice</string> + <string name="action_settings">Settings</string> + +</resources> diff --git a/android/experimental/LOAndroid3/res/values/styles.xml b/android/experimental/LOAndroid3/res/values/styles.xml new file mode 100644 index 000000000000..ff6c9d2c0fb9 --- /dev/null +++ b/android/experimental/LOAndroid3/res/values/styles.xml @@ -0,0 +1,8 @@ +<resources> + + <!-- Base application theme. --> + <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar"> + <!-- Customize your theme here. --> + </style> + +</resources> diff --git a/android/experimental/LOAndroid3/src/java/org/libreoffice/LOEvent.java b/android/experimental/LOAndroid3/src/java/org/libreoffice/LOEvent.java new file mode 100644 index 000000000000..bf4f98b845da --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/libreoffice/LOEvent.java @@ -0,0 +1,67 @@ +package org.libreoffice; + +import android.graphics.Rect; +import android.util.Log; + +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.ViewportMetrics; + +public class LOEvent { + + public static final int SIZE_CHANGED = 1; + public static final int TILE_SIZE = 2; + public static final int VIEWPORT = 3; + public static final int DRAW = 4; + + private ViewportMetrics mViewportMetrics; + + public int mType; + private String mTypeString; + + ViewportMetrics viewportMetrics; + + public LOEvent(int type, int width, int height, int widthPixels, int heightPixels, int tileWidth, int tileHeight) { + mType = type; + mTypeString = "Size Changed"; + } + + public LOEvent(int type, IntSize tileSize) { + mType = type; + mTypeString = "Tile size"; + } + + public LOEvent(int type, ViewportMetrics viewportMetrics) { + mType = type; + mTypeString = "Viewport"; + mViewportMetrics = viewportMetrics; + } + + public LOEvent(int type, Rect rect) { + mType = type; + mTypeString = "Draw"; + } + + public static LOEvent draw(Rect rect) { + return new LOEvent(DRAW, rect); + } + + public static LOEvent sizeChanged(int width, int height, int widthPixels, int heightPixels, int tileWidth, int tileHeight) { + return new LOEvent(SIZE_CHANGED, width, height, widthPixels, heightPixels, tileWidth, tileHeight); + } + + public static LOEvent tileSize(IntSize tileSize) { + return new LOEvent(TILE_SIZE, tileSize); + } + + public static LOEvent viewport(ViewportMetrics viewportMetrics) { + return new LOEvent(VIEWPORT, viewportMetrics); + } + + public String getTypeString() { + return mTypeString; + } + + public ViewportMetrics getViewport() { + return mViewportMetrics; + } +} diff --git a/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitShell.java b/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitShell.java new file mode 100644 index 000000000000..9dde7906d020 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitShell.java @@ -0,0 +1,131 @@ +package org.libreoffice; + + +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; + +import org.mozilla.gecko.gfx.GeckoSoftwareLayerClient; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.gfx.LayerView; + +import java.nio.ByteBuffer; + +public class LOKitShell { + private static final String LOGTAG = LOKitShell.class.getSimpleName(); + + public static int getDpi() { + return 96; + } + + public static int getScreenDepth() { + return 24; + } + + public static float computeRenderIntegrity() { + return 0.0f; + } + + public static ByteBuffer allocateDirectBuffer(int size) { + if (size <= 0) { + throw new IllegalArgumentException("Invalid size " + size); + } + + ByteBuffer directBuffer = ByteBuffer.allocateDirect(size); + //ByteBuffer directBuffer = nativeAllocateDirectBuffer(size); + if (directBuffer == null) { + throw new OutOfMemoryError("allocateDirectBuffer() returned null"); + } else if (!directBuffer.isDirect()) { + throw new AssertionError("allocateDirectBuffer() did not return a direct buffer"); + } + + return directBuffer; + } + + + public static void freeDirectBuffer(ByteBuffer buffer) { + if (buffer == null) { + return; + } + + if (!buffer.isDirect()) { + throw new IllegalArgumentException("buffer must be direct"); + } + //nativeFreeDirectBuffer(buffer); + return ; + } + + public static void bindWidgetTexture() { + } + + public static void sendEvent(LOEvent event) { + Log.i(LOGTAG, "Event: " + event.getTypeString()); + } + + public static void runGecko(String apkPath, String args, String url, boolean restoreSession) { + // run gecko -- it will spawn its own thread + // GeckoAppShell.nativeInit(); + + Log.i(LOGTAG, "post native init"); + + // Tell Gecko where the target byte buffer is for rendering + //GeckoAppShell.setSoftwareLayerClient(GeckoApp.mAppContext.getSoftwareLayerClient()); + + Log.i(LOGTAG, "setSoftwareLayerClient called"); + + // First argument is the .apk path + String combinedArgs = apkPath + " -greomni " + apkPath; + if (args != null) + combinedArgs += " " + args; + if (url != null) + combinedArgs += " -remote " + url; + if (restoreSession) + combinedArgs += " -restoresession"; + + DisplayMetrics metrics = new DisplayMetrics(); + LibreOfficeMainActivity.mAppContext.getWindowManager().getDefaultDisplay().getMetrics(metrics); + combinedArgs += " -width " + metrics.widthPixels + " -height " + metrics.heightPixels; + + LibreOfficeMainActivity.mAppContext.runOnUiThread(new Runnable() { + public void run() { + geckoLoaded(); + } + }); + + //LOKitShell.nativeRun(combinedArgs); + } + + // Called on the UI thread after Gecko loads. + private static void geckoLoaded() { + /*final LayerController layerController = LibreOfficeMainActivity.mAppContext.getLayerController(); + LayerView v = layerController.getView(); + mInputConnection = GeckoInputConnection.create(v); + v.setInputConnectionHandler(mInputConnection); + + layerController.setOnTouchListener(new View.OnTouchListener() { + public boolean onTouch(View view, MotionEvent event) { + if (event == null) + return true; + GeckoAppShell.sendEventToGecko(new GeckoEvent(event)); + return true; + } + }); + + layerController.notifyLayerClientOfGeometryChange();*/ + } + + public static void viewSizeChanged() { + } + + public static void scheduleComposite() { + } + + public static void schedulePauseComposition() { + } + + public static void scheduleResumeComposition() { + + } +} diff --git a/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java b/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java new file mode 100644 index 000000000000..ea3472b8c52c --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java @@ -0,0 +1,135 @@ +package org.libreoffice; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.JsonWriter; + +import org.mozilla.gecko.gfx.ViewportMetrics; + +import java.io.IOException; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.util.Random; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class LOKitThread extends Thread { + private static final String LOGTAG = "GeckoThread"; + + public ConcurrentLinkedQueue<LOEvent> gEvents = new ConcurrentLinkedQueue<LOEvent>(); + private ViewportMetrics mViewportMetrics; + private Random rand = new Random(); + + LOKitThread() { + } + + private boolean draw() throws InterruptedException { + final LibreOfficeMainActivity application = LibreOfficeMainActivity.mAppContext; + + Bitmap bitmap = application.getLayerClient().getLayerController().getDrawable("docu"); + //bitmap = convert(bitmap, Bitmap.Config.ARGB_8888); + + StringWriter stringWriter = new StringWriter(); + + try { + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginObject(); + if (mViewportMetrics == null) { + writer.name("x").value(0); + writer.name("y").value(0); + writer.name("width").value(bitmap.getWidth()); + writer.name("height").value(bitmap.getHeight()); + writer.name("pageWidth").value(bitmap.getWidth()); + writer.name("pageHeight").value(bitmap.getHeight()); + writer.name("offsetX").value(0); + writer.name("offsetY").value(0); + writer.name("zoom").value(0.5); + } else { + writer.name("x").value(mViewportMetrics.getOrigin().x); + writer.name("y").value(mViewportMetrics.getOrigin().y); + writer.name("width").value(mViewportMetrics.getSize().width); + writer.name("height").value(mViewportMetrics.getSize().height); + writer.name("pageWidth").value(mViewportMetrics.getPageSize().width); + writer.name("pageHeight").value(mViewportMetrics.getPageSize().height); + writer.name("offsetX").value(mViewportMetrics.getViewportOffset().x); + writer.name("offsetY").value(mViewportMetrics.getViewportOffset().y); + writer.name("zoom").value(mViewportMetrics.getZoomFactor()); + } + writer.name("backgroundColor").value("rgb(255,255,255)"); + writer.endObject(); + writer.close(); + } catch (IOException ex) { + } + + Rect bufferRect = application.getLayerClient().beginDrawing(bitmap.getWidth(), bitmap.getHeight(), 256, 256, stringWriter.toString(), false); + + if (bufferRect == null) { + return false; + } + ByteBuffer buffer = application.getLayerClient().lockBuffer(); + bitmap.copyPixelsToBuffer(buffer.asIntBuffer()); + application.getLayerClient().unlockBuffer(); + + application.getLayerClient().endDrawing(0, 0, bitmap.getWidth(), bitmap.getHeight()); + + application.runOnUiThread(new Runnable() { + @Override + public void run() { + application.getLayerClient().handleMessage("Viewport:UpdateLater", null); + } + }); + return true; + } + + private short convertTo16Bit(int color) { + int r = Color.red(color) >> 3, g = Color.green(color) >> 2, b = Color.blue(color) >> 3; + int c = ((r << 11) | (g << 5) | b); + // Swap endianness. + return (short) ((c >> 8) | ((c & 0xff) << 8)); + } + + private Bitmap convert(Bitmap bitmap, Bitmap.Config config) { + Bitmap convertedBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), config); + Canvas canvas = new Canvas(convertedBitmap); + Paint paint = new Paint(); + paint.setColor(Color.BLACK); + canvas.drawBitmap(bitmap, 0, 0, paint); + return convertedBitmap; + } + + + public void run() { + try { + boolean drawn = false; + while (true) { + + if (!gEvents.isEmpty()) { + processEvent(gEvents.poll()); + } else { + if (!drawn) { + drawn = draw(); + } + Thread.sleep(100L); + } + } + } catch (InterruptedException ex) { + } + } + + private void processEvent(LOEvent event) throws InterruptedException { + switch (event.mType) { + case LOEvent.VIEWPORT: + mViewportMetrics = event.getViewport(); + break; + case LOEvent.DRAW: + draw(); + break; + case LOEvent.SIZE_CHANGED: + break; + } + } + + +} diff --git a/android/experimental/LOAndroid3/src/java/org/libreoffice/LibreOfficeMainActivity.java b/android/experimental/LOAndroid3/src/java/org/libreoffice/LibreOfficeMainActivity.java new file mode 100644 index 000000000000..d4758443bf13 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/libreoffice/LibreOfficeMainActivity.java @@ -0,0 +1,145 @@ +package org.libreoffice; + +import android.app.Activity; +import android.os.Bundle; +import android.os.SystemClock; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + +import org.mozilla.gecko.gfx.GeckoSoftwareLayerClient; +import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.gfx.LayerView; + +import org.libreoffice.android.Bootstrap; + +import com.sun.star.frame.XComponentLoader; +import com.sun.star.lang.XMultiComponentFactory; +import com.sun.star.uno.XComponentContext; +import com.sun.star.uno.UnoRuntime; + +public class LibreOfficeMainActivity extends Activity { + + private static final String LOGTAG = "LibreOfficeMainActivity"; + + private LinearLayout mMainLayout; + private RelativeLayout mGeckoLayout; + private static LayerController mLayerController; + private static GeckoSoftwareLayerClient mLayerClient; + private static LOKitThread sLOKitThread; + + private XComponentContext context; + private XMultiComponentFactory mcf; + private XComponentLoader componentLoader; + + public static LibreOfficeMainActivity mAppContext; + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + if (id == R.id.action_settings) { + return true; + } + return super.onOptionsItemSelected(item); + } + + public DisplayMetrics getDisplayMetrics() { + DisplayMetrics metrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(metrics); + return metrics; + } + + /** + * Called when the activity is first created. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + mAppContext = this; + + super.onCreate(savedInstanceState); + + try { + Bootstrap.setup(this); + Bootstrap.putenv("SAL_LOG=+WARN+INFO-INFO.legacy.osl"); + + setContentView(R.layout.activity_main); + + Log.w(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - onCreate"); + + String input = "/assets/test1.odt"; + + String[] argv = { "lo-document-loader", input }; + + Bootstrap.setCommandArgs(argv); + + Bootstrap.initVCL(); + + context = com.sun.star.comp.helper.Bootstrap.defaultBootstrap_InitialComponentContext(); + + Log.i(LOGTAG, "context is" + (context!=null ? " not" : "") + " null"); + + mcf = context.getServiceManager(); + + Log.i(LOGTAG, "mcf is" + (mcf!=null ? " not" : "") + " null"); + + Object desktop = mcf.createInstanceWithContext("com.sun.star.frame.Desktop", context); + Log.i(LOGTAG, "desktop is" + (desktop!=null ? " not" : "") + " null"); + + componentLoader = (XComponentLoader) UnoRuntime.queryInterface(XComponentLoader.class, desktop); + Log.i(LOGTAG, "componentLoader is" + (componentLoader!=null ? " not" : "") + " null"); + + } catch (Exception e) { + e.printStackTrace(System.err); + //finish(); + } + + setContentView(R.layout.activity_main); + + // setup gecko layout + mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout); + mMainLayout = (LinearLayout) findViewById(R.id.main_layout); + + + if (mLayerController == null) { + mLayerController = new LayerController(this); + + Log.e(LOGTAG, "### Creating GeckoSoftwareLayerClient"); + mLayerClient = new GeckoSoftwareLayerClient(this); + Log.e(LOGTAG, "### Done creating GeckoSoftwareLayerClient"); + + mLayerController.setLayerClient(mLayerClient); + mGeckoLayout.addView(mLayerController.getView(), 0); + } + + mLayerController.notifyLayerClientOfGeometryChange(); + + sLOKitThread = new LOKitThread(); + sLOKitThread.start(); + + + Log.w(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - UI almost up"); + } + + public static GeckoSoftwareLayerClient getLayerClient() { + return mLayerClient; + } + + public static LayerController getLayerController() { + return mLayerController; + } +} diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/GeckoEventListener.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/GeckoEventListener.java new file mode 100644 index 000000000000..670513f2cfb2 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/GeckoEventListener.java @@ -0,0 +1,44 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Sriram Ramasubramanian <sriram@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko; + +import org.json.JSONObject; + +public interface GeckoEventListener { + public void handleMessage(String event, JSONObject message); +} diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/BufferedCairoImage.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/BufferedCairoImage.java new file mode 100644 index 000000000000..b6c397767619 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/BufferedCairoImage.java @@ -0,0 +1,105 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Bitmap; + +import org.libreoffice.LOKitShell; + +import java.nio.ByteBuffer; + +//import org.mozilla.gecko.GeckoAppShell; + +/** + * A Cairo image that simply saves a buffer of pixel data. + */ +public class BufferedCairoImage extends CairoImage { + private ByteBuffer mBuffer; + private IntSize mSize; + private int mFormat; + private boolean mNeedToFreeBuffer = false; + + /** + * Creates a buffered Cairo image from a byte buffer. + */ + public BufferedCairoImage(ByteBuffer inBuffer, int inWidth, int inHeight, int inFormat) { + mBuffer = inBuffer; + mSize = new IntSize(inWidth, inHeight); + mFormat = inFormat; + } + + /** + * Creates a buffered Cairo image from an Android bitmap. + */ + public BufferedCairoImage(Bitmap bitmap) { + mFormat = CairoUtils.bitmapConfigToCairoFormat(bitmap.getConfig()); + mSize = new IntSize(bitmap.getWidth(), bitmap.getHeight()); + mNeedToFreeBuffer = true; + // XXX Why is this * 4? Shouldn't it depend on mFormat? + mBuffer = /*GeckoAppShell*/LOKitShell.allocateDirectBuffer(mSize.getArea() * 4); + + bitmap.copyPixelsToBuffer(mBuffer.asIntBuffer()); + } + + protected void finalize() throws Throwable { + try { + if (mNeedToFreeBuffer && mBuffer != null) + /*GeckoAppShell*/ LOKitShell.freeDirectBuffer(mBuffer); + mNeedToFreeBuffer = false; + mBuffer = null; + } finally { + super.finalize(); + } + } + + @Override + public ByteBuffer getBuffer() { + return mBuffer; + } + + @Override + public IntSize getSize() { + return mSize; + } + + @Override + public int getFormat() { + return mFormat; + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/CairoGLInfo.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/CairoGLInfo.java new file mode 100644 index 000000000000..bd4eedcaf951 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/CairoGLInfo.java @@ -0,0 +1,72 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import javax.microedition.khronos.opengles.GL10; + +/** + * Information needed to render Cairo bitmaps using OpenGL ES. + */ +public class CairoGLInfo { + public final int internalFormat; + public final int format; + public final int type; + + public CairoGLInfo(int cairoFormat) { + switch (cairoFormat) { + case CairoImage.FORMAT_ARGB32: + internalFormat = format = GL10.GL_RGBA; + type = GL10.GL_UNSIGNED_BYTE; + break; + case CairoImage.FORMAT_RGB24: + internalFormat = format = GL10.GL_RGB; + type = GL10.GL_UNSIGNED_BYTE; + break; + case CairoImage.FORMAT_RGB16_565: + internalFormat = format = GL10.GL_RGB; + type = GL10.GL_UNSIGNED_SHORT_5_6_5; + break; + case CairoImage.FORMAT_A8: + case CairoImage.FORMAT_A1: + throw new RuntimeException("Cairo FORMAT_A1 and FORMAT_A8 unsupported"); + default: + throw new RuntimeException("Unknown Cairo format"); + } + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/CairoImage.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/CairoImage.java new file mode 100644 index 000000000000..06c389dd0524 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/CairoImage.java @@ -0,0 +1,58 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import java.nio.ByteBuffer; + +/* + * A bitmap with pixel data in one of the formats that Cairo understands. + */ +public abstract class CairoImage { + public abstract ByteBuffer getBuffer(); + + public abstract IntSize getSize(); + public abstract int getFormat(); + + public static final int FORMAT_INVALID = -1; + public static final int FORMAT_ARGB32 = 0; + public static final int FORMAT_RGB24 = 1; + public static final int FORMAT_A8 = 2; + public static final int FORMAT_A1 = 3; + public static final int FORMAT_RGB16_565 = 4; +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/CairoUtils.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/CairoUtils.java new file mode 100644 index 000000000000..00bd896e1664 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/CairoUtils.java @@ -0,0 +1,85 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.CairoImage; +import android.graphics.Bitmap; +import javax.microedition.khronos.opengles.GL10; + +/** + * Utility methods useful when displaying Cairo bitmaps using OpenGL ES. + */ +public class CairoUtils { + private CairoUtils() { /* Don't call me. */ } + + public static int bitsPerPixelForCairoFormat(int cairoFormat) { + switch (cairoFormat) { + case CairoImage.FORMAT_A1: return 1; + case CairoImage.FORMAT_A8: return 8; + case CairoImage.FORMAT_RGB16_565: return 16; + case CairoImage.FORMAT_RGB24: return 24; + case CairoImage.FORMAT_ARGB32: return 32; + default: + throw new RuntimeException("Unknown Cairo format"); + } + } + + public static int bitmapConfigToCairoFormat(Bitmap.Config config) { + if (config == null) + return CairoImage.FORMAT_ARGB32; /* Droid Pro fix. */ + + switch (config) { + case ALPHA_8: return CairoImage.FORMAT_A8; + case ARGB_4444: throw new RuntimeException("ARGB_444 unsupported"); + case ARGB_8888: return CairoImage.FORMAT_ARGB32; + case RGB_565: return CairoImage.FORMAT_RGB16_565; + default: throw new RuntimeException("Unknown Skia bitmap config"); + } + } + + public static Bitmap.Config cairoFormatTobitmapConfig(int format) { + switch (format) { + case CairoImage.FORMAT_A8: return Bitmap.Config.ALPHA_8; + case CairoImage.FORMAT_ARGB32: return Bitmap.Config.ARGB_8888; + case CairoImage.FORMAT_RGB16_565: return Bitmap.Config.RGB_565; + default: + throw new RuntimeException("Unknown CairoImage format"); + } + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/CheckerboardImage.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/CheckerboardImage.java new file mode 100644 index 000000000000..392d7e8d8463 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/CheckerboardImage.java @@ -0,0 +1,170 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2012 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.libreoffice.LOKitShell; +import android.graphics.Color; +import java.nio.ByteBuffer; +import java.nio.ShortBuffer; +import java.util.Arrays; + +/** A Cairo image that displays a tinted checkerboard. */ +public class CheckerboardImage extends CairoImage { + // The width and height of the checkerboard tile. + private static final int SIZE = 16; + // The pixel format of the checkerboard tile. + private static final int FORMAT = CairoImage.FORMAT_RGB16_565; + // The color to mix in to tint the background color. + private static final int TINT_COLOR = Color.GRAY; + // The amount to mix in. + private static final float TINT_OPACITY = 0.4f; + + private ByteBuffer mBuffer; + private int mMainColor; + private boolean mShowChecks; + + /** Creates a new checkerboard image. */ + public CheckerboardImage() { + int bpp = CairoUtils.bitsPerPixelForCairoFormat(FORMAT); + mBuffer = LOKitShell.allocateDirectBuffer(SIZE * SIZE * bpp / 8); + update(true, Color.WHITE); + } + + /** Returns the current color of the checkerboard. */ + public int getColor() { + return mMainColor; + } + + /** Returns whether or not we are currently showing checks on the checkerboard. */ + public boolean getShowChecks() { + return mShowChecks; + } + + /** Updates the checkerboard image. If showChecks is true, then create a + checkerboard image that is tinted to the color. Otherwise just return a flat + image of the color. */ + public void update(boolean showChecks, int color) { + mMainColor = color; + mShowChecks = showChecks; + + short mainColor16 = convertTo16Bit(mMainColor); + + mBuffer.rewind(); + ShortBuffer shortBuffer = mBuffer.asShortBuffer(); + + if (!mShowChecks) { + short color16 = convertTo16Bit(mMainColor); + short[] fillBuffer = new short[SIZE]; + Arrays.fill(fillBuffer, color16); + + for (int i = 0; i < SIZE; i++) { + shortBuffer.put(fillBuffer); + } + + return; + } + + short tintColor16 = convertTo16Bit(tint(mMainColor)); + + short[] mainPattern = new short[SIZE / 2], tintPattern = new short[SIZE / 2]; + Arrays.fill(mainPattern, mainColor16); + Arrays.fill(tintPattern, tintColor16); + + // The checkerboard pattern looks like this: + // + // +---+---+ + // | N | T | N = normal + // +---+---+ T = tinted + // | T | N | + // +---+---+ + + for (int i = 0; i < SIZE / 2; i++) { + shortBuffer.put(mainPattern); + shortBuffer.put(tintPattern); + } + for (int i = SIZE / 2; i < SIZE; i++) { + shortBuffer.put(tintPattern); + shortBuffer.put(mainPattern); + } + } + + // Tints the given color appropriately and returns the tinted color. + private int tint(int color) { + float negTintOpacity = 1.0f - TINT_OPACITY; + float r = Color.red(color) * negTintOpacity + Color.red(TINT_COLOR) * TINT_OPACITY; + float g = Color.green(color) * negTintOpacity + Color.green(TINT_COLOR) * TINT_OPACITY; + float b = Color.blue(color) * negTintOpacity + Color.blue(TINT_COLOR) * TINT_OPACITY; + return Color.rgb(Math.round(r), Math.round(g), Math.round(b)); + } + + // Converts a 32-bit ARGB color to 16-bit R5G6B5, truncating values and discarding the alpha + // channel. + private short convertTo16Bit(int color) { + int r = Color.red(color) >> 3, g = Color.green(color) >> 2, b = Color.blue(color) >> 3; + int c = ((r << 11) | (g << 5) | b); + // Swap endianness. + return (short)((c >> 8) | ((c & 0xff) << 8)); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mBuffer != null) { + LOKitShell.freeDirectBuffer(mBuffer); + } + } finally { + super.finalize(); + } + } + + @Override + public ByteBuffer getBuffer() { + return mBuffer; + } + + @Override + public IntSize getSize() { + return new IntSize(SIZE, SIZE); + } + + @Override + public int getFormat() { + return FORMAT; + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/FlexibleGLSurfaceView.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/FlexibleGLSurfaceView.java new file mode 100644 index 000000000000..dc20077b0457 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/FlexibleGLSurfaceView.java @@ -0,0 +1,218 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011-2012 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +//import org.mozilla.gecko.GeckoApp; +import android.content.Context; +import android.graphics.PixelFormat; +import android.opengl.GLSurfaceView; +import android.util.AttributeSet; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import org.libreoffice.LibreOfficeMainActivity; + +public class FlexibleGLSurfaceView extends SurfaceView implements SurfaceHolder.Callback { + private static final String LOGTAG = "GeckoFlexibleGLSurfaceView"; + + private GLSurfaceView.Renderer mRenderer; + private GLThread mGLThread; // Protected by this class's monitor. + private GLController mController; + private Listener mListener; + + public FlexibleGLSurfaceView(Context context) { + super(context); + init(); + } + + public FlexibleGLSurfaceView(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + init(); + } + + public void init() { + SurfaceHolder holder = getHolder(); + holder.addCallback(this); + holder.setFormat(PixelFormat.RGB_565); + + mController = new GLController(this); + } + + public void setRenderer(GLSurfaceView.Renderer renderer) { + mRenderer = renderer; + } + + public GLSurfaceView.Renderer getRenderer() { + return mRenderer; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public synchronized void requestRender() { + if (mGLThread != null) { + mGLThread.renderFrame(); + } + if (mListener != null) { + mListener.renderRequested(); + } + } + + /** + * Creates a Java GL thread. After this is called, the FlexibleGLSurfaceView may be used just + * like a GLSurfaceView. It is illegal to access the controller after this has been called. + */ + public synchronized void createGLThread() { + if (mGLThread != null) { + throw new FlexibleGLSurfaceViewException("createGLThread() called with a GL thread " + + "already in place!"); + } + + Log.e(LOGTAG, "### Creating GL thread!"); + mGLThread = new GLThread(mController); + mGLThread.start(); + notifyAll(); + } + + /** + * Destroys the Java GL thread. Returns a Thread that completes when the Java GL thread is + * fully shut down. + */ + public synchronized Thread destroyGLThread() { + // Wait for the GL thread to be started. + Log.e(LOGTAG, "### Waiting for GL thread to be created..."); + while (mGLThread == null) { + try { + wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + Log.e(LOGTAG, "### Destroying GL thread!"); + Thread glThread = mGLThread; + mGLThread.shutdown(); + mGLThread = null; + return glThread; + } + + public synchronized void recreateSurface() { + if (mGLThread == null) { + throw new FlexibleGLSurfaceViewException("recreateSurface() called with no GL " + + "thread active!"); + } + + mGLThread.recreateSurface(); + } + + public synchronized GLController getGLController() { + if (mGLThread != null) { + throw new FlexibleGLSurfaceViewException("getGLController() called with a GL thread " + + "active; shut down the GL thread first!"); + } + + return mController; + } + + public synchronized void surfaceChanged(SurfaceHolder holder, int format, int width, + int height) { + mController.sizeChanged(width, height); + if (mGLThread != null) { + mGLThread.surfaceChanged(width, height); + } + + if (mListener != null) { + mListener.surfaceChanged(width, height); + } + } + + public synchronized void surfaceCreated(SurfaceHolder holder) { + mController.surfaceCreated(); + if (mGLThread != null) { + mGLThread.surfaceCreated(); + } + } + + public synchronized void surfaceDestroyed(SurfaceHolder holder) { + mController.surfaceDestroyed(); + if (mGLThread != null) { + mGLThread.surfaceDestroyed(); + } + + if (mListener != null) { + mListener.compositionPauseRequested(); + } + } + + // Called from the compositor thread + public static GLController registerCxxCompositor() { + try { + Log.e(LOGTAG, "### registerCxxCompositor point A"); + System.out.println("register layer comp"); + Log.e(LOGTAG, "### registerCxxCompositor point B"); + FlexibleGLSurfaceView flexView = (FlexibleGLSurfaceView) /*GeckoApp*/LibreOfficeMainActivity.mAppContext.getLayerController().getView(); + Log.e(LOGTAG, "### registerCxxCompositor point C: " + flexView); + try { + flexView.destroyGLThread().join(); + } catch (InterruptedException e) {} + Log.e(LOGTAG, "### registerCxxCompositor point D: " + flexView.getGLController()); + return flexView.getGLController(); + } catch (Exception e) { + Log.e(LOGTAG, "### Exception! " + e); + return null; + } + } + + public interface Listener { + void renderRequested(); + void compositionPauseRequested(); + void compositionResumeRequested(); + void surfaceChanged(int width, int height); + } + + public static class FlexibleGLSurfaceViewException extends RuntimeException { + public static final long serialVersionUID = 1L; + + FlexibleGLSurfaceViewException(String e) { + super(e); + } + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/FloatSize.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/FloatSize.java new file mode 100644 index 000000000000..5fb73ec18df9 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/FloatSize.java @@ -0,0 +1,99 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Chris Lord <chrislord.net@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.util.FloatUtils; + +public class FloatSize { + public final float width, height; + + public FloatSize(FloatSize size) { + width = size.width; + height = size.height; + } + + public FloatSize(IntSize size) { + width = size.width; + height = size.height; + } + + public FloatSize(float aWidth, float aHeight) { + width = aWidth; + height = aHeight; + } + + public FloatSize(JSONObject json) { + try { + width = (float) json.getDouble("width"); + height = (float) json.getDouble("height"); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + return "(" + width + "," + height + ")"; + } + + public boolean isPositive() { + return (width > 0 && height > 0); + } + + public boolean fuzzyEquals(FloatSize size) { + return (FloatUtils.fuzzyEquals(size.width, width) && + FloatUtils.fuzzyEquals(size.height, height)); + } + + public FloatSize scale(float factor) { + return new FloatSize(width * factor, height * factor); + } + + /* + * Returns the size that represents a linear transition between this size and `to` at time `t`, + * which is on the scale [0, 1). + */ + public FloatSize interpolate(FloatSize to, float t) { + return new FloatSize(FloatUtils.interpolate(width, to.width, t), + FloatUtils.interpolate(height, to.height, t)); + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GLController.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GLController.java new file mode 100644 index 000000000000..e8f201228666 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GLController.java @@ -0,0 +1,279 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011-2012 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL; +import javax.microedition.khronos.opengles.GL10; + +public class GLController { + private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + private static final String LOGTAG = "GeckoGLController"; + + private FlexibleGLSurfaceView mView; + private int mGLVersion; + private boolean mSurfaceValid; + private int mWidth, mHeight; + + private EGL10 mEGL; + private EGLDisplay mEGLDisplay; + private EGLConfig mEGLConfig; + private EGLContext mEGLContext; + private EGLSurface mEGLSurface; + + private GL mGL; + + private static final int LOCAL_EGL_OPENGL_ES2_BIT = 4; + + private static final int[] CONFIG_SPEC = { + EGL10.EGL_RED_SIZE, 5, + EGL10.EGL_GREEN_SIZE, 6, + EGL10.EGL_BLUE_SIZE, 5, + EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT, + EGL10.EGL_RENDERABLE_TYPE, LOCAL_EGL_OPENGL_ES2_BIT, + EGL10.EGL_NONE + }; + + public GLController(FlexibleGLSurfaceView view) { + mView = view; + mGLVersion = 2; + mSurfaceValid = false; + } + + public void setGLVersion(int version) { + mGLVersion = version; + } + + /** You must call this on the same thread you intend to use OpenGL on. */ + public void initGLContext() { + initEGLContext(); + createEGLSurface(); + } + + public void disposeGLContext() { + if (!mEGL.eglMakeCurrent(mEGLDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_CONTEXT)) { + throw new GLControllerException("EGL context could not be released!"); + } + + if (mEGLSurface != null) { + if (!mEGL.eglDestroySurface(mEGLDisplay, mEGLSurface)) { + throw new GLControllerException("EGL surface could not be destroyed!"); + } + + mEGLSurface = null; + } + + if (mEGLContext == null) { + if (!mEGL.eglDestroyContext(mEGLDisplay, mEGLContext)) { + throw new GLControllerException("EGL context could not be destroyed!"); + } + + mGL = null; + mEGLDisplay = null; + mEGLConfig = null; + mEGLContext = null; + } + } + + public GL getGL() { return mEGLContext.getGL(); } + public EGLDisplay getEGLDisplay() { return mEGLDisplay; } + public EGLConfig getEGLConfig() { return mEGLConfig; } + public EGLContext getEGLContext() { return mEGLContext; } + public EGLSurface getEGLSurface() { return mEGLSurface; } + public FlexibleGLSurfaceView getView() { return mView; } + + public boolean hasSurface() { + return mEGLSurface != null; + } + + public boolean swapBuffers() { + return mEGL.eglSwapBuffers(mEGLDisplay, mEGLSurface); + } + + public boolean checkForLostContext() { + if (mEGL.eglGetError() != EGL11.EGL_CONTEXT_LOST) { + return false; + } + + mEGLDisplay = null; + mEGLConfig = null; + mEGLContext = null; + mEGLSurface = null; + mGL = null; + return true; + } + + public synchronized void waitForValidSurface() { + while (!mSurfaceValid) { + try { + wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + public synchronized int getWidth() { + return mWidth; + } + + public synchronized int getHeight() { + return mHeight; + } + + synchronized void surfaceCreated() { + mSurfaceValid = true; + notifyAll(); + } + + synchronized void surfaceDestroyed() { + mSurfaceValid = false; + notifyAll(); + } + + synchronized void sizeChanged(int newWidth, int newHeight) { + mWidth = newWidth; + mHeight = newHeight; + } + + private void initEGL() { + mEGL = (EGL10)EGLContext.getEGL(); + + mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) { + throw new GLControllerException("eglGetDisplay() failed"); + } + + int[] version = new int[2]; + if (!mEGL.eglInitialize(mEGLDisplay, version)) { + throw new GLControllerException("eglInitialize() failed"); + } + + mEGLConfig = chooseConfig(); + } + + private void initEGLContext() { + initEGL(); + + int[] attribList = { EGL_CONTEXT_CLIENT_VERSION, mGLVersion, EGL10.EGL_NONE }; + mEGLContext = mEGL.eglCreateContext(mEGLDisplay, mEGLConfig, EGL10.EGL_NO_CONTEXT, + attribList); + if (mEGLContext == null || mEGLContext == EGL10.EGL_NO_CONTEXT) { + throw new GLControllerException("createContext() failed"); + } + } + + private EGLConfig chooseConfig() { + int[] numConfigs = new int[1]; + if (!mEGL.eglChooseConfig(mEGLDisplay, CONFIG_SPEC, null, 0, numConfigs) || + numConfigs[0] <= 0) { + throw new GLControllerException("No available EGL configurations"); + } + + EGLConfig[] configs = new EGLConfig[numConfigs[0]]; + if (!mEGL.eglChooseConfig(mEGLDisplay, CONFIG_SPEC, configs, numConfigs[0], numConfigs)) { + throw new GLControllerException("No EGL configuration for that specification"); + } + + // Select the first 565 RGB configuration. + int[] red = new int[1], green = new int[1], blue = new int[1]; + for (EGLConfig config : configs) { + mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_RED_SIZE, red); + mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_GREEN_SIZE, green); + mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_BLUE_SIZE, blue); + if (red[0] == 5 && green[0] == 6 && blue[0] == 5) { + return config; + } + } + + throw new GLControllerException("No suitable EGL configuration found"); + } + + private void createEGLSurface() { + SurfaceHolder surfaceHolder = mView.getHolder(); + mEGLSurface = mEGL.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surfaceHolder, null); + if (mEGLSurface == null || mEGLSurface == EGL10.EGL_NO_SURFACE) { + throw new GLControllerException("EGL window surface could not be created!"); + } + + if (!mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) { + throw new GLControllerException("EGL surface could not be made into the current " + + "surface!"); + } + + mGL = mEGLContext.getGL(); + + if (mView.getRenderer() != null) { + mView.getRenderer().onSurfaceCreated((GL10)mGL, mEGLConfig); + mView.getRenderer().onSurfaceChanged((GL10)mGL, mView.getWidth(), mView.getHeight()); + } + } + + // Provides an EGLSurface without assuming ownership of this surface. + private EGLSurface provideEGLSurface() { + if (mEGL == null) { + initEGL(); + } + + SurfaceHolder surfaceHolder = mView.getHolder(); + mEGLSurface = mEGL.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surfaceHolder, null); + if (mEGLSurface == null || mEGLSurface == EGL10.EGL_NO_SURFACE) { + throw new GLControllerException("EGL window surface could not be created!"); + } + + return mEGLSurface; + } + + public static class GLControllerException extends RuntimeException { + public static final long serialVersionUID = 1L; + + GLControllerException(String e) { + super(e); + } + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GLThread.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GLThread.java new file mode 100644 index 000000000000..4f788f64ebcb --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GLThread.java @@ -0,0 +1,181 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011-2012 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import android.opengl.GLSurfaceView; +import android.view.SurfaceHolder; +import javax.microedition.khronos.opengles.GL10; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; + +// A GL thread managed by Java. It is not necessary to use this class to use the +// FlexibleGLSurfaceView, but it can be helpful, especially if the GL rendering is to be done +// entirely in Java. +class GLThread extends Thread { + private LinkedBlockingQueue<Runnable> mQueue; + private GLController mController; + private boolean mRenderQueued; + + public GLThread(GLController controller) { + mQueue = new LinkedBlockingQueue<Runnable>(); + mController = controller; + } + + @Override + public void run() { + while (true) { + Runnable runnable; + try { + runnable = mQueue.take(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + runnable.run(); + if (runnable instanceof ShutdownMessage) { + break; + } + } + } + + public void recreateSurface() { + mQueue.add(new RecreateSurfaceMessage()); + } + + public void renderFrame() { + // Make sure there's only one render event in the queue at a time. + synchronized (this) { + if (!mRenderQueued) { + mQueue.add(new RenderFrameMessage()); + mRenderQueued = true; + } + } + } + + public void shutdown() { + mQueue.add(new ShutdownMessage()); + } + + public void surfaceChanged(int width, int height) { + mQueue.add(new SizeChangedMessage(width, height)); + } + + public void surfaceCreated() { + mQueue.add(new SurfaceCreatedMessage()); + } + + public void surfaceDestroyed() { + mQueue.add(new SurfaceDestroyedMessage()); + } + + private void doRecreateSurface() { + mController.disposeGLContext(); + mController.initGLContext(); + } + + private GLSurfaceView.Renderer getRenderer() { + return mController.getView().getRenderer(); + } + + private class RecreateSurfaceMessage implements Runnable { + public void run() { + doRecreateSurface(); + } + } + + private class RenderFrameMessage implements Runnable { + public void run() { + synchronized (GLThread.this) { + mRenderQueued = false; + } + + // Bail out if the surface was lost. + if (mController.getEGLSurface() == null) { + return; + } + + GLSurfaceView.Renderer renderer = getRenderer(); + if (renderer != null) { + renderer.onDrawFrame((GL10)mController.getGL()); + } + + mController.swapBuffers(); + //if (!mController.swapBuffers() && mController.checkForLostContext()) { + // doRecreateSurface(); + //} + } + } + + private class ShutdownMessage implements Runnable { + public void run() { + mController.disposeGLContext(); + mController = null; + } + } + + private class SizeChangedMessage implements Runnable { + private int mWidth, mHeight; + + public SizeChangedMessage(int width, int height) { + mWidth = width; + mHeight = height; + } + + public void run() { + GLSurfaceView.Renderer renderer = getRenderer(); + if (renderer != null) { + renderer.onSurfaceChanged((GL10)mController.getGL(), mWidth, mHeight); + } + } + } + + private class SurfaceCreatedMessage implements Runnable { + public void run() { + if (!mController.hasSurface()) { + mController.initGLContext(); + } + } + } + + private class SurfaceDestroyedMessage implements Runnable { + public void run() { + mController.disposeGLContext(); + } + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoGLLayerClient.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoGLLayerClient.java new file mode 100644 index 000000000000..9e4f376e7ad2 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoGLLayerClient.java @@ -0,0 +1,265 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.util.Log; +import android.view.View; + +import org.libreoffice.LOEvent; +import org.libreoffice.LOKitShell; + +public class GeckoGLLayerClient extends GeckoLayerClient + implements FlexibleGLSurfaceView.Listener, VirtualLayer.Listener { + private static final String LOGTAG = "GeckoGLLayerClient"; + + private LayerRenderer mLayerRenderer; + private boolean mLayerRendererInitialized; + + public GeckoGLLayerClient(Context context) { + super(context); + } + + @Override + public Rect beginDrawing(int width, int height, int tileWidth, int tileHeight, + String metadata, boolean hasDirectTexture) { + Rect bufferRect = super.beginDrawing(width, height, tileWidth, tileHeight, + metadata, hasDirectTexture); + if (bufferRect == null) { + return null; + } + + // Be sure to adjust the buffer size; if it's not at least as large as the viewport size, + // ViewportMetrics.getOptimumViewportOffset() gets awfully confused and severe display + // corruption results! + if (mBufferSize.width != width || mBufferSize.height != height) { + mBufferSize = new IntSize(width, height); + } + + return bufferRect; + } + + @Override + protected boolean handleDirectTextureChange(boolean hasDirectTexture) { + Log.e(LOGTAG, "### handleDirectTextureChange"); + if (mTileLayer != null) { + return false; + } + + Log.e(LOGTAG, "### Creating virtual layer"); + VirtualLayer virtualLayer = new VirtualLayer(); + virtualLayer.setListener(this); + virtualLayer.setSize(getBufferSize()); + getLayerController().setRoot(virtualLayer); + mTileLayer = virtualLayer; + + sendResizeEventIfNecessary(true); + return true; + } + + @Override + public void setLayerController(LayerController layerController) { + super.setLayerController(layerController); + + LayerView view = layerController.getView(); + view.setListener(this); + + mLayerRenderer = new LayerRenderer(view); + } + + @Override + protected boolean shouldDrawProceed(int tileWidth, int tileHeight) { + Log.e(LOGTAG, "### shouldDrawProceed"); + // Always draw. + return true; + } + + @Override + protected void updateLayerAfterDraw(Rect updatedRect) { + Log.e(LOGTAG, "### updateLayerAfterDraw"); + // Nothing to do. + } + + @Override + protected IntSize getBufferSize() { + View view = (View) getLayerController().getView(); + IntSize size = new IntSize(view.getWidth(), view.getHeight()); + Log.e(LOGTAG, "### getBufferSize " + size); + return size; + } + + @Override + protected IntSize getTileSize() { + Log.e(LOGTAG, "### getTileSize " + getBufferSize()); + return getBufferSize(); + } + + @Override + protected void tileLayerUpdated() { + // Set the new origin and resolution instantly. + mTileLayer.performUpdates(null); + } + + @Override + public Bitmap getBitmap() { + Log.e(LOGTAG, "### getBitmap"); + IntSize size = getBufferSize(); + try { + return Bitmap.createBitmap(size.width, size.height, Bitmap.Config.RGB_565); + } catch (OutOfMemoryError oom) { + Log.e(LOGTAG, "Unable to create bitmap", oom); + return null; + } + } + + @Override + public int getType() { + Log.e(LOGTAG, "### getType"); + return LAYER_CLIENT_TYPE_GL; + } + + public void dimensionsChanged(Point newOrigin, float newResolution) { + Log.e(LOGTAG, "### dimensionsChanged " + newOrigin + " " + newResolution); + } + + /* Informs Gecko that the screen size has changed. */ + @Override + protected void sendResizeEventIfNecessary(boolean force) { + Log.e(LOGTAG, "### sendResizeEventIfNecessary " + force); + + IntSize newSize = getBufferSize(); + if (!force && mScreenSize != null && mScreenSize.equals(newSize)) { + return; + } + + mScreenSize = newSize; + + Log.e(LOGTAG, "### Screen-size changed to " + mScreenSize); + //GeckoEvent event = GeckoEvent.createSizeChangedEvent(mScreenSize.width, mScreenSize.height, + // mScreenSize.width, mScreenSize.height, + // mScreenSize.width, mScreenSize.height); + //GeckoAppShell.sendEventToGecko(event); + LOEvent event = LOEvent.sizeChanged(mScreenSize.width, mScreenSize.height, + mScreenSize.width, mScreenSize.height, + mScreenSize.width, mScreenSize.height); + LOKitShell.sendEvent(event); + + } + + /** + * For Gecko to use. + */ + public ViewTransform getViewTransform() { + Log.e(LOGTAG, "### getViewTransform()"); + + // NB: We don't begin a transaction here because this can be called in a synchronous + // manner between beginDrawing() and endDrawing(), and that will cause a deadlock. + + LayerController layerController = getLayerController(); + synchronized (layerController) { + ViewportMetrics viewportMetrics = layerController.getViewportMetrics(); + PointF viewportOrigin = viewportMetrics.getOrigin(); + Point tileOrigin = mTileLayer.getOrigin(); + float scrollX = viewportOrigin.x; + float scrollY = viewportOrigin.y; + float zoomFactor = viewportMetrics.getZoomFactor(); + Log.e(LOGTAG, "### Viewport metrics = " + viewportMetrics + " tile reso = " + + mTileLayer.getResolution()); + return new ViewTransform(scrollX, scrollY, zoomFactor); + } + } + + public void renderRequested() { + Log.e(LOGTAG, "### Render requested, scheduling composite"); + LOKitShell.scheduleComposite(); + } + + public void compositionPauseRequested() { + Log.e(LOGTAG, "### Scheduling PauseComposition"); + LOKitShell.schedulePauseComposition(); + } + + public void compositionResumeRequested() { + Log.e(LOGTAG, "### Scheduling ResumeComposition"); + LOKitShell.scheduleResumeComposition(); + } + + public void surfaceChanged(int width, int height) { + compositionPauseRequested(); + LayerController layerController = getLayerController(); + layerController.setViewportSize(new FloatSize(width, height)); + compositionResumeRequested(); + renderRequested(); + } + + /** + * For Gecko to use. + */ + public LayerRenderer.Frame createFrame() { + // Create the shaders and textures if necessary. + if (!mLayerRendererInitialized) { + mLayerRenderer.createProgram(); + mLayerRendererInitialized = true; + } + + // Build the contexts and create the frame. + Layer.RenderContext pageContext = mLayerRenderer.createPageContext(); + Layer.RenderContext screenContext = mLayerRenderer.createScreenContext(); + return mLayerRenderer.createFrame(pageContext, screenContext); + } + + /** + * For Gecko to use. + */ + public void activateProgram() { + mLayerRenderer.activateProgram(); + } + + /** + * For Gecko to use. + */ + public void deactivateProgram() { + mLayerRenderer.deactivateProgram(); + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java new file mode 100644 index 000000000000..09349b4eea07 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java @@ -0,0 +1,414 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Chris Lord <chrislord.net@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.libreoffice.LOEvent; +import org.libreoffice.LOKitShell; +import org.libreoffice.LibreOfficeMainActivity; +import org.mozilla.gecko.util.FloatUtils; +//import org.mozilla.gecko.GeckoApp; +//import org.mozilla.gecko.GeckoAppShell; +//import org.mozilla.gecko.GeckoEvent; +import org.mozilla.gecko.GeckoEventListener; +import org.json.JSONException; +import org.json.JSONObject; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.SystemClock; +import android.util.DisplayMetrics; +import android.util.Log; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class GeckoLayerClient extends LayerClient implements GeckoEventListener { + private static final String LOGTAG = "GeckoLayerClient"; + + public static final int LAYER_CLIENT_TYPE_NONE = 0; + public static final int LAYER_CLIENT_TYPE_SOFTWARE = 1; + public static final int LAYER_CLIENT_TYPE_GL = 2; + + protected IntSize mScreenSize; + protected IntSize mBufferSize; + + protected Layer mTileLayer; + + /* The viewport that Gecko is currently displaying. */ + protected ViewportMetrics mGeckoViewport; + + /* The viewport that Gecko will display when drawing is finished */ + protected ViewportMetrics mNewGeckoViewport; + + private static final long MIN_VIEWPORT_CHANGE_DELAY = 25L; + private long mLastViewportChangeTime; + private boolean mPendingViewportAdjust; + private boolean mViewportSizeChanged; + + // mUpdateViewportOnEndDraw is used to indicate that we received a + // viewport update notification while drawing. therefore, when the + // draw finishes, we need to update the entire viewport rather than + // just the page size. this boolean should always be accessed from + // inside a transaction, so no synchronization is needed. + private boolean mUpdateViewportOnEndDraw; + + private String mLastCheckerboardColor; + + private static Pattern sColorPattern; + + /* Used by robocop for testing purposes */ + private DrawListener mDrawListener; + + protected abstract boolean handleDirectTextureChange(boolean hasDirectTexture); + protected abstract boolean shouldDrawProceed(int tileWidth, int tileHeight); + protected abstract void updateLayerAfterDraw(Rect updatedRect); + protected abstract IntSize getBufferSize(); + protected abstract IntSize getTileSize(); + protected abstract void tileLayerUpdated(); + public abstract Bitmap getBitmap(); + public abstract int getType(); + + public GeckoLayerClient(Context context) { + mScreenSize = new IntSize(0, 0); + mBufferSize = new IntSize(0, 0); + } + + /** Attaches the root layer to the layer controller so that Gecko appears. */ + @Override + public void setLayerController(LayerController layerController) { + super.setLayerController(layerController); + + layerController.setRoot(mTileLayer); + if (mGeckoViewport != null) { + layerController.setViewportMetrics(mGeckoViewport); + } + + //GeckoAppShell.registerGeckoEventListener("Viewport:UpdateAndDraw", this); + //GeckoAppShell.registerGeckoEventListener("Viewport:UpdateLater", this); + + sendResizeEventIfNecessary(); + } + + public Rect beginDrawing(int width, int height, int tileWidth, int tileHeight, + String metadata, boolean hasDirectTexture) { + Log.e(LOGTAG, "### beginDrawing " + width + " " + height + " " + tileWidth + " " + + tileHeight + " " + hasDirectTexture); + + // If we've changed surface types, cancel this draw + if (handleDirectTextureChange(hasDirectTexture)) { + Log.e(LOGTAG, "### Cancelling draw due to direct texture change"); + return null; + } + + if (!shouldDrawProceed(tileWidth, tileHeight)) { + Log.e(LOGTAG, "### Cancelling draw due to shouldDrawProceed()"); + return null; + } + + LayerController controller = getLayerController(); + + try { + JSONObject viewportObject = new JSONObject(metadata); + mNewGeckoViewport = new ViewportMetrics(viewportObject); + + Log.e(LOGTAG, "### beginDrawing new Gecko viewport " + mNewGeckoViewport); + + // Update the background color, if it's present. + String backgroundColorString = viewportObject.optString("backgroundColor"); + if (backgroundColorString != null && !backgroundColorString.equals(mLastCheckerboardColor)) { + mLastCheckerboardColor = backgroundColorString; + controller.setCheckerboardColor(parseColorFromGecko(backgroundColorString)); + } + } catch (JSONException e) { + Log.e(LOGTAG, "Aborting draw, bad viewport description: " + metadata); + return null; + } + + + // Make sure we don't spend time painting areas we aren't interested in. + // Only do this if the Gecko viewport isn't going to override our viewport. + Rect bufferRect = new Rect(0, 0, width, height); + + if (!mUpdateViewportOnEndDraw) { + // First, find out our ideal displayport. We do this by taking the + // clamped viewport origin and taking away the optimum viewport offset. + // This would be what we would send to Gecko if adjustViewport were + // called now. + ViewportMetrics currentMetrics = controller.getViewportMetrics(); + PointF currentBestOrigin = RectUtils.getOrigin(currentMetrics.getClampedViewport()); + PointF viewportOffset = currentMetrics.getOptimumViewportOffset(new IntSize(width, height)); + currentBestOrigin.offset(-viewportOffset.x, -viewportOffset.y); + + Rect currentRect = RectUtils.round(new RectF(currentBestOrigin.x, currentBestOrigin.y, + currentBestOrigin.x + width, currentBestOrigin.y + height)); + + // Second, store Gecko's displayport. + PointF currentOrigin = mNewGeckoViewport.getDisplayportOrigin(); + bufferRect = RectUtils.round(new RectF(currentOrigin.x, currentOrigin.y, + currentOrigin.x + width, currentOrigin.y + height)); + + + // Take the intersection of the two as the area we're interested in rendering. + if (!bufferRect.intersect(currentRect)) { + // If there's no intersection, we have no need to render anything, + // but make sure to update the viewport size. + beginTransaction(mTileLayer); + try { + updateViewport(true); + } finally { + endTransaction(mTileLayer); + } + return null; + } + bufferRect.offset(Math.round(-currentOrigin.x), Math.round(-currentOrigin.y)); + } + + beginTransaction(mTileLayer); + return bufferRect; + } + + /* + * TODO: Would be cleaner if this took an android.graphics.Rect instead, but that would require + * a little more JNI magic. + */ + public void endDrawing(int x, int y, int width, int height) { + synchronized (getLayerController()) { + try { + updateViewport(!mUpdateViewportOnEndDraw); + mUpdateViewportOnEndDraw = false; + + Rect rect = new Rect(x, y, x + width, y + height); + updateLayerAfterDraw(rect); + } finally { + endTransaction(mTileLayer); + } + } + Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - endDrawing"); + + /* Used by robocop for testing purposes */ + if (mDrawListener != null) { + mDrawListener.drawFinished(x, y, width, height); + } + } + + protected void updateViewport(boolean onlyUpdatePageSize) { + // save and restore the viewport size stored in java; never let the + // JS-side viewport dimensions override the java-side ones because + // java is the One True Source of this information, and allowing JS + // to override can lead to race conditions where this data gets clobbered. + FloatSize viewportSize = getLayerController().getViewportSize(); + mGeckoViewport = mNewGeckoViewport; + mGeckoViewport.setSize(viewportSize); + + LayerController controller = getLayerController(); + PointF displayportOrigin = mGeckoViewport.getDisplayportOrigin(); + mTileLayer.setOrigin(PointUtils.round(displayportOrigin)); + mTileLayer.setResolution(mGeckoViewport.getZoomFactor()); + + this.tileLayerUpdated(); + Log.e(LOGTAG, "### updateViewport onlyUpdatePageSize=" + onlyUpdatePageSize + + " getTileViewport " + mGeckoViewport); + + if (onlyUpdatePageSize) { + // Don't adjust page size when zooming unless zoom levels are + // approximately equal. + if (FloatUtils.fuzzyEquals(controller.getZoomFactor(), + mGeckoViewport.getZoomFactor())) + controller.setPageSize(mGeckoViewport.getPageSize()); + } else { + controller.setViewportMetrics(mGeckoViewport); + controller.abortPanZoomAnimation(); + } + } + + /* Informs Gecko that the screen size has changed. */ + protected void sendResizeEventIfNecessary(boolean force) { + Log.e(LOGTAG, "### sendResizeEventIfNecessary " + force); + + DisplayMetrics metrics = new DisplayMetrics(); + /*GeckoApp*/LibreOfficeMainActivity.mAppContext.getWindowManager().getDefaultDisplay().getMetrics(metrics); + + // Return immediately if the screen size hasn't changed or the viewport + // size is zero (which indicates that the rendering surface hasn't been + // allocated yet). + boolean screenSizeChanged = (metrics.widthPixels != mScreenSize.width || + metrics.heightPixels != mScreenSize.height); + boolean viewportSizeValid = (getLayerController() != null && + getLayerController().getViewportSize().isPositive()); + if (!(force || (screenSizeChanged && viewportSizeValid))) { + return; + } + + mScreenSize = new IntSize(metrics.widthPixels, metrics.heightPixels); + IntSize bufferSize = getBufferSize(), tileSize = getTileSize(); + + Log.e(LOGTAG, "### Screen-size changed to " + mScreenSize); + //GeckoEvent event = GeckoEvent.createSizeChangedEvent(bufferSize.width, bufferSize.height, + // metrics.widthPixels, metrics.heightPixels, + // tileSize.width, tileSize.height); + //GeckoAppShell.sendEventToGecko(event); + LOEvent event = LOEvent.sizeChanged(bufferSize.width, bufferSize.height, + metrics.widthPixels, metrics.heightPixels, + tileSize.width, tileSize.height); + } + + // Parses a color from an RGB triple of the form "rgb([0-9]+, [0-9]+, [0-9]+)". If the color + // cannot be parsed, returns white. + private static int parseColorFromGecko(String string) { + if (sColorPattern == null) { + sColorPattern = Pattern.compile("rgb\\((\\d+),\\s*(\\d+),\\s*(\\d+)\\)"); + } + + Matcher matcher = sColorPattern.matcher(string); + if (!matcher.matches()) { + return Color.WHITE; + } + + int r = Integer.parseInt(matcher.group(1)); + int g = Integer.parseInt(matcher.group(2)); + int b = Integer.parseInt(matcher.group(3)); + return Color.rgb(r, g, b); + } + + @Override + public void render() { + adjustViewportWithThrottling(); + } + + private void adjustViewportWithThrottling() { + if (!getLayerController().getRedrawHint()) + return; + + if (mPendingViewportAdjust) + return; + + long timeDelta = System.currentTimeMillis() - mLastViewportChangeTime; + if (timeDelta < MIN_VIEWPORT_CHANGE_DELAY) { + getLayerController().getView().postDelayed( + new Runnable() { + public void run() { + mPendingViewportAdjust = false; + adjustViewport(); + } + }, MIN_VIEWPORT_CHANGE_DELAY - timeDelta); + mPendingViewportAdjust = true; + return; + } + + adjustViewport(); + } + + @Override + public void viewportSizeChanged() { + mViewportSizeChanged = true; + } + + private void adjustViewport() { + ViewportMetrics viewportMetrics = + new ViewportMetrics(getLayerController().getViewportMetrics()); + + PointF viewportOffset = viewportMetrics.getOptimumViewportOffset(mBufferSize); + viewportMetrics.setViewportOffset(viewportOffset); + viewportMetrics.setViewport(viewportMetrics.getClampedViewport()); + + //GeckoAppShell.sendEventToGecko(GeckoEvent.createViewportEvent(viewportMetrics)); + LOKitShell.sendEvent(LOEvent.viewport(viewportMetrics)); + if (mViewportSizeChanged) { + mViewportSizeChanged = false; + //GeckoAppShell.viewSizeChanged(); + LOKitShell.viewSizeChanged(); + } + + mLastViewportChangeTime = System.currentTimeMillis(); + } + + public void handleMessage(String event, JSONObject message) { + if ("Viewport:UpdateAndDraw".equals(event)) { + Log.e(LOGTAG, "### Java side Viewport:UpdateAndDraw()!"); + mUpdateViewportOnEndDraw = true; + + // Redraw everything. + Rect rect = new Rect(0, 0, mBufferSize.width, mBufferSize.height); + //GeckoAppShell.sendEventToGecko(GeckoEvent.createDrawEvent(rect)); + LOKitShell.sendEvent(LOEvent.draw(rect)); + } else if ("Viewport:UpdateLater".equals(event)) { + Log.e(LOGTAG, "### Java side Viewport:UpdateLater()!"); + mUpdateViewportOnEndDraw = true; + } + } + + @Override + public void geometryChanged() { + /* Let Gecko know if the screensize has changed */ + sendResizeEventIfNecessary(); + render(); + } + + public int getWidth() { + return mBufferSize.width; + } + + public int getHeight() { + return mBufferSize.height; + } + + public ViewportMetrics getGeckoViewportMetrics() { + // Return a copy, as we modify this inside the Gecko thread + if (mGeckoViewport != null) + return new ViewportMetrics(mGeckoViewport); + return null; + } + + private void sendResizeEventIfNecessary() { + sendResizeEventIfNecessary(false); + } + + /** Used by robocop for testing purposes. Not for production use! This is called via reflection by robocop. */ + public void setDrawListener(DrawListener listener) { + mDrawListener = listener; + } + + /** Used by robocop for testing purposes. Not for production use! This is used via reflection by robocop. */ + public interface DrawListener { + public void drawFinished(int x, int y, int width, int height); + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoSoftwareLayerClient.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoSoftwareLayerClient.java new file mode 100644 index 000000000000..fa1d5adb3c70 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoSoftwareLayerClient.java @@ -0,0 +1,322 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Chris Lord <chrislord.net@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.libreoffice.LOKitShell; +import org.mozilla.gecko.gfx.CairoImage; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.LayerClient; +import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.gfx.LayerRenderer; +import org.mozilla.gecko.gfx.MultiTileLayer; +import org.mozilla.gecko.gfx.PointUtils; +import org.mozilla.gecko.gfx.WidgetTileLayer; +//import org.mozilla.gecko.GeckoAppShell; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.Log; +import java.nio.ByteBuffer; + +/** + * Transfers a software-rendered Gecko to an ImageLayer so that it can be rendered by our + * compositor. + * + * TODO: Throttle down Gecko's priority when we pan and zoom. + */ +public class GeckoSoftwareLayerClient extends GeckoLayerClient { + private static final String LOGTAG = "GeckoSoftwareLayerClient"; + + private int mFormat; + private IntSize mViewportSize; + private ByteBuffer mBuffer; + + private CairoImage mCairoImage; + + private static final IntSize TILE_SIZE = new IntSize(256, 256); + + // Whether or not the last paint we got used direct texturing + private boolean mHasDirectTexture; + + public GeckoSoftwareLayerClient(Context context) { + super(context); + + mFormat = CairoImage.FORMAT_ARGB32; + + mCairoImage = new CairoImage() { + @Override + public ByteBuffer getBuffer() { return mBuffer; } + @Override + public IntSize getSize() { return mBufferSize; } + @Override + public int getFormat() { return mFormat; } + }; + } + + protected void finalize() throws Throwable { + try { + if (mBuffer != null) + LOKitShell.freeDirectBuffer(mBuffer); + mBuffer = null; + } finally { + super.finalize(); + } + } + + public void setLayerController(LayerController layerController) { + super.setLayerController(layerController); + + layerController.setRoot(mTileLayer); + if (mGeckoViewport != null) { + layerController.setViewportMetrics(mGeckoViewport); + } + + //GeckoAppShell.registerGeckoEventListener("Viewport:UpdateAndDraw", this); + //GeckoAppShell.registerGeckoEventListener("Viewport:UpdateLater", this); + //GeckoAppShell.registerGeckoEventListener("Checkerboard:Toggle", this); + + // XXX: Review pcwalton. This signature changed on m-c, should force = false here? + sendResizeEventIfNecessary(false); + } + + @Override + protected boolean handleDirectTextureChange(boolean hasDirectTexture) { + if (mTileLayer != null && hasDirectTexture == mHasDirectTexture) + return false; + + mHasDirectTexture = hasDirectTexture; + + if (mHasDirectTexture) { + Log.i(LOGTAG, "Creating WidgetTileLayer"); + mTileLayer = new WidgetTileLayer(mCairoImage); + } else { + Log.i(LOGTAG, "Creating MultiTileLayer"); + mTileLayer = new MultiTileLayer(mCairoImage, TILE_SIZE); + } + + getLayerController().setRoot(mTileLayer); + + // Force a resize event to be sent because the results of this + // are different depending on what tile system we're using + sendResizeEventIfNecessary(true); + + return true; + } + + @Override + protected boolean shouldDrawProceed(int tileWidth, int tileHeight) { + // Make sure the tile-size matches. If it doesn't, we could crash trying + // to access invalid memory. + if (mHasDirectTexture) { + if (tileWidth != 0 || tileHeight != 0) { + Log.e(LOGTAG, "Aborting draw, incorrect tile size of " + tileWidth + "x" + + tileHeight); + return false; + } + } else { + if (tileWidth != TILE_SIZE.width || tileHeight != TILE_SIZE.height) { + Log.e(LOGTAG, "Aborting draw, incorrect tile size of " + tileWidth + "x" + + tileHeight); + return false; + } + } + + return true; + } + + @Override + public Rect beginDrawing(int width, int height, int tileWidth, int tileHeight, + String metadata, boolean hasDirectTexture) { + Rect bufferRect = super.beginDrawing(width, height, tileWidth, tileHeight, + metadata, hasDirectTexture); + if (bufferRect == null) { + return bufferRect; + } + + // If the window size has changed, reallocate the buffer to match. + if (mBufferSize.width != width || mBufferSize.height != height) { + mBufferSize = new IntSize(width, height); + + // Reallocate the buffer if necessary + if (mTileLayer instanceof MultiTileLayer) { + int bpp = CairoUtils.bitsPerPixelForCairoFormat(mFormat) / 8; + int size = mBufferSize.getArea() * bpp; + if (mBuffer == null || mBuffer.capacity() != size) { + // Free the old buffer + if (mBuffer != null) { + LOKitShell.freeDirectBuffer(mBuffer); + mBuffer = null; + } + + mBuffer = LOKitShell.allocateDirectBuffer(size); + } + } + } + + return bufferRect; + } + + @Override + protected void updateLayerAfterDraw(Rect updatedRect) { + if (!(mTileLayer instanceof MultiTileLayer)) { + return; + } + + ((MultiTileLayer)mTileLayer).invalidate(updatedRect); + } + + private void copyPixelsFromMultiTileLayer(Bitmap target) { + Canvas c = new Canvas(target); + ByteBuffer tileBuffer = mBuffer.slice(); + int bpp = CairoUtils.bitsPerPixelForCairoFormat(mFormat) / 8; + + for (int y = 0; y < mBufferSize.height; y += TILE_SIZE.height) { + for (int x = 0; x < mBufferSize.width; x += TILE_SIZE.width) { + // Calculate tile size + IntSize tileSize = new IntSize(Math.min(mBufferSize.width - x, TILE_SIZE.width), + Math.min(mBufferSize.height - y, TILE_SIZE.height)); + + // Create a Bitmap from this tile + Bitmap tile = Bitmap.createBitmap(tileSize.width, tileSize.height, + CairoUtils.cairoFormatTobitmapConfig(mFormat)); + tile.copyPixelsFromBuffer(tileBuffer.asIntBuffer()); + + // Copy the tile to the master Bitmap and recycle it + c.drawBitmap(tile, x, y, null); + tile.recycle(); + + // Progress the buffer to the next tile + tileBuffer.position(tileSize.getArea() * bpp); + tileBuffer = tileBuffer.slice(); + } + } + } + + @Override + protected void tileLayerUpdated() { + /* No-op. */ + } + + @Override + public Bitmap getBitmap() { + if (mTileLayer == null) + return null; + + // Begin a tile transaction, otherwise the buffer can be destroyed while + // we're reading from it. + beginTransaction(mTileLayer); + try { + if (mBuffer == null || mBufferSize.width <= 0 || mBufferSize.height <= 0) + return null; + try { + Bitmap b = null; + + if (mTileLayer instanceof MultiTileLayer) { + b = Bitmap.createBitmap(mBufferSize.width, mBufferSize.height, + CairoUtils.cairoFormatTobitmapConfig(mFormat)); + copyPixelsFromMultiTileLayer(b); + } else { + Log.w(LOGTAG, "getBitmap() called on a layer (" + mTileLayer + ") we don't know how to get a bitmap from"); + } + + return b; + } catch (OutOfMemoryError oom) { + Log.w(LOGTAG, "Unable to create bitmap", oom); + return null; + } + } finally { + endTransaction(mTileLayer); + } + } + + /** Returns the back buffer. This function is for Gecko to use. */ + public ByteBuffer lockBuffer() { + return mBuffer; + } + + /** + * Gecko calls this function to signal that it is done with the back buffer. After this call, + * it is forbidden for Gecko to touch the buffer. + */ + public void unlockBuffer() { + /* no-op */ + } + + @Override + public int getType() { + return LAYER_CLIENT_TYPE_SOFTWARE; + } + + @Override + protected IntSize getBufferSize() { + // Round up depending on layer implementation to remove texture wastage + if (!mHasDirectTexture) { + // Round to the next multiple of the tile size + return new IntSize(((mScreenSize.width + LayerController.MIN_BUFFER.width - 1) / + TILE_SIZE.width + 1) * TILE_SIZE.width, + ((mScreenSize.height + LayerController.MIN_BUFFER.height - 1) / + TILE_SIZE.height + 1) * TILE_SIZE.height); + } + + int maxSize = getLayerController().getView().getMaxTextureSize(); + + // XXX Integrate gralloc/tiling work to circumvent this + if (mScreenSize.width > maxSize || mScreenSize.height > maxSize) { + throw new RuntimeException("Screen size of " + mScreenSize + + " larger than maximum texture size of " + maxSize); + } + + // Round to next power of two until we have NPOT texture support, respecting maximum + // texture size + return new IntSize(Math.min(maxSize, IntSize.nextPowerOfTwo(mScreenSize.width + + LayerController.MIN_BUFFER.width)), + Math.min(maxSize, IntSize.nextPowerOfTwo(mScreenSize.height + + LayerController.MIN_BUFFER.height))); + } + + @Override + protected IntSize getTileSize() { + // Round up depending on layer implementation to remove texture wastage + return !mHasDirectTexture ? TILE_SIZE : new IntSize(0, 0); + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/InputConnectionHandler.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/InputConnectionHandler.java new file mode 100644 index 000000000000..6fef53fb0535 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/InputConnectionHandler.java @@ -0,0 +1,15 @@ +package org.mozilla.gecko.gfx; + +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.KeyEvent; + +public interface InputConnectionHandler +{ + InputConnection onCreateInputConnection(EditorInfo outAttrs); + boolean onKeyPreIme(int keyCode, KeyEvent event); + boolean onKeyDown(int keyCode, KeyEvent event); + boolean onKeyLongPress(int keyCode, KeyEvent event); + boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event); + boolean onKeyUp(int keyCode, KeyEvent event); +} diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/IntSize.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/IntSize.java new file mode 100644 index 000000000000..ee43b1bf557b --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/IntSize.java @@ -0,0 +1,103 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.FloatSize; +import org.json.JSONException; +import org.json.JSONObject; +import java.lang.Math; + +public class IntSize { + public final int width, height; + + public IntSize(IntSize size) { width = size.width; height = size.height; } + public IntSize(int inWidth, int inHeight) { width = inWidth; height = inHeight; } + + public IntSize(FloatSize size) { + width = Math.round(size.width); + height = Math.round(size.height); + } + + public IntSize(JSONObject json) { + try { + width = json.getInt("width"); + height = json.getInt("height"); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + public int getArea() { + return width * height; + } + + public boolean equals(IntSize size) { + return ((size.width == width) && (size.height == height)); + } + + public boolean isPositive() { + return (width > 0 && height > 0); + } + + @Override + public String toString() { return "(" + width + "," + height + ")"; } + + public IntSize scale(float factor) { + return new IntSize(Math.round(width * factor), + Math.round(height * factor)); + } + + /* Returns the power of two that is greater than or equal to value */ + public static int nextPowerOfTwo(int value) { + // code taken from http://acius2.blogspot.com/2007/11/calculating-next-power-of-2.html + if (0 == value--) { + return 1; + } + value = (value >> 1) | value; + value = (value >> 2) | value; + value = (value >> 4) | value; + value = (value >> 8) | value; + value = (value >> 16) | value; + return value + 1; + } + + public IntSize nextPowerOfTwo() { + return new IntSize(nextPowerOfTwo(width), nextPowerOfTwo(height)); + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/Layer.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/Layer.java new file mode 100644 index 000000000000..2dd3316f0016 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/Layer.java @@ -0,0 +1,242 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Chris Lord <chrislord.net@gmail.com> + * Arkady Blyakher <rkadyb@mit.edu> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Point; +import android.graphics.RectF; +import android.graphics.Region; + +import org.mozilla.gecko.util.FloatUtils; + +import java.nio.FloatBuffer; +import java.util.concurrent.locks.ReentrantLock; + +public abstract class Layer { + private final ReentrantLock mTransactionLock; + protected Point mOrigin; + protected float mResolution; + private boolean mInTransaction; + private Point mNewOrigin; + private float mNewResolution; + private LayerView mView; + + public Layer() { + mTransactionLock = new ReentrantLock(); + mOrigin = new Point(0, 0); + mResolution = 1.0f; + } + + /** + * Updates the layer. This returns false if there is still work to be done + * after this update. + */ + public final boolean update(RenderContext context) { + if (mTransactionLock.isHeldByCurrentThread()) { + throw new RuntimeException("draw() called while transaction lock held by this " + + "thread?!"); + } + + if (mTransactionLock.tryLock()) { + try { + return performUpdates(context); + } finally { + mTransactionLock.unlock(); + } + } + + return false; + } + + /** + * Subclasses override this function to draw the layer. + */ + public abstract void draw(RenderContext context); + + /** + * Subclasses override this function to provide access to the size of the layer. + */ + public abstract IntSize getSize(); + + /** + * Given the intrinsic size of the layer, returns the pixel boundaries of the layer rect. + */ + protected RectF getBounds(RenderContext context, FloatSize size) { + float scaleFactor = context.zoomFactor / mResolution; + float x = mOrigin.x * scaleFactor, y = mOrigin.y * scaleFactor; + float width = size.width * scaleFactor, height = size.height * scaleFactor; + return new RectF(x, y, x + width, y + height); + } + + /** + * Returns the region of the layer that is considered valid. The default + * implementation of this will return the bounds of the layer, but this + * may be overridden. + */ + public Region getValidRegion(RenderContext context) { + return new Region(RectUtils.round(getBounds(context, new FloatSize(getSize())))); + } + + /** + * Call this before modifying the layer. Note that, for TileLayers, "modifying the layer" + * includes altering the underlying CairoImage in any way. Thus you must call this function + * before modifying the byte buffer associated with this layer. + * <p/> + * This function may block, so you should never call this on the main UI thread. + */ + public void beginTransaction(LayerView aView) { + if (mTransactionLock.isHeldByCurrentThread()) + throw new RuntimeException("Nested transactions are not supported"); + mTransactionLock.lock(); + mView = aView; + mInTransaction = true; + mNewResolution = mResolution; + } + + public void beginTransaction() { + beginTransaction(null); + } + + /** + * Call this when you're done modifying the layer. + */ + public void endTransaction() { + if (!mInTransaction) + throw new RuntimeException("endTransaction() called outside a transaction"); + mInTransaction = false; + mTransactionLock.unlock(); + + if (mView != null) + mView.requestRender(); + } + + /** + * Returns true if the layer is currently in a transaction and false otherwise. + */ + protected boolean inTransaction() { + return mInTransaction; + } + + /** + * Returns the current layer origin. + */ + public Point getOrigin() { + return mOrigin; + } + + /** + * Sets the origin. Only valid inside a transaction. + */ + public void setOrigin(Point newOrigin) { + if (!mInTransaction) + throw new RuntimeException("setOrigin() is only valid inside a transaction"); + mNewOrigin = newOrigin; + } + + /** + * Returns the current layer's resolution. + */ + public float getResolution() { + return mResolution; + } + + /** + * Sets the layer resolution. This value is used to determine how many pixels per + * device pixel this layer was rendered at. This will be reflected by scaling by + * the reciprocal of the resolution in the layer's transform() function. + * Only valid inside a transaction. + */ + public void setResolution(float newResolution) { + if (!mInTransaction) + throw new RuntimeException("setResolution() is only valid inside a transaction"); + mNewResolution = newResolution; + } + + /** + * Subclasses may override this method to perform custom layer updates. This will be called + * with the transaction lock held. Subclass implementations of this method must call the + * superclass implementation. Returns false if there is still work to be done after this + * update is complete. + */ + protected boolean performUpdates(RenderContext context) { + if (mNewOrigin != null) { + mOrigin = mNewOrigin; + mNewOrigin = null; + } + if (mNewResolution != 0.0f) { + mResolution = mNewResolution; + mNewResolution = 0.0f; + } + + return true; + } + + protected boolean dimensionChangesPending() { + return (mNewOrigin != null) || (mNewResolution != 0.0f); + } + + public static class RenderContext { + public final RectF viewport; + public final FloatSize pageSize; + public final float zoomFactor; + public final int positionHandle; + public final int textureHandle; + public final FloatBuffer coordBuffer; + + public RenderContext(RectF aViewport, FloatSize aPageSize, float aZoomFactor, + int aPositionHandle, int aTextureHandle, FloatBuffer aCoordBuffer) { + viewport = aViewport; + pageSize = aPageSize; + zoomFactor = aZoomFactor; + positionHandle = aPositionHandle; + textureHandle = aTextureHandle; + coordBuffer = aCoordBuffer; + } + + public boolean fuzzyEquals(RenderContext other) { + if (other == null) { + return false; + } + return RectUtils.fuzzyEquals(viewport, other.viewport) + && pageSize.fuzzyEquals(other.pageSize) + && FloatUtils.fuzzyEquals(zoomFactor, other.zoomFactor); + } + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerClient.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerClient.java new file mode 100644 index 000000000000..4f461088203e --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerClient.java @@ -0,0 +1,81 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +/** + * A layer client provides tiles and manages other information used by the layer controller. + */ +public abstract class LayerClient { + private LayerController mLayerController; + + public abstract void geometryChanged(); + + public abstract void viewportSizeChanged(); + + protected abstract void render(); + + public LayerController getLayerController() { + return mLayerController; + } + + public void setLayerController(LayerController layerController) { + mLayerController = layerController; + } + + /** + * A utility function for calling Layer.beginTransaction with the + * appropriate LayerView. + */ + public void beginTransaction(Layer aLayer) { + if (mLayerController != null) { + LayerView view = mLayerController.getView(); + if (view != null) { + aLayer.beginTransaction(view); + return; + } + } + + aLayer.beginTransaction(); + } + + // Included for symmetry. + public void endTransaction(Layer aLayer) { + aLayer.endTransaction(); + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerController.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerController.java new file mode 100644 index 000000000000..250dc84c69fe --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerController.java @@ -0,0 +1,534 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Chris Lord <chrislord.net@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.RectF; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View.OnTouchListener; + +import org.mozilla.gecko.ui.PanZoomController; +import org.mozilla.gecko.ui.SimpleScaleGestureDetector; + +import java.util.Timer; +import java.util.TimerTask; + +/** + * The layer controller manages a tile that represents the visible page. It does panning and + * zooming natively by delegating to a panning/zooming controller. Touch events can be dispatched + * to a higher-level view. + * <p/> + * Many methods require that the monitor be held, with a synchronized (controller) { ... } block. + */ +public class LayerController { + /* The extra area on the sides of the page that we want to buffer to help with + * smooth, asynchronous scrolling. Depending on a device's support for NPOT + * textures, this may be rounded up to the nearest power of two. + */ + public static final IntSize MIN_BUFFER = new IntSize(512, 1024); + private static final String LOGTAG = "GeckoLayerController"; + /* If the visible rect is within the danger zone (measured in pixels from each edge of a tile), + * we start aggressively redrawing to minimize checkerboarding. */ + private static final int DANGER_ZONE_X = 75; + private static final int DANGER_ZONE_Y = 150; + /* The time limit for pages to respond with preventDefault on touchevents + * before we begin panning the page */ + private static final int PREVENT_DEFAULT_TIMEOUT = 200; + private Layer mRootLayer; /* The root layer. */ + private LayerView mView; /* The main rendering view. */ + /* + * The panning and zooming controller, which interprets pan and zoom gestures for us and + * updates our visible rect appropriately. + */ + private Context mContext; /* The current context. */ + private ViewportMetrics mViewportMetrics; /* The current viewport metrics. */ + private boolean mWaitForTouchListeners; + private PanZoomController mPanZoomController; + private OnTouchListener mOnTouchListener; /* The touch listener. */ + private LayerClient mLayerClient; /* The layer client. */ + /* The new color for the checkerboard. */ + private int mCheckerboardColor; + private boolean mCheckerboardShouldShowChecks; + private boolean mForceRedraw; + private boolean allowDefaultActions = true; + private Timer allowDefaultTimer = null; + private boolean inTouchSession = false; + private PointF initialTouchLocation = null; + + public LayerController(Context context) { + mContext = context; + + mForceRedraw = true; + mViewportMetrics = new ViewportMetrics(); + mPanZoomController = new PanZoomController(this); + mView = new LayerView(context, this); + } + + public void setForceRedraw() { + mForceRedraw = true; + } + + public LayerClient getLayerClient() { + return mLayerClient; + } + + public void setLayerClient(LayerClient layerClient) { + mLayerClient = layerClient; + layerClient.setLayerController(this); + } + + public Layer getRoot() { + return mRootLayer; + } + + public void setRoot(Layer layer) { + mRootLayer = layer; + } + + public LayerView getView() { + return mView; + } + + public Context getContext() { + return mContext; + } + + public ViewportMetrics getViewportMetrics() { + return mViewportMetrics; + } + + /** + * Sets the entire viewport metrics at once. This function does not notify the layer client or + * the pan/zoom controller, so you will need to call notifyLayerClientOfGeometryChange() or + * notifyPanZoomControllerOfGeometryChange() after calling this. You must hold the monitor + * while calling this. + */ + public void setViewportMetrics(ViewportMetrics viewport) { + mViewportMetrics = new ViewportMetrics(viewport); + Log.d(LOGTAG, "setViewportMetrics: " + mViewportMetrics); + // this function may or may not be called on the UI thread, + // but repositionPluginViews must only be called on the UI thread. + //GeckoApp.mAppContext.runOnUiThread(new Runnable() { + // public void run() { + // GeckoApp.mAppContext.repositionPluginViews(false); + // } + //}); + mView.requestRender(); + } + + public RectF getViewport() { + return mViewportMetrics.getViewport(); + } + + public FloatSize getViewportSize() { + return mViewportMetrics.getSize(); + } + + /** + * The view calls this function to indicate that the viewport changed size. It must hold the + * monitor while calling it. + * <p/> + * TODO: Refactor this to use an interface. Expose that interface only to the view and not + * to the layer client. That way, the layer client won't be tempted to call this, which might + * result in an infinite loop. + */ + public void setViewportSize(FloatSize size) { + // Resize the viewport, and modify its zoom factor so that the page retains proportionally + // zoomed relative to the screen. + float oldHeight = mViewportMetrics.getSize().height; + float oldWidth = mViewportMetrics.getSize().width; + float oldZoomFactor = mViewportMetrics.getZoomFactor(); + mViewportMetrics.setSize(size); + + // if the viewport got larger (presumably because the vkb went away), and the page + // is smaller than the new viewport size, increase the page size so that the panzoomcontroller + // doesn't zoom in to make it fit (bug 718270). this page size change is in anticipation of + // gecko increasing the page size to match the new viewport size, which will happen the next + // time we get a draw update. + if (size.width >= oldWidth && size.height >= oldHeight) { + FloatSize pageSize = mViewportMetrics.getPageSize(); + if (pageSize.width < size.width || pageSize.height < size.height) { + mViewportMetrics.setPageSize(new FloatSize(Math.max(pageSize.width, size.width), + Math.max(pageSize.height, size.height))); + } + } + + PointF newFocus = new PointF(size.width / 2.0f, size.height / 2.0f); + float newZoomFactor = size.width * oldZoomFactor / oldWidth; + mViewportMetrics.scaleTo(newZoomFactor, newFocus); + + Log.d(LOGTAG, "setViewportSize: " + mViewportMetrics); + setForceRedraw(); + + if (mLayerClient != null) + mLayerClient.viewportSizeChanged(); + + notifyLayerClientOfGeometryChange(); + mPanZoomController.abortAnimation(); + mView.requestRender(); + } + + public FloatSize getPageSize() { + return mViewportMetrics.getPageSize(); + } + + /** + * Sets the current page size. You must hold the monitor while calling this. + */ + public void setPageSize(FloatSize size) { + if (mViewportMetrics.getPageSize().fuzzyEquals(size)) + return; + + mViewportMetrics.setPageSize(size); + Log.d(LOGTAG, "setPageSize: " + mViewportMetrics); + + // Page size is owned by the LayerClient, so no need to notify it of + // this change. + + mView.post(new Runnable() { + public void run() { + mPanZoomController.pageSizeUpdated(); + mView.requestRender(); + } + }); + } + + public PointF getOrigin() { + return mViewportMetrics.getOrigin(); + } + + public float getZoomFactor() { + return mViewportMetrics.getZoomFactor(); + } + + public Bitmap getBackgroundPattern() { + return getDrawable("background"); + } + + public Bitmap getShadowPattern() { + return getDrawable("shadow"); + } + + public PanZoomController getPanZoomController() { + return mPanZoomController; + } + + public GestureDetector.OnGestureListener getGestureListener() { + return mPanZoomController; + } + + public SimpleScaleGestureDetector.SimpleScaleGestureListener getScaleGestureListener() { + return mPanZoomController; + } + + public GestureDetector.OnDoubleTapListener getDoubleTapListener() { + return mPanZoomController; + } + + public Bitmap getDrawable(String name) { + Resources resources = mContext.getResources(); + int resourceID = resources.getIdentifier(name, "drawable", mContext.getPackageName()); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inScaled = false; + return BitmapFactory.decodeResource(mContext.getResources(), resourceID, options); + } + + public Bitmap getDrawable16(String name) { + Resources resources = mContext.getResources(); + int resourceID = resources.getIdentifier(name, "drawable", mContext.getPackageName()); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inScaled = false; + options.inPreferredConfig = Bitmap.Config.RGB_565; + return BitmapFactory.decodeResource(mContext.getResources(), resourceID, options); + } + + /** + * Scrolls the viewport by the given offset. You must hold the monitor while calling this. + */ + public void scrollBy(PointF point) { + PointF origin = mViewportMetrics.getOrigin(); + origin.offset(point.x, point.y); + mViewportMetrics.setOrigin(origin); + Log.d(LOGTAG, "scrollBy: " + mViewportMetrics); + + notifyLayerClientOfGeometryChange(); + //GeckoApp.mAppContext.repositionPluginViews(false); + mView.requestRender(); + } + + /** + * Scales the viewport, keeping the given focus point in the same place before and after the + * scale operation. You must hold the monitor while calling this. + */ + public void scaleWithFocus(float zoomFactor, PointF focus) { + mViewportMetrics.scaleTo(zoomFactor, focus); + Log.d(LOGTAG, "scaleWithFocus: " + mViewportMetrics + "; zf=" + zoomFactor); + + // We assume the zoom level will only be modified by the + // PanZoomController, so no need to notify it of this change. + notifyLayerClientOfGeometryChange(); + //GeckoApp.mAppContext.repositionPluginViews(false); + mView.requestRender(); + } + + public boolean post(Runnable action) { + return mView.post(action); + } + + public void setOnTouchListener(OnTouchListener onTouchListener) { + mOnTouchListener = onTouchListener; + } + + /** + * The view as well as the controller itself use this method to notify the layer client that + * the geometry changed. + */ + public void notifyLayerClientOfGeometryChange() { + if (mLayerClient != null) + mLayerClient.geometryChanged(); + } + + /** + * Aborts any pan/zoom animation that is currently in progress. + */ + public void abortPanZoomAnimation() { + if (mPanZoomController != null) { + mView.post(new Runnable() { + public void run() { + mPanZoomController.abortAnimation(); + } + }); + } + } + + /** + * Returns true if this controller is fine with performing a redraw operation or false if it + * would prefer that the action didn't take place. + */ + public boolean getRedrawHint() { + // FIXME: Allow redraw while a finger is down, but only if we're about to checkerboard. + // This requires fixing aboutToCheckerboard() to know about the new buffer size. + + if (mForceRedraw) { + mForceRedraw = false; + return true; + } + + return mPanZoomController.getRedrawHint(); + } + + private RectF getTileRect() { + if (mRootLayer == null) + return new RectF(); + + float x = mRootLayer.getOrigin().x, y = mRootLayer.getOrigin().y; + IntSize layerSize = mRootLayer.getSize(); + return new RectF(x, y, x + layerSize.width, y + layerSize.height); + } + + // Returns true if a checkerboard is about to be visible. + private boolean aboutToCheckerboard() { + // Increase the size of the viewport (and clamp to page boundaries), and + // intersect it with the tile's displayport to determine whether we're + // close to checkerboarding. + FloatSize pageSize = getPageSize(); + RectF adjustedViewport = RectUtils.expand(getViewport(), DANGER_ZONE_X, DANGER_ZONE_Y); + if (adjustedViewport.top < 0) adjustedViewport.top = 0; + if (adjustedViewport.left < 0) adjustedViewport.left = 0; + if (adjustedViewport.right > pageSize.width) adjustedViewport.right = pageSize.width; + if (adjustedViewport.bottom > pageSize.height) adjustedViewport.bottom = pageSize.height; + + return !getTileRect().contains(adjustedViewport); + } + + /** + * Converts a point from layer view coordinates to layer coordinates. In other words, given a + * point measured in pixels from the top left corner of the layer view, returns the point in + * pixels measured from the top left corner of the root layer, in the coordinate system of the + * layer itself. This method is used by the viewport controller as part of the process of + * translating touch events to Gecko's coordinate system. + */ + public PointF convertViewPointToLayerPoint(PointF viewPoint) { + if (mRootLayer == null) + return null; + + // Undo the transforms. + PointF origin = mViewportMetrics.getOrigin(); + PointF newPoint = new PointF(origin.x, origin.y); + newPoint.offset(viewPoint.x, viewPoint.y); + + Point rootOrigin = mRootLayer.getOrigin(); + newPoint.offset(-rootOrigin.x, -rootOrigin.y); + + return newPoint; + } + + /* + * Gesture detection. This is handled only at a high level in this class; we dispatch to the + * pan/zoom controller to do the dirty work. + */ + public boolean onTouchEvent(MotionEvent event) { + int action = event.getAction(); + PointF point = new PointF(event.getX(), event.getY()); + + if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { + mView.clearEventQueue(); + initialTouchLocation = point; + allowDefaultActions = !mWaitForTouchListeners; + post(new Runnable() { + public void run() { + preventPanning(mWaitForTouchListeners); + } + }); + } + + // After the initial touch, ignore touch moves until they exceed a minimum distance. + if (initialTouchLocation != null && (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_MOVE) { + if (PointUtils.subtract(point, initialTouchLocation).length() > PanZoomController.PAN_THRESHOLD) { + initialTouchLocation = null; + } else { + return !allowDefaultActions; + } + } + + if (mOnTouchListener != null) + mOnTouchListener.onTouch(mView, event); + + if (!mWaitForTouchListeners) + return !allowDefaultActions; + + boolean createTimer = false; + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_MOVE: { + if (!inTouchSession && allowDefaultTimer == null) { + inTouchSession = true; + createTimer = true; + } + break; + } + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + // if we still have initialTouchLocation, we haven't fired any + // touchmove events. We should start the timer to wait for preventDefault + // from touchstart. If we don't hear from it we fire mouse events + if (initialTouchLocation != null) + createTimer = true; + inTouchSession = false; + } + } + + if (createTimer) { + if (allowDefaultTimer != null) { + allowDefaultTimer.cancel(); + } + allowDefaultTimer = new Timer(); + allowDefaultTimer.schedule(new TimerTask() { + public void run() { + post(new Runnable() { + public void run() { + preventPanning(false); + } + }); + } + }, PREVENT_DEFAULT_TIMEOUT); + } + + return !allowDefaultActions; + } + + public void preventPanning(boolean aValue) { + if (allowDefaultTimer != null) { + allowDefaultTimer.cancel(); + allowDefaultTimer.purge(); + allowDefaultTimer = null; + } + if (aValue == allowDefaultActions) { + allowDefaultActions = !aValue; + + if (aValue) { + mView.clearEventQueue(); + mPanZoomController.cancelTouch(); + } else { + mView.processEventQueue(); + } + } + } + + public void setWaitForTouchListeners(boolean aValue) { + mWaitForTouchListeners = aValue; + } + + /** + * Retrieves whether we should show checkerboard checks or not. + */ + public boolean checkerboardShouldShowChecks() { + return mCheckerboardShouldShowChecks; + } + + /** + * Retrieves the color that the checkerboard should be. + */ + public int getCheckerboardColor() { + return mCheckerboardColor; + } + + /** + * Sets a new color for the checkerboard. + */ + public void setCheckerboardColor(int newColor) { + mCheckerboardColor = newColor; + mView.requestRender(); + } + + /** + * Sets whether or not the checkerboard should show checkmarks. + */ + public void setCheckerboardShowChecks(boolean showChecks) { + mCheckerboardShouldShowChecks = showChecks; + mView.requestRender(); + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerRenderer.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerRenderer.java new file mode 100644 index 000000000000..6690aad8ec16 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerRenderer.java @@ -0,0 +1,689 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Chris Lord <chrislord.net@gmail.com> + * Arkady Blyakher <rkadyb@mit.edu> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.libreoffice.LOKitShell; +import org.mozilla.gecko.gfx.BufferedCairoImage; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.Layer.RenderContext; +import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.gfx.NinePatchTileLayer; +import org.mozilla.gecko.gfx.SingleTileLayer; +import org.mozilla.gecko.gfx.TextureReaper; +import org.mozilla.gecko.gfx.TextureGenerator; +import org.mozilla.gecko.gfx.TextLayer; +import org.mozilla.gecko.gfx.TileLayer; +//import org.mozilla.gecko.GeckoAppShell; +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.graphics.RegionIterator; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.os.SystemClock; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.WindowManager; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.ArrayList; + +/** + * The layer renderer implements the rendering logic for a layer view. + */ +public class LayerRenderer implements GLSurfaceView.Renderer { + private static final String LOGTAG = "GeckoLayerRenderer"; + private static final String PROFTAG = "GeckoLayerRendererProf"; + + /* + * The amount of time a frame is allowed to take to render before we declare it a dropped + * frame. + */ + private static final int MAX_FRAME_TIME = 16; /* 1000 ms / 60 FPS */ + + private static final int FRAME_RATE_METER_WIDTH = 64; + private static final int FRAME_RATE_METER_HEIGHT = 32; + + private final LayerView mView; + private final SingleTileLayer mBackgroundLayer; + private final CheckerboardImage mCheckerboardImage; + private final SingleTileLayer mCheckerboardLayer; + private final NinePatchTileLayer mShadowLayer; + private final TextLayer mFrameRateLayer; + private final ScrollbarLayer mHorizScrollLayer; + private final ScrollbarLayer mVertScrollLayer; + private final FadeRunnable mFadeRunnable; + private final FloatBuffer mCoordBuffer; + private RenderContext mLastPageContext; + private int mMaxTextureSize; + + private ArrayList<Layer> mExtraLayers = new ArrayList<Layer>(); + + // Dropped frames display + private int[] mFrameTimings; + private int mCurrentFrame, mFrameTimingsSum, mDroppedFrames; + private boolean mShowFrameRate; + + // Render profiling output + private int mFramesRendered; + private float mCompleteFramesRendered; + private boolean mProfileRender; + private long mProfileOutputTime; + + /* Used by robocop for testing purposes */ + private IntBuffer mPixelBuffer; + + // Used by GLES 2.0 + private int mProgram; + private int mPositionHandle; + private int mTextureHandle; + private int mSampleHandle; + private int mTMatrixHandle; + + // column-major matrix applied to each vertex to shift the viewport from + // one ranging from (-1, -1),(1,1) to (0,0),(1,1) and to scale all sizes by + // a factor of 2 to fill up the screen + private static final float[] TEXTURE_MATRIX = { + 2.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 2.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 2.0f, 0.0f, + -1.0f, -1.0f, 0.0f, 1.0f + }; + + private static final int COORD_BUFFER_SIZE = 20; + + // The shaders run on the GPU directly, the vertex shader is only applying the + // matrix transform detailed above + private static final String VERTEX_SHADER = + "uniform mat4 uTMatrix;\n" + + "attribute vec4 vPosition;\n" + + "attribute vec2 aTexCoord;\n" + + "varying vec2 vTexCoord;\n" + + "void main() {\n" + + " gl_Position = uTMatrix * vPosition;\n" + + " vTexCoord = aTexCoord;\n" + + "}\n"; + + // Note we flip the y-coordinate in the fragment shader from a + // coordinate system with (0,0) in the top left to one with (0,0) in + // the bottom left. + private static final String FRAGMENT_SHADER = + "precision mediump float;\n" + + "varying vec2 vTexCoord;\n" + + "uniform sampler2D sTexture;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(sTexture, vec2(vTexCoord.x, 1.0 - vTexCoord.y));\n" + + "}\n"; + + public LayerRenderer(LayerView view) { + mView = view; + + LayerController controller = view.getController(); + + CairoImage backgroundImage = new BufferedCairoImage(controller.getBackgroundPattern()); + mBackgroundLayer = new SingleTileLayer(true, backgroundImage); + + mCheckerboardImage = new CheckerboardImage(); + mCheckerboardLayer = new SingleTileLayer(true, mCheckerboardImage); + + CairoImage shadowImage = new BufferedCairoImage(controller.getShadowPattern()); + mShadowLayer = new NinePatchTileLayer(shadowImage); + + IntSize frameRateLayerSize = new IntSize(FRAME_RATE_METER_WIDTH, FRAME_RATE_METER_HEIGHT); + mFrameRateLayer = TextLayer.create(frameRateLayerSize, "-- ms/--"); + + mHorizScrollLayer = ScrollbarLayer.create(false); + mVertScrollLayer = ScrollbarLayer.create(true); + mFadeRunnable = new FadeRunnable(); + + mFrameTimings = new int[60]; + mCurrentFrame = mFrameTimingsSum = mDroppedFrames = 0; + mShowFrameRate = false; + + // Initialize the FloatBuffer that will be used to store all vertices and texture + // coordinates in draw() commands. + ByteBuffer byteBuffer = LOKitShell.allocateDirectBuffer(COORD_BUFFER_SIZE * 4); + byteBuffer.order(ByteOrder.nativeOrder()); + mCoordBuffer = byteBuffer.asFloatBuffer(); + } + + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + checkMonitoringEnabled(); + createProgram(); + activateProgram(); + } + + public void createProgram() { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER); + int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER); + + mProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(mProgram, vertexShader); // add the vertex shader to program + GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program + GLES20.glLinkProgram(mProgram); // creates OpenGL program executables + + // Get handles to the vertex shader's vPosition, aTexCoord, sTexture, and uTMatrix members. + mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); + mTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoord"); + mSampleHandle = GLES20.glGetUniformLocation(mProgram, "sTexture"); + mTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uTMatrix"); + + int maxTextureSizeResult[] = new int[1]; + GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeResult, 0); + mMaxTextureSize = maxTextureSizeResult[0]; + } + + // Activates the shader program. + public void activateProgram() { + // Add the program to the OpenGL environment + GLES20.glUseProgram(mProgram); + + // Set the transformation matrix + GLES20.glUniformMatrix4fv(mTMatrixHandle, 1, false, TEXTURE_MATRIX, 0); + + Log.e(LOGTAG, "### Position handle is " + mPositionHandle + ", texture handle is " + + mTextureHandle + ", last error is " + GLES20.glGetError()); + + // Enable the arrays from which we get the vertex and texture coordinates + GLES20.glEnableVertexAttribArray(mPositionHandle); + GLES20.glEnableVertexAttribArray(mTextureHandle); + + GLES20.glUniform1i(mSampleHandle, 0); + + TextureGenerator.get().fill(); + + // TODO: Move these calls into a separate deactivate() call that is called after the + // underlay and overlay are rendered. + } + + // Deactivates the shader program. This must be done to avoid crashes after returning to the + // Gecko C++ compositor from Java. + public void deactivateProgram() { + GLES20.glDisableVertexAttribArray(mTextureHandle); + GLES20.glDisableVertexAttribArray(mPositionHandle); + GLES20.glUseProgram(0); + } + + public int getMaxTextureSize() { + return mMaxTextureSize; + } + + public void addLayer(Layer layer) { + LayerController controller = mView.getController(); + + synchronized (controller) { + if (mExtraLayers.contains(layer)) { + mExtraLayers.remove(layer); + } + + mExtraLayers.add(layer); + } + } + + public void removeLayer(Layer layer) { + LayerController controller = mView.getController(); + + synchronized (controller) { + mExtraLayers.remove(layer); + } + } + + /** + * Called whenever a new frame is about to be drawn. + */ + public void onDrawFrame(GL10 gl) { + RenderContext pageContext = createPageContext(), screenContext = createScreenContext(); + Frame frame = createFrame(pageContext, screenContext); + synchronized (mView.getController()) { + frame.beginDrawing(); + frame.drawBackground(); + frame.drawRootLayer(); + frame.drawForeground(); + frame.endDrawing(); + } + } + + private void printCheckerboardStats() { + Log.d(PROFTAG, "Frames rendered over last 1000ms: " + mCompleteFramesRendered + "/" + mFramesRendered); + mFramesRendered = 0; + mCompleteFramesRendered = 0; + } + + /** Used by robocop for testing purposes. Not for production use! */ + IntBuffer getPixels() { + IntBuffer pixelBuffer = IntBuffer.allocate(mView.getWidth() * mView.getHeight()); + synchronized (pixelBuffer) { + mPixelBuffer = pixelBuffer; + mView.requestRender(); + try { + pixelBuffer.wait(); + } catch (InterruptedException ie) { + } + mPixelBuffer = null; + } + return pixelBuffer; + } + + public RenderContext createScreenContext() { + LayerController layerController = mView.getController(); + IntSize viewportSize = new IntSize(layerController.getViewportSize()); + RectF viewport = new RectF(0.0f, 0.0f, viewportSize.width, viewportSize.height); + FloatSize pageSize = new FloatSize(layerController.getPageSize()); + return createContext(viewport, pageSize, 1.0f); + } + + public RenderContext createPageContext() { + LayerController layerController = mView.getController(); + + Rect viewport = new Rect(); + layerController.getViewport().round(viewport); + + FloatSize pageSize = new FloatSize(layerController.getPageSize()); + float zoomFactor = layerController.getZoomFactor(); + return createContext(new RectF(viewport), pageSize, zoomFactor); + } + + private RenderContext createContext(RectF viewport, FloatSize pageSize, float zoomFactor) { + return new RenderContext(viewport, pageSize, zoomFactor, mPositionHandle, mTextureHandle, + mCoordBuffer); + } + + private Rect getPageRect() { + LayerController controller = mView.getController(); + + Point origin = PointUtils.round(controller.getOrigin()); + IntSize pageSize = new IntSize(controller.getPageSize()); + + origin.negate(); + + return new Rect(origin.x, origin.y, + origin.x + pageSize.width, origin.y + pageSize.height); + } + + private Rect transformToScissorRect(Rect rect) { + LayerController controller = mView.getController(); + IntSize screenSize = new IntSize(controller.getViewportSize()); + + int left = Math.max(0, rect.left); + int top = Math.max(0, rect.top); + int right = Math.min(screenSize.width, rect.right); + int bottom = Math.min(screenSize.height, rect.bottom); + + return new Rect(left, screenSize.height - bottom, right, + (screenSize.height - bottom) + (bottom - top)); + } + + public void onSurfaceChanged(GL10 gl, final int width, final int height) { + GLES20.glViewport(0, 0, width, height); + + // updating the state in the view/controller/client should be + // done on the main UI thread, not the GL renderer thread + mView.post(new Runnable() { + public void run() { + mView.setViewportSize(new IntSize(width, height)); + moveFrameRateLayer(width, height); + } + }); + + /* TODO: Throw away tile images? */ + } + + private void updateDroppedFrames(long frameStartTime) { + int frameElapsedTime = (int)(SystemClock.uptimeMillis() - frameStartTime); + + /* Update the running statistics. */ + mFrameTimingsSum -= mFrameTimings[mCurrentFrame]; + mFrameTimingsSum += frameElapsedTime; + mDroppedFrames -= (mFrameTimings[mCurrentFrame] + 1) / MAX_FRAME_TIME; + mDroppedFrames += (frameElapsedTime + 1) / MAX_FRAME_TIME; + + mFrameTimings[mCurrentFrame] = frameElapsedTime; + mCurrentFrame = (mCurrentFrame + 1) % mFrameTimings.length; + + int averageTime = mFrameTimingsSum / mFrameTimings.length; + mFrameRateLayer.beginTransaction(); + try { + mFrameRateLayer.setText(averageTime + " ms/" + mDroppedFrames); + } finally { + mFrameRateLayer.endTransaction(); + } + } + + /* Given the new dimensions for the surface, moves the frame rate layer appropriately. */ + private void moveFrameRateLayer(int width, int height) { + mFrameRateLayer.beginTransaction(); + try { + Point origin = new Point(width - FRAME_RATE_METER_WIDTH - 8, + height - FRAME_RATE_METER_HEIGHT + 8); + mFrameRateLayer.setOrigin(origin); + } finally { + mFrameRateLayer.endTransaction(); + } + } + + private void checkMonitoringEnabled() { + /* Do this I/O off the main thread to minimize its impact on startup time. */ + new Thread(new Runnable() { + @Override + public void run() { + Context context = mView.getContext(); + SharedPreferences preferences = context.getSharedPreferences("GeckoApp", 0); + mShowFrameRate = preferences.getBoolean("showFrameRate", false); + mProfileRender = Log.isLoggable(PROFTAG, Log.DEBUG); + } + }).start(); + } + + private void updateCheckerboardLayer(RenderContext renderContext) { + int checkerboardColor = mView.getController().getCheckerboardColor(); + boolean showChecks = mView.getController().checkerboardShouldShowChecks(); + if (checkerboardColor == mCheckerboardImage.getColor() && + showChecks == mCheckerboardImage.getShowChecks()) { + return; + } + + mCheckerboardLayer.beginTransaction(); + try { + mCheckerboardImage.update(showChecks, checkerboardColor); + mCheckerboardLayer.invalidate(); + } finally { + mCheckerboardLayer.endTransaction(); + } + + mCheckerboardLayer.update(renderContext); + } + + /* + * create a vertex shader type (GLES20.GL_VERTEX_SHADER) + * or a fragment shader type (GLES20.GL_FRAGMENT_SHADER) + */ + private int loadShader(int type, String shaderCode) { + int shader = GLES20.glCreateShader(type); + GLES20.glShaderSource(shader, shaderCode); + GLES20.glCompileShader(shader); + return shader; + } + + public Frame createFrame(RenderContext pageContext, RenderContext screenContext) { + return new Frame(pageContext, screenContext); + } + + class FadeRunnable implements Runnable { + private boolean mStarted; + private long mRunAt; + + void scheduleStartFade(long delay) { + mRunAt = SystemClock.elapsedRealtime() + delay; + if (!mStarted) { + mView.postDelayed(this, delay); + mStarted = true; + } + } + + void scheduleNextFadeFrame() { + if (mStarted) { + Log.e(LOGTAG, "scheduleNextFadeFrame() called while scheduled for starting fade"); + } + mView.postDelayed(this, 1000L / 60L); // request another frame at 60fps + } + + boolean timeToFade() { + return !mStarted; + } + + public void run() { + long timeDelta = mRunAt - SystemClock.elapsedRealtime(); + if (timeDelta > 0) { + // the run-at time was pushed back, so reschedule + mView.postDelayed(this, timeDelta); + } else { + // reached the run-at time, execute + mStarted = false; + mView.requestRender(); + } + } + } + + public class Frame { + // The timestamp recording the start of this frame. + private long mFrameStartTime; + // A rendering context for page-positioned layers, and one for screen-positioned layers. + private RenderContext mPageContext, mScreenContext; + // Whether a layer was updated. + private boolean mUpdated; + + public Frame(RenderContext pageContext, RenderContext screenContext) { + mPageContext = pageContext; + mScreenContext = screenContext; + } + + private void setScissorRect() { + Rect scissorRect = transformToScissorRect(getPageRect()); + GLES20.glEnable(GLES20.GL_SCISSOR_TEST); + GLES20.glScissor(scissorRect.left, scissorRect.top, + scissorRect.width(), scissorRect.height()); + } + + public void beginDrawing() { + mFrameStartTime = SystemClock.uptimeMillis(); + + TextureReaper.get().reap(); + TextureGenerator.get().fill(); + + mUpdated = true; + + LayerController controller = mView.getController(); + Layer rootLayer = controller.getRoot(); + + if (!mPageContext.fuzzyEquals(mLastPageContext)) { + // the viewport or page changed, so show the scrollbars again + // as per UX decision + mVertScrollLayer.unfade(); + mHorizScrollLayer.unfade(); + mFadeRunnable.scheduleStartFade(ScrollbarLayer.FADE_DELAY); + } else if (mFadeRunnable.timeToFade()) { + boolean stillFading = mVertScrollLayer.fade() | mHorizScrollLayer.fade(); + if (stillFading) { + mFadeRunnable.scheduleNextFadeFrame(); + } + } + mLastPageContext = mPageContext; + + /* Update layers. */ + if (rootLayer != null) mUpdated &= rootLayer.update(mPageContext); + mUpdated &= mBackgroundLayer.update(mScreenContext); + mUpdated &= mShadowLayer.update(mPageContext); + updateCheckerboardLayer(mScreenContext); + mUpdated &= mFrameRateLayer.update(mScreenContext); + mUpdated &= mVertScrollLayer.update(mPageContext); + mUpdated &= mHorizScrollLayer.update(mPageContext); + + for (Layer layer : mExtraLayers) + mUpdated &= layer.update(mPageContext); + + GLES20.glDisable(GLES20.GL_SCISSOR_TEST); + + // If a layer update requires further work, schedule another redraw + if (!mUpdated) + mView.requestRender(); + + PanningPerfAPI.recordFrameTime(); + + /* Used by robocop for testing purposes */ + IntBuffer pixelBuffer = mPixelBuffer; + if (mUpdated && pixelBuffer != null) { + synchronized (pixelBuffer) { + pixelBuffer.position(0); + GLES20.glReadPixels(0, 0, (int)mScreenContext.viewport.width(), + (int)mScreenContext.viewport.height(), GLES20.GL_RGBA, + GLES20.GL_UNSIGNED_BYTE, pixelBuffer); + pixelBuffer.notify(); + } + } + } + + public void drawBackground() { + /* Draw the background. */ + mBackgroundLayer.draw(mScreenContext); + + /* Draw the drop shadow, if we need to. */ + Rect pageRect = getPageRect(); + RectF untransformedPageRect = new RectF(0.0f, 0.0f, pageRect.width(), + pageRect.height()); + if (!untransformedPageRect.contains(mView.getController().getViewport())) + mShadowLayer.draw(mPageContext); + + /* Draw the checkerboard. */ + setScissorRect(); + mCheckerboardLayer.draw(mScreenContext); + GLES20.glDisable(GLES20.GL_SCISSOR_TEST); + } + + // Draws the layer the client added to us. + void drawRootLayer() { + Layer rootLayer = mView.getController().getRoot(); + if (rootLayer == null) { + return; + } + + setScissorRect(); + rootLayer.draw(mPageContext); + GLES20.glDisable(GLES20.GL_SCISSOR_TEST); + } + + public void drawForeground() { + Rect pageRect = getPageRect(); + LayerController controller = mView.getController(); + + /* Draw any extra layers that were added (likely plugins) */ + for (Layer layer : mExtraLayers) + layer.draw(mPageContext); + + /* Draw the vertical scrollbar. */ + IntSize screenSize = new IntSize(controller.getViewportSize()); + if (pageRect.height() > screenSize.height) + mVertScrollLayer.draw(mPageContext); + + /* Draw the horizontal scrollbar. */ + if (pageRect.width() > screenSize.width) + mHorizScrollLayer.draw(mPageContext); + + /* Measure how much of the screen is checkerboarding */ + Layer rootLayer = controller.getRoot(); + if ((rootLayer != null) && + (mProfileRender || PanningPerfAPI.isRecordingCheckerboard())) { + // Find out how much of the viewport area is valid + Rect viewport = RectUtils.round(mPageContext.viewport); + Region validRegion = rootLayer.getValidRegion(mPageContext); + validRegion.op(viewport, Region.Op.INTERSECT); + + float checkerboard = 0.0f; + if (!(validRegion.isRect() && validRegion.getBounds().equals(viewport))) { + int screenArea = viewport.width() * viewport.height(); + validRegion.op(viewport, Region.Op.REVERSE_DIFFERENCE); + + // XXX The assumption here is that a Region never has overlapping + // rects. This is true, as evidenced by reading the SkRegion + // source, but is not mentioned in the Android documentation, + // and so is liable to change. + // If it does change, this code will need to be reevaluated. + Rect r = new Rect(); + int checkerboardArea = 0; + for (RegionIterator i = new RegionIterator(validRegion); i.next(r);) { + checkerboardArea += r.width() * r.height(); + } + + checkerboard = checkerboardArea / (float)screenArea; + } + + PanningPerfAPI.recordCheckerboard(checkerboard); + + mCompleteFramesRendered += 1.0f - checkerboard; + mFramesRendered ++; + + if (mFrameStartTime - mProfileOutputTime > 1000) { + mProfileOutputTime = mFrameStartTime; + printCheckerboardStats(); + } + } + + /* Draw the FPS. */ + if (mShowFrameRate) { + updateDroppedFrames(mFrameStartTime); + + try { + GLES20.glEnable(GLES20.GL_BLEND); + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + mFrameRateLayer.draw(mScreenContext); + } finally { + GLES20.glDisable(GLES20.GL_BLEND); + } + } + } + + public void endDrawing() { + // If a layer update requires further work, schedule another redraw + if (!mUpdated) + mView.requestRender(); + + PanningPerfAPI.recordFrameTime(); + + /* Used by robocop for testing purposes */ + IntBuffer pixelBuffer = mPixelBuffer; + if (mUpdated && pixelBuffer != null) { + synchronized (pixelBuffer) { + pixelBuffer.position(0); + GLES20.glReadPixels(0, 0, (int)mScreenContext.viewport.width(), + (int)mScreenContext.viewport.height(), GLES20.GL_RGBA, + GLES20.GL_UNSIGNED_BYTE, pixelBuffer); + pixelBuffer.notify(); + } + } + } + } +} diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerView.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerView.java new file mode 100644 index 000000000000..a3c04fa600f2 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerView.java @@ -0,0 +1,230 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Arkady Blyakher <rkadyb@mit.edu> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +//import org.mozilla.gecko.GeckoInputConnection; +import org.mozilla.gecko.gfx.FloatSize; +import org.mozilla.gecko.gfx.InputConnectionHandler; +import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.ui.SimpleScaleGestureDetector; +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.view.View; +import android.view.GestureDetector; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.ScaleGestureDetector; +import android.widget.RelativeLayout; +import android.util.Log; +import java.nio.IntBuffer; +import java.util.LinkedList; + +/** + * A view rendered by the layer compositor. + * + * This view delegates to LayerRenderer to actually do the drawing. Its role is largely that of a + * mediator between the LayerRenderer and the LayerController. + */ +public class LayerView extends FlexibleGLSurfaceView { + private Context mContext; + private LayerController mController; + private InputConnectionHandler mInputConnectionHandler; + private LayerRenderer mRenderer; + private GestureDetector mGestureDetector; + private SimpleScaleGestureDetector mScaleGestureDetector; + private long mRenderTime; + private boolean mRenderTimeReset; + private static String LOGTAG = "GeckoLayerView"; + /* List of events to be processed if the page does not prevent them. Should only be touched on the main thread */ + private LinkedList<MotionEvent> mEventQueue = new LinkedList<MotionEvent>(); + + + public LayerView(Context context, LayerController controller) { + super(context); + + mContext = context; + mController = controller; + mRenderer = new LayerRenderer(this); + setRenderer(mRenderer); + mGestureDetector = new GestureDetector(context, controller.getGestureListener()); + mScaleGestureDetector = + new SimpleScaleGestureDetector(controller.getScaleGestureListener()); + mGestureDetector.setOnDoubleTapListener(controller.getDoubleTapListener()); + mInputConnectionHandler = null; + + setFocusable(true); + setFocusableInTouchMode(true); + + createGLThread(); + } + + private void addToEventQueue(MotionEvent event) { + MotionEvent copy = MotionEvent.obtain(event); + mEventQueue.add(copy); + } + + public void processEventQueue() { + MotionEvent event = mEventQueue.poll(); + while(event != null) { + processEvent(event); + event = mEventQueue.poll(); + } + } + + public void clearEventQueue() { + mEventQueue.clear(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mController.onTouchEvent(event)) { + addToEventQueue(event); + return true; + } + return processEvent(event); + } + + private boolean processEvent(MotionEvent event) { + if (mGestureDetector.onTouchEvent(event)) + return true; + mScaleGestureDetector.onTouchEvent(event); + if (mScaleGestureDetector.isInProgress()) + return true; + mController.getPanZoomController().onTouchEvent(event); + return true; + } + + public LayerController getController() { return mController; } + + /** The LayerRenderer calls this to indicate that the window has changed size. */ + public void setViewportSize(IntSize size) { + mController.setViewportSize(new FloatSize(size)); + } + + //public GeckoInputConnection setInputConnectionHandler() { + // GeckoInputConnection geckoInputConnection = GeckoInputConnection.create(this); + // mInputConnectionHandler = geckoInputConnection; + // return geckoInputConnection; + //} + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + if (mInputConnectionHandler != null) + return mInputConnectionHandler.onCreateInputConnection(outAttrs); + return null; + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (mInputConnectionHandler != null) + return mInputConnectionHandler.onKeyPreIme(keyCode, event); + return false; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (mInputConnectionHandler != null) + return mInputConnectionHandler.onKeyDown(keyCode, event); + return false; + } + + @Override + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + if (mInputConnectionHandler != null) + return mInputConnectionHandler.onKeyLongPress(keyCode, event); + return false; + } + + @Override + public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + if (mInputConnectionHandler != null) + return mInputConnectionHandler.onKeyMultiple(keyCode, repeatCount, event); + return false; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (mInputConnectionHandler != null) + return mInputConnectionHandler.onKeyUp(keyCode, event); + return false; + } + + @Override + public void requestRender() { + super.requestRender(); + + synchronized(this) { + if (!mRenderTimeReset) { + mRenderTimeReset = true; + mRenderTime = System.nanoTime(); + } + } + } + + public void addLayer(Layer layer) { + mRenderer.addLayer(layer); + } + + public void removeLayer(Layer layer) { + mRenderer.removeLayer(layer); + } + + /** + * Returns the time elapsed between the first call of requestRender() after + * the last call of getRenderTime(), in nanoseconds. + */ + public long getRenderTime() { + synchronized(this) { + mRenderTimeReset = false; + return System.nanoTime() - mRenderTime; + } + } + + public int getMaxTextureSize() { + return mRenderer.getMaxTextureSize(); + } + + /** Used by robocop for testing purposes. Not for production use! This is called via reflection by robocop. */ + public IntBuffer getPixels() { + return mRenderer.getPixels(); + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/MultiTileLayer.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/MultiTileLayer.java new file mode 100644 index 000000000000..3514b4207461 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/MultiTileLayer.java @@ -0,0 +1,300 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- +* ***** BEGIN LICENSE BLOCK ***** +* Version: MPL 1.1/GPL 2.0/LGPL 2.1 +* +* The contents of this file are subject to the Mozilla Public License Version +* 1.1 (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* http://www.mozilla.org/MPL/ +* +* Software distributed under the License is distributed on an "AS IS" basis, +* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +* for the specific language governing rights and limitations under the +* License. +* +* The Original Code is Mozilla Android code. +* +* The Initial Developer of the Original Code is Mozilla Foundation. +* Portions created by the Initial Developer are Copyright (C) 2011-2012 +* the Initial Developer. All Rights Reserved. +* +* Contributor(s): +* Chris Lord <chrislord.net@gmail.com> +* Arkady Blyakher <rkadyb@mit.edu> +* +* Alternatively, the contents of this file may be used under the terms of +* either the GNU General Public License Version 2 or later (the "GPL"), or +* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +* in which case the provisions of the GPL or the LGPL are applicable instead +* of those above. If you wish to allow use of your version of this file only +* under the terms of either the GPL or the LGPL, and not to allow others to +* use your version of this file under the terms of the MPL, indicate your +* decision by deleting the provisions above and replace them with the notice +* and other provisions required by the GPL or the LGPL. If you do not delete +* the provisions above, a recipient may use your version of this file under +* the terms of any one of the MPL, the GPL or the LGPL. +* +* ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; + +import java.nio.ByteBuffer; +import java.util.ArrayList; + +/** + * Encapsulates the logic needed to draw a layer made of multiple tiles. + * <p/> + * TODO: Support repeating. + */ +public class MultiTileLayer extends Layer { + private static final String LOGTAG = "GeckoMultiTileLayer"; + + private final CairoImage mImage; + private final ArrayList<SubTile> mTiles; + private IntSize mTileSize; + private IntSize mBufferSize; + + public MultiTileLayer(CairoImage image, IntSize tileSize) { + super(); + + mImage = image; + mTileSize = tileSize; + mBufferSize = new IntSize(0, 0); + mTiles = new ArrayList<SubTile>(); + } + + public void invalidate(Rect dirtyRect) { + if (!inTransaction()) { + throw new RuntimeException("invalidate() is only valid inside a transaction"); + } + + for (SubTile layer : mTiles) { + IntSize tileSize = layer.getSize(); + Rect tileRect = new Rect(layer.x, layer.y, layer.x + tileSize.width, layer.y + tileSize.height); + + if (tileRect.intersect(dirtyRect)) { + tileRect.offset(-layer.x, -layer.y); + layer.invalidate(tileRect); + } + } + } + + public void invalidate() { + for (SubTile layer : mTiles) { + layer.invalidate(); + } + } + + @Override + public IntSize getSize() { + return mImage.getSize(); + } + + private void validateTiles() { + IntSize size = getSize(); + + if (size.equals(mBufferSize)) { + return; + } + + // Regenerate tiles + mTiles.clear(); + int offset = 0; + final int format = mImage.getFormat(); + final ByteBuffer buffer = mImage.getBuffer().slice(); + final int bpp = CairoUtils.bitsPerPixelForCairoFormat(format) / 8; + for (int y = 0; y < size.height; y += mTileSize.height) { + for (int x = 0; x < size.width; x += mTileSize.width) { + // Create a CairoImage implementation that returns a + // tile from the parent CairoImage. It's assumed that + // the tiles are stored in series. + final IntSize layerSize = + new IntSize(Math.min(mTileSize.width, size.width - x), + Math.min(mTileSize.height, size.height - y)); + final int tileOffset = offset; + + CairoImage subImage = new CairoImage() { + @Override + public ByteBuffer getBuffer() { + // Create a ByteBuffer that shares the data of the original + // buffer, but is positioned and limited so that only the + // tile data is accessible. + buffer.position(tileOffset); + ByteBuffer tileBuffer = buffer.slice(); + tileBuffer.limit(layerSize.getArea() * bpp); + + return tileBuffer; + } + + @Override + public IntSize getSize() { + return layerSize; + } + + @Override + public int getFormat() { + return format; + } + }; + + mTiles.add(new SubTile(subImage, x, y)); + offset += layerSize.getArea() * bpp; + } + } + + // Set tile origins and resolution + refreshTileMetrics(getOrigin(), getResolution(), false); + + mBufferSize = size; + } + + @Override + protected boolean performUpdates(RenderContext context) { + super.performUpdates(context); + + validateTiles(); + + // Iterate over the tiles and decide which ones we'll be drawing + int dirtyTiles = 0; + boolean screenUpdateDone = false; + SubTile firstDirtyTile = null; + for (SubTile layer : mTiles) { + // First do a non-texture update to make sure coordinates are + // up-to-date. + boolean invalid = layer.getSkipTextureUpdate(); + layer.setSkipTextureUpdate(true); + layer.performUpdates(context); + + RectF layerBounds = layer.getBounds(context, new FloatSize(layer.getSize())); + boolean isDirty = layer.isDirty(); + + if (isDirty) { + if (!RectF.intersects(layerBounds, context.viewport)) { + if (firstDirtyTile == null) + firstDirtyTile = layer; + dirtyTiles++; + invalid = true; + } else { + // This tile intersects with the screen and is dirty, + // update it immediately. + layer.setSkipTextureUpdate(false); + screenUpdateDone = true; + layer.performUpdates(context); + invalid = false; + } + } + + // We use the SkipTextureUpdate flag as a marker of a tile's + // validity. This is required, as sometimes layers are drawn + // without updating first, and we mustn't draw tiles that have + // been marked as invalid that we haven't updated. + layer.setSkipTextureUpdate(invalid); + } + + // Now if no tiles that intersect with the screen were updated, update + // a single tile that doesn't (if there are any). This has the effect + // of spreading out non-critical texture upload over time, and smoothing + // upload-related hitches. + if (!screenUpdateDone && firstDirtyTile != null) { + firstDirtyTile.setSkipTextureUpdate(false); + firstDirtyTile.performUpdates(context); + dirtyTiles--; + } + + return (dirtyTiles == 0); + } + + private void refreshTileMetrics(Point origin, float resolution, boolean inTransaction) { + IntSize size = getSize(); + for (SubTile layer : mTiles) { + if (!inTransaction) { + layer.beginTransaction(); + } + + if (origin != null) { + layer.setOrigin(new Point(origin.x + layer.x, origin.y + layer.y)); + } + if (resolution >= 0.0f) { + layer.setResolution(resolution); + } + + if (!inTransaction) { + layer.endTransaction(); + } + } + } + + @Override + public void setOrigin(Point newOrigin) { + super.setOrigin(newOrigin); + refreshTileMetrics(newOrigin, -1, true); + } + + @Override + public void setResolution(float newResolution) { + super.setResolution(newResolution); + refreshTileMetrics(null, newResolution, true); + } + + @Override + public void beginTransaction() { + super.beginTransaction(); + + for (SubTile layer : mTiles) { + layer.beginTransaction(); + } + } + + @Override + public void endTransaction() { + for (SubTile layer : mTiles) { + layer.endTransaction(); + } + + super.endTransaction(); + } + + @Override + public void draw(RenderContext context) { + for (SubTile layer : mTiles) { + // We use the SkipTextureUpdate flag as a validity flag. If it's false, + // the contents of this tile are invalid and we shouldn't draw it. + if (layer.getSkipTextureUpdate()) + continue; + + // Avoid work, only draw tiles that intersect with the viewport + RectF layerBounds = layer.getBounds(context, new FloatSize(layer.getSize())); + if (RectF.intersects(layerBounds, context.viewport)) + layer.draw(context); + } + } + + @Override + public Region getValidRegion(RenderContext context) { + Region validRegion = new Region(); + for (SubTile tile : mTiles) { + if (tile.getSkipTextureUpdate()) + continue; + validRegion.op(tile.getValidRegion(context), Region.Op.UNION); + } + + return validRegion; + } + + class SubTile extends SingleTileLayer { + public int x; + public int y; + + public SubTile(CairoImage mImage, int mX, int mY) { + super(mImage); + x = mX; + y = mY; + } + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/NinePatchTileLayer.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/NinePatchTileLayer.java new file mode 100644 index 000000000000..f139b202c7b5 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/NinePatchTileLayer.java @@ -0,0 +1,124 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.FloatSize; +import android.graphics.PointF; +import android.graphics.RectF; +import android.opengl.GLES11; +import android.opengl.GLES11Ext; +import android.util.Log; +import javax.microedition.khronos.opengles.GL10; +import java.nio.FloatBuffer; + +/** + * Encapsulates the logic needed to draw a nine-patch bitmap using OpenGL ES. + * + * For more information on nine-patch bitmaps, see the following document: + * http://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch + */ +public class NinePatchTileLayer extends TileLayer { + private static final int PATCH_SIZE = 16; + private static final int TEXTURE_SIZE = 48; + + public NinePatchTileLayer(CairoImage image) { + super(false, image); + } + + @Override + public void draw(RenderContext context) { + if (!initialized()) + return; + + GLES11.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); + GLES11.glEnable(GL10.GL_BLEND); + try { + GLES11.glBindTexture(GL10.GL_TEXTURE_2D, getTextureID()); + drawPatches(context); + } finally { + GLES11.glDisable(GL10.GL_BLEND); + } + } + + private void drawPatches(RenderContext context) { + /* + * We divide the nine-patch bitmap up as follows: + * + * +---+---+---+ + * | 0 | 1 | 2 | + * +---+---+---+ + * | 3 | | 4 | + * +---+---+---+ + * | 5 | 6 | 7 | + * +---+---+---+ + */ + + FloatSize size = context.pageSize; + float width = size.width, height = size.height; + + drawPatch(context, 0, 0, /* 0 */ + 0.0f, 0.0f, PATCH_SIZE, PATCH_SIZE); + drawPatch(context, PATCH_SIZE, 0, /* 1 */ + PATCH_SIZE, 0.0f, width, PATCH_SIZE); + drawPatch(context, PATCH_SIZE * 2, 0, /* 2 */ + PATCH_SIZE + width, 0.0f, PATCH_SIZE, PATCH_SIZE); + drawPatch(context, 0, PATCH_SIZE, /* 3 */ + 0.0f, PATCH_SIZE, PATCH_SIZE, height); + drawPatch(context, PATCH_SIZE * 2, PATCH_SIZE, /* 4 */ + PATCH_SIZE + width, PATCH_SIZE, PATCH_SIZE, height); + drawPatch(context, 0, PATCH_SIZE * 2, /* 5 */ + 0.0f, PATCH_SIZE + height, PATCH_SIZE, PATCH_SIZE); + drawPatch(context, PATCH_SIZE, PATCH_SIZE * 2, /* 6 */ + PATCH_SIZE, PATCH_SIZE + height, width, PATCH_SIZE); + drawPatch(context, PATCH_SIZE * 2, PATCH_SIZE * 2, /* 7 */ + PATCH_SIZE + width, PATCH_SIZE + height, PATCH_SIZE, PATCH_SIZE); + } + + private void drawPatch(RenderContext context, int textureX, int textureY, float tileX, + float tileY, float tileWidth, float tileHeight) { + int[] cropRect = { textureX, textureY + PATCH_SIZE, PATCH_SIZE, -PATCH_SIZE }; + GLES11.glTexParameteriv(GL10.GL_TEXTURE_2D, GLES11Ext.GL_TEXTURE_CROP_RECT_OES, cropRect, + 0); + + RectF viewport = context.viewport; + float viewportHeight = viewport.height(); + float drawX = tileX - viewport.left - PATCH_SIZE; + float drawY = viewportHeight - (tileY + tileHeight - viewport.top - PATCH_SIZE); + GLES11Ext.glDrawTexfOES(drawX, drawY, 0.0f, tileWidth, tileHeight); + } +} diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanningPerfAPI.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanningPerfAPI.java new file mode 100644 index 000000000000..c9414ac8ecf0 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PanningPerfAPI.java @@ -0,0 +1,125 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Kartikaya Gupta <kgupta@mozilla.com> + * Chris Lord <chrislord.net@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import java.util.ArrayList; +import java.util.List; +import android.os.SystemClock; +import android.util.Log; + +public class PanningPerfAPI { + private static final String LOGTAG = "GeckoPanningPerfAPI"; + + // make this large enough to avoid having to resize the frame time + // list, as that may be expensive and impact the thing we're trying + // to measure. + private static final int EXPECTED_FRAME_COUNT = 2048; + + private static boolean mRecordingFrames = false; + private static List<Long> mFrameTimes; + private static long mFrameStartTime; + + private static boolean mRecordingCheckerboard = false; + private static List<Float> mCheckerboardAmounts; + private static long mCheckerboardStartTime; + + public static void startFrameTimeRecording() { + if (mRecordingFrames) { + Log.e(LOGTAG, "Error: startFrameTimeRecording() called while already recording!"); + return; + } + mRecordingFrames = true; + if (mFrameTimes == null) { + mFrameTimes = new ArrayList<Long>(EXPECTED_FRAME_COUNT); + } else { + mFrameTimes.clear(); + } + mFrameStartTime = SystemClock.uptimeMillis(); + } + + public static List<Long> stopFrameTimeRecording() { + if (!mRecordingFrames) { + Log.e(LOGTAG, "Error: stopFrameTimeRecording() called when not recording!"); + return null; + } + mRecordingFrames = false; + return mFrameTimes; + } + + public static void recordFrameTime() { + // this will be called often, so try to make it as quick as possible + if (mRecordingFrames) { + mFrameTimes.add(SystemClock.uptimeMillis() - mFrameStartTime); + } + } + + public static boolean isRecordingCheckerboard() { + return mRecordingCheckerboard; + } + + public static void startCheckerboardRecording() { + if (mRecordingCheckerboard) { + Log.e(LOGTAG, "Error: startCheckerboardRecording() called while already recording!"); + return; + } + mRecordingCheckerboard = true; + if (mCheckerboardAmounts == null) { + mCheckerboardAmounts = new ArrayList<Float>(EXPECTED_FRAME_COUNT); + } else { + mCheckerboardAmounts.clear(); + } + mCheckerboardStartTime = SystemClock.uptimeMillis(); + } + + public static List<Float> stopCheckerboardRecording() { + if (!mRecordingCheckerboard) { + Log.e(LOGTAG, "Error: stopCheckerboardRecording() called when not recording!"); + return null; + } + mRecordingCheckerboard = false; + return mCheckerboardAmounts; + } + + public static void recordCheckerboard(float amount) { + // this will be called often, so try to make it as quick as possible + if (mRecordingCheckerboard) { + mCheckerboardAmounts.add(amount); + } + } +}
\ No newline at end of file diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PointUtils.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PointUtils.java new file mode 100644 index 000000000000..bff9f9f345a1 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/PointUtils.java @@ -0,0 +1,96 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Kartikaya Gupta <kgupta@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Point; +import android.graphics.PointF; +import android.util.FloatMath; + +import org.json.JSONObject; +import org.json.JSONException; +import org.mozilla.gecko.util.FloatUtils; + +import java.lang.Math; + +public final class PointUtils { + public static PointF add(PointF one, PointF two) { + return new PointF(one.x + two.x, one.y + two.y); + } + + public static PointF subtract(PointF one, PointF two) { + return new PointF(one.x - two.x, one.y - two.y); + } + + public static PointF scale(PointF point, float factor) { + return new PointF(point.x * factor, point.y * factor); + } + + public static Point round(PointF point) { + return new Point(Math.round(point.x), Math.round(point.y)); + } + + /* Returns a new point that is a linear interpolation between start and end points. weight conrols the weighting + * of each of the original points (weight = 1 returns endPoint, weight = 0 returns startPoint) + */ + public static PointF interpolate(PointF startPoint, PointF endPoint, float weight) { + float x = FloatUtils.interpolate(startPoint.x, endPoint.x, weight); + float y = FloatUtils.interpolate(startPoint.y, endPoint.y, weight); + return new PointF(x, y); + } + + /* Computes the magnitude of the given vector. */ + public static float distance(PointF point) { + return (float)Math.sqrt(point.x * point.x + point.y * point.y); + } + + /** Computes the scalar distance between two points. */ + public static float distance(PointF one, PointF two) { + return PointF.length(one.x - two.x, one.y - two.y); + } + + public static JSONObject toJSON(PointF point) throws JSONException { + // Ensure we put ints, not longs, because Gecko message handlers call getInt(). + int x = Math.round(point.x); + int y = Math.round(point.y); + JSONObject json = new JSONObject(); + json.put("x", x); + json.put("y", y); + return json; + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/RectUtils.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/RectUtils.java new file mode 100644 index 000000000000..9a5049781c45 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/RectUtils.java @@ -0,0 +1,139 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Kartikaya Gupta <kgupta@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.util.FloatUtils; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import org.json.JSONException; +import org.json.JSONObject; + +public final class RectUtils { + public static Rect create(JSONObject json) { + try { + int x = json.getInt("x"); + int y = json.getInt("y"); + int width = json.getInt("width"); + int height = json.getInt("height"); + return new Rect(x, y, x + width, y + height); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + public static Rect contract(Rect rect, int lessWidth, int lessHeight) { + float halfLessWidth = lessWidth / 2.0f; + float halfLessHeight = lessHeight / 2.0f; + return new Rect(Math.round(rect.left + halfLessWidth), + Math.round(rect.top + halfLessHeight), + Math.round(rect.right - halfLessWidth), + Math.round(rect.bottom - halfLessHeight)); + } + + public static RectF contract(RectF rect, float lessWidth, float lessHeight) { + float halfLessWidth = lessWidth / 2; + float halfLessHeight = lessHeight / 2; + return new RectF(rect.left + halfLessWidth, + rect.top + halfLessHeight, + rect.right - halfLessWidth, + rect.bottom - halfLessHeight); + } + + public static RectF expand(RectF rect, float moreWidth, float moreHeight) { + float halfMoreWidth = moreWidth / 2; + float halfMoreHeight = moreHeight / 2; + return new RectF(rect.left - halfMoreWidth, + rect.top - halfMoreHeight, + rect.right + halfMoreWidth, + rect.bottom + halfMoreHeight); + } + + public static RectF intersect(RectF one, RectF two) { + float left = Math.max(one.left, two.left); + float top = Math.max(one.top, two.top); + float right = Math.min(one.right, two.right); + float bottom = Math.min(one.bottom, two.bottom); + return new RectF(left, top, Math.max(right, left), Math.max(bottom, top)); + } + + public static RectF scale(RectF rect, float scale) { + float x = rect.left * scale; + float y = rect.top * scale; + return new RectF(x, y, + x + (rect.width() * scale), + y + (rect.height() * scale)); + } + + /** Returns the nearest integer rect of the given rect. */ + public static Rect round(RectF rect) { + return new Rect(Math.round(rect.left), Math.round(rect.top), + Math.round(rect.right), Math.round(rect.bottom)); + } + + public static IntSize getSize(Rect rect) { + return new IntSize(rect.width(), rect.height()); + } + + public static Point getOrigin(Rect rect) { + return new Point(rect.left, rect.top); + } + + public static PointF getOrigin(RectF rect) { + return new PointF(rect.left, rect.top); + } + + /* + * Returns the rect that represents a linear transition between `from` and `to` at time `t`, + * which is on the scale [0, 1). + */ + public static RectF interpolate(RectF from, RectF to, float t) { + return new RectF(FloatUtils.interpolate(from.left, to.left, t), + FloatUtils.interpolate(from.top, to.top, t), + FloatUtils.interpolate(from.right, to.right, t), + FloatUtils.interpolate(from.bottom, to.bottom, t)); + } + + public static boolean fuzzyEquals(RectF a, RectF b) { + return FloatUtils.fuzzyEquals(a.top, b.top) + && FloatUtils.fuzzyEquals(a.left, b.left) + && FloatUtils.fuzzyEquals(a.right, b.right) + && FloatUtils.fuzzyEquals(a.bottom, b.bottom); + } +} diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/ScrollbarLayer.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/ScrollbarLayer.java new file mode 100644 index 000000000000..68b7265b368b --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/ScrollbarLayer.java @@ -0,0 +1,425 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Kartikaya Gupta <kgupta@mozilla.com> + * Arkady Blyakher <rkadyb@mit.edu> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.RectF; +import android.opengl.GLES20; + +import org.libreoffice.LOKitShell; +import org.mozilla.gecko.util.FloatUtils; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; + +/** + * Draws a small rect. This is scaled to become a scrollbar. + */ +public class ScrollbarLayer extends TileLayer { + public static final long FADE_DELAY = 500; // milliseconds before fade-out starts + private static final float FADE_AMOUNT = 0.03f; // how much (as a percent) the scrollbar should fade per frame + + private static final int PADDING = 1; // gap between scrollbar and edge of viewport + private static final int BAR_SIZE = 6; + private static final int CAP_RADIUS = (BAR_SIZE / 2); + // Dimensions of the texture image + private static final float TEX_HEIGHT = 8.0f; + private static final float TEX_WIDTH = 8.0f; + // Texture coordinates for the scrollbar's body + // We take a 1x1 pixel from the center of the image and scale it to become the bar + private static final float[] BODY_TEX_COORDS = { + // x, y + CAP_RADIUS / TEX_WIDTH, CAP_RADIUS / TEX_HEIGHT, + CAP_RADIUS / TEX_WIDTH, (CAP_RADIUS + 1) / TEX_HEIGHT, + (CAP_RADIUS + 1) / TEX_WIDTH, CAP_RADIUS / TEX_HEIGHT, + (CAP_RADIUS + 1) / TEX_WIDTH, (CAP_RADIUS + 1) / TEX_HEIGHT + }; + // Texture coordinates for the top cap of the scrollbar + private static final float[] TOP_CAP_TEX_COORDS = { + // x, y + 0, 1.0f - CAP_RADIUS / TEX_HEIGHT, + 0, 1.0f, + BAR_SIZE / TEX_WIDTH, 1.0f - CAP_RADIUS / TEX_HEIGHT, + BAR_SIZE / TEX_WIDTH, 1.0f + }; + // Texture coordinates for the bottom cap of the scrollbar + private static final float[] BOT_CAP_TEX_COORDS = { + // x, y + 0, 1.0f - BAR_SIZE / TEX_HEIGHT, + 0, 1.0f - CAP_RADIUS / TEX_HEIGHT, + BAR_SIZE / TEX_WIDTH, 1.0f - BAR_SIZE / TEX_HEIGHT, + BAR_SIZE / TEX_WIDTH, 1.0f - CAP_RADIUS / TEX_HEIGHT + }; + // Texture coordinates for the left cap of the scrollbar + private static final float[] LEFT_CAP_TEX_COORDS = { + // x, y + 0, 1.0f - BAR_SIZE / TEX_HEIGHT, + 0, 1.0f, + CAP_RADIUS / TEX_WIDTH, 1.0f - BAR_SIZE / TEX_HEIGHT, + CAP_RADIUS / TEX_WIDTH, 1.0f + }; + // Texture coordinates for the right cap of the scrollbar + private static final float[] RIGHT_CAP_TEX_COORDS = { + // x, y + CAP_RADIUS / TEX_WIDTH, 1.0f - BAR_SIZE / TEX_HEIGHT, + CAP_RADIUS / TEX_WIDTH, 1.0f, + BAR_SIZE / TEX_WIDTH, 1.0f - BAR_SIZE / TEX_HEIGHT, + BAR_SIZE / TEX_WIDTH, 1.0f + }; + private final boolean mVertical; + private final ByteBuffer mBuffer; + private final Bitmap mBitmap; + private final Canvas mCanvas; + private float mOpacity; + private boolean mFinalized = false; + + private ScrollbarLayer(CairoImage image, boolean vertical, ByteBuffer buffer) { + super(false, image); + mVertical = vertical; + mBuffer = buffer; + + IntSize size = image.getSize(); + mBitmap = Bitmap.createBitmap(size.width, size.height, Bitmap.Config.ARGB_8888); + mCanvas = new Canvas(mBitmap); + } + + public static ScrollbarLayer create(boolean vertical) { + // just create an empty image for now, it will get drawn + // on demand anyway + int imageSize = IntSize.nextPowerOfTwo(BAR_SIZE); + ByteBuffer buffer = LOKitShell.allocateDirectBuffer(imageSize * imageSize * 4); + CairoImage image = new BufferedCairoImage(buffer, imageSize, imageSize, + CairoImage.FORMAT_ARGB32); + return new ScrollbarLayer(image, vertical, buffer); + } + + protected void finalize() throws Throwable { + try { + if (!mFinalized && mBuffer != null) + LOKitShell.freeDirectBuffer(mBuffer); + mFinalized = true; + } finally { + super.finalize(); + } + } + + /** + * Decrease the opacity of the scrollbar by one frame's worth. + * Return true if the opacity was decreased, or false if the scrollbars + * are already fully faded out. + */ + public boolean fade() { + if (FloatUtils.fuzzyEquals(mOpacity, 0.0f)) { + return false; + } + beginTransaction(); + try { + setOpacity(Math.max(mOpacity - FADE_AMOUNT, 0.0f)); + invalidate(); + } finally { + endTransaction(); + } + return true; + } + + /** + * Restore the opacity of the scrollbar to fully opaque. + * Return true if the opacity was changed, or false if the scrollbars + * are already fully opaque. + */ + public boolean unfade() { + if (FloatUtils.fuzzyEquals(mOpacity, 1.0f)) { + return false; + } + beginTransaction(); + try { + setOpacity(1.0f); + invalidate(); + } finally { + endTransaction(); + } + return true; + } + + private void setOpacity(float opacity) { + mOpacity = opacity; + + Paint foregroundPaint = new Paint(); + foregroundPaint.setAntiAlias(true); + foregroundPaint.setStyle(Paint.Style.FILL); + // use a (a,r,g,b) color of (127,0,0,0), and multiply the alpha by mOpacity for fading + foregroundPaint.setColor(Color.argb(Math.round(mOpacity * 127), 0, 0, 0)); + + mCanvas.drawColor(Color.argb(0, 0, 0, 0), PorterDuff.Mode.CLEAR); + mCanvas.drawCircle(CAP_RADIUS, CAP_RADIUS, CAP_RADIUS, foregroundPaint); + + mBitmap.copyPixelsToBuffer(mBuffer.asIntBuffer()); + } + + @Override + public void draw(RenderContext context) { + if (!initialized()) + return; + + try { + GLES20.glEnable(GLES20.GL_BLEND); + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + + Rect rect = RectUtils.round(mVertical + ? getVerticalRect(context) + : getHorizontalRect(context)); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTextureID()); + + float viewWidth = context.viewport.width(); + float viewHeight = context.viewport.height(); + + float top = viewHeight - rect.top; + float bot = viewHeight - rect.bottom; + + // Coordinates for the scrollbar's body combined with the texture coordinates + float[] bodyCoords = { + // x, y, z, texture_x, texture_y + rect.left / viewWidth, bot / viewHeight, 0, + BODY_TEX_COORDS[0], BODY_TEX_COORDS[1], + + rect.left / viewWidth, (bot + rect.height()) / viewHeight, 0, + BODY_TEX_COORDS[2], BODY_TEX_COORDS[3], + + (rect.left + rect.width()) / viewWidth, bot / viewHeight, 0, + BODY_TEX_COORDS[4], BODY_TEX_COORDS[5], + + (rect.left + rect.width()) / viewWidth, (bot + rect.height()) / viewHeight, 0, + BODY_TEX_COORDS[6], BODY_TEX_COORDS[7] + }; + + // Get the buffer and handles from the context + FloatBuffer coordBuffer = context.coordBuffer; + int positionHandle = context.positionHandle; + int textureHandle = context.textureHandle; + + // Make sure we are at position zero in the buffer in case other draw methods did not + // clean up after themselves + coordBuffer.position(0); + coordBuffer.put(bodyCoords); + + // Vertex coordinates are x,y,z starting at position 0 into the buffer. + coordBuffer.position(0); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + // Texture coordinates are texture_x, texture_y starting at position 3 into the buffer. + coordBuffer.position(3); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + // Reset the position in the buffer for the next set of vertex and texture coordinates. + coordBuffer.position(0); + + if (mVertical) { + // top endcap + float[] topCap = { + // x, y, z, texture_x, texture_y + rect.left / viewWidth, top / viewHeight, 0, + TOP_CAP_TEX_COORDS[0], TOP_CAP_TEX_COORDS[1], + + rect.left / viewWidth, (top + CAP_RADIUS) / viewHeight, 0, + TOP_CAP_TEX_COORDS[2], TOP_CAP_TEX_COORDS[3], + + (rect.left + BAR_SIZE) / viewWidth, top / viewHeight, 0, + TOP_CAP_TEX_COORDS[4], TOP_CAP_TEX_COORDS[5], + + (rect.left + BAR_SIZE) / viewWidth, (top + CAP_RADIUS) / viewHeight, 0, + TOP_CAP_TEX_COORDS[6], TOP_CAP_TEX_COORDS[7] + }; + + coordBuffer.put(topCap); + + // Vertex coordinates are x,y,z starting at position 0 into the buffer. + coordBuffer.position(0); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + // Texture coordinates are texture_x, texture_y starting at position 3 into the + // buffer. + coordBuffer.position(3); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + // Reset the position in the buffer for the next set of vertex and texture + // coordinates. + coordBuffer.position(0); + + // bottom endcap + float[] botCap = { + // x, y, z, texture_x, texture_y + rect.left / viewWidth, (bot - CAP_RADIUS) / viewHeight, 0, + BOT_CAP_TEX_COORDS[0], BOT_CAP_TEX_COORDS[1], + + rect.left / viewWidth, (bot) / viewHeight, 0, + BOT_CAP_TEX_COORDS[2], BOT_CAP_TEX_COORDS[3], + + (rect.left + BAR_SIZE) / viewWidth, (bot - CAP_RADIUS) / viewHeight, 0, + BOT_CAP_TEX_COORDS[4], BOT_CAP_TEX_COORDS[5], + + (rect.left + BAR_SIZE) / viewWidth, (bot) / viewHeight, 0, + BOT_CAP_TEX_COORDS[6], BOT_CAP_TEX_COORDS[7] + }; + + coordBuffer.put(botCap); + + // Vertex coordinates are x,y,z starting at position 0 into the buffer. + coordBuffer.position(0); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + // Texture coordinates are texture_x, texture_y starting at position 3 into the + // buffer. + coordBuffer.position(3); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + // Reset the position in the buffer for the next set of vertex and texture + // coordinates. + coordBuffer.position(0); + } else { + // left endcap + float[] leftCap = { + // x, y, z, texture_x, texture_y + (rect.left - CAP_RADIUS) / viewWidth, bot / viewHeight, 0, + LEFT_CAP_TEX_COORDS[0], LEFT_CAP_TEX_COORDS[1], + (rect.left - CAP_RADIUS) / viewWidth, (bot + BAR_SIZE) / viewHeight, 0, + LEFT_CAP_TEX_COORDS[2], LEFT_CAP_TEX_COORDS[3], + (rect.left) / viewWidth, bot / viewHeight, 0, LEFT_CAP_TEX_COORDS[4], + LEFT_CAP_TEX_COORDS[5], + (rect.left) / viewWidth, (bot + BAR_SIZE) / viewHeight, 0, + LEFT_CAP_TEX_COORDS[6], LEFT_CAP_TEX_COORDS[7] + }; + + coordBuffer.put(leftCap); + + // Vertex coordinates are x,y,z starting at position 0 into the buffer. + coordBuffer.position(0); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + // Texture coordinates are texture_x, texture_y starting at position 3 into the + // buffer. + coordBuffer.position(3); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + // Reset the position in the buffer for the next set of vertex and texture + // coordinates. + coordBuffer.position(0); + + // right endcap + float[] rightCap = { + // x, y, z, texture_x, texture_y + rect.right / viewWidth, (bot) / viewHeight, 0, + RIGHT_CAP_TEX_COORDS[0], RIGHT_CAP_TEX_COORDS[1], + + rect.right / viewWidth, (bot + BAR_SIZE) / viewHeight, 0, + RIGHT_CAP_TEX_COORDS[2], RIGHT_CAP_TEX_COORDS[3], + + (rect.right + CAP_RADIUS) / viewWidth, (bot) / viewHeight, 0, + RIGHT_CAP_TEX_COORDS[4], RIGHT_CAP_TEX_COORDS[5], + + (rect.right + CAP_RADIUS) / viewWidth, (bot + BAR_SIZE) / viewHeight, 0, + RIGHT_CAP_TEX_COORDS[6], RIGHT_CAP_TEX_COORDS[7] + }; + + coordBuffer.put(rightCap); + + // Vertex coordinates are x,y,z starting at position 0 into the buffer. + coordBuffer.position(0); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + // Texture coordinates are texture_x, texture_y starting at position 3 into the + // buffer. + coordBuffer.position(3); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + } + } finally { + GLES20.glDisable(GLES20.GL_BLEND); + } + } + + private RectF getVerticalRect(RenderContext context) { + RectF viewport = context.viewport; + FloatSize pageSize = context.pageSize; + float barStart = (viewport.height() * viewport.top / pageSize.height) + CAP_RADIUS; + float barEnd = (viewport.height() * viewport.bottom / pageSize.height) - CAP_RADIUS; + if (barStart > barEnd) { + float middle = (barStart + barEnd) / 2.0f; + barStart = barEnd = middle; + } + float right = viewport.width() - PADDING; + return new RectF(right - BAR_SIZE, barStart, right, barEnd); + } + + private RectF getHorizontalRect(RenderContext context) { + RectF viewport = context.viewport; + FloatSize pageSize = context.pageSize; + float barStart = (viewport.width() * viewport.left / pageSize.width) + CAP_RADIUS; + float barEnd = (viewport.width() * viewport.right / pageSize.width) - CAP_RADIUS; + if (barStart > barEnd) { + float middle = (barStart + barEnd) / 2.0f; + barStart = barEnd = middle; + } + float bottom = viewport.height() - PADDING; + return new RectF(barStart, bottom - BAR_SIZE, barEnd, bottom); + } +} diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/SingleTileLayer.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/SingleTileLayer.java new file mode 100644 index 000000000000..d18579e1a117 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/SingleTileLayer.java @@ -0,0 +1,127 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Arkady Blyakher <rkadyb@mit.edu> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.CairoImage; +import org.mozilla.gecko.gfx.CairoUtils; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.gfx.TileLayer; +import android.graphics.PointF; +import android.graphics.RectF; +import android.opengl.GLES20; +import android.util.Log; +import java.nio.FloatBuffer; +import javax.microedition.khronos.opengles.GL10; + +/** + * Encapsulates the logic needed to draw a single textured tile. + * + * TODO: Repeating textures really should be their own type of layer. + */ +public class SingleTileLayer extends TileLayer { + public SingleTileLayer(CairoImage image) { this(false, image); } + + public SingleTileLayer(boolean repeat, CairoImage image) { + super(repeat, image); + } + + @Override + public void draw(RenderContext context) { + // mTextureIDs may be null here during startup if Layer.java's draw method + // failed to acquire the transaction lock and call performUpdates. + if (!initialized()) + return; + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTextureID()); + + RectF bounds; + int[] cropRect; + IntSize size = getSize(); + RectF viewport = context.viewport; + + if (repeats()) { + bounds = new RectF(0.0f, 0.0f, viewport.width(), viewport.height()); + int width = Math.round(viewport.width()); + int height = Math.round(viewport.height()); + cropRect = new int[] { 0, 0, width, height }; + } else { + bounds = getBounds(context, new FloatSize(size)); + cropRect = new int[] { 0, 0, size.width, size.height }; + } + + float height = bounds.height(); + float left = bounds.left - viewport.left; + float top = viewport.height() - (bounds.top + height - viewport.top); + + float[] coords = { + //x, y, z, texture_x, texture_y + left/viewport.width(), top/viewport.height(), 0, + cropRect[0]/(float)size.width, cropRect[1]/(float)size.height, + + left/viewport.width(), (top+height)/viewport.height(), 0, + cropRect[0]/(float)size.width, cropRect[3]/(float)size.height, + + (left+bounds.width())/viewport.width(), top/viewport.height(), 0, + cropRect[2]/(float)size.width, cropRect[1]/(float)size.height, + + (left+bounds.width())/viewport.width(), (top+height)/viewport.height(), 0, + cropRect[2]/(float)size.width, cropRect[3]/(float)size.height + }; + + FloatBuffer coordBuffer = context.coordBuffer; + int positionHandle = context.positionHandle; + int textureHandle = context.textureHandle; + + // Make sure we are at position zero in the buffer in case other draw methods did not clean + // up after themselves + coordBuffer.position(0); + coordBuffer.put(coords); + + // Vertex coordinates are x,y,z starting at position 0 into the buffer. + coordBuffer.position(0); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, coordBuffer); + + // Texture coordinates are texture_x, texture_y starting at position 3 into the buffer. + coordBuffer.position(3); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, coordBuffer); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TextLayer.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TextLayer.java new file mode 100644 index 000000000000..f2cc640baaa7 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TextLayer.java @@ -0,0 +1,117 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +//import org.mozilla.gecko.GeckoAppShell; +import org.libreoffice.LOKitShell; +import org.mozilla.gecko.gfx.BufferedCairoImage; +import org.mozilla.gecko.gfx.CairoImage; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.SingleTileLayer; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.util.Log; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +/** + * Draws text on a layer. This is used for the frame rate meter. + */ +public class TextLayer extends SingleTileLayer { + private final ByteBuffer mBuffer; + private final IntSize mSize; + private boolean mFinalized = false; + + /* + * This awkward pattern is necessary due to Java's restrictions on when one can call superclass + * constructors. + */ + private TextLayer(ByteBuffer buffer, BufferedCairoImage image, IntSize size, String text) { + super(false, image); + mBuffer = buffer; + mSize = size; + renderText(text); + } + + protected void finalize() throws Throwable { + try { + if (!mFinalized && mBuffer != null) + /*GeckoAppShell*/ LOKitShell.freeDirectBuffer(mBuffer); + mFinalized = true; + } finally { + super.finalize(); + } + } + + public static TextLayer create(IntSize size, String text) { + ByteBuffer buffer = /*GeckoAppShell*/LOKitShell.allocateDirectBuffer(size.width * size.height * 4); + BufferedCairoImage image = new BufferedCairoImage(buffer, size.width, size.height, + CairoImage.FORMAT_ARGB32); + return new TextLayer(buffer, image, size, text); + } + + public void setText(String text) { + renderText(text); + invalidate(); + } + + private void renderText(String text) { + Bitmap bitmap = Bitmap.createBitmap(mSize.width, mSize.height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + Paint textPaint = new Paint(); + textPaint.setAntiAlias(true); + textPaint.setColor(Color.WHITE); + textPaint.setFakeBoldText(true); + textPaint.setTextSize(18.0f); + textPaint.setTypeface(Typeface.DEFAULT_BOLD); + float width = textPaint.measureText(text) + 18.0f; + + Paint backgroundPaint = new Paint(); + backgroundPaint.setColor(Color.argb(127, 0, 0, 0)); + canvas.drawRect(0.0f, 0.0f, width, 18.0f + 6.0f, backgroundPaint); + + canvas.drawText(text, 6.0f, 18.0f, textPaint); + + bitmap.copyPixelsToBuffer(mBuffer.asIntBuffer()); + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TextureGenerator.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TextureGenerator.java new file mode 100644 index 000000000000..4392c55e1ae7 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TextureGenerator.java @@ -0,0 +1,73 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * James Willcox <jwillcox@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import android.opengl.GLES10; +import java.util.Stack; + +public class TextureGenerator { + private static final int MIN_TEXTURES = 5; + + private static TextureGenerator sSharedInstance; + private Stack<Integer> mTextureIds; + + private TextureGenerator() { mTextureIds = new Stack<Integer>(); } + + public static TextureGenerator get() { + if (sSharedInstance == null) + sSharedInstance = new TextureGenerator(); + return sSharedInstance; + } + + public synchronized int take() { + if (mTextureIds.empty()) + return 0; + + return (int)mTextureIds.pop(); + } + + public synchronized void fill() { + int[] textures = new int[1]; + while (mTextureIds.size() < MIN_TEXTURES) { + GLES10.glGenTextures(1, textures, 0); + mTextureIds.push(textures[0]); + } + } +} + + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TextureReaper.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TextureReaper.java new file mode 100644 index 000000000000..e18139cb9bc9 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TextureReaper.java @@ -0,0 +1,76 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Arkady Blyakher <rkadyb@mit.edu> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import android.opengl.GLES20; +import java.util.ArrayList; + +/** Manages a list of dead tiles, so we don't leak resources. */ +public class TextureReaper { + private static TextureReaper sSharedInstance; + private ArrayList<Integer> mDeadTextureIDs; + + private TextureReaper() { mDeadTextureIDs = new ArrayList<Integer>(); } + + public static TextureReaper get() { + if (sSharedInstance == null) + sSharedInstance = new TextureReaper(); + return sSharedInstance; + } + + public void add(int[] textureIDs) { + for (int textureID : textureIDs) + add(textureID); + } + + public void add(int textureID) { + mDeadTextureIDs.add(textureID); + } + + public void reap() { + int[] deadTextureIDs = new int[mDeadTextureIDs.size()]; + for (int i = 0; i < deadTextureIDs.length; i++) + deadTextureIDs[i] = mDeadTextureIDs.get(i); + mDeadTextureIDs.clear(); + + GLES20.glDeleteTextures(deadTextureIDs.length, deadTextureIDs, 0); + } +} + + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TileLayer.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TileLayer.java new file mode 100644 index 000000000000..64ec94dc694a --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/TileLayer.java @@ -0,0 +1,256 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Arkady Blyakher <rkadyb@mit.edu> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.opengl.GLES20; +import android.util.Log; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +/** + * Base class for tile layers, which encapsulate the logic needed to draw textured tiles in OpenGL + * ES. + */ +public abstract class TileLayer extends Layer { + private static final String LOGTAG = "GeckoTileLayer"; + + private final Rect mDirtyRect; + private final CairoImage mImage; + private final boolean mRepeat; + private IntSize mSize; + private boolean mSkipTextureUpdate; + private int[] mTextureIDs; + + public TileLayer(boolean repeat, CairoImage image) { + mRepeat = repeat; + mImage = image; + mSize = new IntSize(0, 0); + mSkipTextureUpdate = false; + + IntSize bufferSize = mImage.getSize(); + mDirtyRect = new Rect(); + } + + @Override + public IntSize getSize() { return mImage.getSize(); } + + protected boolean repeats() { return mRepeat; } + protected int getTextureID() { return mTextureIDs[0]; } + protected boolean initialized() { return mImage != null && mTextureIDs != null; } + + @Override + protected void finalize() throws Throwable { + if (mTextureIDs != null) + TextureReaper.get().add(mTextureIDs); + } + + /** + * Invalidates the given rect so that it will be uploaded again. Only valid inside a + * transaction. + */ + public void invalidate(Rect rect) { + if (!inTransaction()) + throw new RuntimeException("invalidate() is only valid inside a transaction"); + mDirtyRect.union(rect); + } + + public void invalidate() { + IntSize bufferSize = mImage.getSize(); + invalidate(new Rect(0, 0, bufferSize.width, bufferSize.height)); + } + + public boolean isDirty() { + return mImage.getSize().isPositive() && (mTextureIDs == null || !mDirtyRect.isEmpty()); + } + + private void validateTexture() { + /* Calculate the ideal texture size. This must be a power of two if + * the texture is repeated or OpenGL ES 2.0 isn't supported, as + * OpenGL ES 2.0 is required for NPOT texture support (without + * extensions), but doesn't support repeating NPOT textures. + * + * XXX Currently, we don't pick a GLES 2.0 context, so always round. + */ + IntSize bufferSize = mImage.getSize(); + IntSize textureSize = bufferSize; + + textureSize = bufferSize.nextPowerOfTwo(); + + if (!textureSize.equals(mSize)) { + mSize = textureSize; + + // Delete the old texture + if (mTextureIDs != null) { + TextureReaper.get().add(mTextureIDs); + mTextureIDs = null; + + // Free the texture immediately, so we don't incur a + // temporarily increased memory usage. + TextureReaper.get().reap(); + } + } + } + + /** Tells the tile not to update the texture on the next update. */ + public void setSkipTextureUpdate(boolean skip) { + mSkipTextureUpdate = skip; + } + + public boolean getSkipTextureUpdate() { + return mSkipTextureUpdate; + } + + @Override + protected boolean performUpdates(RenderContext context) { + super.performUpdates(context); + + if (mSkipTextureUpdate) { + return false; + } + + // Reallocate the texture if the size has changed + validateTexture(); + + // Don't do any work if the image has an invalid size. + if (!mImage.getSize().isPositive()) + return true; + + // If we haven't allocated a texture, assume the whole region is dirty + if (mTextureIDs == null) { + uploadFullTexture(); + } else { + uploadDirtyRect(mDirtyRect); + } + + mDirtyRect.setEmpty(); + + return true; + } + + private void uploadFullTexture() { + IntSize bufferSize = mImage.getSize(); + uploadDirtyRect(new Rect(0, 0, bufferSize.width, bufferSize.height)); + } + + private void uploadDirtyRect(Rect dirtyRect) { + // If we have nothing to upload, just return for now + if (dirtyRect.isEmpty()) + return; + + // It's possible that the buffer will be null, check for that and return + ByteBuffer imageBuffer = mImage.getBuffer(); + if (imageBuffer == null) + return; + + boolean newlyCreated = false; + + if (mTextureIDs == null) { + mTextureIDs = new int[1]; + GLES20.glGenTextures(mTextureIDs.length, mTextureIDs, 0); + newlyCreated = true; + } + + IntSize bufferSize = mImage.getSize(); + Rect bufferRect = new Rect(0, 0, bufferSize.width, bufferSize.height); + + int cairoFormat = mImage.getFormat(); + CairoGLInfo glInfo = new CairoGLInfo(cairoFormat); + + bindAndSetGLParameters(); + + if (newlyCreated || dirtyRect.contains(bufferRect)) { + if (mSize.equals(bufferSize)) { + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, glInfo.internalFormat, mSize.width, + mSize.height, 0, glInfo.format, glInfo.type, imageBuffer); + return; + } else { + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, glInfo.internalFormat, mSize.width, + mSize.height, 0, glInfo.format, glInfo.type, null); + GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, bufferSize.width, + bufferSize.height, glInfo.format, glInfo.type, imageBuffer); + return; + } + } + + // Make sure that the dirty region intersects with the buffer rect, + // otherwise we'll end up with an invalid buffer pointer. + if (!Rect.intersects(dirtyRect, bufferRect)) { + return; + } + + /* + * Upload the changed rect. We have to widen to the full width of the texture + * because we can't count on the device having support for GL_EXT_unpack_subimage, + * and going line-by-line is too slow. + * + * XXX We should still use GL_EXT_unpack_subimage when available. + */ + Buffer viewBuffer = imageBuffer.slice(); + int bpp = CairoUtils.bitsPerPixelForCairoFormat(cairoFormat) / 8; + int position = dirtyRect.top * bufferSize.width * bpp; + if (position > viewBuffer.limit()) { + Log.e(LOGTAG, "### Position outside tile! " + dirtyRect.top); + return; + } + + viewBuffer.position(position); + GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, dirtyRect.top, bufferSize.width, + Math.min(bufferSize.height - dirtyRect.top, dirtyRect.height()), + glInfo.format, glInfo.type, viewBuffer); + } + + private void bindAndSetGLParameters() { + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIDs[0]); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_NEAREST); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR); + + int repeatMode = mRepeat ? GLES20.GL_REPEAT : GLES20.GL_CLAMP_TO_EDGE; + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, repeatMode); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, repeatMode); + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/ViewTransform.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/ViewTransform.java new file mode 100644 index 000000000000..9f443ea52894 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/ViewTransform.java @@ -0,0 +1,51 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +public class ViewTransform { + public float x; + public float y; + public float scale; + + public ViewTransform(float inX, float inY, float inScale) { + x = inX; + y = inY; + scale = inScale; + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/ViewportMetrics.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/ViewportMetrics.java new file mode 100644 index 000000000000..d98b47421520 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/ViewportMetrics.java @@ -0,0 +1,306 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Chris Lord <chrislord.net@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import android.graphics.PointF; +import android.graphics.RectF; +import android.util.DisplayMetrics; + +import org.json.JSONException; +import org.json.JSONObject; +import org.libreoffice.LibreOfficeMainActivity; +import org.mozilla.gecko.util.FloatUtils; + +/** + * ViewportMetrics manages state and contains some utility functions related to + * the page viewport for the Gecko layer client to use. + */ +public class ViewportMetrics { + private static final String LOGTAG = "GeckoViewportMetrics"; + private static final float MAX_BIAS = 0.8f; + private FloatSize mPageSize; + private RectF mViewportRect; + private PointF mViewportOffset; + private float mZoomFactor; + // A scale from -1,-1 to 1,1 that represents what edge of the displayport + // we want the viewport to be biased towards. + private PointF mViewportBias; + + public ViewportMetrics() { + DisplayMetrics metrics = new DisplayMetrics(); + LibreOfficeMainActivity.mAppContext.getWindowManager().getDefaultDisplay().getMetrics(metrics); + + mPageSize = new FloatSize(metrics.widthPixels, metrics.heightPixels); + mViewportRect = new RectF(0, 0, metrics.widthPixels, metrics.heightPixels); + mViewportOffset = new PointF(0, 0); + mZoomFactor = 1.0f; + mViewportBias = new PointF(0.0f, 0.0f); + } + + public ViewportMetrics(ViewportMetrics viewport) { + mPageSize = new FloatSize(viewport.getPageSize()); + mViewportRect = new RectF(viewport.getViewport()); + PointF offset = viewport.getViewportOffset(); + mViewportOffset = new PointF(offset.x, offset.y); + mZoomFactor = viewport.getZoomFactor(); + mViewportBias = viewport.mViewportBias; + } + + public ViewportMetrics(JSONObject json) throws JSONException { + float x = (float) json.getDouble("x"); + float y = (float) json.getDouble("y"); + float width = (float) json.getDouble("width"); + float height = (float) json.getDouble("height"); + float pageWidth = (float) json.getDouble("pageWidth"); + float pageHeight = (float) json.getDouble("pageHeight"); + float offsetX = (float) json.getDouble("offsetX"); + float offsetY = (float) json.getDouble("offsetY"); + float zoom = (float) json.getDouble("zoom"); + + mPageSize = new FloatSize(pageWidth, pageHeight); + mViewportRect = new RectF(x, y, x + width, y + height); + mViewportOffset = new PointF(offsetX, offsetY); + mZoomFactor = zoom; + mViewportBias = new PointF(0.0f, 0.0f); + } + + public PointF getOptimumViewportOffset(IntSize displayportSize) { + /* XXX Until bug #524925 is fixed, changing the viewport origin will + * cause unnecessary relayouts. This may cause rendering time to + * increase and should be considered. + */ + RectF viewport = getClampedViewport(); + + FloatSize bufferSpace = new FloatSize(displayportSize.width - viewport.width(), + displayportSize.height - viewport.height()); + PointF optimumOffset = + new PointF(bufferSpace.width * ((mViewportBias.x + 1.0f) / 2.0f), + bufferSpace.height * ((mViewportBias.y + 1.0f) / 2.0f)); + + // Make sure this offset won't cause wasted pixels in the displayport + // (i.e. make sure the resultant displayport intersects with the page + // as much as possible) + if (viewport.left - optimumOffset.x < 0) + optimumOffset.x = viewport.left; + else if ((bufferSpace.width - optimumOffset.x) + viewport.right > mPageSize.width) + optimumOffset.x = bufferSpace.width - (mPageSize.width - viewport.right); + + if (viewport.top - optimumOffset.y < 0) + optimumOffset.y = viewport.top; + else if ((bufferSpace.height - optimumOffset.y) + viewport.bottom > mPageSize.height) + optimumOffset.y = bufferSpace.height - (mPageSize.height - viewport.bottom); + + return new PointF(Math.round(optimumOffset.x), Math.round(optimumOffset.y)); + } + + public PointF getOrigin() { + return new PointF(mViewportRect.left, mViewportRect.top); + } + + public void setOrigin(PointF origin) { + // When the origin is set, we compare it with the last value set and + // change the viewport bias accordingly, so that any viewport based + // on these metrics will have a larger buffer in the direction of + // movement. + + // XXX Note the comment about bug #524925 in getOptimumViewportOffset. + // Ideally, the viewport bias would be a sliding scale, but we + // don't want to change it too often at the moment. + if (FloatUtils.fuzzyEquals(origin.x, mViewportRect.left)) + mViewportBias.x = 0; + else + mViewportBias.x = ((mViewportRect.left - origin.x) > 0) ? MAX_BIAS : -MAX_BIAS; + if (FloatUtils.fuzzyEquals(origin.y, mViewportRect.top)) + mViewportBias.y = 0; + else + mViewportBias.y = ((mViewportRect.top - origin.y) > 0) ? MAX_BIAS : -MAX_BIAS; + + mViewportRect.set(origin.x, origin.y, + origin.x + mViewportRect.width(), + origin.y + mViewportRect.height()); + } + + public PointF getDisplayportOrigin() { + return new PointF(mViewportRect.left - mViewportOffset.x, + mViewportRect.top - mViewportOffset.y); + } + + public FloatSize getSize() { + return new FloatSize(mViewportRect.width(), mViewportRect.height()); + } + + public void setSize(FloatSize size) { + mViewportRect.right = mViewportRect.left + size.width; + mViewportRect.bottom = mViewportRect.top + size.height; + } + + public RectF getViewport() { + return mViewportRect; + } + + public void setViewport(RectF viewport) { + mViewportRect = viewport; + } + + /** + * Returns the viewport rectangle, clamped within the page-size. + */ + public RectF getClampedViewport() { + RectF clampedViewport = new RectF(mViewportRect); + + // While the viewport size ought to never exceed the page size, we + // do the clamping in this order to make sure that the origin is + // never negative. + if (clampedViewport.right > mPageSize.width) + clampedViewport.offset(mPageSize.width - clampedViewport.right, 0); + if (clampedViewport.left < 0) + clampedViewport.offset(-clampedViewport.left, 0); + + if (clampedViewport.bottom > mPageSize.height) + clampedViewport.offset(0, mPageSize.height - clampedViewport.bottom); + if (clampedViewport.top < 0) + clampedViewport.offset(0, -clampedViewport.top); + + return clampedViewport; + } + + public PointF getViewportOffset() { + return mViewportOffset; + } + + public void setViewportOffset(PointF offset) { + mViewportOffset = offset; + } + + public FloatSize getPageSize() { + return mPageSize; + } + + public void setPageSize(FloatSize pageSize) { + mPageSize = pageSize; + } + + public float getZoomFactor() { + return mZoomFactor; + } + + public void setZoomFactor(float zoomFactor) { + mZoomFactor = zoomFactor; + } + + /* This will set the zoom factor and re-scale page-size and viewport offset + * accordingly. The given focus will remain at the same point on the screen + * after scaling. + */ + public void scaleTo(float newZoomFactor, PointF focus) { + float scaleFactor = newZoomFactor / mZoomFactor; + + mPageSize = mPageSize.scale(scaleFactor); + + PointF origin = getOrigin(); + origin.offset(focus.x, focus.y); + origin = PointUtils.scale(origin, scaleFactor); + origin.offset(-focus.x, -focus.y); + setOrigin(origin); + + mZoomFactor = newZoomFactor; + + // Similar to setOrigin, set the viewport bias based on the focal point + // of the zoom so that a viewport based on these metrics will have a + // larger buffer based on the direction of movement when scaling. + // + // This is biased towards scaling outwards, as zooming in doesn't + // really require a viewport bias. + mViewportBias.set(((focus.x / mViewportRect.width()) * (2.0f * MAX_BIAS)) - MAX_BIAS, + ((focus.y / mViewportRect.height()) * (2.0f * MAX_BIAS)) - MAX_BIAS); + } + + /* + * Returns the viewport metrics that represent a linear transition between `from` and `to` at + * time `t`, which is on the scale [0, 1). This function interpolates the viewport rect, the + * page size, the offset, and the zoom factor. + */ + public ViewportMetrics interpolate(ViewportMetrics to, float t) { + ViewportMetrics result = new ViewportMetrics(); + result.mPageSize = mPageSize.interpolate(to.mPageSize, t); + result.mZoomFactor = FloatUtils.interpolate(mZoomFactor, to.mZoomFactor, t); + result.mViewportRect = RectUtils.interpolate(mViewportRect, to.mViewportRect, t); + result.mViewportOffset = PointUtils.interpolate(mViewportOffset, to.mViewportOffset, t); + return result; + } + + public boolean fuzzyEquals(ViewportMetrics other) { + return mPageSize.fuzzyEquals(other.mPageSize) + && RectUtils.fuzzyEquals(mViewportRect, other.mViewportRect) + && FloatUtils.fuzzyEquals(mViewportOffset, other.mViewportOffset) + && FloatUtils.fuzzyEquals(mZoomFactor, other.mZoomFactor); + } + + public String toJSON() { + // Round off height and width. Since the height and width are the size of the screen, it + // makes no sense to send non-integer coordinates to Gecko. + int height = Math.round(mViewportRect.height()); + int width = Math.round(mViewportRect.width()); + + StringBuffer sb = new StringBuffer(256); + sb.append("{ \"x\" : ").append(mViewportRect.left) + .append(", \"y\" : ").append(mViewportRect.top) + .append(", \"width\" : ").append(width) + .append(", \"height\" : ").append(height) + .append(", \"pageWidth\" : ").append(mPageSize.width) + .append(", \"pageHeight\" : ").append(mPageSize.height) + .append(", \"offsetX\" : ").append(mViewportOffset.x) + .append(", \"offsetY\" : ").append(mViewportOffset.y) + .append(", \"zoom\" : ").append(mZoomFactor) + .append(" }"); + return sb.toString(); + } + + @Override + public String toString() { + StringBuffer buff = new StringBuffer(128); + buff.append("v=").append(mViewportRect.toString()) + .append(" p=").append(mPageSize.toString()) + .append(" z=").append(mZoomFactor) + .append(" o=").append(mViewportOffset.x) + .append(',').append(mViewportOffset.y); + return buff.toString(); + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/VirtualLayer.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/VirtualLayer.java new file mode 100644 index 000000000000..0f314ae71620 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/VirtualLayer.java @@ -0,0 +1,80 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Chris Lord <chrislord.net@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Point; + +public class VirtualLayer extends Layer { + private Listener mListener; + private IntSize mSize; + + public void setListener(Listener listener) { + mListener = listener; + } + + @Override + public void draw(RenderContext context) { + // No-op. + } + + @Override + public IntSize getSize() { + return mSize; + } + + public void setSize(IntSize size) { + mSize = size; + } + + @Override + protected boolean performUpdates(RenderContext context) { + boolean dimensionsChanged = dimensionChangesPending(); + boolean result = super.performUpdates(context); + if (dimensionsChanged && mListener != null) { + mListener.dimensionsChanged(getOrigin(), getResolution()); + } + + return result; + } + + public interface Listener { + void dimensionsChanged(Point newOrigin, float newResolution); + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/WidgetTileLayer.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/WidgetTileLayer.java new file mode 100644 index 000000000000..b123d55c403d --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/WidgetTileLayer.java @@ -0,0 +1,160 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * James Willcox <jwillcox@mozilla.com> + * Arkady Blyakher <rkadyb@mit.edu> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.libreoffice.LOKitShell; +import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.gfx.SingleTileLayer; +//import org.mozilla.gecko.GeckoAppShell; +import android.graphics.RectF; +import android.util.Log; +import android.opengl.GLES20; +import java.nio.FloatBuffer; + +/** + * Encapsulates the logic needed to draw the single-tiled Gecko texture + */ +public class WidgetTileLayer extends Layer { + private static final String LOGTAG = "WidgetTileLayer"; + + private int[] mTextureIDs; + private CairoImage mImage; + + public WidgetTileLayer(CairoImage image) { + mImage = image; + } + + protected boolean initialized() { return mTextureIDs != null; } + + @Override + public IntSize getSize() { return mImage.getSize(); } + + protected void bindAndSetGLParameters() { + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIDs[0]); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + } + + @Override + protected void finalize() throws Throwable { + if (mTextureIDs != null) + TextureReaper.get().add(mTextureIDs); + } + + @Override + protected boolean performUpdates(RenderContext context) { + super.performUpdates(context); + + if (mTextureIDs == null) { + mTextureIDs = new int[1]; + GLES20.glGenTextures(1, mTextureIDs, 0); + } + + bindAndSetGLParameters(); + LOKitShell.bindWidgetTexture(); + + return true; + } + + @Override + public void draw(RenderContext context) { + // mTextureIDs may be null here during startup if Layer.java's draw method + // failed to acquire the transaction lock and call performUpdates. + if (!initialized()) + return; + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIDs[0]); + + RectF bounds; + int[] cropRect; + IntSize size = getSize(); + RectF viewport = context.viewport; + + bounds = getBounds(context, new FloatSize(size)); + cropRect = new int[] { 0, 0, size.width, size.height }; + bounds.offset(-viewport.left, -viewport.top); + + float top = viewport.height() - (bounds.top + bounds.height()); + + // There may be errors from a previous GL call, so clear them first because + // we want to check for one below + while (GLES20.glGetError() != GLES20.GL_NO_ERROR); + + float[] coords = { + //x, y, z, texture_x, texture_y + bounds.left/viewport.width(), top/viewport.height(), 0, + cropRect[0]/size.width, cropRect[1]/size.height, + + bounds.left/viewport.width(), (top+bounds.height())/viewport.height(), 0, + cropRect[0]/size.width, cropRect[3]/size.height, + + (bounds.left+bounds.width())/viewport.width(), top/viewport.height(), 0, + cropRect[2]/size.width, cropRect[1]/size.height, + + (bounds.left+bounds.width())/viewport.width(), (top+bounds.height())/viewport.height(), + 0, + cropRect[2]/size.width, cropRect[3]/size.height + }; + + // Get the buffer and handles from the context + FloatBuffer coordBuffer = context.coordBuffer; + int positionHandle = context.positionHandle; + int textureHandle = context.textureHandle; + + // Make sure we are at position zero in the buffer in case other draw methods did not clean + // up after themselves + coordBuffer.position(0); + coordBuffer.put(coords); + + // Vertex coordinates are x,y,z starting at position 0 into the buffer. + coordBuffer.position(0); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, coordBuffer); + + // Texture coordinates are texture_x, texture_y starting at position 3 into the buffer. + coordBuffer.position(3); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, coordBuffer); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + int error = GLES20.glGetError(); + if (error != GLES20.GL_NO_ERROR) { + Log.i(LOGTAG, "Failed to draw texture: " + error); + } + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/Axis.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/Axis.java new file mode 100644 index 000000000000..8c0fce4c73b3 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/Axis.java @@ -0,0 +1,271 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2012 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Kartikaya Gupta <kgupta@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.ui; + +import org.mozilla.gecko.util.FloatUtils; + +/** + * This class represents the physics for one axis of movement (i.e. either + * horizontal or vertical). It tracks the different properties of movement + * like displacement, velocity, viewport dimensions, etc. pertaining to + * a particular axis. + */ +abstract class Axis { + // This fraction of velocity remains after every animation frame when the velocity is low. + private static final float FRICTION_SLOW = 0.85f; + // This fraction of velocity remains after every animation frame when the velocity is high. + private static final float FRICTION_FAST = 0.97f; + // Below this velocity (in pixels per frame), the friction starts increasing from FRICTION_FAST + // to FRICTION_SLOW. + private static final float VELOCITY_THRESHOLD = 10.0f; + // The maximum velocity change factor between events, per ms, in %. + // Direction changes are excluded. + private static final float MAX_EVENT_ACCELERATION = 0.012f; + + // The rate of deceleration when the surface has overscrolled. + private static final float OVERSCROLL_DECEL_RATE = 0.04f; + // The percentage of the surface which can be overscrolled before it must snap back. + private static final float SNAP_LIMIT = 0.75f; + + // The minimum amount of space that must be present for an axis to be considered scrollable, + // in pixels. + private static final float MIN_SCROLLABLE_DISTANCE = 0.5f; + // The number of milliseconds per frame assuming 60 fps + private static final float MS_PER_FRAME = 1000.0f / 60.0f; + + private enum FlingStates { + STOPPED, + PANNING, + FLINGING, + } + + private enum Overscroll { + NONE, + MINUS, // Overscrolled in the negative direction + PLUS, // Overscrolled in the positive direction + BOTH, // Overscrolled in both directions (page is zoomed to smaller than screen) + } + + private final SubdocumentScrollHelper mSubscroller; + + private float mFirstTouchPos; /* Position of the first touch event on the current drag. */ + private float mTouchPos; /* Position of the most recent touch event on the current drag. */ + private float mLastTouchPos; /* Position of the touch event before touchPos. */ + private float mVelocity; /* Velocity in this direction; pixels per animation frame. */ + public boolean mScrollingDisabled; /* Whether movement on this axis is locked. */ + private boolean mDisableSnap; /* Whether overscroll snapping is disabled. */ + private float mDisplacement; + + private FlingStates mFlingState; /* The fling state we're in on this axis. */ + + protected abstract float getOrigin(); + protected abstract float getViewportLength(); + protected abstract float getPageLength(); + + Axis(SubdocumentScrollHelper subscroller) { + mSubscroller = subscroller; + } + + private float getViewportEnd() { + return getOrigin() + getViewportLength(); + } + + void startTouch(float pos) { + mVelocity = 0.0f; + mScrollingDisabled = false; + mFirstTouchPos = mTouchPos = mLastTouchPos = pos; + } + + float panDistance(float currentPos) { + return currentPos - mFirstTouchPos; + } + + void setScrollingDisabled(boolean disabled) { + mScrollingDisabled = disabled; + } + + void saveTouchPos() { + mLastTouchPos = mTouchPos; + } + + void updateWithTouchAt(float pos, float timeDelta) { + float newVelocity = (mTouchPos - pos) / timeDelta * MS_PER_FRAME; + + // If there's a direction change, or current velocity is very low, + // allow setting of the velocity outright. Otherwise, use the current + // velocity and a maximum change factor to set the new velocity. + boolean curVelocityIsLow = Math.abs(mVelocity) < 1.0f; + boolean directionChange = (mVelocity > 0) != (newVelocity > 0); + if (curVelocityIsLow || (directionChange && !FloatUtils.fuzzyEquals(newVelocity, 0.0f))) { + mVelocity = newVelocity; + } else { + float maxChange = Math.abs(mVelocity * timeDelta * MAX_EVENT_ACCELERATION); + mVelocity = Math.min(mVelocity + maxChange, Math.max(mVelocity - maxChange, newVelocity)); + } + + mTouchPos = pos; + } + + boolean overscrolled() { + return getOverscroll() != Overscroll.NONE; + } + + private Overscroll getOverscroll() { + boolean minus = (getOrigin() < 0.0f); + boolean plus = (getViewportEnd() > getPageLength()); + if (minus && plus) { + return Overscroll.BOTH; + } else if (minus) { + return Overscroll.MINUS; + } else if (plus) { + return Overscroll.PLUS; + } else { + return Overscroll.NONE; + } + } + + // Returns the amount that the page has been overscrolled. If the page hasn't been + // overscrolled on this axis, returns 0. + private float getExcess() { + switch (getOverscroll()) { + case MINUS: return -getOrigin(); + case PLUS: return getViewportEnd() - getPageLength(); + case BOTH: return getViewportEnd() - getPageLength() - getOrigin(); + default: return 0.0f; + } + } + + /* + * Returns true if the page is zoomed in to some degree along this axis such that scrolling is + * possible and this axis has not been scroll locked while panning. Otherwise, returns false. + */ + private boolean scrollable() { + return getViewportLength() <= getPageLength() - MIN_SCROLLABLE_DISTANCE && + !mScrollingDisabled; + } + + /* + * Returns the resistance, as a multiplier, that should be taken into account when + * tracking or pinching. + */ + float getEdgeResistance() { + float excess = getExcess(); + if (excess > 0.0f) { + // excess can be greater than viewport length, but the resistance + // must never drop below 0.0 + return Math.max(0.0f, SNAP_LIMIT - excess / getViewportLength()); + } + return 1.0f; + } + + /* Returns the velocity. If the axis is locked, returns 0. */ + float getRealVelocity() { + return scrollable() ? mVelocity : 0f; + } + + void startPan() { + mFlingState = FlingStates.PANNING; + } + + void startFling(boolean stopped) { + mDisableSnap = mSubscroller.scrolling(); + + if (stopped) { + mFlingState = FlingStates.STOPPED; + } else { + mFlingState = FlingStates.FLINGING; + } + } + + /* Advances a fling animation by one step. */ + boolean advanceFling() { + if (mFlingState != FlingStates.FLINGING) { + return false; + } + if (mSubscroller.scrolling() && !mSubscroller.lastScrollSucceeded()) { + // if the subdocument stopped scrolling, it's because it reached the end + // of the subdocument. we don't do overscroll on subdocuments, so there's + // no point in continuing this fling. + return false; + } + + float excess = getExcess(); + if (mDisableSnap || FloatUtils.fuzzyEquals(excess, 0.0f)) { + // If we aren't overscrolled, just apply friction. + if (Math.abs(mVelocity) >= VELOCITY_THRESHOLD) { + mVelocity *= FRICTION_FAST; + } else { + float t = mVelocity / VELOCITY_THRESHOLD; + mVelocity *= FloatUtils.interpolate(FRICTION_SLOW, FRICTION_FAST, t); + } + } else { + // Otherwise, decrease the velocity linearly. + float elasticity = 1.0f - excess / (getViewportLength() * SNAP_LIMIT); + if (getOverscroll() == Overscroll.MINUS) { + mVelocity = Math.min((mVelocity + OVERSCROLL_DECEL_RATE) * elasticity, 0.0f); + } else { // must be Overscroll.PLUS + mVelocity = Math.max((mVelocity - OVERSCROLL_DECEL_RATE) * elasticity, 0.0f); + } + } + + return true; + } + + void stopFling() { + mVelocity = 0.0f; + mFlingState = FlingStates.STOPPED; + } + + // Performs displacement of the viewport position according to the current velocity. + void displace() { + if (!mSubscroller.scrolling() && !scrollable()) + return; + + if (mFlingState == FlingStates.PANNING) + mDisplacement += (mLastTouchPos - mTouchPos) * getEdgeResistance(); + else + mDisplacement += mVelocity; + } + + float resetDisplacement() { + float d = mDisplacement; + mDisplacement = 0.0f; + return d; + } +} diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java new file mode 100644 index 000000000000..c3cacccf805e --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java @@ -0,0 +1,921 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2012 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Kartikaya Gupta <kgupta@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.ui; + +import org.json.JSONObject; +import org.json.JSONException; +import org.libreoffice.LOKitShell; +import org.libreoffice.LibreOfficeMainActivity; +import org.mozilla.gecko.gfx.FloatSize; +import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.gfx.PointUtils; +import org.mozilla.gecko.gfx.ViewportMetrics; +import org.mozilla.gecko.util.FloatUtils; +import org.mozilla.gecko.GeckoEventListener; +import android.graphics.PointF; +import android.graphics.RectF; +import android.util.FloatMath; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import java.util.Timer; +import java.util.TimerTask; + +/* + * Handles the kinetic scrolling and zooming physics for a layer controller. + * + * Many ideas are from Joe Hewitt's Scrollability: + * https://github.com/joehewitt/scrollability/ + */ +public class PanZoomController + extends GestureDetector.SimpleOnGestureListener + implements SimpleScaleGestureDetector.SimpleScaleGestureListener, GeckoEventListener +{ + private static final String LOGTAG = "GeckoPanZoomController"; + + private static String MESSAGE_ZOOM_RECT = "Browser:ZoomToRect"; + private static String MESSAGE_ZOOM_PAGE = "Browser:ZoomToPageWidth"; + + // Animation stops if the velocity is below this value when overscrolled or panning. + private static final float STOPPED_THRESHOLD = 4.0f; + + // Animation stops is the velocity is below this threshold when flinging. + private static final float FLING_STOPPED_THRESHOLD = 0.1f; + + // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans + // between the touch-down and touch-up of a click). In units of density-independent pixels. + public static final float PAN_THRESHOLD = 1/16f * LOKitShell.getDpi(); + + // Angle from axis within which we stay axis-locked + private static final double AXIS_LOCK_ANGLE = Math.PI / 6.0; // 30 degrees + + // The maximum amount we allow you to zoom into a page + private static final float MAX_ZOOM = 4.0f; + + /* 16 precomputed frames of the _ease-out_ animation from the CSS Transitions specification. */ + private static final float[] EASE_OUT_ANIMATION_FRAMES = { + 0.00000f, /* 0 */ + 0.10211f, /* 1 */ + 0.19864f, /* 2 */ + 0.29043f, /* 3 */ + 0.37816f, /* 4 */ + 0.46155f, /* 5 */ + 0.54054f, /* 6 */ + 0.61496f, /* 7 */ + 0.68467f, /* 8 */ + 0.74910f, /* 9 */ + 0.80794f, /* 10 */ + 0.86069f, /* 11 */ + 0.90651f, /* 12 */ + 0.94471f, /* 13 */ + 0.97401f, /* 14 */ + 0.99309f, /* 15 */ + }; + + private enum PanZoomState { + NOTHING, /* no touch-start events received */ + FLING, /* all touches removed, but we're still scrolling page */ + TOUCHING, /* one touch-start event received */ + PANNING_LOCKED, /* touch-start followed by move (i.e. panning with axis lock) */ + PANNING, /* panning without axis lock */ + PANNING_HOLD, /* in panning, but not moving. + * similar to TOUCHING but after starting a pan */ + PANNING_HOLD_LOCKED, /* like PANNING_HOLD, but axis lock still in effect */ + PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */ + ANIMATED_ZOOM /* animated zoom to a new rect */ + } + + private final LayerController mController; + private final SubdocumentScrollHelper mSubscroller; + private final Axis mX; + private final Axis mY; + + private Thread mMainThread; + + /* The timer that handles flings or bounces. */ + private Timer mAnimationTimer; + /* The runnable being scheduled by the animation timer. */ + private AnimationRunnable mAnimationRunnable; + /* The zoom focus at the first zoom event (in page coordinates). */ + private PointF mLastZoomFocus; + /* The time the last motion event took place. */ + private long mLastEventTime; + /* Current state the pan/zoom UI is in. */ + private PanZoomState mState; + + public PanZoomController(LayerController controller) { + mController = controller; + mSubscroller = new SubdocumentScrollHelper(this); + mX = new AxisX(mSubscroller); + mY = new AxisY(mSubscroller); + + mMainThread = LibreOfficeMainActivity.mAppContext.getMainLooper().getThread(); + checkMainThread(); + + mState = PanZoomState.NOTHING; + + //GeckoAppShell.registerGeckoEventListener(MESSAGE_ZOOM_RECT, this); + //GeckoAppShell.registerGeckoEventListener(MESSAGE_ZOOM_PAGE, this); + } + + // for debugging bug 713011; it can be taken out once that is resolved. + private void checkMainThread() { + if (mMainThread != Thread.currentThread()) { + // log with full stack trace + Log.e(LOGTAG, "Uh-oh, we're running on the wrong thread!", new Exception()); + } + } + + public void handleMessage(String event, JSONObject message) { + Log.i(LOGTAG, "Got message: " + event); + try { + if (MESSAGE_ZOOM_RECT.equals(event)) { + float x = (float)message.getDouble("x"); + float y = (float)message.getDouble("y"); + final RectF zoomRect = new RectF(x, y, + x + (float)message.getDouble("w"), + y + (float)message.getDouble("h")); + mController.post(new Runnable() { + public void run() { + animatedZoomTo(zoomRect); + } + }); + } else if (MESSAGE_ZOOM_PAGE.equals(event)) { + FloatSize pageSize = mController.getPageSize(); + + RectF viewableRect = mController.getViewport(); + float y = viewableRect.top; + // attempt to keep zoom keep focused on the center of the viewport + float newHeight = viewableRect.height() * pageSize.width / viewableRect.width(); + float dh = viewableRect.height() - newHeight; // increase in the height + final RectF r = new RectF(0.0f, + y + dh/2, + pageSize.width, + y + dh/2 + newHeight); + mController.post(new Runnable() { + public void run() { + animatedZoomTo(r); + } + }); + } + } catch (Exception e) { + Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); + } + } + + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: return onTouchStart(event); + case MotionEvent.ACTION_MOVE: return onTouchMove(event); + case MotionEvent.ACTION_UP: return onTouchEnd(event); + case MotionEvent.ACTION_CANCEL: return onTouchCancel(event); + default: return false; + } + } + + /** This function must be called from the UI thread. */ + public void abortAnimation() { + checkMainThread(); + // this happens when gecko changes the viewport on us or if the device is rotated. + // if that's the case, abort any animation in progress and re-zoom so that the page + // snaps to edges. for other cases (where the user's finger(s) are down) don't do + // anything special. + switch (mState) { + case FLING: + mX.stopFling(); + mY.stopFling(); + mState = PanZoomState.NOTHING; + // fall through + case ANIMATED_ZOOM: + // the zoom that's in progress likely makes no sense any more (such as if + // the screen orientation changed) so abort it + // fall through + case NOTHING: + // Don't do animations here; they're distracting and can cause flashes on page + // transitions. + mController.setViewportMetrics(getValidViewportMetrics()); + mController.notifyLayerClientOfGeometryChange(); + break; + } + } + + /** This must be called on the UI thread. */ + public void pageSizeUpdated() { + if (mState == PanZoomState.NOTHING) { + ViewportMetrics validated = getValidViewportMetrics(); + if (! mController.getViewportMetrics().fuzzyEquals(validated)) { + // page size changed such that we are now in overscroll. snap to the + // the nearest valid viewport + mController.setViewportMetrics(validated); + mController.notifyLayerClientOfGeometryChange(); + } + } + } + + /* + * Panning/scrolling + */ + + private boolean onTouchStart(MotionEvent event) { + Log.d(LOGTAG, "onTouchStart in state " + mState); + // user is taking control of movement, so stop + // any auto-movement we have going + stopAnimationTimer(); + mSubscroller.cancel(); + + switch (mState) { + case ANIMATED_ZOOM: + return false; + case FLING: + case NOTHING: + startTouch(event.getX(0), event.getY(0), event.getEventTime()); + return false; + case TOUCHING: + case PANNING: + case PANNING_LOCKED: + case PANNING_HOLD: + case PANNING_HOLD_LOCKED: + case PINCHING: + Log.e(LOGTAG, "Received impossible touch down while in " + mState); + return false; + } + Log.e(LOGTAG, "Unhandled case " + mState + " in onTouchStart"); + return false; + } + + private boolean onTouchMove(MotionEvent event) { + Log.d(LOGTAG, "onTouchMove in state " + mState); + + switch (mState) { + case NOTHING: + case FLING: + // should never happen + Log.e(LOGTAG, "Received impossible touch move while in " + mState); + return false; + + case TOUCHING: + if (panDistance(event) < PAN_THRESHOLD) { + return false; + } + cancelTouch(); + startPanning(event.getX(0), event.getY(0), event.getEventTime()); + //GeckoApp.mAppContext.hidePlugins(false /* don't hide layers */); + //GeckoApp.mAutoCompletePopup.hide(); + track(event); + return true; + + case PANNING_HOLD_LOCKED: + //GeckoApp.mAutoCompletePopup.hide(); + mState = PanZoomState.PANNING_LOCKED; + // fall through + case PANNING_LOCKED: + track(event); + return true; + + case PANNING_HOLD: + //GeckoApp.mAutoCompletePopup.hide(); + mState = PanZoomState.PANNING; + // fall through + case PANNING: + track(event); + return true; + + case ANIMATED_ZOOM: + case PINCHING: + // scale gesture listener will handle this + return false; + } + Log.e(LOGTAG, "Unhandled case " + mState + " in onTouchMove"); + return false; + } + + private boolean onTouchEnd(MotionEvent event) { + Log.d(LOGTAG, "onTouchEnd in " + mState); + + switch (mState) { + case NOTHING: + case FLING: + // should never happen + Log.e(LOGTAG, "Received impossible touch end while in " + mState); + return false; + case TOUCHING: + mState = PanZoomState.NOTHING; + // the switch into TOUCHING might have happened while the page was + // snapping back after overscroll. we need to finish the snap if that + // was the case + bounce(); + return false; + case PANNING: + case PANNING_LOCKED: + case PANNING_HOLD: + case PANNING_HOLD_LOCKED: + mState = PanZoomState.FLING; + fling(); + return true; + case PINCHING: + mState = PanZoomState.NOTHING; + return true; + case ANIMATED_ZOOM: + return false; + } + Log.e(LOGTAG, "Unhandled case " + mState + " in onTouchEnd"); + return false; + } + + private boolean onTouchCancel(MotionEvent event) { + Log.d(LOGTAG, "onTouchCancel in " + mState); + + mState = PanZoomState.NOTHING; + // ensure we snap back if we're overscrolled + bounce(); + return false; + } + + private void startTouch(float x, float y, long time) { + mX.startTouch(x); + mY.startTouch(y); + mState = PanZoomState.TOUCHING; + mLastEventTime = time; + } + + private void startPanning(float x, float y, long time) { + float dx = mX.panDistance(x); + float dy = mY.panDistance(y); + double angle = Math.atan2(dy, dx); // range [-pi, pi] + angle = Math.abs(angle); // range [0, pi] + + // When the touch move breaks through the pan threshold, reposition the touch down origin + // so the page won't jump when we start panning. + mX.startTouch(x); + mY.startTouch(y); + mLastEventTime = time; + + if (angle < AXIS_LOCK_ANGLE || angle > (Math.PI - AXIS_LOCK_ANGLE)) { + mY.setScrollingDisabled(true); + mState = PanZoomState.PANNING_LOCKED; + } else if (Math.abs(angle - (Math.PI / 2)) < AXIS_LOCK_ANGLE) { + mX.setScrollingDisabled(true); + mState = PanZoomState.PANNING_LOCKED; + } else { + mState = PanZoomState.PANNING; + } + } + + private float panDistance(MotionEvent move) { + float dx = mX.panDistance(move.getX(0)); + float dy = mY.panDistance(move.getY(0)); + return FloatMath.sqrt(dx * dx + dy * dy); + } + + private void track(float x, float y, long time) { + float timeDelta = (float)(time - mLastEventTime); + if (FloatUtils.fuzzyEquals(timeDelta, 0)) { + // probably a duplicate event, ignore it. using a zero timeDelta will mess + // up our velocity + return; + } + mLastEventTime = time; + + mX.updateWithTouchAt(x, timeDelta); + mY.updateWithTouchAt(y, timeDelta); + } + + private void track(MotionEvent event) { + mX.saveTouchPos(); + mY.saveTouchPos(); + + for (int i = 0; i < event.getHistorySize(); i++) { + track(event.getHistoricalX(0, i), + event.getHistoricalY(0, i), + event.getHistoricalEventTime(i)); + } + track(event.getX(0), event.getY(0), event.getEventTime()); + + if (stopped()) { + if (mState == PanZoomState.PANNING) { + mState = PanZoomState.PANNING_HOLD; + } else if (mState == PanZoomState.PANNING_LOCKED) { + mState = PanZoomState.PANNING_HOLD_LOCKED; + } else { + // should never happen, but handle anyway for robustness + Log.e(LOGTAG, "Impossible case " + mState + " when stopped in track"); + mState = PanZoomState.PANNING_HOLD_LOCKED; + } + } + + mX.startPan(); + mY.startPan(); + updatePosition(); + } + + private void fling() { + updatePosition(); + + stopAnimationTimer(); + + boolean stopped = stopped(); + mX.startFling(stopped); + mY.startFling(stopped); + + startAnimationTimer(new FlingRunnable()); + } + + /* Performs a bounce-back animation to the given viewport metrics. */ + private void bounce(ViewportMetrics metrics) { + stopAnimationTimer(); + + ViewportMetrics bounceStartMetrics = new ViewportMetrics(mController.getViewportMetrics()); + if (bounceStartMetrics.fuzzyEquals(metrics)) { + mState = PanZoomState.NOTHING; + return; + } + + mState = PanZoomState.FLING; + Log.d(LOGTAG, "end bounce at " + metrics); + + startAnimationTimer(new BounceRunnable(bounceStartMetrics, metrics)); + } + + /* Performs a bounce-back animation to the nearest valid viewport metrics. */ + private void bounce() { + bounce(getValidViewportMetrics()); + } + + /* Starts the fling or bounce animation. */ + private void startAnimationTimer(final AnimationRunnable runnable) { + if (mAnimationTimer != null) { + Log.e(LOGTAG, "Attempted to start a new fling without canceling the old one!"); + stopAnimationTimer(); + } + + //GeckoApp.mAppContext.hidePlugins(false /* don't hide layers */); + + mAnimationTimer = new Timer("Animation Timer"); + mAnimationRunnable = runnable; + mAnimationTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { mController.post(runnable); } + }, 0, 1000L/60L); + } + + /* Stops the fling or bounce animation. */ + private void stopAnimationTimer() { + if (mAnimationTimer != null) { + mAnimationTimer.cancel(); + mAnimationTimer = null; + } + if (mAnimationRunnable != null) { + mAnimationRunnable.terminate(); + mAnimationRunnable = null; + } + + //GeckoApp.mAppContext.showPlugins(); + } + + private float getVelocity() { + float xvel = mX.getRealVelocity(); + float yvel = mY.getRealVelocity(); + return FloatMath.sqrt(xvel * xvel + yvel * yvel); + } + + private boolean stopped() { + return getVelocity() < STOPPED_THRESHOLD; + } + + PointF getDisplacement() { + return new PointF(mX.resetDisplacement(), mY.resetDisplacement()); + } + + private void updatePosition() { + mX.displace(); + mY.displace(); + PointF displacement = getDisplacement(); + if (! mSubscroller.scrollBy(displacement)) { + synchronized (mController) { + mController.scrollBy(displacement); + } + } + } + + private abstract class AnimationRunnable implements Runnable { + private boolean mAnimationTerminated; + + /* This should always run on the UI thread */ + public final void run() { + /* + * Since the animation timer queues this runnable on the UI thread, it + * is possible that even when the animation timer is cancelled, there + * are multiple instances of this queued, so we need to have another + * mechanism to abort. This is done by using the mAnimationTerminated flag. + */ + if (mAnimationTerminated) { + return; + } + animateFrame(); + } + + protected abstract void animateFrame(); + + /* This should always run on the UI thread */ + protected final void terminate() { + mAnimationTerminated = true; + } + } + + /* The callback that performs the bounce animation. */ + private class BounceRunnable extends AnimationRunnable { + /* The current frame of the bounce-back animation */ + private int mBounceFrame; + /* + * The viewport metrics that represent the start and end of the bounce-back animation, + * respectively. + */ + private ViewportMetrics mBounceStartMetrics; + private ViewportMetrics mBounceEndMetrics; + + BounceRunnable(ViewportMetrics startMetrics, ViewportMetrics endMetrics) { + mBounceStartMetrics = startMetrics; + mBounceEndMetrics = endMetrics; + } + + protected void animateFrame() { + /* + * The pan/zoom controller might have signaled to us that it wants to abort the + * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail + * out. + */ + if (mState != PanZoomState.FLING) { + finishAnimation(); + return; + } + + /* Perform the next frame of the bounce-back animation. */ + if (mBounceFrame < EASE_OUT_ANIMATION_FRAMES.length) { + advanceBounce(); + return; + } + + /* Finally, if there's nothing else to do, complete the animation and go to sleep. */ + finishBounce(); + finishAnimation(); + mState = PanZoomState.NOTHING; + } + + /* Performs one frame of a bounce animation. */ + private void advanceBounce() { + synchronized (mController) { + float t = EASE_OUT_ANIMATION_FRAMES[mBounceFrame]; + ViewportMetrics newMetrics = mBounceStartMetrics.interpolate(mBounceEndMetrics, t); + mController.setViewportMetrics(newMetrics); + mController.notifyLayerClientOfGeometryChange(); + mBounceFrame++; + } + } + + /* Concludes a bounce animation and snaps the viewport into place. */ + private void finishBounce() { + synchronized (mController) { + mController.setViewportMetrics(mBounceEndMetrics); + mController.notifyLayerClientOfGeometryChange(); + mBounceFrame = -1; + } + } + } + + // The callback that performs the fling animation. + private class FlingRunnable extends AnimationRunnable { + protected void animateFrame() { + /* + * The pan/zoom controller might have signaled to us that it wants to abort the + * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail + * out. + */ + if (mState != PanZoomState.FLING) { + finishAnimation(); + return; + } + + /* Advance flings, if necessary. */ + boolean flingingX = mX.advanceFling(); + boolean flingingY = mY.advanceFling(); + + boolean overscrolled = (mX.overscrolled() || mY.overscrolled()); + + /* If we're still flinging in any direction, update the origin. */ + if (flingingX || flingingY) { + updatePosition(); + + /* + * Check to see if we're still flinging with an appreciable velocity. The threshold is + * higher in the case of overscroll, so we bounce back eagerly when overscrolling but + * coast smoothly to a stop when not. In other words, require a greater velocity to + * maintain the fling once we enter overscroll. + */ + float threshold = (overscrolled && !mSubscroller.scrolling() ? STOPPED_THRESHOLD : FLING_STOPPED_THRESHOLD); + if (getVelocity() >= threshold) { + // we're still flinging + return; + } + + mX.stopFling(); + mY.stopFling(); + } + + /* Perform a bounce-back animation if overscrolled. */ + if (overscrolled) { + bounce(); + } else { + finishAnimation(); + mState = PanZoomState.NOTHING; + } + } + } + + private void finishAnimation() { + checkMainThread(); + + Log.d(LOGTAG, "Finishing animation at " + mController.getViewportMetrics()); + stopAnimationTimer(); + + // Force a viewport synchronisation + //GeckoApp.mAppContext.showPlugins(); + mController.setForceRedraw(); + mController.notifyLayerClientOfGeometryChange(); + } + + /* Returns the nearest viewport metrics with no overscroll visible. */ + private ViewportMetrics getValidViewportMetrics() { + return getValidViewportMetrics(new ViewportMetrics(mController.getViewportMetrics())); + } + + private ViewportMetrics getValidViewportMetrics(ViewportMetrics viewportMetrics) { + Log.d(LOGTAG, "generating valid viewport using " + viewportMetrics); + + /* First, we adjust the zoom factor so that we can make no overscrolled area visible. */ + float zoomFactor = viewportMetrics.getZoomFactor(); + FloatSize pageSize = viewportMetrics.getPageSize(); + RectF viewport = viewportMetrics.getViewport(); + + float focusX = viewport.width() / 2.0f; + float focusY = viewport.height() / 2.0f; + float minZoomFactor = 0.0f; + if (viewport.width() > pageSize.width && pageSize.width > 0) { + float scaleFactor = viewport.width() / pageSize.width; + minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor); + focusX = 0.0f; + } + if (viewport.height() > pageSize.height && pageSize.height > 0) { + float scaleFactor = viewport.height() / pageSize.height; + minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor); + focusY = 0.0f; + } + + if (!FloatUtils.fuzzyEquals(minZoomFactor, 0.0f)) { + // if one (or both) of the page dimensions is smaller than the viewport, + // zoom using the top/left as the focus on that axis. this prevents the + // scenario where, if both dimensions are smaller than the viewport, but + // by different scale factors, we end up scrolled to the end on one axis + // after applying the scale + PointF center = new PointF(focusX, focusY); + viewportMetrics.scaleTo(minZoomFactor, center); + } else if (zoomFactor > MAX_ZOOM) { + PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f); + viewportMetrics.scaleTo(MAX_ZOOM, center); + } + + /* Now we pan to the right origin. */ + viewportMetrics.setViewport(viewportMetrics.getClampedViewport()); + Log.d(LOGTAG, "generated valid viewport as " + viewportMetrics); + + return viewportMetrics; + } + + private class AxisX extends Axis { + AxisX(SubdocumentScrollHelper subscroller) { super(subscroller); } + @Override + public float getOrigin() { return mController.getOrigin().x; } + @Override + protected float getViewportLength() { return mController.getViewportSize().width; } + @Override + protected float getPageLength() { return mController.getPageSize().width; } + } + + private class AxisY extends Axis { + AxisY(SubdocumentScrollHelper subscroller) { super(subscroller); } + @Override + public float getOrigin() { return mController.getOrigin().y; } + @Override + protected float getViewportLength() { return mController.getViewportSize().height; } + @Override + protected float getPageLength() { return mController.getPageSize().height; } + } + + /* + * Zooming + */ + @Override + public boolean onScaleBegin(SimpleScaleGestureDetector detector) { + Log.d(LOGTAG, "onScaleBegin in " + mState); + + if (mState == PanZoomState.ANIMATED_ZOOM) + return false; + + mState = PanZoomState.PINCHING; + mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY()); + //GeckoApp.mAppContext.hidePlugins(false /* don't hide layers, only views */); + //GeckoApp.mAutoCompletePopup.hide(); + cancelTouch(); + + return true; + } + + @Override + public boolean onScale(SimpleScaleGestureDetector detector) { + Log.d(LOGTAG, "onScale in state " + mState); + + if (mState == PanZoomState.ANIMATED_ZOOM) + return false; + + float prevSpan = detector.getPreviousSpan(); + if (FloatUtils.fuzzyEquals(prevSpan, 0.0f)) { + // let's eat this one to avoid setting the new zoom to infinity (bug 711453) + return true; + } + + float spanRatio = detector.getCurrentSpan() / prevSpan; + + /* + * Apply edge resistance if we're zoomed out smaller than the page size by scaling the zoom + * factor toward 1.0. + */ + float resistance = Math.min(mX.getEdgeResistance(), mY.getEdgeResistance()); + if (spanRatio > 1.0f) + spanRatio = 1.0f + (spanRatio - 1.0f) * resistance; + else + spanRatio = 1.0f - (1.0f - spanRatio) * resistance; + + synchronized (mController) { + float newZoomFactor = mController.getZoomFactor() * spanRatio; + if (newZoomFactor >= MAX_ZOOM) { + // apply resistance when zooming past MAX_ZOOM, + // such that it asymptotically reaches MAX_ZOOM + 1.0 + // but never exceeds that + float excessZoom = newZoomFactor - MAX_ZOOM; + excessZoom = 1.0f - (float)Math.exp(-excessZoom); + newZoomFactor = MAX_ZOOM + excessZoom; + } + + mController.scrollBy(new PointF(mLastZoomFocus.x - detector.getFocusX(), + mLastZoomFocus.y - detector.getFocusY())); + PointF focus = new PointF(detector.getFocusX(), detector.getFocusY()); + mController.scaleWithFocus(newZoomFactor, focus); + } + + mLastZoomFocus.set(detector.getFocusX(), detector.getFocusY()); + + return true; + } + + @Override + public void onScaleEnd(SimpleScaleGestureDetector detector) { + Log.d(LOGTAG, "onScaleEnd in " + mState); + + if (mState == PanZoomState.ANIMATED_ZOOM) + return; + + // switch back to the touching state + startTouch(detector.getFocusX(), detector.getFocusY(), detector.getEventTime()); + + // Force a viewport synchronisation + //GeckoApp.mAppContext.showPlugins(); + mController.setForceRedraw(); + mController.notifyLayerClientOfGeometryChange(); + } + + public boolean getRedrawHint() { + return (mState != PanZoomState.PINCHING && mState != PanZoomState.ANIMATED_ZOOM); + } + + private void sendPointToGecko(String event, MotionEvent motionEvent) { + String json; + try { + PointF point = new PointF(motionEvent.getX(), motionEvent.getY()); + point = mController.convertViewPointToLayerPoint(point); + if (point == null) { + return; + } + json = PointUtils.toJSON(point).toString(); + } catch (Exception e) { + Log.e(LOGTAG, "Unable to convert point to JSON for " + event, e); + return; + } + + //GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(event, json)); + } + + @Override + public void onLongPress(MotionEvent motionEvent) { + sendPointToGecko("Gesture:LongPress", motionEvent); + } + + @Override + public boolean onDown(MotionEvent motionEvent) { + sendPointToGecko("Gesture:ShowPress", motionEvent); + return false; + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent motionEvent) { + //GeckoApp.mAutoCompletePopup.hide(); + sendPointToGecko("Gesture:SingleTap", motionEvent); + return true; + } + + @Override + public boolean onDoubleTap(MotionEvent motionEvent) { + sendPointToGecko("Gesture:DoubleTap", motionEvent); + return true; + } + + public void cancelTouch() { + //GeckoEvent e = GeckoEvent.createBroadcastEvent("Gesture:CancelTouch", ""); + //GeckoAppShell.sendEventToGecko(e); + } + + private boolean animatedZoomTo(RectF zoomToRect) { + //GeckoApp.mAutoCompletePopup.hide(); + + mState = PanZoomState.ANIMATED_ZOOM; + final float startZoom = mController.getZoomFactor(); + + RectF viewport = mController.getViewport(); + // 1. adjust the aspect ratio of zoomToRect to match that of the current viewport, + // enlarging as necessary (if it gets too big, it will get shrunk in the next step). + // while enlarging make sure we enlarge equally on both sides to keep the target rect + // centered. + float targetRatio = viewport.width() / viewport.height(); + float rectRatio = zoomToRect.width() / zoomToRect.height(); + if (FloatUtils.fuzzyEquals(targetRatio, rectRatio)) { + // all good, do nothing + } else if (targetRatio < rectRatio) { + // need to increase zoomToRect height + float newHeight = zoomToRect.width() / targetRatio; + zoomToRect.top -= (newHeight - zoomToRect.height()) / 2; + zoomToRect.bottom = zoomToRect.top + newHeight; + } else { // targetRatio > rectRatio) { + // need to increase zoomToRect width + float newWidth = targetRatio * zoomToRect.height(); + zoomToRect.left -= (newWidth - zoomToRect.width()) / 2; + zoomToRect.right = zoomToRect.left + newWidth; + } + + float finalZoom = viewport.width() * startZoom / zoomToRect.width(); + + ViewportMetrics finalMetrics = new ViewportMetrics(mController.getViewportMetrics()); + finalMetrics.setOrigin(new PointF(zoomToRect.left, zoomToRect.top)); + finalMetrics.scaleTo(finalZoom, new PointF(0.0f, 0.0f)); + + // 2. now run getValidViewportMetrics on it, so that the target viewport is + // clamped down to prevent overscroll, over-zoom, and other bad conditions. + finalMetrics = getValidViewportMetrics(finalMetrics); + + bounce(finalMetrics); + return true; + } +} diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SimpleScaleGestureDetector.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SimpleScaleGestureDetector.java new file mode 100644 index 000000000000..4f9e39857e81 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SimpleScaleGestureDetector.java @@ -0,0 +1,332 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2012 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.ui; + +import org.mozilla.gecko.gfx.PointUtils; +import org.json.JSONException; +import android.graphics.PointF; +import android.util.Log; +import android.view.MotionEvent; +import java.util.LinkedList; +import java.util.ListIterator; +import java.util.Stack; + +/** + * A less buggy, and smoother, replacement for the built-in Android ScaleGestureDetector. + * + * This gesture detector is more reliable than the built-in ScaleGestureDetector because: + * + * - It doesn't assume that pointer IDs are numbered 0 and 1. + * + * - It doesn't attempt to correct for "slop" when resting one's hand on the device. On some + * devices (e.g. the Droid X) this can cause the ScaleGestureDetector to lose track of how many + * pointers are down, with disastrous results (bug 706684). + * + * - Cancelling a zoom into a pan is handled correctly. + * + * - Starting with three or more fingers down, releasing fingers so that only two are down, and + * then performing a scale gesture is handled correctly. + * + * - It doesn't take pressure into account, which results in smoother scaling. + */ +public class SimpleScaleGestureDetector { + private static final String LOGTAG = "GeckoSimpleScaleGestureDetector"; + + private SimpleScaleGestureListener mListener; + private long mLastEventTime; + + /* Information about all pointers that are down. */ + private LinkedList<PointerInfo> mPointerInfo; + + /** Creates a new gesture detector with the given listener. */ + public SimpleScaleGestureDetector(SimpleScaleGestureListener listener) { + mListener = listener; + mPointerInfo = new LinkedList<PointerInfo>(); + } + + /** Forward touch events to this function. */ + public void onTouchEvent(MotionEvent event) { + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + onTouchStart(event); + break; + case MotionEvent.ACTION_MOVE: + onTouchMove(event); + break; + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + onTouchEnd(event); + break; + } + } + + private int getPointersDown() { + return mPointerInfo.size(); + } + + private int getActionIndex(MotionEvent event) { + return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) + >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; + } + + private void onTouchStart(MotionEvent event) { + mLastEventTime = event.getEventTime(); + mPointerInfo.addFirst(PointerInfo.create(event, getActionIndex(event))); + if (getPointersDown() == 2) { + sendScaleGesture(EventType.BEGIN); + } + } + + private void onTouchMove(MotionEvent event) { + mLastEventTime = event.getEventTime(); + for (int i = 0; i < event.getPointerCount(); i++) { + PointerInfo pointerInfo = pointerInfoForEventIndex(event, i); + if (pointerInfo != null) { + pointerInfo.populate(event, i); + } + } + + if (getPointersDown() == 2) { + sendScaleGesture(EventType.CONTINUE); + } + } + + private void onTouchEnd(MotionEvent event) { + mLastEventTime = event.getEventTime(); + + int id = event.getPointerId(getActionIndex(event)); + ListIterator<PointerInfo> iterator = mPointerInfo.listIterator(); + while (iterator.hasNext()) { + PointerInfo pointerInfo = iterator.next(); + if (pointerInfo.getId() != id) { + continue; + } + + // One of the pointers we were tracking was lifted. Remove its info object from the + // list, recycle it to avoid GC pauses, and send an onScaleEnd() notification if this + // ended the gesture. + iterator.remove(); + pointerInfo.recycle(); + if (getPointersDown() == 1) { + sendScaleGesture(EventType.END); + } + } + } + + /** + * Returns the X coordinate of the focus location (the midpoint of the two fingers). If only + * one finger is down, returns the location of that finger. + */ + public float getFocusX() { + switch (getPointersDown()) { + case 1: + return mPointerInfo.getFirst().getCurrent().x; + case 2: + PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); + return (pointerA.getCurrent().x + pointerB.getCurrent().x) / 2.0f; + } + + Log.e(LOGTAG, "No gesture taking place in getFocusX()!"); + return 0.0f; + } + + /** + * Returns the Y coordinate of the focus location (the midpoint of the two fingers). If only + * one finger is down, returns the location of that finger. + */ + public float getFocusY() { + switch (getPointersDown()) { + case 1: + return mPointerInfo.getFirst().getCurrent().y; + case 2: + PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); + return (pointerA.getCurrent().y + pointerB.getCurrent().y) / 2.0f; + } + + Log.e(LOGTAG, "No gesture taking place in getFocusY()!"); + return 0.0f; + } + + /** Returns the most recent distance between the two pointers. */ + public float getCurrentSpan() { + if (getPointersDown() != 2) { + Log.e(LOGTAG, "No gesture taking place in getCurrentSpan()!"); + return 0.0f; + } + + PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); + return PointUtils.distance(pointerA.getCurrent(), pointerB.getCurrent()); + } + + /** Returns the second most recent distance between the two pointers. */ + public float getPreviousSpan() { + if (getPointersDown() != 2) { + Log.e(LOGTAG, "No gesture taking place in getPreviousSpan()!"); + return 0.0f; + } + + PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); + PointF a = pointerA.getPrevious(), b = pointerB.getPrevious(); + if (a == null || b == null) { + a = pointerA.getCurrent(); + b = pointerB.getCurrent(); + } + + return PointUtils.distance(a, b); + } + + /** Returns the time of the last event related to the gesture. */ + public long getEventTime() { + return mLastEventTime; + } + + /** Returns true if the scale gesture is in progress and false otherwise. */ + public boolean isInProgress() { + return getPointersDown() == 2; + } + + /* Sends the requested scale gesture notification to the listener. */ + private void sendScaleGesture(EventType eventType) { + switch (eventType) { + case BEGIN: mListener.onScaleBegin(this); break; + case CONTINUE: mListener.onScale(this); break; + case END: mListener.onScaleEnd(this); break; + } + } + + /* + * Returns the pointer info corresponding to the given pointer index, or null if the pointer + * isn't one that's being tracked. + */ + private PointerInfo pointerInfoForEventIndex(MotionEvent event, int index) { + int id = event.getPointerId(index); + for (PointerInfo pointerInfo : mPointerInfo) { + if (pointerInfo.getId() == id) { + return pointerInfo; + } + } + return null; + } + + private enum EventType { + BEGIN, + CONTINUE, + END, + } + + /* Encapsulates information about one of the two fingers involved in the gesture. */ + private static class PointerInfo { + /* A free list that recycles pointer info objects, to reduce GC pauses. */ + private static Stack<PointerInfo> sPointerInfoFreeList; + + private int mId; + private PointF mCurrent, mPrevious; + + private PointerInfo() { + // External users should use create() instead. + } + + /* Creates or recycles a new PointerInfo instance from an event and a pointer index. */ + public static PointerInfo create(MotionEvent event, int index) { + if (sPointerInfoFreeList == null) { + sPointerInfoFreeList = new Stack<PointerInfo>(); + } + + PointerInfo pointerInfo; + if (sPointerInfoFreeList.empty()) { + pointerInfo = new PointerInfo(); + } else { + pointerInfo = sPointerInfoFreeList.pop(); + } + + pointerInfo.populate(event, index); + return pointerInfo; + } + + /* + * Fills in the fields of this instance from the given motion event and pointer index + * within that event. + */ + public void populate(MotionEvent event, int index) { + mId = event.getPointerId(index); + mPrevious = mCurrent; + mCurrent = new PointF(event.getX(index), event.getY(index)); + } + + public void recycle() { + mId = -1; + mPrevious = mCurrent = null; + sPointerInfoFreeList.push(this); + } + + public int getId() { return mId; } + public PointF getCurrent() { return mCurrent; } + public PointF getPrevious() { return mPrevious; } + + @Override + public String toString() { + if (mId == -1) { + return "(up)"; + } + + try { + String prevString; + if (mPrevious == null) { + prevString = "n/a"; + } else { + prevString = PointUtils.toJSON(mPrevious).toString(); + } + + // The current position should always be non-null. + String currentString = PointUtils.toJSON(mCurrent).toString(); + return "id=" + mId + " cur=" + currentString + " prev=" + prevString; + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + } + + public static interface SimpleScaleGestureListener { + public boolean onScale(SimpleScaleGestureDetector detector); + public boolean onScaleBegin(SimpleScaleGestureDetector detector); + public void onScaleEnd(SimpleScaleGestureDetector detector); + } +} + diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SubdocumentScrollHelper.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SubdocumentScrollHelper.java new file mode 100644 index 000000000000..f24a5b7adaa1 --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/SubdocumentScrollHelper.java @@ -0,0 +1,140 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2012 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Kartikaya Gupta <kgupta@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.ui; + + +import android.graphics.PointF; +import android.os.Handler; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.GeckoEventListener; + +class SubdocumentScrollHelper implements GeckoEventListener { + private static final String LOGTAG = "GeckoSubdocumentScrollHelper"; + + private static String MESSAGE_PANNING_OVERRIDE = "Panning:Override"; + private static String MESSAGE_CANCEL_OVERRIDE = "Panning:CancelOverride"; + private static String MESSAGE_SCROLL = "Gesture:Scroll"; + private static String MESSAGE_SCROLL_ACK = "Gesture:ScrollAck"; + + private final PanZoomController mPanZoomController; + private final Handler mUiHandler; + + private boolean mOverridePanning; + private boolean mOverrideScrollAck; + private boolean mOverrideScrollPending; + private boolean mScrollSucceeded; + + SubdocumentScrollHelper(PanZoomController controller) { + mPanZoomController = controller; + // mUiHandler will be bound to the UI thread since that's where this constructor runs + mUiHandler = new Handler(); + + //GeckoAppShell.registerGeckoEventListener(MESSAGE_PANNING_OVERRIDE, this); + //GeckoAppShell.registerGeckoEventListener(MESSAGE_CANCEL_OVERRIDE, this); + //GeckoAppShell.registerGeckoEventListener(MESSAGE_SCROLL_ACK, this); + } + + boolean scrollBy(PointF displacement) { + if (!mOverridePanning) { + return false; + } + + if (!mOverrideScrollAck) { + mOverrideScrollPending = true; + return true; + } + + mOverrideScrollAck = false; + mOverrideScrollPending = false; + + JSONObject json = new JSONObject(); + try { + json.put("x", displacement.x); + json.put("y", displacement.y); + } catch (JSONException e) { + Log.e(LOGTAG, "Error forming subwindow scroll message: ", e); + } + //GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(MESSAGE_SCROLL, json.toString())); + + return true; + } + + void cancel() { + mOverridePanning = false; + } + + boolean scrolling() { + return mOverridePanning; + } + + boolean lastScrollSucceeded() { + return mScrollSucceeded; + } + + // GeckoEventListener implementation + + public void handleMessage(final String event, final JSONObject message) { + // this comes in on the gecko thread; hand off the handling to the UI thread + mUiHandler.post(new Runnable() { + public void run() { + Log.i(LOGTAG, "Got message: " + event); + try { + if (MESSAGE_PANNING_OVERRIDE.equals(event)) { + mOverridePanning = true; + mOverrideScrollAck = true; + mOverrideScrollPending = false; + mScrollSucceeded = true; + } else if (MESSAGE_CANCEL_OVERRIDE.equals(event)) { + mOverridePanning = false; + } else if (MESSAGE_SCROLL_ACK.equals(event)) { + mOverrideScrollAck = true; + mScrollSucceeded = message.getBoolean("scrolled"); + if (mOverridePanning && mOverrideScrollPending) { + scrollBy(mPanZoomController.getDisplacement()); + } + } + } catch (Exception e) { + Log.e(LOGTAG, "Exception handling message", e); + } + } + }); + } +} diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/util/FloatUtils.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/util/FloatUtils.java new file mode 100644 index 000000000000..b4bb249c4d5f --- /dev/null +++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/util/FloatUtils.java @@ -0,0 +1,43 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.util; + +import android.graphics.PointF; + +import java.lang.IllegalArgumentException; + +public final class FloatUtils { + private FloatUtils() {} + + public static boolean fuzzyEquals(float a, float b) { + return (Math.abs(a - b) < 1e-6); + } + + public static boolean fuzzyEquals(PointF a, PointF b) { + return fuzzyEquals(a.x, b.x) && fuzzyEquals(a.y, b.y); + } + + /* + * Returns the value that represents a linear transition between `from` and `to` at time `t`, + * which is on the scale [0, 1). Thus with t = 0.0f, this returns `from`; with t = 1.0f, this + * returns `to`; with t = 0.5f, this returns the value halfway from `from` to `to`. + */ + public static float interpolate(float from, float to, float t) { + return from + (to - from) * t; + } + + /** + * Returns 'value', clamped so that it isn't any lower than 'low', and it + * isn't any higher than 'high'. + */ + public static float clamp(float value, float low, float high) { + if (high < low) { + throw new IllegalArgumentException( + "clamp called with invalid parameters (" + high + " < " + low + ")" ); + } + return Math.max(low, Math.min(high, value)); + } +} |