diff --git a/.classpath b/.classpath index a4763d1..bb0c759 100644 --- a/.classpath +++ b/.classpath @@ -4,5 +4,6 @@ + diff --git a/.project b/.project index 643a2a9..2b41741 100644 --- a/.project +++ b/.project @@ -30,4 +30,11 @@ com.android.ide.eclipse.adt.AndroidNature org.eclipse.jdt.core.javanature + + + android-17 + 2 + /Users/davis/Downloads/adt-bundle-mac-x86_64-20130522/sdk/sources/android-17 + + diff --git a/README.md b/README.md index 0777126..e7b3ec0 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,12 @@ ChormeView works like Android's WebView, but is backed by the latest Chromium code. +## Required Reading +* [Chromium for Android Build Instructions](https://code.google.com/p/chromium/wiki/AndroidBuildInstructions) +* [Chrome For Developers Docs](http://dev.chromium.org/developers) +* [Chromium Source Code Search](https://code.google.com/p/chromium/codesearch) +* [Android Chromium Build Instructions](https://code.google.com/p/chromium/wiki/AndroidBuildInstructions) +* [Android WebView Talk at Google I/O 2012](https://developers.google.com/events/io/2012/sessions/gooio2012/122/) ## Why ChromeView diff --git a/assets/webviewchromium.pak b/assets/webviewchromium.pak index 2ac39bd..a7c6b4f 100644 Binary files a/assets/webviewchromium.pak and b/assets/webviewchromium.pak differ diff --git a/crbuild/update.sh b/crbuild/update.sh index 84f51eb..d88fffb 100755 --- a/crbuild/update.sh +++ b/crbuild/update.sh @@ -1,42 +1,47 @@ +#!/bin/bash + # Updates this project with the Chrome build files. # This script assumes the Chrome build VM is up at crbuild.local +# set this to your build machine's scp user@host +export BLD=crbuild@ubuntu.local + # Clean up. rm -r assets/* rm -r libs/* -rm -r src/com/googlecode -rm -r src/org/chromium +rm -rf src/com/googlecode/* +rm -rf src/org/chromium/* # ContentShell core -- use this if android_webview doesn't work out. -#scp crbuild@crbuild.local:chromium/src/out/Release/content_shell/assets/* \ -# assets/ -#scp -r crbuild@crbuild.local:chromium/src/out/Release/content_shell_apk/libs/* \ -# libs -#scp -r crbuild@crbuild.local:chromium/src/content/shell/android/java/res/* res -#scp -r crbuild@crbuild.local:chromium/src/content/shell/android/java/src/* src -#scp -r crbuild@crbuild.local:chromium/src/content/shell_apk/android/java/res/* res +#scp $BLD:chromium/src/out/Release/content_shell/assets/* assets/ +#rm libs/**/gdbserver +#scp -r $BLD:chromium/src/out/Release/content_shell_apk/libs/* libs +#scp -r $BLD:chromium/src/content/shell/android/java/res/* res +#scp -r $BLD:chromium/src/content/shell/android/java/src/* src +#scp -r $BLD:chromium/src/content/shell_apk/android/java/res/* res # android_webview -scp crbuild@crbuild.local:chromium/src/out/Release/android_webview_apk/assets/*.pak \ - assets -scp -r crbuild@crbuild.local:chromium/src/out/Release/android_webview_apk/libs/* \ - libs +scp $BLD:chromium/src/out/Release/android_webview_apk/assets/*.pak assets +scp -r $BLD:chromium/src/out/Release/android_webview_apk/libs/* libs rm libs/**/gdbserver -scp -r crbuild@crbuild.local:chromium/src/android_webview/java/src/* src/ +scp -r $BLD:chromium/src/android_webview/java/src/* src/ ## Dependencies inferred from android_webview/Android.mk # Resources. -scp -r crbuild@crbuild.local:chromium/src/content/public/android/java/resource_map/* src/ -scp -r crbuild@crbuild.local:chromium/src/ui/android/java/resource_map/* src/ +scp -r $BLD:chromium/src/content/public/android/java/resource_map/* src/ +scp -r $BLD:chromium/src/ui/android/java/resource_map/* src/ # ContentView dependencies. -scp -r crbuild@crbuild.local:chromium/src/base/android/java/src/* src/ -scp -r crbuild@crbuild.local:chromium/src/content/public/android/java/src/* src/ -scp -r crbuild@crbuild.local:chromium/src/media/base/android/java/src/* src/ -scp -r crbuild@crbuild.local:chromium/src/net/android/java/src/* src/ -scp -r crbuild@crbuild.local:chromium/src/ui/android/java/src/* src/ -scp -r crbuild@crbuild.local:chromium/src/third_party/eyesfree/src/android/java/src/* src/ +scp -r $BLD:chromium/src/base/android/java/src/* src/ +scp -r $BLD:chromium/src/content/public/android/java/src/* src/ +scp -r $BLD:chromium/src/media/base/android/java/src/* src/ +scp -r $BLD:chromium/src/net/android/java/src/* src/ +scp -r $BLD:chromium/src/ui/android/java/src/* src/ +scp -r $BLD:chromium/src/third_party/eyesfree/src/android/java/src/* src/ + +# Grab this resource file or org.chromium.chrome.browser.ResourceId.java won't compile +scp -r $BLD:chromium/src/out/Release/chromium_testshell/gen/* src/ # Strip a ContentView file that's not supposed to be here. rm src/org/chromium/content/common/common.aidl @@ -45,15 +50,15 @@ rm src/org/chromium/content/common/common.aidl rm -r src/com/googlecode/eyesfree/braille/.svn # Browser components. -scp -r crbuild@crbuild.local:chromium/src/components/web_contents_delegate_android/android/java/src/* src/ -scp -r crbuild@crbuild.local:chromium/src/components/navigation_interception/android/java/src/* src/ +scp -r $BLD:chromium/src/components/web_contents_delegate_android/android/java/src/* src/ +scp -r $BLD:chromium/src/components/navigation_interception/android/java/src/* src/ # Generated files. -scp -r crbuild@crbuild.local:chromium/src/out/Release/gen/templates/* src/ +scp -r $BLD:chromium/src/out/Release/gen/templates/* src/ # JARs. -scp -r crbuild@crbuild.local:chromium/src/out/Release/lib.java/guava_javalib.jar libs/ -scp -r crbuild@crbuild.local:chromium/src/out/Release/lib.java/jsr_305_javalib.jar libs/ +scp -r $BLD:chromium/src/out/Release/lib.java/guava_javalib.jar libs/ +scp -r $BLD:chromium/src/out/Release/lib.java/jsr_305_javalib.jar libs/ # android_webview generated sources. Must come after all the other sources. -scp -r crbuild@crbuild.local:chromium/src/android_webview/java/generated_src/* src/ +scp -r $BLD:chromium/src/android_webview/java/generated_src/* src/ diff --git a/crbuild/vm-build.md b/crbuild/vm-build.md index 9e25539..b680067 100644 --- a/crbuild/vm-build.md +++ b/crbuild/vm-build.md @@ -13,8 +13,8 @@ and building the files used in this library. These are the manual steps for setting up a VM. They only need to be done once. -1. Get the 64-bit ISO for Ubuntu Server 12.10. - * Go to http://releases.ubuntu.com/12.10/ +1. Get the 64-bit ISO for Ubuntu Server precise (12.04*). + * Go to http://releases.ubuntu.com/precise/ * Get the `64-bit PC (AMD64) server install image` 2. Set up a VirtualBox VM. diff --git a/crbuild/vm-build.sh b/crbuild/vm-build.sh old mode 100644 new mode 100755 index 2239bb4..f263c5d --- a/crbuild/vm-build.sh +++ b/crbuild/vm-build.sh @@ -1,15 +1,17 @@ -#!/bin/sh +#!/bin/bash # Builds the Chromium bits needed by ChromeView. set -o errexit # Stop the script on the first error. -set -o nounset # Catch un-initialized variables. + +CPUS=${CPUS-4} +echo "Building with '-j$CPUS'. Set the environment variable CPUS to change the number." cd ~/chromium/ # https://code.google.com/p/chromium/wiki/UsingGit gclient sync --jobs 16 cd ~/chromium/src -if [ -f ~/.build_android ] ; then +if [ -f ~/.build_arm ] ; then . build/android/envsetup.sh --target-arch=arm android_gyp ninja -C out/Release -k0 -j$CPUS libwebviewchromium android_webview_apk \ diff --git a/crbuild/vm-setup.sh b/crbuild/vm-setup.sh index 76a57f3..5fb0b1d 100755 --- a/crbuild/vm-setup.sh +++ b/crbuild/vm-setup.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # Idempotent VM setup / upgrade script. set -o errexit # Stop the script on the first error. @@ -6,6 +6,7 @@ set -o nounset # Catch un-initialized variables. # Enable password-less sudo for the current user. sudo sh -c "echo '$USER ALL=(ALL:ALL) NOPASSWD: ALL' > /etc/sudoers.d/$USER" +sudo chmod 0440 /etc/sudoers.d/crbuild # Sun JDK 6. if [ ! -f /usr/bin/javac ] ; then @@ -75,6 +76,12 @@ fi # Subversion and git-svn. sudo apt-get install -y git-svn subversion +# curl is needed for Chrome OS fonts +sudo apt-get install curl + +# automatically accept msttcorefonts eula +sudo sh -c "echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | debconf-set-selections" + # Chromium source. # https://code.google.com/p/chromium/wiki/UsingGit # http://dev.chromium.org/developers/how-tos/get-the-code diff --git a/libs/armeabi-v7a/libwebviewchromium.so b/libs/armeabi-v7a/libwebviewchromium.so index c476be5..2edd82c 100755 Binary files a/libs/armeabi-v7a/libwebviewchromium.so and b/libs/armeabi-v7a/libwebviewchromium.so differ diff --git a/libs/guava_javalib.jar b/libs/guava_javalib.jar index 24b4e2e..1a92c41 100644 Binary files a/libs/guava_javalib.jar and b/libs/guava_javalib.jar differ diff --git a/libs/jsr_305_javalib.jar b/libs/jsr_305_javalib.jar index a2a6691..7b0bc45 100644 Binary files a/libs/jsr_305_javalib.jar and b/libs/jsr_305_javalib.jar differ diff --git a/libs/x86/libwebviewchromium.so b/libs/x86/libwebviewchromium.so deleted file mode 100755 index 4213ac2..0000000 Binary files a/libs/x86/libwebviewchromium.so and /dev/null differ diff --git a/res/drawable/progress.xml b/res/drawable/progress.xml new file mode 100644 index 0000000..93322b9 --- /dev/null +++ b/res/drawable/progress.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/res/layout/shell_view.xml b/res/layout/shell_view.xml new file mode 100644 index 0000000..d6c15d6 --- /dev/null +++ b/res/layout/shell_view.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 2334481..6d70039 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,5 +1,12 @@ + + + Type URL Here text/plain - \ No newline at end of file + diff --git a/src/OWNERS b/src/OWNERS new file mode 100644 index 0000000..d85278d --- /dev/null +++ b/src/OWNERS @@ -0,0 +1,13 @@ +# Changes in this folder are guaranteed to affect the downstream +# android_webview build. Adding one of the folks from the below list to the +# review gives us time to prepare a matching change ahead of time so that our +# downstream build can continue running smoothly. +set noparent + +benm@chromium.org +boliu@chomium.org +joth@chromium.org +mkosiba@chromium.org +mnaganov@chromium.org +primiano@chromium.org +torne@chromium.org diff --git a/src/R.txt b/src/R.txt new file mode 100644 index 0000000..56a5dc8 --- /dev/null +++ b/src/R.txt @@ -0,0 +1,233 @@ +int dimen autofill_dialog_footer_height 0x7f060016 +int dimen autofill_dialog_max_height 0x7f060013 +int dimen autofill_dialog_title_height 0x7f060015 +int dimen autofill_dialog_vertical_margin 0x7f060014 +int dimen autofill_dialog_width 0x7f060012 +int dimen autofill_edit_height 0x7f060003 +int dimen autofill_edit_margin 0x7f060004 +int dimen autofill_edit_text_size 0x7f060005 +int dimen autofill_field_icon_height 0x7f060006 +int dimen autofill_field_icon_width 0x7f060007 +int dimen autofill_menu_icon_height 0x7f060008 +int dimen autofill_menu_icon_width 0x7f060009 +int dimen autofill_menu_item_padding 0x7f06000a +int dimen autofill_menu_item_size 0x7f06000b +int dimen autofill_notification_padding 0x7f06000c +int dimen autofill_notification_text_size 0x7f06000d +int dimen autofill_spinner_offset 0x7f06000e +int dimen autofill_steady_margin 0x7f06000f +int dimen autofill_steady_text_size 0x7f060010 +int dimen autofill_translation_anim_distance 0x7f060011 +int dimen certificate_viewer_padding 0x7f060002 +int dimen color_picker_gradient_margin 0x7f060000 +int dimen link_preview_overlay_radius 0x7f060001 +int drawable arrow 0x7f020000 +int drawable autofill_popup_background 0x7f020001 +int drawable autofill_popup_background_down 0x7f020002 +int drawable autofill_popup_background_up 0x7f020003 +int drawable bubble 0x7f020004 +int drawable bubble_arrow_up 0x7f020005 +int drawable color_picker_advanced_select_handle 0x7f020006 +int drawable color_picker_border 0x7f020007 +int drawable controlled_setting_mandatory_large 0x7f020008 +int drawable divider_horizontal_bright 0x7f020009 +int drawable globe_favicon 0x7f02000a +int drawable ic_menu_search_holo_light 0x7f02000b +int drawable ic_menu_share_holo_light 0x7f02000c +int drawable infobar_autofill 0x7f02000d +int drawable infobar_autologin 0x7f02000e +int drawable infobar_camera 0x7f02000f +int drawable infobar_cookie 0x7f020010 +int drawable infobar_desktop_notifications 0x7f020011 +int drawable infobar_didyoumean 0x7f020012 +int drawable infobar_geolocation 0x7f020013 +int drawable infobar_incomplete 0x7f020014 +int drawable infobar_lock 0x7f020015 +int drawable infobar_microphone 0x7f020016 +int drawable infobar_multiple_downloads 0x7f020017 +int drawable infobar_plugin 0x7f020018 +int drawable infobar_plugin_crashed 0x7f020019 +int drawable infobar_questionmark 0x7f02001a +int drawable infobar_restore 0x7f02001b +int drawable infobar_savepassword 0x7f02001c +int drawable infobar_theme 0x7f02001d +int drawable infobar_translate 0x7f02001e +int drawable infobar_waning 0x7f02001f +int drawable infobar_warning 0x7f020020 +int drawable master_card 0x7f020021 +int drawable missing 0x7f020022 +int drawable ondemand_overlay 0x7f020023 +int drawable pageinfo_bad 0x7f020024 +int drawable pageinfo_good 0x7f020025 +int drawable pageinfo_info 0x7f020026 +int drawable pageinfo_warning_major 0x7f020027 +int drawable pageinfo_warning_minor 0x7f020028 +int drawable visa 0x7f020029 +int drawable wallet_logo 0x7f02002a +int id accounts_logo 0x7f080007 +int id accounts_spinner 0x7f080008 +int id adapter_item_edit_button 0x7f08003b +int id adapter_item_line_1 0x7f080039 +int id adapter_item_line_2 0x7f08003a +int id address_spinner 0x7f08002f +int id arrow_image 0x7f080054 +int id autofill_editing_spinner_item 0x7f080024 +int id autofill_label 0x7f08003d +int id autofill_menu_item 0x7f080036 +int id autofill_menu_text 0x7f080037 +int id autofill_popup_window 0x7f080000 +int id autofill_sublabel 0x7f08003e +int id billing_city 0x7f080014 +int id billing_country_spinner 0x7f080017 +int id billing_label 0x7f08002c +int id billing_phone_number 0x7f080018 +int id billing_spinner 0x7f08002d +int id billing_state 0x7f080015 +int id billing_street_address_1 0x7f080012 +int id billing_street_address_2 0x7f080013 +int id billing_zip_code 0x7f080016 +int id bottom_notifications 0x7f080033 +int id card_number 0x7f08000c +int id cardholder_name 0x7f080010 +int id cc_billing_label 0x7f080028 +int id cc_billing_spinner 0x7f080029 +int id cc_icon 0x7f080038 +int id cc_label 0x7f08002a +int id cc_spinner 0x7f08002b +int id color_picker_advanced 0x7f080043 +int id color_picker_simple 0x7f080045 +int id color_picker_simple_border 0x7f080044 +int id content 0x7f080001 +int id cvc_challenge 0x7f08003c +int id cvc_code 0x7f08000f +int id date_picker 0x7f08004a +int id editing_layout_billing 0x7f080011 +int id editing_layout_cc_billing 0x7f08000a +int id editing_layout_credit_card 0x7f08000b +int id editing_layout_email 0x7f080019 +int id editing_layout_shipping 0x7f08001b +int id email_address 0x7f08001a +int id email_label 0x7f080030 +int id email_spinner 0x7f080031 +int id expiration_month_spinner 0x7f08000d +int id expiration_year_spinner 0x7f08000e +int id footer 0x7f080003 +int id general_layout 0x7f080025 +int id gradient 0x7f080041 +int id gradient_border 0x7f080040 +int id icon_view 0x7f080052 +int id js_modal_dialog_prompt 0x7f08004c +int id line_bottom 0x7f080034 +int id line_top 0x7f080009 +int id loading_icon 0x7f080002 +int id main_text 0x7f080055 +int id more_colors_button 0x7f080047 +int id more_colors_button_border 0x7f080046 +int id negative_button 0x7f080004 +int id pickers 0x7f08004e +int id position_in_year 0x7f08004f +int id positive_button 0x7f080005 +int id recipient_name 0x7f08001c +int id save_locally_checkbox 0x7f080032 +int id seek_bar 0x7f080042 +int id selected_color_view 0x7f080049 +int id selected_color_view_border 0x7f080048 +int id shipping_city 0x7f08001f +int id shipping_country_spinner 0x7f080022 +int id shipping_label 0x7f08002e +int id shipping_phone_number 0x7f080023 +int id shipping_state 0x7f080020 +int id shipping_street_address_1 0x7f08001d +int id shipping_street_address_2 0x7f08001e +int id shipping_zip_code 0x7f080021 +int id sub_text 0x7f080056 +int id suppress_js_modal_dialogs 0x7f08004d +int id terms_info 0x7f080035 +int id text 0x7f08003f +int id text_wrapper 0x7f080053 +int id time_picker 0x7f08004b +int id title 0x7f080006 +int id top_checkbox_notification 0x7f080026 +int id top_notifications 0x7f080027 +int id top_view 0x7f080051 +int id website_settings_description 0x7f080059 +int id website_settings_headline 0x7f080058 +int id website_settings_icon 0x7f080057 +int id year 0x7f080050 +int layout autofill_dialog_content 0x7f040000 +int layout autofill_dialog_title 0x7f040001 +int layout autofill_editing_layout_credit_card 0x7f040002 +int layout autofill_editing_layout_email 0x7f040003 +int layout autofill_editing_layout_shipping 0x7f040004 +int layout autofill_editing_spinner_item 0x7f040005 +int layout autofill_general_layout 0x7f040006 +int layout autofill_menu_item 0x7f040007 +int layout autofill_simple_menu_item 0x7f040008 +int layout autofill_text 0x7f040009 +int layout color_picker_advanced_component 0x7f04000a +int layout color_picker_dialog_content 0x7f04000b +int layout color_picker_dialog_title 0x7f04000c +int layout date_time_picker_dialog 0x7f04000d +int layout js_modal_dialog 0x7f04000e +int layout two_field_date_picker 0x7f04000f +int layout validation_message_bubble 0x7f040010 +int layout website_settings 0x7f040011 +int mipmap app_icon 0x7f030000 +int mipmap bookmark_widget_bg 0x7f030001 +int mipmap bookmark_widget_bg_highlights 0x7f030002 +int string accessibility_content_view 0x7f05000b +int string accessibility_date_picker_month 0x7f050014 +int string accessibility_date_picker_week 0x7f050015 +int string accessibility_date_picker_year 0x7f050016 +int string accessibility_datetime_picker_date 0x7f050012 +int string accessibility_datetime_picker_time 0x7f050013 +int string accessibility_js_modal_dialog_prompt 0x7f050023 +int string actionbar_share 0x7f050009 +int string actionbar_web_search 0x7f05000a +int string autofill_add_button 0x7f05002c +int string autofill_edit_button 0x7f05002d +int string autofill_fetch_message 0x7f05002f +int string autofill_negative_button_editing 0x7f05002b +int string autofill_positive_button_editing 0x7f05002a +int string autofill_terms_of_service 0x7f05002e +int string certtitle 0x7f050024 +int string color_picker_button_cancel 0x7f050007 +int string color_picker_button_more 0x7f050002 +int string color_picker_button_set 0x7f050006 +int string color_picker_dialog_title 0x7f050008 +int string color_picker_hue 0x7f050003 +int string color_picker_saturation 0x7f050004 +int string color_picker_value 0x7f050005 +int string date_picker_dialog_clear 0x7f05000d +int string date_picker_dialog_set 0x7f05000c +int string date_picker_dialog_title 0x7f05000e +int string date_time_picker_dialog_title 0x7f05000f +int string dont_reload_this_page 0x7f05001f +int string firstrun_signed_in_description 0x7f050031 +int string firstrun_signed_in_title 0x7f050030 +int string js_modal_dialog_cancel 0x7f050021 +int string js_modal_dialog_confirm 0x7f050020 +int string leave_this_page 0x7f05001c +int string low_memory_error 0x7f050000 +int string media_player_error_button 0x7f05001a +int string media_player_error_text_invalid_progressive_playback 0x7f050018 +int string media_player_error_text_unknown 0x7f050019 +int string media_player_error_title 0x7f050017 +int string media_player_loading_video 0x7f05001b +int string month_picker_dialog_title 0x7f050010 +int string opening_file_error 0x7f050001 +int string reload_this_page 0x7f05001e +int string sad_tab_help_link 0x7f050035 +int string sad_tab_help_message 0x7f050034 +int string sad_tab_message 0x7f050033 +int string sad_tab_reload_label 0x7f050036 +int string sad_tab_title 0x7f050032 +int string stay_on_this_page 0x7f05001d +int string suppress_js_modal_dialogs 0x7f050022 +int string sync_error_connection 0x7f050027 +int string sync_error_domain 0x7f050028 +int string sync_error_ga 0x7f050026 +int string sync_error_generic 0x7f050025 +int string sync_error_service_unavailable 0x7f050029 +int string week_picker_dialog_title 0x7f050011 +int style AutofillPopupWindow 0x7f070000 diff --git a/src/org/chromium/android_webview/AwAutofillManagerDelegate.java b/src/org/chromium/android_webview/AwAutofillManagerDelegate.java new file mode 100644 index 0000000..c3a74af --- /dev/null +++ b/src/org/chromium/android_webview/AwAutofillManagerDelegate.java @@ -0,0 +1,89 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.android_webview; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.content.browser.ContentViewCore; +import org.chromium.ui.autofill.AutofillPopup; +import org.chromium.ui.autofill.AutofillSuggestion; + +// Java counterpart to the AwAutofillManagerDelegate. This class is owned by +// AwContents and has a weak reference from native side. +@JNINamespace("android_webview") +public class AwAutofillManagerDelegate { + + private final int mNativeAwAutofillManagerDelegate; + private AutofillPopup mAutofillPopup; + private ViewGroup mContainerView; + private ContentViewCore mContentViewCore; + + @CalledByNative + public static AwAutofillManagerDelegate create(int nativeDelegate) { + return new AwAutofillManagerDelegate(nativeDelegate); + } + + private AwAutofillManagerDelegate(int nativeAwAutofillManagerDelegate) { + mNativeAwAutofillManagerDelegate = nativeAwAutofillManagerDelegate; + } + + public void init(ContentViewCore contentViewCore) { + mContentViewCore = contentViewCore; + mContainerView = contentViewCore.getContainerView(); + } + + @CalledByNative + private void showAutofillPopup(float x, float y, float width, float height, + AutofillSuggestion[] suggestions) { + + if (mContentViewCore == null) return; + + if (mAutofillPopup == null) { + mAutofillPopup = new AutofillPopup( + mContentViewCore.getContext(), + mContentViewCore.getViewAndroidDelegate(), + new AutofillPopup.AutofillPopupDelegate() { + public void requestHide() { } + public void suggestionSelected(int listIndex) { + nativeSuggestionSelected(mNativeAwAutofillManagerDelegate, listIndex); + } + }); + } + mAutofillPopup.setAnchorRect(x, y, width, height); + mAutofillPopup.show(suggestions); + } + + @CalledByNative + public void hideAutofillPopup() { + if (mAutofillPopup == null) + return; + mAutofillPopup.hide(); + mAutofillPopup = null; + } + + @CalledByNative + private static AutofillSuggestion[] createAutofillSuggestionArray(int size) { + return new AutofillSuggestion[size]; + } + + /** + * @param array AutofillSuggestion array that should get a new suggestion added. + * @param index Index in the array where to place a new suggestion. + * @param name Name of the suggestion. + * @param label Label of the suggestion. + * @param uniqueId Unique suggestion id. + */ + @CalledByNative + private static void addToAutofillSuggestionArray(AutofillSuggestion[] array, int index, + String name, String label, int uniqueId) { + array[index] = new AutofillSuggestion(name, label, uniqueId); + } + + private native void nativeSuggestionSelected(int nativeAwAutofillManagerDelegate, int position); +} diff --git a/src/org/chromium/android_webview/AwBrowserContext.java b/src/org/chromium/android_webview/AwBrowserContext.java index 2536d7b..0d0ccac 100644 --- a/src/org/chromium/android_webview/AwBrowserContext.java +++ b/src/org/chromium/android_webview/AwBrowserContext.java @@ -4,6 +4,7 @@ package org.chromium.android_webview; +import android.content.Context; import android.content.SharedPreferences; import org.chromium.content.browser.ContentViewStatics; @@ -19,11 +20,14 @@ */ public class AwBrowserContext { + private static final String HTTP_AUTH_DATABASE_FILE = "http_auth.db"; + private SharedPreferences mSharedPreferences; private AwGeolocationPermissions mGeolocationPermissions; private AwCookieManager mCookieManager; private AwFormDatabase mFormDatabase; + private HttpAuthDatabase mHttpAuthDatabase; public AwBrowserContext(SharedPreferences sharedPreferences) { mSharedPreferences = sharedPreferences; @@ -50,6 +54,13 @@ public AwFormDatabase getFormDatabase() { return mFormDatabase; } + public HttpAuthDatabase getHttpAuthDatabase(Context context) { + if (mHttpAuthDatabase == null) { + mHttpAuthDatabase = new HttpAuthDatabase(context, HTTP_AUTH_DATABASE_FILE); + }; + return mHttpAuthDatabase; + } + /** * @see android.webkit.WebView#pauseTimers() */ @@ -63,4 +74,4 @@ public void pauseTimers() { public void resumeTimers() { ContentViewStatics.setWebKitSharedTimersSuspended(false); } -} \ No newline at end of file +} diff --git a/src/org/chromium/android_webview/AwContentVideoViewDelegate.java b/src/org/chromium/android_webview/AwContentVideoViewDelegate.java deleted file mode 100644 index 6a82e62..0000000 --- a/src/org/chromium/android_webview/AwContentVideoViewDelegate.java +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package org.chromium.android_webview; - -import android.content.Context; -import android.content.pm.ActivityInfo; -import android.view.View; -import android.webkit.WebChromeClient.CustomViewCallback; - -import org.chromium.content.browser.ContentVideoViewContextDelegate; - -/** - * This further delegates the responsibility displaying full-screen video to the - * Webview client. - */ -public class AwContentVideoViewDelegate implements ContentVideoViewContextDelegate { - private AwContentsClient mAwContentsClient; - private Context mContext; - - public AwContentVideoViewDelegate(AwContentsClient client, Context context) { - mAwContentsClient = client; - mContext = context; - } - - @Override - public void onShowCustomView(View view) { - CustomViewCallback cb = new CustomViewCallback() { - @Override - public void onCustomViewHidden() { - // TODO: we need to invoke ContentVideoView.onDestroyContentVideoView() here. - } - }; - mAwContentsClient.onShowCustomView(view, cb); - } - - @Override - public void onDestroyContentVideoView() { - mAwContentsClient.onHideCustomView(); - } - - @Override - public Context getContext() { - return mContext; - } - - @Override - public View getVideoLoadingProgressView() { - return mAwContentsClient.getVideoLoadingProgressView(); - } -} diff --git a/src/org/chromium/android_webview/AwContents.java b/src/org/chromium/android_webview/AwContents.java index 499a42c..f522486 100644 --- a/src/org/chromium/android_webview/AwContents.java +++ b/src/org/chromium/android_webview/AwContents.java @@ -4,7 +4,7 @@ package org.chromium.android_webview; -import android.annotation.SuppressLint; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -16,6 +16,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Message; +import android.os.Process; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; @@ -25,11 +26,15 @@ import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.webkit.GeolocationPermissions; import android.webkit.ValueCallback; +import android.widget.OverScroller; + import com.google.common.annotations.VisibleForTesting; + import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; import org.chromium.base.ThreadUtils; @@ -46,10 +51,12 @@ import org.chromium.components.navigation_interception.NavigationParams; import org.chromium.net.GURLUtils; import org.chromium.ui.gfx.DeviceDisplayInfo; + import java.io.File; import java.lang.annotation.Annotation; import java.net.MalformedURLException; import java.net.URL; +import java.util.concurrent.Callable; /** * Exposes the native AwContents class, and together these classes wrap the ContentViewCore @@ -61,7 +68,7 @@ */ @JNINamespace("android_webview") public class AwContents { - private static final String TAG = AwContents.class.getSimpleName(); + private static final String TAG = "AwContents"; private static final String WEB_ARCHIVE_EXTENSION = ".mht"; @@ -87,6 +94,28 @@ public static class HitTestData { * dispatching of view methods through the containing view. */ public interface InternalAccessDelegate extends ContentViewCore.InternalAccessDelegate { + /** + * @see View#onScrollChanged(int, int, int, int) + * + * TODO(mkosiba): WebViewClassic calls this, AwContents doesn't. Check if there + * are any cases we're missing, if not - remove. + */ + void onScrollChanged(int lPix, int tPix, int oldlPix, int oldtPix); + + /** + * @see View#overScrollBy(int, int, int, int, int, int, int, int, boolean); + */ + void overScrollBy(int deltaX, int deltaY, + int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, + int maxOverScrollX, int maxOverScrollY, + boolean isTouchEvent); + + /** + * @see View#scrollTo(int, int) + */ + void super_scrollTo(int scrollX, int scrollY); + /** * @see View#setMeasuredDimension(int, int) */ @@ -105,39 +134,46 @@ public interface InternalAccessDelegate extends ContentViewCore.InternalAccessDe } private int mNativeAwContents; - private AwBrowserContext mBrowserContext; - private ViewGroup mContainerView; + private final AwBrowserContext mBrowserContext; + private final ViewGroup mContainerView; private ContentViewCore mContentViewCore; - private AwContentsClient mContentsClient; - private AwContentsClientBridge mContentsClientBridge; - private AwWebContentsDelegate mWebContentsDelegate; - private AwContentsIoThreadClient mIoThreadClient; - private InterceptNavigationDelegateImpl mInterceptNavigationDelegate; - private InternalAccessDelegate mInternalAccessAdapter; + private final AwContentsClient mContentsClient; + private final AwContentsClientBridge mContentsClientBridge; + private final AwWebContentsDelegate mWebContentsDelegate; + private final AwContentsIoThreadClient mIoThreadClient; + private final InterceptNavigationDelegateImpl mInterceptNavigationDelegate; + private final InternalAccessDelegate mInternalAccessAdapter; private final AwLayoutSizer mLayoutSizer; - private AwZoomControls mZoomControls; + private final AwZoomControls mZoomControls; + private final AwScrollOffsetManager mScrollOffsetManager; + private OverScrollGlow mOverScrollGlow; // This can be accessed on any thread after construction. See AwContentsIoThreadClient. private final AwSettings mSettings; - private boolean mIsPaused; + + private boolean mIsVisible; // Equivalent to windowVisible && viewVisible. + private boolean mIsAttachedToWindow; private Bitmap mFavicon; private boolean mHasRequestedVisitedHistoryFromClient; // TODO(boliu): This should be in a global context, not per webview. private final double mDIPScale; + // The base background color, i.e. not accounting for any CSS body from the current page. + private int mBaseBackgroundColor = Color.WHITE; + // Must call nativeUpdateLastHitTestData first to update this before use. - private final HitTestData mPossiblyStaleHitTestData; + private final HitTestData mPossiblyStaleHitTestData = new HitTestData(); private DefaultVideoPosterRequestHandler mDefaultVideoPosterRequestHandler; - private boolean mNewPictureInvalidationOnly; - - private Rect mGlobalVisibleBounds; - private int mLastGlobalVisibleWidth; - private int mLastGlobalVisibleHeight; + // Bound method for suppling Picture instances to the AwContentsClient. Will be null if the + // picture listener API has not yet been enabled, or if it is using invalidation-only mode. + private Callable mPictureListenerContentProvider; private boolean mContainerViewFocused; private boolean mWindowFocused; + private AwAutofillManagerDelegate mAwAutofillManagerDelegate; + private static final class DestroyRunnable implements Runnable { private int mNativeAwContents; private DestroyRunnable(int nativeAwContents) { @@ -228,8 +264,24 @@ public void onUrlLoadRequested(String url) { @Override public boolean shouldIgnoreNavigation(NavigationParams navigationParams) { final String url = navigationParams.url; + final int transitionType = navigationParams.pageTransitionType; + final boolean isLoadUrl = + (transitionType & PageTransitionTypes.PAGE_TRANSITION_FROM_API) != 0; + final boolean isBackForward = + (transitionType & PageTransitionTypes.PAGE_TRANSITION_FORWARD_BACK) != 0; + final boolean isReload = + (transitionType & PageTransitionTypes.PAGE_TRANSITION_CORE_MASK) == + PageTransitionTypes.PAGE_TRANSITION_RELOAD; + final boolean isRedirect = navigationParams.isRedirect; + boolean ignoreNavigation = false; - if (mLastLoadUrlAddress != null && mLastLoadUrlAddress.equals(url)) { + + // Any navigation from loadUrl, goBack/Forward, or reload, are considered application + // initiated and hence will not yield a shouldOverrideUrlLoading() callback. + // TODO(joth): Using PageTransitionTypes should be sufficient to determine all app + // initiated navigations, and so mLastLoadUrlAddress should be removed. + if ((isLoadUrl && !isRedirect) || isBackForward || isReload || + mLastLoadUrlAddress != null && mLastLoadUrlAddress.equals(url)) { // Support the case where the user clicks on a link that takes them back to the // same page. mLastLoadUrlAddress = null; @@ -276,7 +328,57 @@ public void setMeasuredDimension(int measuredWidth, int measuredHeight) { } //-------------------------------------------------------------------------------------------- - private class AwPinchGestureStateListener implements ContentViewCore.PinchGestureStateListener { + // NOTE: This content size change notification comes from the compositor and reflects the size + // of the content on screen (but not neccessarily in the renderer main thread). + private class AwContentUpdateFrameInfoListener + implements ContentViewCore.UpdateFrameInfoListener { + @Override + public void onFrameInfoUpdated(float widthCss, float heightCss, float pageScaleFactor) { + int widthPix = (int) Math.floor(widthCss * mDIPScale * pageScaleFactor); + int heightPix = (int) Math.floor(heightCss * mDIPScale * pageScaleFactor); + mScrollOffsetManager.setContentSize(widthPix, heightPix); + + nativeSetDisplayedPageScaleFactor(mNativeAwContents, pageScaleFactor); + } + } + + //-------------------------------------------------------------------------------------------- + private class AwScrollOffsetManagerDelegate implements AwScrollOffsetManager.Delegate { + @Override + public void overScrollContainerViewBy(int deltaX, int deltaY, int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, boolean isTouchEvent) { + mInternalAccessAdapter.overScrollBy(deltaX, deltaY, scrollX, scrollY, + scrollRangeX, scrollRangeY, 0, 0, isTouchEvent); + } + + @Override + public void scrollContainerViewTo(int x, int y) { + mInternalAccessAdapter.super_scrollTo(x, y); + } + + @Override + public void scrollNativeTo(int x, int y) { + nativeScrollTo(mNativeAwContents, x, y); + } + + @Override + public int getContainerViewScrollX() { + return mContainerView.getScrollX(); + } + + @Override + public int getContainerViewScrollY() { + return mContainerView.getScrollY(); + } + + @Override + public void invalidate() { + mContainerView.invalidate(); + } + } + + //-------------------------------------------------------------------------------------------- + private class AwGestureStateListener implements ContentViewCore.GestureStateListener { @Override public void onPinchGestureStart() { // While it's possible to re-layout the view during a pinch gesture, the effect is very @@ -288,24 +390,27 @@ public void onPinchGestureStart() { mLayoutSizer.freezeLayoutRequests(); } + @Override public void onPinchGestureEnd() { mLayoutSizer.unfreezeLayoutRequests(); } - } - //-------------------------------------------------------------------------------------------- - private class ScrollChangeListener implements ViewTreeObserver.OnScrollChangedListener { @Override - public void onScrollChanged() { - // We do this to cover the case that when the view hierarchy is scrolled, - // more of the containing view becomes visible (i.e. a containing view - // with a width/height of "wrap_content" and dimensions greater than - // that of the screen). - AwContents.this.updatePhysicalBackingSizeIfNeeded(); - } - }; + public void onFlingStartGesture(int velocityX, int velocityY) { + mScrollOffsetManager.onFlingStartGesture(velocityX, velocityY); + } + - private ScrollChangeListener mScrollChangeListener; + @Override + public void onFlingCancelGesture() { + mScrollOffsetManager.onFlingCancelGesture(); + } + + @Override + public void onUnhandledFlingStartEvent() { + mScrollOffsetManager.onUnhandledFlingStartEvent(); + } + } /** * @param browserContext the browsing context to associate this view contents with. @@ -325,7 +430,7 @@ public AwContents(AwBrowserContext browserContext, ViewGroup containerView, private static ContentViewCore createAndInitializeContentViewCore(ViewGroup containerView, InternalAccessDelegate internalDispatcher, int nativeWebContents, - ContentViewCore.PinchGestureStateListener pinchGestureStateListener, + ContentViewCore.GestureStateListener pinchGestureStateListener, ContentViewClient contentViewClient, ContentViewCore.ZoomControlsDelegate zoomControlsDelegate) { ContentViewCore contentViewCore = new ContentViewCore(containerView.getContext()); @@ -333,7 +438,7 @@ private static ContentViewCore createAndInitializeContentViewCore(ViewGroup cont // compositor, not because input events are delivered immediately. contentViewCore.initialize(containerView, internalDispatcher, nativeWebContents, null, ContentViewCore.INPUT_EVENTS_DELIVERED_IMMEDIATELY); - contentViewCore.setPinchGestureStateListener(pinchGestureStateListener); + contentViewCore.setGestureStateListener(pinchGestureStateListener); contentViewCore.setContentViewClient(contentViewClient); contentViewCore.setZoomControlsDelegate(zoomControlsDelegate); return contentViewCore; @@ -351,93 +456,149 @@ public AwContents(AwBrowserContext browserContext, ViewGroup containerView, mBrowserContext = browserContext; mContainerView = containerView; mInternalAccessAdapter = internalAccessAdapter; - mDIPScale = DeviceDisplayInfo.create(containerView.getContext()).getDIPScale(); - // Note that ContentViewCore must be set up before AwContents, as ContentViewCore - // setup performs process initialisation work needed by AwContents. - mContentsClientBridge = new AwContentsClientBridge(contentsClient); + mContentsClient = contentsClient; mLayoutSizer = layoutSizer; + mDIPScale = DeviceDisplayInfo.create(containerView.getContext()).getDIPScale(); mLayoutSizer.setDelegate(new AwLayoutSizerDelegate()); mLayoutSizer.setDIPScale(mDIPScale); mWebContentsDelegate = new AwWebContentsDelegateAdapter(contentsClient, mLayoutSizer.getPreferredSizeChangedListener()); - mNativeAwContents = nativeInit(mWebContentsDelegate, mContentsClientBridge); - mContentsClient = contentsClient; + mContentsClientBridge = new AwContentsClientBridge(contentsClient); + mZoomControls = new AwZoomControls(this); + mIoThreadClient = new IoThreadClientImpl(); + mInterceptNavigationDelegate = new InterceptNavigationDelegateImpl(); + + boolean hasInternetPermission = containerView.getContext().checkPermission( + android.Manifest.permission.INTERNET, + Process.myPid(), + Process.myUid()) == PackageManager.PERMISSION_GRANTED; + AwSettings.ZoomSupportChangeListener zoomListener = + new AwSettings.ZoomSupportChangeListener() { + @Override + public void onMultiTouchZoomSupportChanged(boolean supportsMultiTouchZoom) { + mContentViewCore.updateMultiTouchZoomSupport(supportsMultiTouchZoom); + } + }; + mSettings = new AwSettings(mContainerView.getContext(), hasInternetPermission, zoomListener, + isAccessFromFileURLsGrantedByDefault, mDIPScale); + mDefaultVideoPosterRequestHandler = new DefaultVideoPosterRequestHandler(mContentsClient); + mSettings.setDefaultVideoPosterURL( + mDefaultVideoPosterRequestHandler.getDefaultVideoPosterURL()); + mContentsClient.setDIPScale(mDIPScale); + mScrollOffsetManager = new AwScrollOffsetManager(new AwScrollOffsetManagerDelegate(), + new OverScroller(mContainerView.getContext())); + + setOverScrollMode(mContainerView.getOverScrollMode()); + + setNewAwContents(nativeInit(browserContext)); + } + + /** + * Common initialization routine for adopting a native AwContents instance into this + * java instance. + * + * TAKE CARE! This method can get called multiple times per java instance. Code accordingly. + * ^^^^^^^^^ See the native class declaration for more details on relative object lifetimes. + */ + private void setNewAwContents(int newAwContentsPtr) { + if (mNativeAwContents != 0) { + destroy(); + mContentViewCore = null; + } + + assert mNativeAwContents == 0 && mCleanupReference == null && mContentViewCore == null; + + mNativeAwContents = newAwContentsPtr; + // TODO(joth): when the native and java counterparts of AwBrowserContext are hooked up to + // each other, we should update |mBrowserContext| according to the newly received native + // WebContent's browser context. + + // The native side object has been bound to this java instance, so now is the time to + // bind all the native->java relationships. mCleanupReference = new CleanupReference(this, new DestroyRunnable(mNativeAwContents)); int nativeWebContents = nativeGetWebContents(mNativeAwContents); - mZoomControls = new AwZoomControls(this); mContentViewCore = createAndInitializeContentViewCore( - containerView, internalAccessAdapter, nativeWebContents, - new AwPinchGestureStateListener(), mContentsClient.getContentViewClient(), + mContainerView, mInternalAccessAdapter, nativeWebContents, + new AwGestureStateListener(), mContentsClient.getContentViewClient(), mZoomControls); + nativeSetJavaPeers(mNativeAwContents, this, mWebContentsDelegate, mContentsClientBridge, + mIoThreadClient, mInterceptNavigationDelegate); mContentsClient.installWebContentsObserver(mContentViewCore); + mContentViewCore.setUpdateFrameInfoListener(new AwContentUpdateFrameInfoListener()); + mSettings.setWebContents(nativeWebContents); + nativeSetDipScale(mNativeAwContents, (float) mDIPScale); + updateGlobalVisibleRect(); - mSettings = new AwSettings(mContentViewCore.getContext(), nativeWebContents, - mContentViewCore, isAccessFromFileURLsGrantedByDefault); - setIoThreadClient(new IoThreadClientImpl()); - setInterceptNavigationDelegate(new InterceptNavigationDelegateImpl()); - - mPossiblyStaleHitTestData = new HitTestData(); - nativeDidInitializeContentViewCore(mNativeAwContents, - mContentViewCore.getNativeContentViewCore()); - - mContentsClient.setDIPScale(mDIPScale); - mSettings.setDIPScale(mDIPScale); - mDefaultVideoPosterRequestHandler = new DefaultVideoPosterRequestHandler(mContentsClient); - mSettings.setDefaultVideoPosterURL( - mDefaultVideoPosterRequestHandler.getDefaultVideoPosterURL()); + // The only call to onShow. onHide should never be called. + mContentViewCore.onShow(); + } - ContentVideoView.registerContentVideoViewContextDelegate( - new AwContentVideoViewDelegate(contentsClient, containerView.getContext())); - mGlobalVisibleBounds = new Rect(); - } - - private void updatePhysicalBackingSizeIfNeeded() { - // We musn't let the physical backing size get too big, otherwise we - // will try to allocate a SurfaceTexture beyond what the GL driver can - // cope with. In most cases, limiting the SurfaceTexture size to that - // of the visible bounds of the WebView will be good enough i.e. the maximum - // SurfaceTexture dimensions will match the screen dimensions). - mContainerView.getGlobalVisibleRect(mGlobalVisibleBounds); - int width = mGlobalVisibleBounds.width(); - int height = mGlobalVisibleBounds.height(); - if (width != mLastGlobalVisibleWidth || height != mLastGlobalVisibleHeight) { - mLastGlobalVisibleWidth = width; - mLastGlobalVisibleHeight = height; - mContentViewCore.onPhysicalBackingSizeChanged(width, height); + /** + * Called on the "source" AwContents that is opening the popup window to + * provide the AwContents to host the pop up content. + */ + public void supplyContentsForPopup(AwContents newContents) { + int popupNativeAwContents = nativeReleasePopupAwContents(mNativeAwContents); + if (popupNativeAwContents == 0) { + Log.w(TAG, "Popup WebView bind failed: no pending content."); + if (newContents != null) newContents.destroy(); + return; + } + if (newContents == null) { + nativeDestroy(popupNativeAwContents); + return; } - } - @VisibleForTesting - public ContentViewCore getContentViewCore() { - return mContentViewCore; + newContents.receivePopupContents(popupNativeAwContents); } - // Can be called from any thread. - public AwSettings getSettings() { - return mSettings; - } + // Recap: supplyContentsForPopup() is called on the parent window's content, this method is + // called on the popup window's content. + private void receivePopupContents(int popupNativeAwContents) { + // Save existing view state. + final boolean wasAttached = mIsAttachedToWindow; + final boolean wasVisible = getContainerViewVisible(); + final boolean wasPaused = mUnimplementedIsPaused; + final boolean wasFocused = mWindowFocused; - public void setIoThreadClient(AwContentsIoThreadClient ioThreadClient) { - mIoThreadClient = ioThreadClient; - nativeSetIoThreadClient(mNativeAwContents, mIoThreadClient); - } + // Properly clean up existing mContentViewCore and mNativeAwContents. + if (wasFocused) onWindowFocusChanged(false); + if (wasVisible) setVisibilityInternal(false); + // TODO(boliu): This may destroy GL resources outside of functor. + if (wasAttached) onDetachedFromWindow(); + if (!wasPaused) onPause(); - private void setInterceptNavigationDelegate(InterceptNavigationDelegateImpl delegate) { - mInterceptNavigationDelegate = delegate; - nativeSetInterceptNavigationDelegate(mNativeAwContents, delegate); + setNewAwContents(popupNativeAwContents); + + // Finally refresh all view state for mContentViewCore and mNativeAwContents. + if (!wasPaused) onResume(); + if (wasAttached) onAttachedToWindow(); + onSizeChanged(mContainerView.getWidth(), mContainerView.getHeight(), 0, 0); + if (wasVisible) setVisibilityInternal(true); + if (wasFocused) onWindowFocusChanged(true); } public void destroy() { mContentViewCore.destroy(); - // The native part of AwSettings isn't needed for the IoThreadClient instance. - mSettings.destroy(); // We explicitly do not null out the mContentViewCore reference here // because ContentViewCore already has code to deal with the case // methods are called on it after it's been destroyed, and other // code relies on AwContents.mContentViewCore to be non-null. - mCleanupReference.cleanupNow(); + + if (mCleanupReference != null) mCleanupReference.cleanupNow(); mNativeAwContents = 0; + mCleanupReference = null; + } + + @VisibleForTesting + public ContentViewCore getContentViewCore() { + return mContentViewCore; + } + + // Can be called from any thread. + public AwSettings getSettings() { + return mSettings; } public static void setAwDrawSWFunctionTable(int functionTablePointer) { @@ -452,31 +613,65 @@ public static int getAwDrawGLFunction() { return nativeGetAwDrawGLFunction(); } + /** + * Intended for test code. + * @return the number of native instances of this class. + */ + @VisibleForTesting + public static int getNativeInstanceCount() { + return nativeGetNativeInstanceCount(); + } + public int getAwDrawGLViewContext() { // Using the native pointer as the returned viewContext. This is matched by the // reinterpret_cast back to BrowserViewRenderer pointer in the native DrawGLFunction. return nativeGetAwDrawGLViewContext(mNativeAwContents); } + // This is only to avoid heap allocations inside updateGLobalVisibleRect. It should treated + // as a local variable in the function and not used anywhere else. + private static final Rect sLocalGlobalVisibleRect = new Rect(); + + @CalledByNative + private void updateGlobalVisibleRect() { + mContainerView.getGlobalVisibleRect(sLocalGlobalVisibleRect); + nativeSetGlobalVisibleRect(mNativeAwContents, sLocalGlobalVisibleRect.left, + sLocalGlobalVisibleRect.top, sLocalGlobalVisibleRect.right, + sLocalGlobalVisibleRect.bottom); + } + + //-------------------------------------------------------------------------------------------- + // WebView[Provider] method implementations (where not provided by ContentViewCore) + //-------------------------------------------------------------------------------------------- + + // Only valid within onDraw(). + private final Rect mClipBoundsTemporary = new Rect(); + public void onDraw(Canvas canvas) { - if (mNativeAwContents == 0) return; - if (canvas.isHardwareAccelerated() && - nativePrepareDrawGL(mNativeAwContents, - mContainerView.getScrollX(), mContainerView.getScrollY()) && - mInternalAccessAdapter.requestDrawGL(canvas)) { + if (mNativeAwContents == 0) { + canvas.drawColor(getEffectiveBackgroundColor()); return; } - Rect clip = canvas.getClipBounds(); - if (!nativeDrawSW(mNativeAwContents, canvas, clip.left, clip.top, - clip.right - clip.left, clip.bottom - clip.top)) { - Log.w(TAG, "Native DrawSW failed; clearing to background color."); - int c = mContentViewCore.getBackgroundColor(); - canvas.drawRGB(Color.red(c), Color.green(c), Color.blue(c)); + + mScrollOffsetManager.syncScrollOffsetFromOnDraw(); + + canvas.getClipBounds(mClipBoundsTemporary); + if (!nativeOnDraw(mNativeAwContents, canvas, canvas.isHardwareAccelerated(), + mContainerView.getScrollX(), mContainerView.getScrollY(), + mClipBoundsTemporary.left, mClipBoundsTemporary.top, + mClipBoundsTemporary.right, mClipBoundsTemporary.bottom)) { + Log.w(TAG, "nativeOnDraw failed; clearing to background color."); + canvas.drawColor(getEffectiveBackgroundColor()); + } + + if (mOverScrollGlow != null && mOverScrollGlow.drawEdgeGlows(canvas, + mScrollOffsetManager.computeMaximumHorizontalScrollOffset(), + mScrollOffsetManager.computeMaximumVerticalScrollOffset())) { + mContainerView.invalidate(); } } - @SuppressLint("WrongCall") - public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { mLayoutSizer.onMeasure(widthMeasureSpec, heightMeasureSpec); } @@ -489,27 +684,30 @@ public int getContentWidthCss() { } public Picture capturePicture() { - return nativeCapturePicture(mNativeAwContents); + return nativeCapturePicture(mNativeAwContents, + mScrollOffsetManager.computeHorizontalScrollRange(), + mScrollOffsetManager.computeVerticalScrollRange()); } /** - * Enable the OnNewPicture callback. + * Enable the onNewPicture callback. * @param enabled Flag to enable the callback. * @param invalidationOnly Flag to call back only on invalidation without providing a picture. */ public void enableOnNewPicture(boolean enabled, boolean invalidationOnly) { - mNewPictureInvalidationOnly = invalidationOnly; + if (invalidationOnly) { + mPictureListenerContentProvider = null; + } else if (enabled && mPictureListenerContentProvider == null) { + mPictureListenerContentProvider = new Callable() { + @Override + public Picture call() { + return capturePicture(); + } + }; + } nativeEnableOnNewPicture(mNativeAwContents, enabled); } - // This is no longer synchronous and just calls the Async version and return 0. - // TODO(boliu): Remove this method. - @Deprecated - public int findAllSync(String searchString) { - findAllAsync(searchString); - return 0; - } - public void findAllAsync(String searchString) { if (mNativeAwContents == 0) return; nativeFindAllAsync(mNativeAwContents, searchString); @@ -575,6 +773,8 @@ public void loadUrl(LoadUrlParams params) { params.getTransitionType() == PageTransitionTypes.PAGE_TRANSITION_LINK) { params.setTransitionType(PageTransitionTypes.PAGE_TRANSITION_RELOAD); } + params.setTransitionType( + params.getTransitionType() | PageTransitionTypes.PAGE_TRANSITION_FROM_API); // For WebView, always use the user agent override, which is set // every time the user agent in AwSettings is modified. @@ -611,50 +811,6 @@ public String getUrl() { if (url == null || url.trim().isEmpty()) return null; return url; } - /** - * Called on the "source" AwContents that is opening the popup window to - * provide the AwContents to host the pop up content. - */ - public void supplyContentsForPopup(AwContents newContents) { - int popupWebContents = nativeReleasePopupWebContents(mNativeAwContents); - assert popupWebContents != 0; - newContents.setNewWebContents(popupWebContents); - } - - private void setNewWebContents(int newWebContentsPtr) { - // When setting a new WebContents, we new up a ContentViewCore that will - // wrap it and then swap it. - ContentViewCore newCore = createAndInitializeContentViewCore( - mContainerView, mInternalAccessAdapter, newWebContentsPtr, - new AwPinchGestureStateListener(), mContentsClient.getContentViewClient(), - mZoomControls); - mContentsClient.installWebContentsObserver(newCore); - - // Now swap the Java side reference. - mContentViewCore.destroy(); - mContentViewCore = newCore; - - // Now rewire native side to use the new WebContents. - nativeSetWebContents(mNativeAwContents, newWebContentsPtr); - nativeSetIoThreadClient(mNativeAwContents, mIoThreadClient); - nativeSetInterceptNavigationDelegate(mNativeAwContents, mInterceptNavigationDelegate); - - // This will also apply settings to the new WebContents. - mSettings.setWebContents(newWebContentsPtr); - - // Finally poke the new ContentViewCore with the size of the container view and show it. - if (mContainerView.getWidth() != 0 || mContainerView.getHeight() != 0) { - mContentViewCore.onSizeChanged( - mContainerView.getWidth(), mContainerView.getHeight(), 0, 0); - } - nativeDidInitializeContentViewCore(mNativeAwContents, - mContentViewCore.getNativeContentViewCore()); - if (mContainerView.getVisibility() == View.VISIBLE) { - // The popup window was hidden when we prompted the embedder to display - // it, so show it again now we have a container. - mContentViewCore.onShow(); - } - } public void requestFocus() { if (!mContainerView.isInTouchMode() && mSettings.shouldFocusFirstNode()) { @@ -662,6 +818,21 @@ public void requestFocus() { } } + public void setBackgroundColor(int color) { + mBaseBackgroundColor = color; + if (mNativeAwContents != 0) nativeSetBackgroundColor(mNativeAwContents, color); + } + + private int getEffectiveBackgroundColor() { + // Do not ask the ContentViewCore for the background color, as it will always + // report white prior to initial navigation or post destruction, whereas we want + // to use the client supplied base value in those cases. + if (mNativeAwContents == 0 || !mContentsClient.isCachedRendererBackgroundColorValid()) { + return mBaseBackgroundColor; + } + return mContentsClient.getCachedRendererBackgroundColor(); + } + public boolean isMultiTouchZoomSupported() { return mSettings.supportsMultiTouchZoom(); } @@ -670,10 +841,6 @@ public View getZoomControlsForTest() { return mZoomControls.getZoomControlsViewForTest(); } - //-------------------------------------------------------------------------------------------- - // WebView[Provider] method implementations (where not provided by ContentViewCore) - //-------------------------------------------------------------------------------------------- - /** * @see ContentViewCore#getContentSettings() */ @@ -682,38 +849,83 @@ public ContentSettings getContentSettings() { } /** - * @see ContentViewCore#computeHorizontalScrollRange() + * @see View#setOverScrollMode(int) + */ + public void setOverScrollMode(int mode) { + if (mode != View.OVER_SCROLL_NEVER) { + mOverScrollGlow = new OverScrollGlow(mContainerView); + } else { + mOverScrollGlow = null; + } + } + + /** + * Called by the embedder when the scroll offset of the containing view has changed. + * @see View#onScrollChanged(int,int) + */ + public void onContainerViewScrollChanged(int l, int t, int oldl, int oldt) { + mScrollOffsetManager.onContainerViewScrollChanged(l, t); + } + + /** + * Called by the embedder when the containing view is to be scrolled or overscrolled. + * @see View#onOverScrolled(int,int,int,int) + */ + public void onContainerViewOverScrolled(int scrollX, int scrollY, boolean clampedX, + boolean clampedY) { + int oldX = mContainerView.getScrollX(); + int oldY = mContainerView.getScrollY(); + + mScrollOffsetManager.onContainerViewOverScrolled(scrollX, scrollY, clampedX, clampedY); + + if (mOverScrollGlow != null) { + mOverScrollGlow.pullGlow(mContainerView.getScrollX(), mContainerView.getScrollY(), + oldX, oldY, + mScrollOffsetManager.computeMaximumHorizontalScrollOffset(), + mScrollOffsetManager.computeMaximumVerticalScrollOffset()); + } + } + + /** + * @see View.computeScroll() + */ + public void computeScroll() { + mScrollOffsetManager.computeScrollAndAbsorbGlow(mOverScrollGlow); + } + + /** + * @see View#computeHorizontalScrollRange() */ public int computeHorizontalScrollRange() { - return mContentViewCore.computeHorizontalScrollRange(); + return mScrollOffsetManager.computeHorizontalScrollRange(); } /** - * @see ContentViewCore#computeHorizontalScrollOffset() + * @see View#computeHorizontalScrollOffset() */ public int computeHorizontalScrollOffset() { - return mContentViewCore.computeHorizontalScrollOffset(); + return mScrollOffsetManager.computeHorizontalScrollOffset(); } /** - * @see ContentViewCore#computeVerticalScrollRange() + * @see View#computeVerticalScrollRange() */ public int computeVerticalScrollRange() { - return mContentViewCore.computeVerticalScrollRange(); + return mScrollOffsetManager.computeVerticalScrollRange(); } /** - * @see ContentViewCore#computeVerticalScrollOffset() + * @see View#computeVerticalScrollOffset() */ public int computeVerticalScrollOffset() { - return mContentViewCore.computeVerticalScrollOffset(); + return mScrollOffsetManager.computeVerticalScrollOffset(); } /** - * @see ContentViewCore#computeVerticalScrollExtent() + * @see View#computeVerticalScrollExtent() */ public int computeVerticalScrollExtent() { - return mContentViewCore.computeVerticalScrollExtent(); + return mScrollOffsetManager.computeVerticalScrollExtent(); } /** @@ -781,7 +993,6 @@ public void goBackOrForward(int steps) { /** * @see android.webkit.WebView#pauseTimers() */ - // TODO(kristianm): Remove public void pauseTimers() { ContentViewStatics.setWebKitSharedTimersSuspended(true); } @@ -789,32 +1000,31 @@ public void pauseTimers() { /** * @see android.webkit.WebView#resumeTimers() */ - // TODO(kristianm): Remove public void resumeTimers() { ContentViewStatics.setWebKitSharedTimersSuspended(false); } + private boolean mUnimplementedIsPaused; + /** * @see android.webkit.WebView#onPause() */ public void onPause() { - mIsPaused = true; - mContentViewCore.onHide(); + mUnimplementedIsPaused = true; } /** * @see android.webkit.WebView#onResume() */ public void onResume() { - mContentViewCore.onShow(); - mIsPaused = false; + mUnimplementedIsPaused = false; } /** * @see android.webkit.WebView#isPaused() */ public boolean isPaused() { - return mIsPaused; + return mUnimplementedIsPaused; } /** @@ -906,13 +1116,13 @@ public void clearHistory() { } public String[] getHttpAuthUsernamePassword(String host, String realm) { - return HttpAuthDatabase.getInstance(mContentViewCore.getContext()) + return mBrowserContext.getHttpAuthDatabase(mContentViewCore.getContext()) .getHttpAuthUsernamePassword(host, realm); } public void setHttpAuthUsernamePassword(String host, String realm, String username, String password) { - HttpAuthDatabase.getInstance(mContentViewCore.getContext()) + mBrowserContext.getHttpAuthDatabase(mContentViewCore.getContext()) .setHttpAuthUsernamePassword(host, realm, username, password); } @@ -984,8 +1194,8 @@ public float getScale() { /** * @see android.webkit.WebView#flingScroll(int, int) */ - public void flingScroll(int vx, int vy) { - mContentViewCore.flingScroll(vx, vy); + public void flingScroll(int velocityX, int velocityY) { + mContentViewCore.flingScroll(velocityX, velocityY); } /** @@ -1037,6 +1247,23 @@ public void invokeZoomPicker() { mContentViewCore.invokeZoomPicker(); } + /** + * @see ContentViewCore.evaluateJavaScript(String, ContentViewCOre.JavaScriptCallback) + */ + public void evaluateJavaScript(String script, final ValueCallback callback) { + ContentViewCore.JavaScriptCallback jsCallback = null; + if (callback != null) { + jsCallback = new ContentViewCore.JavaScriptCallback() { + @Override + public void handleJavaScriptResult(String jsonResult) { + callback.onReceiveValue(jsonResult); + } + }; + } + + mContentViewCore.evaluateJavaScript(script, jsCallback); + } + //-------------------------------------------------------------------------------------------- // View and ViewGroup method implementations //-------------------------------------------------------------------------------------------- @@ -1046,7 +1273,10 @@ public void invokeZoomPicker() { */ public boolean onTouchEvent(MotionEvent event) { if (mNativeAwContents == 0) return false; + + mScrollOffsetManager.setProcessingTouchEvent(true); boolean rv = mContentViewCore.onTouchEvent(event); + mScrollOffsetManager.setProcessingTouchEvent(false); if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { int actionIndex = event.getActionIndex(); @@ -1057,6 +1287,10 @@ public boolean onTouchEvent(MotionEvent event) { (int)Math.round(event.getY(actionIndex) / mDIPScale)); } + if (mOverScrollGlow != null && event.getActionMasked() == MotionEvent.ACTION_UP) { + mOverScrollGlow.releaseAll(); + } + return rv; } @@ -1083,37 +1317,27 @@ public void onConfigurationChanged(Configuration newConfig) { /** * @see android.view.View#onAttachedToWindow() + * + * Note that this is also called from receivePopupContents. */ public void onAttachedToWindow() { - if (mScrollChangeListener == null) { - mScrollChangeListener = new ScrollChangeListener(); - } - mContainerView.getViewTreeObserver().addOnScrollChangedListener(mScrollChangeListener); + mIsAttachedToWindow = true; mContentViewCore.onAttachedToWindow(); nativeOnAttachedToWindow(mNativeAwContents, mContainerView.getWidth(), mContainerView.getHeight()); - - // This is for the case where this is created by restoreState, which - // needs to call to NavigationController::LoadIfNecessary to actually - // load the restored page. - if (!mIsPaused) onResume(); } /** * @see android.view.View#onDetachedFromWindow() */ public void onDetachedFromWindow() { + hideAutofillPopup(); + mIsAttachedToWindow = false; if (mNativeAwContents != 0) { nativeOnDetachedFromWindow(mNativeAwContents); } - if (mScrollChangeListener != null) { - mContainerView.getViewTreeObserver().removeOnScrollChangedListener( - mScrollChangeListener); - mScrollChangeListener = null; - } - mContentViewCore.onDetachedFromWindow(); } @@ -1138,7 +1362,8 @@ public void onFocusChanged(boolean focused, int direction, Rect previouslyFocuse */ public void onSizeChanged(int w, int h, int ow, int oh) { if (mNativeAwContents == 0) return; - updatePhysicalBackingSizeIfNeeded(); + mScrollOffsetManager.setContainerViewSize(w, h); + mContentViewCore.onPhysicalBackingSizeChanged(w, h); mContentViewCore.onSizeChanged(w, h, ow, oh); nativeOnSizeChanged(mNativeAwContents, w, h, ow, oh); } @@ -1147,29 +1372,37 @@ public void onSizeChanged(int w, int h, int ow, int oh) { * @see android.view.View#onVisibilityChanged() */ public void onVisibilityChanged(View changedView, int visibility) { - updateVisiblityState(); + updateVisibilityState(); } /** * @see android.view.View#onWindowVisibilityChanged() */ public void onWindowVisibilityChanged(int visibility) { - updateVisiblityState(); + updateVisibilityState(); } - private void updateVisiblityState() { - if (mNativeAwContents == 0 || mIsPaused) return; + private void updateVisibilityState() { + boolean visible = getContainerViewVisible(); + if (mIsVisible == visible) return; + + setVisibilityInternal(visible); + } + + private boolean getContainerViewVisible() { boolean windowVisible = mContainerView.getWindowVisibility() == View.VISIBLE; boolean viewVisible = mContainerView.getVisibility() == View.VISIBLE; - nativeSetWindowViewVisibility(mNativeAwContents, windowVisible, viewVisible); - if (viewVisible) { - mContentViewCore.onShow(); - } else { - mContentViewCore.onHide(); - } + return windowVisible && viewVisible; } + private void setVisibilityInternal(boolean visible) { + // Note that this skips mIsVisible check and unconditionally sets + // visibility. In general, callers should use updateVisibilityState + // instead. + mIsVisible = visible; + nativeSetVisibility(mNativeAwContents, mIsVisible); + } /** * Key for opaque state in bundle. Note this is only public for tests. @@ -1227,6 +1460,16 @@ public void removeJavascriptInterface(String interfaceName) { mContentViewCore.removeJavascriptInterface(interfaceName); } + /** + * If native accessibility (not script injection) is enabled, and if this is + * running on JellyBean or later, returns an AccessibilityNodeProvider that + * implements native accessibility for this view. Returns null otherwise. + * @return The AccessibilityNodeProvider, if available, or null otherwise. + */ + public AccessibilityNodeProvider getAccessibilityNodeProvider() { + return mContentViewCore.getAccessibilityNodeProvider(); + } + /** * @see android.webkit.WebView#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo) */ @@ -1252,6 +1495,14 @@ public boolean performAccessibilityAction(int action, Bundle arguments) { return mContentViewCore.performAccessibilityAction(action, arguments); } + /** + * @see android.webkit.WebView#clearFormData() + */ + public void hideAutofillPopup() { + if (mAwAutofillManagerDelegate != null) + mAwAutofillManagerDelegate.hideAutofillPopup(); + } + //-------------------------------------------------------------------------------------------- // Methods called from native via JNI //-------------------------------------------------------------------------------------------- @@ -1337,7 +1588,10 @@ public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, @CalledByNative public void onNewPicture() { - mContentsClient.onNewPicture(mNewPictureInvalidationOnly ? null : capturePicture()); + // Don't call capturePicture() here but instead defer it until the posted task runs within + // the callback helper, to avoid doubling back into the renderer compositor in the middle + // of the notification it is sending up to here. + mContentsClient.getCallbackHelper().postOnNewPicture(mPictureListenerContentProvider); } // Called as a result of nativeUpdateLastHitTestData. @@ -1352,13 +1606,20 @@ private void updateHitTestData( } @CalledByNative - private void requestProcessMode() { - mInternalAccessAdapter.requestDrawGL(null); + private boolean requestDrawGL(Canvas canvas) { + return mInternalAccessAdapter.requestDrawGL(canvas); } + private static final boolean SUPPORTS_ON_ANIMATION = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + @CalledByNative - private void invalidate() { - mContainerView.invalidate(); + private void postInvalidateOnAnimation() { + if (SUPPORTS_ON_ANIMATION) { + mContainerView.postInvalidateOnAnimation(); + } else { + mContainerView.postInvalidate(); + } } @CalledByNative @@ -1374,9 +1635,33 @@ private int[] getLocationOnScreen() { } @CalledByNative - private void onPageScaleFactorChanged(float pageScaleFactor) { + private void onWebLayoutPageScaleFactorChanged(float webLayoutPageScaleFactor) { // This change notification comes from the renderer thread, not from the cc/ impl thread. - mLayoutSizer.onPageScaleChanged(pageScaleFactor); + mLayoutSizer.onPageScaleChanged(webLayoutPageScaleFactor); + } + + @CalledByNative + private void scrollContainerViewTo(int x, int y) { + mScrollOffsetManager.scrollContainerViewTo(x, y); + } + + @CalledByNative + private void setAwAutofillManagerDelegate(AwAutofillManagerDelegate delegate) { + mAwAutofillManagerDelegate = delegate; + delegate.init(mContentViewCore); + } + + @CalledByNative + private void didOverscroll(int deltaX, int deltaY) { + if (mOverScrollGlow != null) { + mOverScrollGlow.setOverScrollDeltas(deltaX, deltaY); + } + + mScrollOffsetManager.overScrollBy(deltaX, deltaY); + + if (mOverScrollGlow != null && mOverScrollGlow.isAnimating()) { + mContainerView.invalidate(); + } } // ------------------------------------------------------------------------------------------- @@ -1434,29 +1719,29 @@ private static String generateArchiveAutoNamePath(String originalUrl, String bas // Native methods //-------------------------------------------------------------------------------------------- - private native int nativeInit(AwWebContentsDelegate webViewWebContentsDelegate, - AwContentsClientBridge contentsClientBridge); + private static native int nativeInit(AwBrowserContext browserContext); private static native void nativeDestroy(int nativeAwContents); private static native void nativeSetAwDrawSWFunctionTable(int functionTablePointer); private static native void nativeSetAwDrawGLFunctionTable(int functionTablePointer); private static native int nativeGetAwDrawGLFunction(); - + private static native int nativeGetNativeInstanceCount(); + private native void nativeSetJavaPeers(int nativeAwContents, AwContents awContents, + AwWebContentsDelegate webViewWebContentsDelegate, + AwContentsClientBridge contentsClientBridge, + AwContentsIoThreadClient ioThreadClient, + InterceptNavigationDelegate navigationInterceptionDelegate); private native int nativeGetWebContents(int nativeAwContents); - private native void nativeDidInitializeContentViewCore(int nativeAwContents, - int nativeContentViewCore); private native void nativeDocumentHasImages(int nativeAwContents, Message message); private native void nativeGenerateMHTML( int nativeAwContents, String path, ValueCallback callback); - private native void nativeSetIoThreadClient(int nativeAwContents, - AwContentsIoThreadClient ioThreadClient); - private native void nativeSetInterceptNavigationDelegate(int nativeAwContents, - InterceptNavigationDelegate navigationInterceptionDelegate); - private native void nativeAddVisitedLinks(int nativeAwContents, String[] visitedLinks); - - private native boolean nativePrepareDrawGL(int nativeAwContents, int scrollX, int scrollY); + private native boolean nativeOnDraw(int nativeAwContents, Canvas canvas, + boolean isHardwareAccelerated, int scrollX, int ScrollY, + int clipLeft, int clipTop, int clipRight, int clipBottom); + private native void nativeSetGlobalVisibleRect(int nativeAwContents, int visibleLeft, + int visibleTop, int visibleRight, int visibleBottom); private native void nativeFindAllAsync(int nativeAwContents, String searchString); private native void nativeFindNext(int nativeAwContents, boolean forward); private native void nativeClearMatches(int nativeAwContents); @@ -1468,10 +1753,13 @@ private native void nativeSetInterceptNavigationDelegate(int nativeAwContents, private native void nativeUpdateLastHitTestData(int nativeAwContents); private native void nativeOnSizeChanged(int nativeAwContents, int w, int h, int ow, int oh); - private native void nativeSetWindowViewVisibility(int nativeAwContents, boolean windowVisible, - boolean viewVisible); + private native void nativeScrollTo(int nativeAwContents, int x, int y); + private native void nativeSetVisibility(int nativeAwContents, boolean visible); private native void nativeOnAttachedToWindow(int nativeAwContents, int w, int h); private native void nativeOnDetachedFromWindow(int nativeAwContents); + private native void nativeSetDipScale(int nativeAwContents, float dipScale); + private native void nativeSetDisplayedPageScaleFactor(int nativeAwContents, + float pageScaleFactor); // Returns null if save state fails. private native byte[] nativeGetOpaqueState(int nativeAwContents); @@ -1479,14 +1767,12 @@ private native void nativeSetWindowViewVisibility(int nativeAwContents, boolean // Returns false if restore state fails. private native boolean nativeRestoreFromOpaqueState(int nativeAwContents, byte[] state); - private native int nativeReleasePopupWebContents(int nativeAwContents); - private native void nativeSetWebContents(int nativeAwContents, int nativeNewWebContents); + private native int nativeReleasePopupAwContents(int nativeAwContents); private native void nativeFocusFirstNode(int nativeAwContents); + private native void nativeSetBackgroundColor(int nativeAwContents, int color); - private native boolean nativeDrawSW(int nativeAwContents, Canvas canvas, int clipX, int clipY, - int clipW, int clipH); private native int nativeGetAwDrawGLViewContext(int nativeAwContents); - private native Picture nativeCapturePicture(int nativeAwContents); + private native Picture nativeCapturePicture(int nativeAwContents, int width, int height); private native void nativeEnableOnNewPicture(int nativeAwContents, boolean enabled); private native void nativeInvokeGeolocationCallback( diff --git a/src/org/chromium/android_webview/AwContentsClient.java b/src/org/chromium/android_webview/AwContentsClient.java index 70cd68c..f1c7f45 100644 --- a/src/org/chromium/android_webview/AwContentsClient.java +++ b/src/org/chromium/android_webview/AwContentsClient.java @@ -4,8 +4,8 @@ package org.chromium.android_webview; -import android.content.pm.ActivityInfo; import android.content.Context; +import android.content.pm.ActivityInfo; import android.graphics.Bitmap; import android.graphics.Picture; import android.graphics.Rect; @@ -23,6 +23,9 @@ import android.webkit.ValueCallback; import android.webkit.WebChromeClient; +import org.chromium.content.browser.ContentVideoView; +import org.chromium.content.browser.ContentVideoViewClient; +import org.chromium.content.browser.ContentVideoViewControls; import org.chromium.content.browser.ContentViewClient; import org.chromium.content.browser.ContentViewCore; import org.chromium.content.browser.WebContentsObserverAndroid; @@ -49,6 +52,12 @@ public abstract class AwContentsClient { private double mDIPScale; + // Last background color reported from the renderer. Holds the sentinal value INVALID_COLOR + // if not valid. + private int mCachedRendererBackgroundColor = INVALID_COLOR; + + private static final int INVALID_COLOR = 0; + class AwWebContentsObserver extends WebContentsObserverAndroid { public AwWebContentsObserver(ContentViewCore contentViewCore) { super(contentViewCore); @@ -87,6 +96,12 @@ public void didNavigateAnyFrame(String url, String baseUrl, boolean isReload) { } private class AwContentViewClient extends ContentViewClient { + @Override + public void onBackgroundColorChanged(int color) { + // Avoid storing the sentinal INVALID_COLOR (note that both 0 and 1 are both + // fully transparent so this transpose makes no visible difference). + mCachedRendererBackgroundColor = color == INVALID_COLOR ? 1 : color; + } @Override public void onScaleChanged(float oldScale, float newScale) { @@ -101,7 +116,7 @@ public void onStartContentIntent(Context context, String contentUrl) { } @Override - public void onTabCrash() { + public void onRendererCrash(boolean crashedWhileOomProtected) { // This is not possible so long as the webview is run single process! throw new RuntimeException("Renderer crash reported."); } @@ -116,6 +131,10 @@ public boolean shouldOverrideKeyEvent(KeyEvent event) { return AwContentsClient.this.shouldOverrideKeyEvent(event); } + @Override + final public ContentVideoViewClient getContentVideoViewClient() { + return new AwContentVideoViewClient(); + } } final void installWebContentsObserver(ContentViewCore contentViewCore) { @@ -125,6 +144,36 @@ final void installWebContentsObserver(ContentViewCore contentViewCore) { mWebContentsObserver = new AwWebContentsObserver(contentViewCore); } + private class AwContentVideoViewClient implements ContentVideoViewClient { + @Override + public void onShowCustomView(View view) { + WebChromeClient.CustomViewCallback cb = new WebChromeClient.CustomViewCallback() { + @Override + public void onCustomViewHidden() { + ContentVideoView contentVideoView = ContentVideoView.getContentVideoView(); + if (contentVideoView != null) + contentVideoView.exitFullscreen(false); + } + }; + AwContentsClient.this.onShowCustomView(view, cb); + } + + @Override + public void onDestroyContentVideoView() { + AwContentsClient.this.onHideCustomView(); + } + + @Override + public View getVideoLoadingProgressView() { + return AwContentsClient.this.getVideoLoadingProgressView(); + } + + @Override + public ContentVideoViewControls createControls() { + return null; + } + } + final void setDIPScale(double dipScale) { mDIPScale = dipScale; } @@ -137,6 +186,15 @@ final ContentViewClient getContentViewClient() { return mContentViewClient; } + final int getCachedRendererBackgroundColor() { + assert isCachedRendererBackgroundColorValid(); + return mCachedRendererBackgroundColor; + } + + final boolean isCachedRendererBackgroundColorValid() { + return mCachedRendererBackgroundColor != INVALID_COLOR; + } + //-------------------------------------------------------------------------------------------- // WebView specific methods that map directly to WebViewClient / WebChromeClient //-------------------------------------------------------------------------------------------- diff --git a/src/org/chromium/android_webview/AwContentsClientCallbackHelper.java b/src/org/chromium/android_webview/AwContentsClientCallbackHelper.java index 8dbd571..14962ec 100644 --- a/src/org/chromium/android_webview/AwContentsClientCallbackHelper.java +++ b/src/org/chromium/android_webview/AwContentsClientCallbackHelper.java @@ -4,12 +4,15 @@ package org.chromium.android_webview; +import android.graphics.Picture; import android.os.Handler; import android.os.Looper; import android.os.Message; import org.chromium.content.browser.ContentViewCore; +import java.util.concurrent.Callable; + /** * This class is responsible for calling certain client callbacks on the UI thread. * @@ -70,6 +73,7 @@ private static class OnReceivedErrorInfo { private final static int MSG_ON_DOWNLOAD_START = 3; private final static int MSG_ON_RECEIVED_LOGIN_REQUEST = 4; private final static int MSG_ON_RECEIVED_ERROR = 5; + private final static int MSG_ON_NEW_PICTURE = 6; private final AwContentsClient mContentsClient; @@ -104,6 +108,16 @@ public void handleMessage(Message msg) { info.mFailingUrl); break; } + case MSG_ON_NEW_PICTURE: { + Picture picture = null; + try { + if (msg.obj != null) picture = (Picture) ((Callable) msg.obj).call(); + } catch (Exception e) { + throw new RuntimeException("Error getting picture", e); + } + mContentsClient.onNewPicture(picture); + break; + } default: throw new IllegalStateException( "AwContentsClientCallbackHelper: unhandled message " + msg.what); @@ -139,4 +153,8 @@ public void postOnReceivedError(int errorCode, String description, String failin OnReceivedErrorInfo info = new OnReceivedErrorInfo(errorCode, description, failingUrl); mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_RECEIVED_ERROR, info)); } + + public void postOnNewPicture(Callable pictureProvider) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_NEW_PICTURE, pictureProvider)); + } } diff --git a/src/org/chromium/android_webview/AwGeolocationPermissions.java b/src/org/chromium/android_webview/AwGeolocationPermissions.java index b091b3b..ddf2efb 100644 --- a/src/org/chromium/android_webview/AwGeolocationPermissions.java +++ b/src/org/chromium/android_webview/AwGeolocationPermissions.java @@ -21,7 +21,7 @@ public final class AwGeolocationPermissions { private static final String PREF_PREFIX = - AwGeolocationPermissions.class.getCanonicalName() + "%"; + "AwGeolocationPermissions%"; private final SharedPreferences mSharedPreferences; public AwGeolocationPermissions(SharedPreferences sharedPreferences) { diff --git a/src/org/chromium/android_webview/AwLayoutSizer.java b/src/org/chromium/android_webview/AwLayoutSizer.java index 7a48bfe..e09259b 100644 --- a/src/org/chromium/android_webview/AwLayoutSizer.java +++ b/src/org/chromium/android_webview/AwLayoutSizer.java @@ -60,6 +60,8 @@ public void setDIPScale(double dipScale) { /** * This is used to register the AwLayoutSizer to preferred content size change notifications in * the AwWebContentsDelegate. + * NOTE: The preferred size notifications come in from the Renderer main thread and might be + * out of sync with the content size as seen by the InProcessViewRenderer (and Compositor). */ public AwWebContentsDelegateAdapter.PreferredSizeChangedListener getPreferredSizeChangedListener() { diff --git a/src/org/chromium/android_webview/AwScrollOffsetManager.java b/src/org/chromium/android_webview/AwScrollOffsetManager.java new file mode 100644 index 0000000..86f693b --- /dev/null +++ b/src/org/chromium/android_webview/AwScrollOffsetManager.java @@ -0,0 +1,293 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.android_webview; + +import android.widget.OverScroller; + +import com.google.common.annotations.VisibleForTesting; + +import org.chromium.base.CalledByNative; + +/** + * Takes care of syncing the scroll offset between the Android View system and the + * InProcessViewRenderer. + * + * Unless otherwise values (sizes, scroll offsets) are in physical pixels. + */ +@VisibleForTesting +public class AwScrollOffsetManager { + // The unit of all the values in this delegate are physical pixels. + public interface Delegate { + // Call View#overScrollBy on the containerView. + void overScrollContainerViewBy(int deltaX, int deltaY, int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, boolean isTouchEvent); + // Call View#scrollTo on the containerView. + void scrollContainerViewTo(int x, int y); + // Store the scroll offset in the native side. This should really be a simple store + // operation, the native side shouldn't synchronously alter the scroll offset from within + // this call. + void scrollNativeTo(int x, int y); + + int getContainerViewScrollX(); + int getContainerViewScrollY(); + + void invalidate(); + } + + private final Delegate mDelegate; + + // Scroll offset as seen by the native side. + private int mNativeScrollX; + private int mNativeScrollY; + + // Content size. + private int mContentWidth; + private int mContentHeight; + + // Size of the container view. + private int mContainerViewWidth; + private int mContainerViewHeight; + + // Whether we're in the middle of processing a touch event. + private boolean mProcessingTouchEvent; + + // Whether (and to what value) to update the native side scroll offset after we've finished + // provessing a touch event. + private boolean mApplyDeferredNativeScroll; + private int mDeferredNativeScrollX; + private int mDeferredNativeScrollY; + + // The velocity of the last recorded fling, + private int mLastFlingVelocityX; + private int mLastFlingVelocityY; + + private OverScroller mScroller; + + public AwScrollOffsetManager(Delegate delegate, OverScroller overScroller) { + mDelegate = delegate; + mScroller = overScroller; + } + + //----- Scroll range and extent calculation methods ------------------------------------------- + + public int computeHorizontalScrollRange() { + return Math.max(mContainerViewWidth, mContentWidth); + } + + public int computeMaximumHorizontalScrollOffset() { + return computeHorizontalScrollRange() - mContainerViewWidth; + } + + public int computeHorizontalScrollOffset() { + return mDelegate.getContainerViewScrollX(); + } + + public int computeVerticalScrollRange() { + return Math.max(mContainerViewHeight, mContentHeight); + } + + public int computeMaximumVerticalScrollOffset() { + return computeVerticalScrollRange() - mContainerViewHeight; + } + + public int computeVerticalScrollOffset() { + return mDelegate.getContainerViewScrollY(); + } + + public int computeVerticalScrollExtent() { + return mContainerViewHeight; + } + + //--------------------------------------------------------------------------------------------- + + // Called when the content size changes. This needs to be the size of the on-screen content and + // therefore we can't use the WebContentsDelegate preferred size. + public void setContentSize(int width, int height) { + mContentWidth = width; + mContentHeight = height; + } + + // Called when the physical size of the view changes. + public void setContainerViewSize(int width, int height) { + mContainerViewWidth = width; + mContainerViewHeight = height; + } + + public void syncScrollOffsetFromOnDraw() { + // Unfortunately apps override onScrollChanged without calling super which is why we need + // to sync the scroll offset on every onDraw. + onContainerViewScrollChanged(mDelegate.getContainerViewScrollX(), + mDelegate.getContainerViewScrollY()); + } + + public void setProcessingTouchEvent(boolean processingTouchEvent) { + assert mProcessingTouchEvent != processingTouchEvent; + mProcessingTouchEvent = processingTouchEvent; + + if (!mProcessingTouchEvent && mApplyDeferredNativeScroll) { + mApplyDeferredNativeScroll = false; + scrollNativeTo(mDeferredNativeScrollX, mDeferredNativeScrollY); + } + } + + // Called by the native side to attempt to scroll the container view. + public void scrollContainerViewTo(int x, int y) { + mNativeScrollX = x; + mNativeScrollY = y; + + final int scrollX = mDelegate.getContainerViewScrollX(); + final int scrollY = mDelegate.getContainerViewScrollY(); + final int deltaX = x - scrollX; + final int deltaY = y - scrollY; + final int scrollRangeX = computeMaximumHorizontalScrollOffset(); + final int scrollRangeY = computeMaximumVerticalScrollOffset(); + + // We use overScrollContainerViewBy to be compatible with WebViewClassic which used this + // method for handling both over-scroll as well as in-bounds scroll. + mDelegate.overScrollContainerViewBy(deltaX, deltaY, scrollX, scrollY, + scrollRangeX, scrollRangeY, mProcessingTouchEvent); + } + + // Called by the native side to over-scroll the container view. + public void overScrollBy(int deltaX, int deltaY) { + // TODO(mkosiba): Once http://crbug.com/260663 and http://crbug.com/261239 are fixed it + // should be possible to uncomment the following asserts: + // if (deltaX < 0) assert mDelegate.getContainerViewScrollX() == 0; + // if (deltaX > 0) assert mDelegate.getContainerViewScrollX() == + // computeMaximumHorizontalScrollOffset(); + scrollBy(deltaX, deltaY); + } + + private void scrollBy(int deltaX, int deltaY) { + if (deltaX == 0 && deltaY == 0) return; + + final int scrollX = mDelegate.getContainerViewScrollX(); + final int scrollY = mDelegate.getContainerViewScrollY(); + final int scrollRangeX = computeMaximumHorizontalScrollOffset(); + final int scrollRangeY = computeMaximumVerticalScrollOffset(); + + // The android.view.View.overScrollBy method is used for both scrolling and over-scrolling + // which is why we use it here. + mDelegate.overScrollContainerViewBy(deltaX, deltaY, scrollX, scrollY, + scrollRangeX, scrollRangeY, mProcessingTouchEvent); + } + + private int clampHorizontalScroll(int scrollX) { + scrollX = Math.max(0, scrollX); + scrollX = Math.min(computeMaximumHorizontalScrollOffset(), scrollX); + return scrollX; + } + + private int clampVerticalScroll(int scrollY) { + scrollY = Math.max(0, scrollY); + scrollY = Math.min(computeMaximumVerticalScrollOffset(), scrollY); + return scrollY; + } + + // Called by the View system as a response to the mDelegate.overScrollContainerViewBy call. + public void onContainerViewOverScrolled(int scrollX, int scrollY, boolean clampedX, + boolean clampedY) { + // Clamp the scroll offset at (0, max). + scrollX = clampHorizontalScroll(scrollX); + scrollY = clampVerticalScroll(scrollY); + + mDelegate.scrollContainerViewTo(scrollX, scrollY); + + // This is only necessary if the containerView scroll offset ends up being different + // than the one set from native in which case we want the value stored on the native side + // to reflect the value stored in the containerView (and not the other way around). + scrollNativeTo(mDelegate.getContainerViewScrollX(), mDelegate.getContainerViewScrollY()); + } + + // Called by the View system when the scroll offset had changed. This might not get called if + // the embedder overrides WebView#onScrollChanged without calling super.onScrollChanged. If + // this method does get called it is called both as a response to the embedder scrolling the + // view as well as a response to mDelegate.scrollContainerViewTo. + public void onContainerViewScrollChanged(int x, int y) { + scrollNativeTo(x, y); + } + + private void scrollNativeTo(int x, int y) { + x = clampHorizontalScroll(x); + y = clampVerticalScroll(y); + + // We shouldn't do the store to native while processing a touch event since that confuses + // the gesture processing logic. + if (mProcessingTouchEvent) { + mDeferredNativeScrollX = x; + mDeferredNativeScrollY = y; + mApplyDeferredNativeScroll = true; + return; + } + + if (x == mNativeScrollX && y == mNativeScrollY) + return; + + // The scrollNativeTo call should be a simple store, so it's OK to assume it always + // succeeds. + mNativeScrollX = x; + mNativeScrollY = y; + + mDelegate.scrollNativeTo(x, y); + } + + // Called at the beginning of every fling gesture. + public void onFlingStartGesture(int velocityX, int velocityY) { + mLastFlingVelocityX = velocityX; + mLastFlingVelocityY = velocityY; + } + + // Called whenever some other touch interaction requires the fling gesture to be canceled. + public void onFlingCancelGesture() { + // TODO(mkosiba): Support speeding up a fling by flinging again. + // http://crbug.com/265841 + mScroller.forceFinished(true); + } + + // Called when a fling gesture is not handled by the renderer. + // We explicitly ask the renderer not to handle fling gestures targeted at the root + // scroll layer. + public void onUnhandledFlingStartEvent() { + flingScroll(-mLastFlingVelocityX, -mLastFlingVelocityY); + } + + // Starts the fling animation. Called both as a response to a fling gesture and as via the + // public WebView#flingScroll(int, int) API. + public void flingScroll(int velocityX, int velocityY) { + final int scrollX = mDelegate.getContainerViewScrollX(); + final int scrollY = mDelegate.getContainerViewScrollY(); + final int rangeX = computeMaximumHorizontalScrollOffset(); + final int rangeY = computeMaximumVerticalScrollOffset(); + + mScroller.fling(scrollX, scrollY, velocityX, velocityY, + 0, rangeX, 0, rangeY); + mDelegate.invalidate(); + } + + // Called immediately before the draw to update the scroll offset. + public void computeScrollAndAbsorbGlow(OverScrollGlow overScrollGlow) { + final boolean stillAnimating = mScroller.computeScrollOffset(); + if (!stillAnimating) return; + + final int oldX = mDelegate.getContainerViewScrollX(); + final int oldY = mDelegate.getContainerViewScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + + int rangeX = computeMaximumHorizontalScrollOffset(); + int rangeY = computeMaximumVerticalScrollOffset(); + + if (overScrollGlow != null) { + overScrollGlow.absorbGlow(x, y, oldX, oldY, rangeX, rangeY, + mScroller.getCurrVelocity()); + } + + // The mScroller is configured not to go outside of the scrollable range, so this call + // should never result in attempting to scroll outside of the scrollable region. + scrollBy(x - oldX, y - oldY); + + mDelegate.invalidate(); + } +} diff --git a/src/org/chromium/android_webview/AwSettings.java b/src/org/chromium/android_webview/AwSettings.java index a578116..b62cc4d 100644 --- a/src/org/chromium/android_webview/AwSettings.java +++ b/src/org/chromium/android_webview/AwSettings.java @@ -5,11 +5,10 @@ package org.chromium.android_webview; import android.content.Context; -import android.content.pm.PackageManager; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.os.Process; +import android.provider.Settings; import android.webkit.WebSettings.PluginState; import android.webkit.WebSettings; import android.webkit.WebView; @@ -43,12 +42,14 @@ public enum LayoutAlgorithm { // used from any thread. Internally, the class uses a message queue // to call native code on the UI thread only. + // Values passed in on construction. + private final boolean mHasInternetPermission; + private final ZoomSupportChangeListener mZoomChangeListener; + private final double mDIPScale; + // Lock to protect all settings. private final Object mAwSettingsLock = new Object(); - private final Context mContext; - private double mDIPScale; - private LayoutAlgorithm mLayoutAlgorithm = LayoutAlgorithm.NARROW_COLUMNS; private int mTextSizePercent = 100; private String mStandardFontFamily = "sans-serif"; @@ -83,6 +84,8 @@ public enum LayoutAlgorithm { private final boolean mSupportDeprecatedTargetDensityDPI = true; + private final boolean mPasswordEchoEnabled; + // Not accessed by the native side. private boolean mBlockNetworkLoads; // Default depends on permission of embedding APK. private boolean mAllowContentUrlAccess = true; @@ -94,6 +97,7 @@ public enum LayoutAlgorithm { private boolean mSupportZoom = true; private boolean mBuiltInZoomControls = false; private boolean mDisplayZoomControls = true; + static class LazyDefaultUserAgent{ // Lazy Holder pattern private static final String sInstance = nativeGetDefaultUserAgent(); @@ -106,11 +110,9 @@ static class LazyDefaultUserAgent{ // client. private static boolean sAppCachePathIsSet = false; - // The native side of this object. + // The native side of this object. It's lifetime is bounded by the WebContent it is attached to. private int mNativeAwSettings = 0; - private ContentViewCore mContentViewCore; - // A flag to avoid sending superfluous synchronization messages. private boolean mIsUpdateWebkitPrefsMessagePending = false; // Custom handler that queues messages to call native code on the UI thread. @@ -166,44 +168,43 @@ private void updateWebkitPreferencesLocked() { } } - public AwSettings(Context context, - int nativeWebContents, - ContentViewCore contentViewCore, - boolean isAccessFromFileURLsGrantedByDefault) { + interface ZoomSupportChangeListener { + public void onMultiTouchZoomSupportChanged(boolean supportsMultiTouchZoom); + } + + public AwSettings(Context context, boolean hasInternetPermission, + ZoomSupportChangeListener zoomChangeListener, + boolean isAccessFromFileURLsGrantedByDefault, + double dipScale) { ThreadUtils.assertOnUiThread(); - mContext = context; - mBlockNetworkLoads = mContext.checkPermission( - android.Manifest.permission.INTERNET, - Process.myPid(), - Process.myUid()) != PackageManager.PERMISSION_GRANTED; - mContentViewCore = contentViewCore; - mContentViewCore.updateMultiTouchZoomSupport(supportsMultiTouchZoomLocked()); + synchronized (mAwSettingsLock) { + mHasInternetPermission = hasInternetPermission; + mZoomChangeListener = zoomChangeListener; + mDIPScale = dipScale; + mEventHandler = new EventHandler(); + mBlockNetworkLoads = !hasInternetPermission; - if (isAccessFromFileURLsGrantedByDefault) { - mAllowUniversalAccessFromFileURLs = true; - mAllowFileAccessFromFileURLs = true; - } + if (isAccessFromFileURLsGrantedByDefault) { + mAllowUniversalAccessFromFileURLs = true; + mAllowFileAccessFromFileURLs = true; + } - mEventHandler = new EventHandler(); - mUserAgent = LazyDefaultUserAgent.sInstance; + mUserAgent = LazyDefaultUserAgent.sInstance; + onMultiTouchZoomSupportChanged(supportsMultiTouchZoomLocked()); - synchronized (mAwSettingsLock) { - mNativeAwSettings = nativeInit(nativeWebContents); + // Respect the system setting for password echoing. + mPasswordEchoEnabled = Settings.System.getInt(context.getContentResolver(), + Settings.System.TEXT_SHOW_PASSWORD, 1) == 1; } - assert mNativeAwSettings != 0; + // Defer initializing the native side until a native WebContents instance is set. } - public void destroy() { - nativeDestroy(mNativeAwSettings); + @CalledByNative + private void nativeAwSettingsGone(int nativeAwSettings) { + assert mNativeAwSettings != 0 && mNativeAwSettings == nativeAwSettings; mNativeAwSettings = 0; } - public void setDIPScale(double dipScale) { - synchronized (mAwSettingsLock) { - mDIPScale = dipScale; - } - } - @CalledByNative private double getDIPScaleLocked() { return mDIPScale; @@ -211,7 +212,14 @@ private double getDIPScaleLocked() { public void setWebContents(int nativeWebContents) { synchronized (mAwSettingsLock) { - nativeSetWebContentsLocked(mNativeAwSettings, nativeWebContents); + if (mNativeAwSettings != 0) { + nativeDestroy(mNativeAwSettings); + assert mNativeAwSettings == 0; // nativeAwSettingsGone should have been called. + } + if (nativeWebContents != 0) { + mNativeAwSettings = nativeInit(nativeWebContents); + nativeUpdateEverythingLocked(mNativeAwSettings); + } } } @@ -220,10 +228,7 @@ public void setWebContents(int nativeWebContents) { */ public void setBlockNetworkLoads(boolean flag) { synchronized (mAwSettingsLock) { - if (!flag && mContext.checkPermission( - android.Manifest.permission.INTERNET, - Process.myPid(), - Process.myUid()) != PackageManager.PERMISSION_GRANTED) { + if (!flag && !mHasInternetPermission) { throw new SecurityException("Permission denied - " + "application missing INTERNET permission"); } @@ -898,7 +903,6 @@ private boolean getAllowFileAccessFromFileURLsLocked() { /** * See {@link android.webkit.WebSettings#setPluginsEnabled}. */ - @Deprecated public void setPluginsEnabled(boolean flag) { setPluginState(flag ? PluginState.ON : PluginState.OFF); } @@ -918,7 +922,6 @@ public void setPluginState(PluginState state) { /** * See {@link android.webkit.WebSettings#getPluginsEnabled}. */ - @Deprecated public boolean getPluginsEnabled() { synchronized (mAwSettingsLock) { return mPluginState == PluginState.ON; @@ -1060,6 +1063,11 @@ private boolean getUseWideViewportLocked() { return mUseWideViewport; } + @CalledByNative + private boolean getPasswordEchoEnabled() { + return mPasswordEchoEnabled; + } + /** * See {@link android.webkit.WebSettings#setAppCacheEnabled}. */ @@ -1241,11 +1249,12 @@ private String getDefaultVideoPosterURLLocked() { return mDefaultVideoPosterURL; } - private void updateMultiTouchZoomSupport(final boolean supportsMultiTouchZoom) { - ThreadUtils.runOnUiThreadBlocking(new Runnable() { + private void onMultiTouchZoomSupportChanged(final boolean supportsMultiTouchZoom) { + // Always post asynchronously here, to avoid doubling back onto the caller. + ThreadUtils.postOnUiThread(new Runnable() { @Override public void run() { - mContentViewCore.updateMultiTouchZoomSupport(supportsMultiTouchZoom); + mZoomChangeListener.onMultiTouchZoomSupportChanged(supportsMultiTouchZoom); } }); } @@ -1257,7 +1266,7 @@ public void setSupportZoom(boolean support) { synchronized (mAwSettingsLock) { if (mSupportZoom != support) { mSupportZoom = support; - updateMultiTouchZoomSupport(supportsMultiTouchZoomLocked()); + onMultiTouchZoomSupportChanged(supportsMultiTouchZoomLocked()); } } } @@ -1278,7 +1287,7 @@ public void setBuiltInZoomControls(boolean enabled) { synchronized (mAwSettingsLock) { if (mBuiltInZoomControls != enabled) { mBuiltInZoomControls = enabled; - updateMultiTouchZoomSupport(supportsMultiTouchZoomLocked()); + onMultiTouchZoomSupportChanged(supportsMultiTouchZoomLocked()); } } } @@ -1311,6 +1320,7 @@ public boolean getDisplayZoomControls() { } private boolean supportsMultiTouchZoomLocked() { + assert Thread.holdsLock(mAwSettingsLock); return mSupportZoom && mBuiltInZoomControls; } @@ -1355,8 +1365,6 @@ private void updateWebkitPreferencesOnUiThreadLocked() { private native void nativeResetScrollAndScaleState(int nativeAwSettings); - private native void nativeSetWebContentsLocked(int nativeAwSettings, int nativeWebContents); - private native void nativeUpdateEverythingLocked(int nativeAwSettings); private native void nativeUpdateInitialPageScaleLocked(int nativeAwSettings); diff --git a/src/org/chromium/android_webview/AwWebContentsDelegateAdapter.java b/src/org/chromium/android_webview/AwWebContentsDelegateAdapter.java index 0b888d3..ecb8dc8 100644 --- a/src/org/chromium/android_webview/AwWebContentsDelegateAdapter.java +++ b/src/org/chromium/android_webview/AwWebContentsDelegateAdapter.java @@ -85,7 +85,7 @@ public void onUpdateUrl(String url) { } @Override - public void openNewTab(String url, boolean incognito) { + public void openNewTab(String url, String extraHeaders, byte[] postData, boolean incognito) { // TODO: implement } diff --git a/src/org/chromium/android_webview/HttpAuthDatabase.java b/src/org/chromium/android_webview/HttpAuthDatabase.java index 0286a67..baa7680 100644 --- a/src/org/chromium/android_webview/HttpAuthDatabase.java +++ b/src/org/chromium/android_webview/HttpAuthDatabase.java @@ -28,14 +28,10 @@ */ public class HttpAuthDatabase { - private static final String DATABASE_FILE = "http_auth.db"; - - private static final String LOGTAG = HttpAuthDatabase.class.getName(); + private static final String LOGTAG = "HttpAuthDatabase"; private static final int DATABASE_VERSION = 1; - private static HttpAuthDatabase sInstance = null; - private SQLiteDatabase mDatabase = null; private static final String ID_COL = "_id"; @@ -72,17 +68,6 @@ public void run() { }.start(); } - /** - * @deprecated Retained for merge convenience. TODO(joth): remove in next patch. - */ - @Deprecated - public static synchronized HttpAuthDatabase getInstance(Context context) { - if (sInstance == null) { - sInstance = new HttpAuthDatabase(context, DATABASE_FILE); - } - return sInstance; - } - /** * Initializes the databases and notifies any callers waiting on waitForInit. * diff --git a/src/org/chromium/android_webview/JavaBrowserViewRendererHelper.java b/src/org/chromium/android_webview/JavaBrowserViewRendererHelper.java index d4e79c8..fb54279 100644 --- a/src/org/chromium/android_webview/JavaBrowserViewRendererHelper.java +++ b/src/org/chromium/android_webview/JavaBrowserViewRendererHelper.java @@ -8,22 +8,53 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Picture; +import android.util.Log; +import android.util.LruCache; import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; +import org.chromium.content.common.TraceEvent; /** * Provides auxiliary methods related to Picture objects and native SkPictures. */ @JNINamespace("android_webview") public class JavaBrowserViewRendererHelper { + private static final String LOGTAG = "JavaBrowserViewRendererHelper"; + + // Until the full HW path is ready, we limit to 5 AwContents on the screen at once. + private static LruCache sBitmapCache = new LruCache(5); /** * Provides a Bitmap object with a given width and height used for auxiliary rasterization. + * |canvas| is optional and if supplied indicates the Canvas that this Bitmap will be + * drawn into. Note the Canvas will not be modified in any way. If |ownerKey| is non-zero + * the Bitmap will be cached in sBitmapCache for future use. */ @CalledByNative - private static Bitmap createBitmap(int width, int height) { - return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + private static Bitmap createBitmap(int width, int height, Canvas canvas, int ownerKey) { + if (canvas != null) { + // When drawing into a Canvas, there is a maximum size imposed + // on Bitmaps that can be drawn. Respect that limit. + width = Math.min(width, canvas.getMaximumBitmapWidth()); + height = Math.min(height, canvas.getMaximumBitmapHeight()); + } + Bitmap bitmap = sBitmapCache.get(ownerKey); + if (bitmap == null || bitmap.getWidth() != width || bitmap.getHeight() != height) { + try { + bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + } catch (OutOfMemoryError e) { + android.util.Log.w(LOGTAG, "Error allocating bitmap"); + return null; + } + if (ownerKey != 0) { + if (sBitmapCache.size() > AwContents.getNativeInstanceCount()) { + sBitmapCache.evictAll(); + } + sBitmapCache.put(ownerKey, bitmap); + } + } + return bitmap; } /** @@ -31,8 +62,8 @@ private static Bitmap createBitmap(int width, int height) { * Used for convenience from the native side and other static helper methods. */ @CalledByNative - private static void drawBitmapIntoCanvas(Bitmap bitmap, Canvas canvas) { - canvas.drawBitmap(bitmap, 0, 0, null); + private static void drawBitmapIntoCanvas(Bitmap bitmap, Canvas canvas, int x, int y) { + canvas.drawBitmap(bitmap, x, y, null); } /** @@ -44,7 +75,7 @@ private static Picture recordBitmapIntoPicture(Bitmap bitmap) { Picture picture = new Picture(); if (bitmap != null) { Canvas recordingCanvas = picture.beginRecording(bitmap.getWidth(), bitmap.getHeight()); - drawBitmapIntoCanvas(bitmap, recordingCanvas); + drawBitmapIntoCanvas(bitmap, recordingCanvas, 0, 0); picture.endRecording(); } return picture; diff --git a/src/org/chromium/android_webview/OverScrollGlow.java b/src/org/chromium/android_webview/OverScrollGlow.java new file mode 100644 index 0000000..5023c39 --- /dev/null +++ b/src/org/chromium/android_webview/OverScrollGlow.java @@ -0,0 +1,210 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.android_webview; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.widget.EdgeEffect; + +/** + * This class manages the edge glow effect when a WebView is flung or pulled beyond the edges. + */ +class OverScrollGlow { + private View mHostView; + + private EdgeEffect mEdgeGlowTop; + private EdgeEffect mEdgeGlowBottom; + private EdgeEffect mEdgeGlowLeft; + private EdgeEffect mEdgeGlowRight; + + private int mOverScrollDeltaX; + private int mOverScrollDeltaY; + + public OverScrollGlow(View host) { + mHostView = host; + Context context = host.getContext(); + mEdgeGlowTop = new EdgeEffect(context); + mEdgeGlowBottom = new EdgeEffect(context); + mEdgeGlowLeft = new EdgeEffect(context); + mEdgeGlowRight = new EdgeEffect(context); + } + + /** + * Pull leftover touch scroll distance into one of the edge glows as appropriate. + * + * @param x Current X scroll offset + * @param y Current Y scroll offset + * @param oldX Old X scroll offset + * @param oldY Old Y scroll offset + * @param maxX Maximum range for horizontal scrolling + * @param maxY Maximum range for vertical scrolling + */ + public void pullGlow(int x, int y, int oldX, int oldY, int maxX, int maxY) { + // Only show overscroll bars if there was no movement in any direction + // as a result of scrolling. + if (oldX == mHostView.getScrollX() && oldY == mHostView.getScrollY()) { + // Don't show left/right glows if we fit the whole content. + // Also don't show if there was vertical movement. + if (maxX > 0) { + final int pulledToX = oldX + mOverScrollDeltaX; + if (pulledToX < 0) { + mEdgeGlowLeft.onPull((float) mOverScrollDeltaX / mHostView.getWidth()); + if (!mEdgeGlowRight.isFinished()) { + mEdgeGlowRight.onRelease(); + } + } else if (pulledToX > maxX) { + mEdgeGlowRight.onPull((float) mOverScrollDeltaX / mHostView.getWidth()); + if (!mEdgeGlowLeft.isFinished()) { + mEdgeGlowLeft.onRelease(); + } + } + mOverScrollDeltaX = 0; + } + + if (maxY > 0 || mHostView.getOverScrollMode() == View.OVER_SCROLL_ALWAYS) { + final int pulledToY = oldY + mOverScrollDeltaY; + if (pulledToY < 0) { + mEdgeGlowTop.onPull((float) mOverScrollDeltaY / mHostView.getHeight()); + if (!mEdgeGlowBottom.isFinished()) { + mEdgeGlowBottom.onRelease(); + } + } else if (pulledToY > maxY) { + mEdgeGlowBottom.onPull((float) mOverScrollDeltaY / mHostView.getHeight()); + if (!mEdgeGlowTop.isFinished()) { + mEdgeGlowTop.onRelease(); + } + } + mOverScrollDeltaY = 0; + } + } + } + + /** + * Absorb leftover fling velocity into one of the edge glows as appropriate. + * + * @param x Current X scroll offset + * @param y Current Y scroll offset + * @param oldX Old X scroll offset + * @param oldY Old Y scroll offset + * @param rangeX Maximum range for horizontal scrolling + * @param rangeY Maximum range for vertical scrolling + * @param currentFlingVelocity Current fling velocity + */ + public void absorbGlow(int x, int y, int oldX, int oldY, int rangeX, int rangeY, + float currentFlingVelocity) { + if (rangeY > 0 || mHostView.getOverScrollMode() == View.OVER_SCROLL_ALWAYS) { + if (y < 0 && oldY >= 0) { + mEdgeGlowTop.onAbsorb((int) currentFlingVelocity); + if (!mEdgeGlowBottom.isFinished()) { + mEdgeGlowBottom.onRelease(); + } + } else if (y > rangeY && oldY <= rangeY) { + mEdgeGlowBottom.onAbsorb((int) currentFlingVelocity); + if (!mEdgeGlowTop.isFinished()) { + mEdgeGlowTop.onRelease(); + } + } + } + + if (rangeX > 0) { + if (x < 0 && oldX >= 0) { + mEdgeGlowLeft.onAbsorb((int) currentFlingVelocity); + if (!mEdgeGlowRight.isFinished()) { + mEdgeGlowRight.onRelease(); + } + } else if (x > rangeX && oldX <= rangeX) { + mEdgeGlowRight.onAbsorb((int) currentFlingVelocity); + if (!mEdgeGlowLeft.isFinished()) { + mEdgeGlowLeft.onRelease(); + } + } + } + } + + /** + * Set touch delta values indicating the current amount of overscroll. + * + * @param deltaX + * @param deltaY + */ + public void setOverScrollDeltas(int deltaX, int deltaY) { + mOverScrollDeltaX += deltaX; + mOverScrollDeltaY += deltaY; + } + + /** + * Draw the glow effect along the sides of the widget. + * + * @param canvas Canvas to draw into, transformed into view coordinates. + * @param maxScrollX maximum horizontal scroll offset + * @param maxScrollY maximum vertical scroll offset + * @return true if glow effects are still animating and the view should invalidate again. + */ + public boolean drawEdgeGlows(Canvas canvas, int maxScrollX, int maxScrollY) { + final int scrollX = mHostView.getScrollX(); + final int scrollY = mHostView.getScrollY(); + final int width = mHostView.getWidth(); + int height = mHostView.getHeight(); + + boolean invalidateForGlow = false; + if (!mEdgeGlowTop.isFinished()) { + final int restoreCount = canvas.save(); + + canvas.translate(scrollX, Math.min(0, scrollY)); + mEdgeGlowTop.setSize(width, height); + invalidateForGlow |= mEdgeGlowTop.draw(canvas); + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowBottom.isFinished()) { + final int restoreCount = canvas.save(); + + canvas.translate(-width + scrollX, Math.max(maxScrollY, scrollY) + height); + canvas.rotate(180, width, 0); + mEdgeGlowBottom.setSize(width, height); + invalidateForGlow |= mEdgeGlowBottom.draw(canvas); + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowLeft.isFinished()) { + final int restoreCount = canvas.save(); + + canvas.rotate(270); + canvas.translate(-height - scrollY, Math.min(0, scrollX)); + mEdgeGlowLeft.setSize(height, width); + invalidateForGlow |= mEdgeGlowLeft.draw(canvas); + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowRight.isFinished()) { + final int restoreCount = canvas.save(); + + canvas.rotate(90); + canvas.translate(scrollY, -(Math.max(scrollX, maxScrollX) + width)); + mEdgeGlowRight.setSize(height, width); + invalidateForGlow |= mEdgeGlowRight.draw(canvas); + canvas.restoreToCount(restoreCount); + } + return invalidateForGlow; + } + + /** + * @return True if any glow is still animating + */ + public boolean isAnimating() { + return (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished() || + !mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished()); + } + + /** + * Release all glows from any touch pulls in progress. + */ + public void releaseAll() { + mEdgeGlowTop.onRelease(); + mEdgeGlowBottom.onRelease(); + mEdgeGlowLeft.onRelease(); + mEdgeGlowRight.onRelease(); + } +} diff --git a/src/org/chromium/android_webview/SslUtil.java b/src/org/chromium/android_webview/SslUtil.java index 41b4c18..329cb15 100644 --- a/src/org/chromium/android_webview/SslUtil.java +++ b/src/org/chromium/android_webview/SslUtil.java @@ -17,7 +17,7 @@ import java.security.cert.X509Certificate; public class SslUtil { - private static final String TAG = SslUtil.class.getSimpleName(); + private static final String TAG = "SslUtil"; /** * Creates an SslError object from a chromium net error code. diff --git a/src/org/chromium/base/ActivityState.java b/src/org/chromium/base/ActivityState.java index c3c8980..5a8a102 100644 --- a/src/org/chromium/base/ActivityState.java +++ b/src/org/chromium/base/ActivityState.java @@ -1,4 +1,12 @@ + + + + package org.chromium.base; + + + + interface ActivityState { public final int CREATED = 1; public final int STARTED = 2; diff --git a/src/org/chromium/base/ApiCompatibilityUtils.java b/src/org/chromium/base/ApiCompatibilityUtils.java new file mode 100644 index 0000000..eb33091 --- /dev/null +++ b/src/org/chromium/base/ApiCompatibilityUtils.java @@ -0,0 +1,129 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.app.Notification; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.View; +import android.view.ViewGroup.MarginLayoutParams; +import android.view.ViewTreeObserver; + +/** + * Utility class to use new APIs that were added after ICS (API level 14). + */ +public class ApiCompatibilityUtils { + + private ApiCompatibilityUtils() { + } + + /** + * Returns true if view's layout direction is right-to-left. + * + * @param view the View whose layout is being considered + */ + public static boolean isLayoutRtl(View view) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } else { + // All layouts are LTR before JB MR1. + return false; + } + } + + /** + * @see android.view.View#setLayoutDirection(int) + */ + public static void setLayoutDirection(View view, int layoutDirection) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + view.setLayoutDirection(layoutDirection); + } else { + // Do nothing. RTL layouts aren't supported before JB MR1. + } + } + + /** + * @see android.view.ViewGroup.MarginLayoutParams#setMarginEnd(int) + */ + public static void setMarginEnd(MarginLayoutParams layoutParams, int end) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + layoutParams.setMarginEnd(end); + } else { + layoutParams.rightMargin = end; + } + } + + /** + * @see android.view.ViewGroup.MarginLayoutParams#setMarginStart(int) + */ + public static void setMarginStart(MarginLayoutParams layoutParams, int start) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + layoutParams.setMarginStart(start); + } else { + layoutParams.leftMargin = start; + } + } + + /** + * @see android.view.ViewGroup.MarginLayoutParams#getMarginStart() + */ + public static int getMarginStart(MarginLayoutParams layoutParams) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return layoutParams.getMarginStart(); + } else { + return layoutParams.leftMargin; + } + } + + /** + * @see android.view.View#postInvalidateOnAnimation() + */ + public static void postInvalidateOnAnimation(View view) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + view.postInvalidateOnAnimation(); + } else { + view.postInvalidate(); + } + } + + // These methods have a new name, and the old name is deprecated. + + /** + * @see android.view.View#setBackground(Drawable) + */ + @SuppressWarnings("deprecation") + public static void setBackgroundForView(View view, Drawable drawable) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + view.setBackground(drawable); + } else { + view.setBackgroundDrawable(drawable); + } + } + + /** + * @see android.view.ViewTreeObserver#removeOnGlobalLayoutListener() + */ + @SuppressWarnings("deprecation") + public static void removeOnGlobalLayoutListener( + View view, ViewTreeObserver.OnGlobalLayoutListener listener) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + view.getViewTreeObserver().removeOnGlobalLayoutListener(listener); + } else { + view.getViewTreeObserver().removeGlobalOnLayoutListener(listener); + } + } + + /** + * @see android.app.Notification.Builder#build() + */ + @SuppressWarnings("deprecation") + public static Notification buildNotification(Notification.Builder builder) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + return builder.build(); + } else { + return builder.getNotification(); + } + } +} diff --git a/src/org/chromium/base/BuildInfo.java b/src/org/chromium/base/BuildInfo.java index 2314051..81a8f34 100644 --- a/src/org/chromium/base/BuildInfo.java +++ b/src/org/chromium/base/BuildInfo.java @@ -67,7 +67,10 @@ public static String getPackageVersionCode(Context context) { try { PackageManager pm = context.getPackageManager(); PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0); - msg = "" + pi.versionCode; + msg = ""; + if (pi.versionCode > 0) { + msg = Integer.toString(pi.versionCode); + } } catch (NameNotFoundException e) { Log.d(TAG, msg); } diff --git a/src/org/chromium/base/MemoryPressureLevelList.java b/src/org/chromium/base/MemoryPressureLevelList.java new file mode 100644 index 0000000..8143bc8 --- /dev/null +++ b/src/org/chromium/base/MemoryPressureLevelList.java @@ -0,0 +1,20 @@ + + + + +package org.chromium.base; + +class MemoryPressureLevelList { + + + + + + +static final int MEMORY_PRESSURE_MODERATE = 0; + + + + +static final int MEMORY_PRESSURE_CRITICAL = 2; +} diff --git a/src/org/chromium/base/MemoryPressureLevelList.template b/src/org/chromium/base/MemoryPressureLevelList.template new file mode 100644 index 0000000..cebca84 --- /dev/null +++ b/src/org/chromium/base/MemoryPressureLevelList.template @@ -0,0 +1,12 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +class MemoryPressureLevelList { +#define DEFINE_MEMORY_PRESSURE_LEVEL(name, value) \ + static final int name = value; +#include "base/memory/memory_pressure_level_list.h" +#undef DEFINE_MEMORY_PRESSURE_LEVEL +} diff --git a/src/org/chromium/base/MemoryPressureListener.java b/src/org/chromium/base/MemoryPressureListener.java new file mode 100644 index 0000000..311b0f6 --- /dev/null +++ b/src/org/chromium/base/MemoryPressureListener.java @@ -0,0 +1,55 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.content.ComponentCallbacks2; +import android.content.Context; +import android.content.res.Configuration; + + +/** + * This is an internal implementation of the C++ counterpart. + * It registers a ComponentCallbacks2 with the system, and dispatches into + * native. + */ +public class MemoryPressureListener { + @CalledByNative + private static void registerSystemCallback(Context context) { + context.registerComponentCallbacks( + new ComponentCallbacks2() { + @Override + public void onTrimMemory(int level) { + maybeNotifyMemoryPresure(level); + } + + @Override + public void onLowMemory() { + nativeOnMemoryPressure(MemoryPressureLevelList.MEMORY_PRESSURE_CRITICAL); + } + + @Override + public void onConfigurationChanged(Configuration configuration) { + } + }); + } + + /** + * Used by applications to simulate a memory pressure signal. + */ + public static void simulateMemoryPressureSignal(int level) { + maybeNotifyMemoryPresure(level); + } + + private static void maybeNotifyMemoryPresure(int level) { + if (level == ComponentCallbacks2.TRIM_MEMORY_COMPLETE) { + nativeOnMemoryPressure(MemoryPressureLevelList.MEMORY_PRESSURE_CRITICAL); + } else if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND || + level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { + nativeOnMemoryPressure(MemoryPressureLevelList.MEMORY_PRESSURE_MODERATE); + } + } + + private static native void nativeOnMemoryPressure(int memoryPressureType); +} diff --git a/src/org/chromium/base/SysUtils.java b/src/org/chromium/base/SysUtils.java new file mode 100644 index 0000000..7af5a40 --- /dev/null +++ b/src/org/chromium/base/SysUtils.java @@ -0,0 +1,30 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.app.ActivityManager; +import android.app.ActivityManager.MemoryInfo; +import android.content.Context; +import android.os.Build; + +/** + * Exposes system related information about the current device. + */ +public class SysUtils { + private static Boolean sLowEndDevice; + + private SysUtils() { } + + /** + * @return Whether or not this device should be considered a low end device. + */ + public static boolean isLowEndDevice() { + if (sLowEndDevice == null) sLowEndDevice = nativeIsLowEndDevice(); + + return sLowEndDevice.booleanValue(); + } + + private static native boolean nativeIsLowEndDevice(); +} diff --git a/src/org/chromium/base/SystemMessageHandler.java b/src/org/chromium/base/SystemMessageHandler.java index f7bb19f..fc25ff8 100644 --- a/src/org/chromium/base/SystemMessageHandler.java +++ b/src/org/chromium/base/SystemMessageHandler.java @@ -18,70 +18,32 @@ class SystemMessageHandler extends Handler { // Native class pointer set by the constructor of the SharedClient native class. private int mMessagePumpDelegateNative = 0; - // Used to ensure we have at most one TIMER_MESSAGE pending at once. - private AtomicBoolean mTimerFired = new AtomicBoolean(true); - - // Used to insert TIMER_MESSAGE on the front of the system message queue during startup only. - // This is a wee hack, to give a priority boost to native tasks during startup as they tend to - // be on the critical path. (After startup, handling the UI with minimum latency is more - // important). - private boolean mStartupComplete = false; - private final long mStartupCompleteTime = System.currentTimeMillis() + 2000; - private final boolean startupComplete() { - if (!mStartupComplete && System.currentTimeMillis() > mStartupCompleteTime) { - mStartupComplete = true; - } - return mStartupComplete; - } - private SystemMessageHandler(int messagePumpDelegateNative) { mMessagePumpDelegateNative = messagePumpDelegateNative; } @Override public void handleMessage(Message msg) { - if (msg.what == TIMER_MESSAGE) { - mTimerFired.set(true); - } - while (nativeDoRunLoopOnce(mMessagePumpDelegateNative)) { - if (startupComplete()) { - setTimer(); - break; - } - } + nativeDoRunLoopOnce(mMessagePumpDelegateNative); } + @SuppressWarnings("unused") @CalledByNative private void setTimer() { - if (!mTimerFired.getAndSet(false)) { - // mTimerFired was already false. - return; - } - if (startupComplete()) { - sendEmptyMessage(TIMER_MESSAGE); - } else { - sendMessageAtFrontOfQueue(obtainMessage(TIMER_MESSAGE)); - } + sendEmptyMessage(TIMER_MESSAGE); } - // If millis <=0, it'll send a TIMER_MESSAGE instead of - // a DELAYED_TIMER_MESSAGE. @SuppressWarnings("unused") @CalledByNative private void setDelayedTimer(long millis) { - if (millis <= 0) { - setTimer(); - } else { - removeMessages(DELAYED_TIMER_MESSAGE); - sendEmptyMessageDelayed(DELAYED_TIMER_MESSAGE, millis); - } + removeMessages(DELAYED_TIMER_MESSAGE); + sendEmptyMessageDelayed(DELAYED_TIMER_MESSAGE, millis); } @SuppressWarnings("unused") @CalledByNative private void removeTimer() { removeMessages(TIMER_MESSAGE); - removeMessages(DELAYED_TIMER_MESSAGE); } @CalledByNative @@ -89,5 +51,5 @@ private static SystemMessageHandler create(int messagePumpDelegateNative) { return new SystemMessageHandler(messagePumpDelegateNative); } - private native boolean nativeDoRunLoopOnce(int messagePumpDelegateNative); + private native void nativeDoRunLoopOnce(int messagePumpDelegateNative); } diff --git a/src/org/chromium/base/ThreadUtils.java b/src/org/chromium/base/ThreadUtils.java index cdf73c3..a880ede 100644 --- a/src/org/chromium/base/ThreadUtils.java +++ b/src/org/chromium/base/ThreadUtils.java @@ -18,8 +18,8 @@ public class ThreadUtils { /** - * Run the supplied Runnable on the main thread. The method will block until - * the Runnable completes. + * Run the supplied Runnable on the main thread. The method will block until the Runnable + * completes. * * @param r The Runnable to run. */ @@ -38,8 +38,8 @@ public static void runOnUiThreadBlocking(final Runnable r) { } /** - * Run the supplied Callable on the main thread, wrapping any exceptions in - * a RuntimeException. The method will block until the Callable completes. + * Run the supplied Callable on the main thread, wrapping any exceptions in a RuntimeException. + * The method will block until the Callable completes. * * @param c The Callable to run * @return The result of the callable @@ -53,8 +53,8 @@ public static T runOnUiThreadBlockingNoException(Callable c) { } /** - * Run the supplied Callable on the main thread, The method will block until - * the Callable completes. + * Run the supplied Callable on the main thread, The method will block until the Callable + * completes. * * @param c The Callable to run * @return The result of the callable @@ -71,8 +71,8 @@ public static T runOnUiThreadBlocking(Callable c) throws ExecutionExcepti } /** - * Run the supplied FutureTask on the main thread. The method will block - * only if the current thread is the main thread. + * Run the supplied FutureTask on the main thread. The method will block only if the current + * thread is the main thread. * * @param task The FutureTask to run * @return The queried task (to aid inline construction) @@ -87,8 +87,8 @@ public static FutureTask runOnUiThread(FutureTask task) { } /** - * Run the supplied Callable on the main thread. The method will block - * only if the current thread is the main thread. + * Run the supplied Callable on the main thread. The method will block only if the current + * thread is the main thread. * * @param c The Callable to run * @return A FutureTask wrapping the callable to retrieve results @@ -98,8 +98,8 @@ public static FutureTask runOnUiThread(Callable c) { } /** - * Run the supplied Runnable on the main thread. The method will block - * only if the current thread is the main thread. + * Run the supplied Runnable on the main thread. The method will block only if the current + * thread is the main thread. * * @param r The Runnable to run */ @@ -112,8 +112,8 @@ public static void runOnUiThread(Runnable r) { } /** - * Post the supplied FutureTask to run on the main thread. The method will - * not block, even if called on the UI thread. + * Post the supplied FutureTask to run on the main thread. The method will not block, even if + * called on the UI thread. * * @param task The FutureTask to run * @return The queried task (to aid inline construction) @@ -124,8 +124,8 @@ public static FutureTask postOnUiThread(FutureTask task) { } /** - * Post the supplied Runnable to run on the main thread. The method will - * not block, even if called on the UI thread. + * Post the supplied Runnable to run on the main thread. The method will not block, even if + * called on the UI thread. * * @param task The Runnable to run */ @@ -133,6 +133,17 @@ public static void postOnUiThread(Runnable r) { LazyHolder.sUiThreadHandler.post(r); } + /** + * Post the supplied Runnable to run on the main thread after the given amount of time. The + * method will not block, even if called on the UI thread. + * + * @param task The Runnable to run + * @param delayMillis The delay in milliseconds until the Runnable will be run + */ + public static void postOnUiThreadDelayed(Runnable r, long delayMillis) { + LazyHolder.sUiThreadHandler.postDelayed(r, delayMillis); + } + /** * Asserts that the current thread is running on the main thread. */ diff --git a/src/org/chromium/chrome/R.java b/src/org/chromium/chrome/R.java new file mode 100644 index 0000000..e814df3 --- /dev/null +++ b/src/org/chromium/chrome/R.java @@ -0,0 +1,266 @@ +/* AUTO-GENERATED FILE. DO NOT MODIFY. + * + * This class was automatically generated by the + * aapt tool from the resource data it found. It + * should not be modified by hand. + */ +package org.chromium.chrome; + +public final class R { + public static final class id { + public static final int suppress_js_modal_dialogs = 0x7f09004d; + public static final int selected_color_view = 0x7f090049; + public static final int billing_country_spinner = 0x7f090017; + public static final int shipping_state = 0x7f090020; + public static final int cc_billing_spinner = 0x7f090029; + public static final int card_number = 0x7f09000c; + public static final int autofill_menu_text = 0x7f090037; + public static final int editing_layout_shipping = 0x7f09001b; + public static final int expiration_year_spinner = 0x7f09000e; + public static final int website_settings_headline = 0x7f09005e; + public static final int autofill_menu_item = 0x7f090036; + public static final int seek_bar = 0x7f090042; + public static final int recipient_name = 0x7f09001c; + public static final int cardholder_name = 0x7f090010; + public static final int position_in_year = 0x7f090055; + public static final int more_colors_button = 0x7f090047; + public static final int editing_layout_cc_billing = 0x7f09000a; + public static final int website_settings_icon = 0x7f09005d; + public static final int gradient_border = 0x7f090040; + public static final int cvc_code = 0x7f09000f; + public static final int top_view = 0x7f090057; + public static final int autofill_label = 0x7f09003d; + public static final int year = 0x7f090056; + public static final int more_colors_button_border = 0x7f090046; + public static final int color_picker_simple = 0x7f090045; + public static final int email_label = 0x7f090030; + public static final int line_top = 0x7f090009; + public static final int text = 0x7f09003f; + public static final int autofill_popup_window = 0x7f090000; + public static final int accounts_spinner = 0x7f090008; + public static final int shipping_city = 0x7f09001f; + public static final int selected_color_view_border = 0x7f090048; + public static final int js_modal_dialog_prompt = 0x7f09004c; + public static final int cc_icon = 0x7f090038; + public static final int content = 0x7f090001; + public static final int general_layout = 0x7f090025; + public static final int negative_button = 0x7f090004; + public static final int time_picker = 0x7f09004b; + public static final int billing_zip_code = 0x7f090016; + public static final int adapter_item_edit_button = 0x7f09003b; + public static final int accounts_logo = 0x7f090007; + public static final int save_locally_checkbox = 0x7f090032; + public static final int billing_phone_number = 0x7f090018; + public static final int adapter_item_line_1 = 0x7f090039; + public static final int adapter_item_line_2 = 0x7f09003a; + public static final int email_address = 0x7f09001a; + public static final int cc_billing_label = 0x7f090028; + public static final int main_text = 0x7f09005b; + public static final int cc_label = 0x7f09002a; + public static final int shipping_label = 0x7f09002e; + public static final int billing_state = 0x7f090015; + public static final int color_picker_advanced = 0x7f090043; + public static final int line_bottom = 0x7f090034; + public static final int shipping_phone_number = 0x7f090023; + public static final int pickers = 0x7f090054; + public static final int editing_layout_billing = 0x7f090011; + public static final int editing_layout_email = 0x7f090019; + public static final int arrow_image = 0x7f09005a; + public static final int title = 0x7f090006; + public static final int icon_view = 0x7f090058; + public static final int terms_info = 0x7f090035; + public static final int billing_spinner = 0x7f09002d; + public static final int expiration_month_spinner = 0x7f09000d; + public static final int shipping_street_address_2 = 0x7f09001e; + public static final int website_settings_description = 0x7f09005f; + public static final int text_wrapper = 0x7f090059; + public static final int shipping_street_address_1 = 0x7f09001d; + public static final int top_checkbox_notification = 0x7f090026; + public static final int autofill_editing_spinner_item = 0x7f090024; + public static final int positive_button = 0x7f090005; + public static final int editing_layout_credit_card = 0x7f09000b; + public static final int sub_text = 0x7f09005c; + public static final int bottom_notifications = 0x7f090033; + public static final int cc_spinner = 0x7f09002b; + public static final int cvc_challenge = 0x7f09003c; + public static final int loading_icon = 0x7f090002; + public static final int autofill_sublabel = 0x7f09003e; + public static final int color_picker_simple_border = 0x7f090044; + public static final int billing_street_address_1 = 0x7f090012; + public static final int footer = 0x7f090003; + public static final int billing_street_address_2 = 0x7f090013; + public static final int billing_label = 0x7f09002c; + public static final int shipping_zip_code = 0x7f090021; + public static final int billing_city = 0x7f090014; + public static final int date_picker = 0x7f09004a; + public static final int top_notifications = 0x7f090027; + public static final int shipping_country_spinner = 0x7f090022; + public static final int address_spinner = 0x7f09002f; + public static final int email_spinner = 0x7f090031; + public static final int gradient = 0x7f090041; + } + public static final class style { + public static final int AutofillPopupWindow = 0x7f080000; + } + public static final class color { + public static final int color_picker_border_color = 0x7f060000; + } + public static final class string { + public static final int suppress_js_modal_dialogs = 0x7f050022; + public static final int firstrun_signed_in_title = 0x7f050036; + public static final int autofill_terms_of_service = 0x7f05002e; + public static final int date_picker_dialog_title = 0x7f05000e; + public static final int js_modal_dialog_confirm = 0x7f050020; + public static final int autofill_positive_button_editing = 0x7f05002a; + public static final int autofill_add_button = 0x7f05002c; + public static final int autofill_edit_button = 0x7f05002d; + public static final int policy_dialog_message = 0x7f050031; + public static final int media_player_error_title = 0x7f050017; + public static final int color_picker_dialog_title = 0x7f050008; + public static final int reload_this_page = 0x7f05001e; + public static final int sad_tab_help_link = 0x7f05003b; + public static final int accessibility_date_picker_week = 0x7f050015; + public static final int date_picker_dialog_set = 0x7f05000c; + public static final int policy_dialog_proceed = 0x7f050032; + public static final int dont_reload_this_page = 0x7f05001f; + public static final int wiping_profile_data_message = 0x7f050035; + public static final int sync_error_domain = 0x7f050028; + public static final int media_player_error_text_invalid_progressive_playback = 0x7f050018; + public static final int color_picker_value = 0x7f050005; + public static final int sync_error_ga = 0x7f050026; + public static final int date_picker_dialog_clear = 0x7f05000d; + public static final int low_memory_error = 0x7f050000; + public static final int sad_tab_help_message = 0x7f05003a; + public static final int color_picker_saturation = 0x7f050004; + public static final int firstrun_signed_in_description = 0x7f050037; + public static final int autofill_fetch_message = 0x7f05002f; + public static final int stay_on_this_page = 0x7f05001d; + public static final int media_player_error_button = 0x7f05001a; + public static final int month_picker_dialog_title = 0x7f050010; + public static final int policy_dialog_title = 0x7f050030; + public static final int color_picker_hue = 0x7f050003; + public static final int accessibility_datetime_picker_time = 0x7f050013; + public static final int actionbar_share = 0x7f050009; + public static final int color_picker_button_set = 0x7f050006; + public static final int accessibility_date_picker_month = 0x7f050014; + public static final int sad_tab_reload_label = 0x7f05003c; + public static final int autofill_negative_button_editing = 0x7f05002b; + public static final int media_player_error_text_unknown = 0x7f050019; + public static final int leave_this_page = 0x7f05001c; + public static final int policy_dialog_cancel = 0x7f050033; + public static final int accessibility_datetime_picker_date = 0x7f050012; + public static final int sync_error_service_unavailable = 0x7f050029; + public static final int sad_tab_title = 0x7f050038; + public static final int actionbar_web_search = 0x7f05000a; + public static final int date_time_picker_dialog_title = 0x7f05000f; + public static final int opening_file_error = 0x7f050001; + public static final int js_modal_dialog_cancel = 0x7f050021; + public static final int color_picker_button_cancel = 0x7f050007; + public static final int accessibility_date_picker_year = 0x7f050016; + public static final int sync_error_generic = 0x7f050025; + public static final int sad_tab_message = 0x7f050039; + public static final int week_picker_dialog_title = 0x7f050011; + public static final int media_player_loading_video = 0x7f05001b; + public static final int sync_error_connection = 0x7f050027; + public static final int wiping_profile_data_title = 0x7f050034; + public static final int color_picker_button_more = 0x7f050002; + public static final int certtitle = 0x7f050024; + public static final int accessibility_js_modal_dialog_prompt = 0x7f050023; + public static final int accessibility_content_view = 0x7f05000b; + } + public static final class layout { + public static final int autofill_editing_layout_credit_card = 0x7f040002; + public static final int js_modal_dialog = 0x7f04000e; + public static final int color_picker_advanced_component = 0x7f04000a; + public static final int date_time_picker_dialog = 0x7f04000d; + public static final int autofill_menu_item = 0x7f040007; + public static final int autofill_editing_layout_email = 0x7f040003; + public static final int autofill_general_layout = 0x7f040006; + public static final int autofill_text = 0x7f040009; + public static final int autofill_simple_menu_item = 0x7f040008; + public static final int validation_message_bubble = 0x7f040011; + public static final int color_picker_dialog_content = 0x7f04000b; + public static final int color_picker_dialog_title = 0x7f04000c; + public static final int autofill_dialog_title = 0x7f040001; + public static final int website_settings = 0x7f040012; + public static final int autofill_editing_layout_shipping = 0x7f040004; + public static final int autofill_dialog_content = 0x7f040000; + public static final int two_field_date_picker = 0x7f040010; + public static final int autofill_editing_spinner_item = 0x7f040005; + } + public static final class drawable { + public static final int infobar_savepassword = 0x7f02001c; + public static final int infobar_plugin_crashed = 0x7f020019; + public static final int infobar_autofill = 0x7f02000d; + public static final int infobar_camera = 0x7f02000f; + public static final int infobar_plugin = 0x7f020018; + public static final int arrow = 0x7f020000; + public static final int infobar_autologin = 0x7f02000e; + public static final int color_picker_border = 0x7f020007; + public static final int master_card = 0x7f020021; + public static final int autofill_popup_background_down = 0x7f020002; + public static final int wallet_logo = 0x7f02002b; + public static final int infobar_incomplete = 0x7f020014; + public static final int missing = 0x7f020022; + public static final int pageinfo_warning_minor = 0x7f020028; + public static final int infobar_warning = 0x7f020020; + public static final int infobar_waning = 0x7f02001f; + public static final int ondemand_overlay = 0x7f020023; + public static final int color_picker_advanced_select_handle = 0x7f020006; + public static final int pageinfo_bad = 0x7f020024; + public static final int bubble_arrow_up = 0x7f020005; + public static final int autofill_popup_background_up = 0x7f020003; + public static final int infobar_translate = 0x7f02001e; + public static final int infobar_theme = 0x7f02001d; + public static final int infobar_didyoumean = 0x7f020012; + public static final int infobar_lock = 0x7f020015; + public static final int infobar_questionmark = 0x7f02001a; + public static final int divider_horizontal_bright = 0x7f020009; + public static final int pageinfo_warning_major = 0x7f020027; + public static final int controlled_setting_mandatory_large = 0x7f020008; + public static final int infobar_microphone = 0x7f020016; + public static final int pageinfo_info = 0x7f020026; + public static final int pageinfo_good = 0x7f020025; + public static final int ic_menu_share_holo_light = 0x7f02000c; + public static final int globe_favicon = 0x7f02000a; + public static final int infobar_geolocation = 0x7f020013; + public static final int bubble = 0x7f020004; + public static final int infobar_restore = 0x7f02001b; + public static final int autofill_popup_background = 0x7f020001; + public static final int infobar_multiple_downloads = 0x7f020017; + public static final int infobar_cookie = 0x7f020010; + public static final int infobar_desktop_notifications = 0x7f020011; + public static final int ic_menu_search_holo_light = 0x7f02000b; + public static final int visa = 0x7f02002a; + } + public static final class dimen { + public static final int autofill_steady_margin = 0x7f07000f; + public static final int autofill_steady_text_size = 0x7f070010; + public static final int autofill_menu_item_size = 0x7f07000b; + public static final int autofill_dialog_width = 0x7f070012; + public static final int autofill_menu_icon_width = 0x7f070009; + public static final int autofill_menu_item_padding = 0x7f07000a; + public static final int color_picker_gradient_margin = 0x7f070000; + public static final int certificate_viewer_padding = 0x7f070002; + public static final int autofill_field_icon_height = 0x7f070006; + public static final int autofill_edit_text_size = 0x7f070005; + public static final int autofill_dialog_title_height = 0x7f070015; + public static final int autofill_translation_anim_distance = 0x7f070011; + public static final int autofill_notification_padding = 0x7f07000c; + public static final int autofill_dialog_footer_height = 0x7f070016; + public static final int autofill_notification_text_size = 0x7f07000d; + public static final int autofill_spinner_offset = 0x7f07000e; + public static final int autofill_edit_margin = 0x7f070004; + public static final int link_preview_overlay_radius = 0x7f070001; + public static final int autofill_edit_height = 0x7f070003; + public static final int autofill_dialog_max_height = 0x7f070013; + public static final int autofill_dialog_vertical_margin = 0x7f070014; + public static final int autofill_field_icon_width = 0x7f070007; + public static final int autofill_menu_icon_height = 0x7f070008; + } + public static final class mipmap { + public static final int bookmark_widget_bg = 0x7f030001; + public static final int bookmark_widget_bg_highlights = 0x7f030002; + public static final int app_icon = 0x7f030000; + } +} diff --git a/src/org/chromium/chrome/browser/ResourceId.java b/src/org/chromium/chrome/browser/ResourceId.java new file mode 100644 index 0000000..f95c951 --- /dev/null +++ b/src/org/chromium/chrome/browser/ResourceId.java @@ -0,0 +1,54 @@ + + + + +package org.chromium.chrome.browser; + +import org.chromium.chrome.R; + +public class ResourceId { + public static int mapToDrawableId(int enumeratedId) { + int[] resourceList = { + + +0, + + +R.drawable.infobar_geolocation, +R.drawable.infobar_didyoumean, +R.drawable.infobar_autofill, +R.drawable.infobar_autologin, +R.drawable.infobar_cookie, +R.drawable.infobar_desktop_notifications, + +R.drawable.infobar_incomplete, +R.drawable.infobar_camera, +R.drawable.infobar_microphone, +R.drawable.infobar_multiple_downloads, + +R.drawable.infobar_plugin_crashed, + +R.drawable.infobar_plugin, +R.drawable.infobar_restore, +R.drawable.infobar_savepassword, +R.drawable.infobar_warning, +R.drawable.infobar_theme, +R.drawable.infobar_translate, + + +R.drawable.controlled_setting_mandatory_large, + +R.drawable.pageinfo_bad, +R.drawable.pageinfo_good, +R.drawable.pageinfo_info, +R.drawable.pageinfo_warning_major, + +R.drawable.pageinfo_warning_minor, + }; + if (enumeratedId >= 0 && enumeratedId < resourceList.length) { + return resourceList[enumeratedId]; + } + assert false : "enumeratedId '" + enumeratedId + "' was out of range."; + return R.drawable.missing; + } +} diff --git a/src/org/chromium/chrome/browser/sync/ModelTypeSelection.java b/src/org/chromium/chrome/browser/sync/ModelTypeSelection.java index e0166d1..da33afe 100644 --- a/src/org/chromium/chrome/browser/sync/ModelTypeSelection.java +++ b/src/org/chromium/chrome/browser/sync/ModelTypeSelection.java @@ -1,16 +1,36 @@ + + + + package org.chromium.chrome.browser.sync; + public class ModelTypeSelection { + + + public static final int AUTOFILL = 1<<0; + public static final int BOOKMARK = 1<<1; + public static final int PASSWORD = 1<<2; + public static final int SESSION = 1<<3; + public static final int TYPED_URL = 1<<4; + public static final int AUTOFILL_PROFILE = 1<<5; + public static final int HISTORY_DELETE_DIRECTIVE = 1<<6; + public static final int PROXY_TABS = 1<<7; + public static final int FAVICON_IMAGE = 1<<8; + public static final int FAVICON_TRACKING = 1<<9; + public static final int NIGORI = 1<<10; + public static final int DEVICE_INFO = 1<<11; + public static final int EXPERIMENTS = 1<<12; } diff --git a/src/org/chromium/chrome/browser/ui/toolbar/ToolbarModelSecurityLevel.java b/src/org/chromium/chrome/browser/ui/toolbar/ToolbarModelSecurityLevel.java index 463522e..c290c81 100644 --- a/src/org/chromium/chrome/browser/ui/toolbar/ToolbarModelSecurityLevel.java +++ b/src/org/chromium/chrome/browser/ui/toolbar/ToolbarModelSecurityLevel.java @@ -1,10 +1,34 @@ + + + + package org.chromium.chrome.browser.ui.toolbar; + public class ToolbarModelSecurityLevel { + + + + + + public static final int NONE = 0; + + public static final int EV_SECURE = 1; + + public static final int SECURE = 2; + + + public static final int SECURITY_WARNING = 3; + + + public static final int SECURITY_POLICY_WARNING = 4; + + public static final int SECURITY_ERROR = 5; + public static final int NUM_SECURITY_LEVELS = 6; } diff --git a/src/org/chromium/chrome/testshell/Manifest.java b/src/org/chromium/chrome/testshell/Manifest.java new file mode 100644 index 0000000..9cc482a --- /dev/null +++ b/src/org/chromium/chrome/testshell/Manifest.java @@ -0,0 +1,14 @@ +/* AUTO-GENERATED FILE. DO NOT MODIFY. + * + * This class was automatically generated by the + * aapt tool from the resource data it found. It + * should not be modified by hand. + */ + +package org.chromium.chrome.testshell; + +public final class Manifest { + public static final class permission { + public static final String SANDBOX="org.chromium.chrome.testshell.permission.SANDBOX"; + } +} diff --git a/src/org/chromium/chrome/testshell/R.java b/src/org/chromium/chrome/testshell/R.java new file mode 100644 index 0000000..c2e4032 --- /dev/null +++ b/src/org/chromium/chrome/testshell/R.java @@ -0,0 +1,293 @@ +/* AUTO-GENERATED FILE. DO NOT MODIFY. + * + * This class was automatically generated by the + * aapt tool from the resource data it found. It + * should not be modified by hand. + */ + +package org.chromium.chrome.testshell; + +public final class R { + public static final class attr { + } + public static final class color { + public static final int color_picker_border_color=0x7f060000; + } + public static final class dimen { + public static final int autofill_dialog_footer_height=0x7f070016; + /** Autofill Dialog + */ + public static final int autofill_dialog_max_height=0x7f070013; + public static final int autofill_dialog_title_height=0x7f070015; + public static final int autofill_dialog_vertical_margin=0x7f070014; + public static final int autofill_dialog_width=0x7f070012; + /** Autofill Dialog + */ + public static final int autofill_edit_height=0x7f070003; + public static final int autofill_edit_margin=0x7f070004; + public static final int autofill_edit_text_size=0x7f070005; + public static final int autofill_field_icon_height=0x7f070006; + public static final int autofill_field_icon_width=0x7f070007; + public static final int autofill_menu_icon_height=0x7f070008; + public static final int autofill_menu_icon_width=0x7f070009; + public static final int autofill_menu_item_padding=0x7f07000a; + public static final int autofill_menu_item_size=0x7f07000b; + public static final int autofill_notification_padding=0x7f07000c; + public static final int autofill_notification_text_size=0x7f07000d; + public static final int autofill_spinner_offset=0x7f07000e; + public static final int autofill_steady_margin=0x7f07000f; + public static final int autofill_steady_text_size=0x7f070010; + public static final int autofill_translation_anim_distance=0x7f070011; + /** Certificate Viewer Dimensions + */ + public static final int certificate_viewer_padding=0x7f070002; + /** + 14.5 = Seekbar thumb width - border width, but it depends on the width + of the seek bar icon. + + */ + public static final int color_picker_gradient_margin=0x7f070000; + /** Link Preview dimensions + */ + public static final int link_preview_overlay_radius=0x7f070001; + } + public static final class drawable { + public static final int arrow=0x7f020000; + public static final int autofill_popup_background=0x7f020001; + public static final int autofill_popup_background_down=0x7f020002; + public static final int autofill_popup_background_up=0x7f020003; + public static final int bubble=0x7f020004; + public static final int bubble_arrow_up=0x7f020005; + public static final int color_picker_advanced_select_handle=0x7f020006; + public static final int color_picker_border=0x7f020007; + public static final int controlled_setting_mandatory_large=0x7f020008; + public static final int divider_horizontal_bright=0x7f020009; + public static final int globe_favicon=0x7f02000a; + public static final int ic_menu_search_holo_light=0x7f02000b; + public static final int ic_menu_share_holo_light=0x7f02000c; + public static final int infobar_autofill=0x7f02000d; + public static final int infobar_autologin=0x7f02000e; + public static final int infobar_camera=0x7f02000f; + public static final int infobar_cookie=0x7f020010; + public static final int infobar_desktop_notifications=0x7f020011; + public static final int infobar_didyoumean=0x7f020012; + public static final int infobar_geolocation=0x7f020013; + public static final int infobar_incomplete=0x7f020014; + public static final int infobar_lock=0x7f020015; + public static final int infobar_microphone=0x7f020016; + public static final int infobar_multiple_downloads=0x7f020017; + public static final int infobar_plugin=0x7f020018; + public static final int infobar_plugin_crashed=0x7f020019; + public static final int infobar_questionmark=0x7f02001a; + public static final int infobar_restore=0x7f02001b; + public static final int infobar_savepassword=0x7f02001c; + public static final int infobar_theme=0x7f02001d; + public static final int infobar_translate=0x7f02001e; + public static final int infobar_waning=0x7f02001f; + public static final int infobar_warning=0x7f020020; + public static final int master_card=0x7f020021; + public static final int missing=0x7f020022; + public static final int ondemand_overlay=0x7f020023; + public static final int pageinfo_bad=0x7f020024; + public static final int pageinfo_good=0x7f020025; + public static final int pageinfo_info=0x7f020026; + public static final int pageinfo_warning_major=0x7f020027; + public static final int pageinfo_warning_minor=0x7f020028; + public static final int progress=0x7f020029; + public static final int visa=0x7f02002a; + public static final int wallet_logo=0x7f02002b; + } + public static final class id { + public static final int accounts_logo=0x7f090007; + public static final int accounts_spinner=0x7f090008; + public static final int adapter_item_edit_button=0x7f09003b; + public static final int adapter_item_line_1=0x7f090039; + public static final int adapter_item_line_2=0x7f09003a; + public static final int address_spinner=0x7f09002f; + public static final int arrow_image=0x7f09005a; + public static final int autofill_editing_spinner_item=0x7f090024; + public static final int autofill_label=0x7f09003d; + public static final int autofill_menu_item=0x7f090036; + public static final int autofill_menu_text=0x7f090037; + /** AutofillPopup constants + */ + public static final int autofill_popup_window=0x7f090000; + public static final int autofill_sublabel=0x7f09003e; + public static final int billing_city=0x7f090014; + public static final int billing_country_spinner=0x7f090017; + public static final int billing_label=0x7f09002c; + public static final int billing_phone_number=0x7f090018; + public static final int billing_spinner=0x7f09002d; + public static final int billing_state=0x7f090015; + public static final int billing_street_address_1=0x7f090012; + public static final int billing_street_address_2=0x7f090013; + public static final int billing_zip_code=0x7f090016; + public static final int bottom_notifications=0x7f090033; + public static final int card_number=0x7f09000c; + public static final int cardholder_name=0x7f090010; + public static final int cc_billing_label=0x7f090028; + public static final int cc_billing_spinner=0x7f090029; + public static final int cc_icon=0x7f090038; + public static final int cc_label=0x7f09002a; + public static final int cc_spinner=0x7f09002b; + public static final int color_picker_advanced=0x7f090043; + public static final int color_picker_simple=0x7f090045; + public static final int color_picker_simple_border=0x7f090044; + public static final int content=0x7f090001; + public static final int content_container=0x7f090053; + public static final int cvc_challenge=0x7f09003c; + public static final int cvc_code=0x7f09000f; + public static final int date_picker=0x7f09004a; + public static final int editing_layout_billing=0x7f090011; + public static final int editing_layout_cc_billing=0x7f09000a; + public static final int editing_layout_credit_card=0x7f09000b; + public static final int editing_layout_email=0x7f090019; + public static final int editing_layout_shipping=0x7f09001b; + public static final int email_address=0x7f09001a; + public static final int email_label=0x7f090030; + public static final int email_spinner=0x7f090031; + public static final int expiration_month_spinner=0x7f09000d; + public static final int expiration_year_spinner=0x7f09000e; + public static final int footer=0x7f090003; + public static final int general_layout=0x7f090025; + public static final int gradient=0x7f090041; + public static final int gradient_border=0x7f090040; + public static final int icon_view=0x7f090058; + public static final int js_modal_dialog_prompt=0x7f09004c; + public static final int line_bottom=0x7f090034; + public static final int line_top=0x7f090009; + public static final int loading_icon=0x7f090002; + public static final int main_text=0x7f09005b; + public static final int more_colors_button=0x7f090047; + public static final int more_colors_button_border=0x7f090046; + public static final int negative_button=0x7f090004; + public static final int next=0x7f090052; + public static final int pickers=0x7f090054; + public static final int position_in_year=0x7f090055; + public static final int positive_button=0x7f090005; + public static final int prev=0x7f090051; + public static final int recipient_name=0x7f09001c; + public static final int save_locally_checkbox=0x7f090032; + public static final int seek_bar=0x7f090042; + public static final int selected_color_view=0x7f090049; + public static final int selected_color_view_border=0x7f090048; + public static final int shipping_city=0x7f09001f; + public static final int shipping_country_spinner=0x7f090022; + public static final int shipping_label=0x7f09002e; + public static final int shipping_phone_number=0x7f090023; + public static final int shipping_state=0x7f090020; + public static final int shipping_street_address_1=0x7f09001d; + public static final int shipping_street_address_2=0x7f09001e; + public static final int shipping_zip_code=0x7f090021; + public static final int sub_text=0x7f09005c; + public static final int suppress_js_modal_dialogs=0x7f09004d; + public static final int tab_manager=0x7f09004e; + public static final int terms_info=0x7f090035; + public static final int text=0x7f09003f; + public static final int text_wrapper=0x7f090059; + public static final int time_picker=0x7f09004b; + public static final int title=0x7f090006; + public static final int toolbar=0x7f09004f; + public static final int top_checkbox_notification=0x7f090026; + public static final int top_notifications=0x7f090027; + public static final int top_view=0x7f090057; + public static final int url=0x7f090050; + public static final int website_settings_description=0x7f09005f; + public static final int website_settings_headline=0x7f09005e; + public static final int website_settings_icon=0x7f09005d; + public static final int year=0x7f090056; + } + public static final class layout { + public static final int autofill_dialog_content=0x7f040000; + public static final int autofill_dialog_title=0x7f040001; + public static final int autofill_editing_layout_credit_card=0x7f040002; + public static final int autofill_editing_layout_email=0x7f040003; + public static final int autofill_editing_layout_shipping=0x7f040004; + public static final int autofill_editing_spinner_item=0x7f040005; + public static final int autofill_general_layout=0x7f040006; + public static final int autofill_menu_item=0x7f040007; + public static final int autofill_simple_menu_item=0x7f040008; + public static final int autofill_text=0x7f040009; + public static final int color_picker_advanced_component=0x7f04000a; + public static final int color_picker_dialog_content=0x7f04000b; + public static final int color_picker_dialog_title=0x7f04000c; + public static final int date_time_picker_dialog=0x7f04000d; + public static final int js_modal_dialog=0x7f04000e; + public static final int testshell_activity=0x7f04000f; + public static final int two_field_date_picker=0x7f040010; + public static final int validation_message_bubble=0x7f040011; + public static final int website_settings=0x7f040012; + } + public static final class mipmap { + public static final int app_icon=0x7f030000; + public static final int bookmark_widget_bg=0x7f030001; + public static final int bookmark_widget_bg_highlights=0x7f030002; + } + public static final class string { + public static final int accessibility_content_view=0x7f05000b; + public static final int accessibility_date_picker_month=0x7f050014; + public static final int accessibility_date_picker_week=0x7f050015; + public static final int accessibility_date_picker_year=0x7f050016; + public static final int accessibility_datetime_picker_date=0x7f050012; + public static final int accessibility_datetime_picker_time=0x7f050013; + public static final int accessibility_js_modal_dialog_prompt=0x7f050023; + public static final int actionbar_share=0x7f050009; + public static final int actionbar_web_search=0x7f05000a; + public static final int autofill_add_button=0x7f05002c; + public static final int autofill_edit_button=0x7f05002d; + public static final int autofill_fetch_message=0x7f05002f; + public static final int autofill_negative_button_editing=0x7f05002b; + public static final int autofill_positive_button_editing=0x7f05002a; + public static final int autofill_terms_of_service=0x7f05002e; + public static final int certtitle=0x7f050024; + public static final int color_picker_button_cancel=0x7f050007; + public static final int color_picker_button_more=0x7f050002; + public static final int color_picker_button_set=0x7f050006; + public static final int color_picker_dialog_title=0x7f050008; + public static final int color_picker_hue=0x7f050003; + public static final int color_picker_saturation=0x7f050004; + public static final int color_picker_value=0x7f050005; + public static final int date_picker_dialog_clear=0x7f05000d; + public static final int date_picker_dialog_set=0x7f05000c; + public static final int date_picker_dialog_title=0x7f05000e; + public static final int date_time_picker_dialog_title=0x7f05000f; + public static final int dont_reload_this_page=0x7f05001f; + public static final int firstrun_signed_in_description=0x7f050037; + public static final int firstrun_signed_in_title=0x7f050036; + public static final int js_modal_dialog_cancel=0x7f050021; + public static final int js_modal_dialog_confirm=0x7f050020; + public static final int leave_this_page=0x7f05001c; + public static final int low_memory_error=0x7f050000; + public static final int media_player_error_button=0x7f05001a; + public static final int media_player_error_text_invalid_progressive_playback=0x7f050018; + public static final int media_player_error_text_unknown=0x7f050019; + public static final int media_player_error_title=0x7f050017; + public static final int media_player_loading_video=0x7f05001b; + public static final int month_picker_dialog_title=0x7f050010; + public static final int opening_file_error=0x7f050001; + public static final int policy_dialog_cancel=0x7f050033; + public static final int policy_dialog_message=0x7f050031; + public static final int policy_dialog_proceed=0x7f050032; + public static final int policy_dialog_title=0x7f050030; + public static final int reload_this_page=0x7f05001e; + public static final int sad_tab_help_link=0x7f05003b; + public static final int sad_tab_help_message=0x7f05003a; + public static final int sad_tab_message=0x7f050039; + public static final int sad_tab_reload_label=0x7f05003c; + public static final int sad_tab_title=0x7f050038; + public static final int stay_on_this_page=0x7f05001d; + public static final int suppress_js_modal_dialogs=0x7f050022; + public static final int sync_error_connection=0x7f050027; + public static final int sync_error_domain=0x7f050028; + public static final int sync_error_ga=0x7f050026; + public static final int sync_error_generic=0x7f050025; + public static final int sync_error_service_unavailable=0x7f050029; + public static final int url_hint=0x7f05003d; + public static final int week_picker_dialog_title=0x7f050011; + public static final int wiping_profile_data_message=0x7f050035; + public static final int wiping_profile_data_title=0x7f050034; + } + public static final class style { + public static final int AutofillPopupWindow=0x7f080000; + } +} diff --git a/src/org/chromium/components/web_contents_delegate_android/ColorChooserAndroid.java b/src/org/chromium/components/web_contents_delegate_android/ColorChooserAndroid.java index cdab32b..babf91e 100644 --- a/src/org/chromium/components/web_contents_delegate_android/ColorChooserAndroid.java +++ b/src/org/chromium/components/web_contents_delegate_android/ColorChooserAndroid.java @@ -10,23 +10,22 @@ import org.chromium.base.JNINamespace; import org.chromium.content.browser.ContentViewCore; import org.chromium.ui.ColorPickerDialog; +import org.chromium.ui.OnColorChangedListener; /** * ColorChooserAndroid communicates with the java ColorPickerDialog and the * native color_chooser_android.cc */ -@JNINamespace("components") +@JNINamespace("web_contents_delegate_android") public class ColorChooserAndroid { private final ColorPickerDialog mDialog; private final int mNativeColorChooserAndroid; private ColorChooserAndroid(int nativeColorChooserAndroid, Context context, int initialColor) { - ColorPickerDialog.OnColorChangedListener listener = - new ColorPickerDialog.OnColorChangedListener() { - + OnColorChangedListener listener = new OnColorChangedListener() { @Override - public void colorChanged(int color) { + public void onColorChanged(int color) { mDialog.dismiss(); nativeOnColorChosen(mNativeColorChooserAndroid, color); } diff --git a/src/org/chromium/components/web_contents_delegate_android/WebContentsDelegateAndroid.java b/src/org/chromium/components/web_contents_delegate_android/WebContentsDelegateAndroid.java index 9769985..62d2403 100644 --- a/src/org/chromium/components/web_contents_delegate_android/WebContentsDelegateAndroid.java +++ b/src/org/chromium/components/web_contents_delegate_android/WebContentsDelegateAndroid.java @@ -14,7 +14,7 @@ /** * Java peer of the native class of the same name. */ -@JNINamespace("components") +@JNINamespace("web_contents_delegate_android") public class WebContentsDelegateAndroid { // Equivalent of WebCore::WebConsoleMessage::LevelTip. @@ -25,6 +25,20 @@ public class WebContentsDelegateAndroid { public static final int LOG_LEVEL_WARNING = 2; // Equivalent of WebCore::WebConsoleMessage::LevelError. public static final int LOG_LEVEL_ERROR = 3; + + // Flags passed to the WebContentsDelegate.navigationStateChanged to tell it + // what has changed. Should match the values in invalidate_type.h. + // Equivalent of InvalidateTypes::INVALIDATE_TYPE_URL. + public static final int INVALIDATE_TYPE_URL = 1 << 0; + // Equivalent of InvalidateTypes::INVALIDATE_TYPE_TAB. + public static final int INVALIDATE_TYPE_TAB = 1 << 1; + // Equivalent of InvalidateTypes::INVALIDATE_TYPE_LOAD. + public static final int INVALIDATE_TYPE_LOAD = 1 << 2; + // Equivalent of InvalidateTypes::INVALIDATE_TYPE_PAGE_ACTIONS. + public static final int INVALIDATE_TYPE_PAGE_ACTIONS = 1 << 3; + // Equivalent of InvalidateTypes::INVALIDATE_TYPE_TITLE. + public static final int INVALIDATE_TYPE_TITLE = 1 << 4; + // The most recent load progress callback received from WebContents, as a percentage. // Initialize to 100 to indicate that we're not in a loading state. private int mMostRecentProgress = 100; @@ -34,7 +48,7 @@ public int getMostRecentProgress() { } @CalledByNative - public void openNewTab(String url, boolean incognito) { + public void openNewTab(String url, String extraHeaders, byte[] postData, boolean incognito) { } @CalledByNative @@ -43,6 +57,10 @@ public boolean addNewContents(int nativeSourceWebContents, int nativeWebContents return false; } + @CalledByNative + public void activateContents() { + } + @CalledByNative public void closeContents() { } @@ -56,7 +74,7 @@ public void onLoadStopped() { } @CalledByNative - public void onTitleUpdated() { + public void navigationStateChanged(int flags) { } @SuppressWarnings("unused") @@ -132,4 +150,13 @@ public void toggleFullscreenModeForTab(boolean enterFullscreen) { public boolean isFullscreenForTabOrPending() { return false; } + + /** + * Called from WebKit to request that the top controls be shown or hidden. + * The implementation should call ContentViewCore.showTopControls to actually + * show or hide the top controls. + */ + @CalledByNative + public void didProgrammaticallyScroll(int scrollX, int scrollY) { + } } diff --git a/src/org/chromium/content/R.java b/src/org/chromium/content/R.java index daa8a77..ea93683 100644 --- a/src/org/chromium/content/R.java +++ b/src/org/chromium/content/R.java @@ -26,18 +26,19 @@ public static final class drawable { } public static final class id { public static int date_picker; - public static int month; + public static int position_in_year; public static int pickers; public static int time_picker; public static int year; } public static final class layout { public static int date_time_picker_dialog; - public static int month_picker; + public static int two_field_date_picker; } public static final class string { public static int accessibility_content_view; public static int accessibility_date_picker_month; + public static int accessibility_date_picker_week; public static int accessibility_date_picker_year; public static int accessibility_datetime_picker_date; public static int accessibility_datetime_picker_time; @@ -53,5 +54,6 @@ public static final class string { public static int media_player_error_title; public static int media_player_loading_video; public static int month_picker_dialog_title; + public static int week_picker_dialog_title; } } diff --git a/src/org/chromium/content/app/SandboxedProcessService.java b/src/org/chromium/content/app/SandboxedProcessService.java index 7b38af8..c48f0c3 100644 --- a/src/org/chromium/content/app/SandboxedProcessService.java +++ b/src/org/chromium/content/app/SandboxedProcessService.java @@ -1,4 +1,4 @@ -// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/src/org/chromium/content/app/SandboxedProcessService0.java b/src/org/chromium/content/app/SandboxedProcessService0.java index 1fcd974..7074ee1 100644 --- a/src/org/chromium/content/app/SandboxedProcessService0.java +++ b/src/org/chromium/content/app/SandboxedProcessService0.java @@ -1,4 +1,4 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/src/org/chromium/content/app/SandboxedProcessService1.java b/src/org/chromium/content/app/SandboxedProcessService1.java index 24846a7..02083f4 100644 --- a/src/org/chromium/content/app/SandboxedProcessService1.java +++ b/src/org/chromium/content/app/SandboxedProcessService1.java @@ -1,4 +1,4 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/src/org/chromium/content/app/SandboxedProcessService10.java b/src/org/chromium/content/app/SandboxedProcessService10.java new file mode 100644 index 0000000..828ffde --- /dev/null +++ b/src/org/chromium/content/app/SandboxedProcessService10.java @@ -0,0 +1,12 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.app; + +// This is needed to register multiple SandboxedProcess services so that we can have +// more than one sandboxed process. + +public class SandboxedProcessService10 extends SandboxedProcessService { + +} diff --git a/src/org/chromium/content/app/SandboxedProcessService11.java b/src/org/chromium/content/app/SandboxedProcessService11.java new file mode 100644 index 0000000..8da928f --- /dev/null +++ b/src/org/chromium/content/app/SandboxedProcessService11.java @@ -0,0 +1,12 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.app; + +// This is needed to register multiple SandboxedProcess services so that we can have +// more than one sandboxed process. + +public class SandboxedProcessService11 extends SandboxedProcessService { + +} diff --git a/src/org/chromium/content/app/SandboxedProcessService12.java b/src/org/chromium/content/app/SandboxedProcessService12.java new file mode 100644 index 0000000..a2b9fa1 --- /dev/null +++ b/src/org/chromium/content/app/SandboxedProcessService12.java @@ -0,0 +1,12 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.app; + +// This is needed to register multiple SandboxedProcess services so that we can have +// more than one sandboxed process. + +public class SandboxedProcessService12 extends SandboxedProcessService { + +} diff --git a/src/org/chromium/content/app/SandboxedProcessService2.java b/src/org/chromium/content/app/SandboxedProcessService2.java index f8d1802..11b2ae1 100644 --- a/src/org/chromium/content/app/SandboxedProcessService2.java +++ b/src/org/chromium/content/app/SandboxedProcessService2.java @@ -1,4 +1,4 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/src/org/chromium/content/app/SandboxedProcessService3.java b/src/org/chromium/content/app/SandboxedProcessService3.java index f5b8fa5..e44b570 100644 --- a/src/org/chromium/content/app/SandboxedProcessService3.java +++ b/src/org/chromium/content/app/SandboxedProcessService3.java @@ -1,4 +1,4 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/src/org/chromium/content/app/SandboxedProcessService4.java b/src/org/chromium/content/app/SandboxedProcessService4.java index bec8dea..8608b06 100644 --- a/src/org/chromium/content/app/SandboxedProcessService4.java +++ b/src/org/chromium/content/app/SandboxedProcessService4.java @@ -1,4 +1,4 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/src/org/chromium/content/app/SandboxedProcessService5.java b/src/org/chromium/content/app/SandboxedProcessService5.java index 9a852e1..16621b8 100644 --- a/src/org/chromium/content/app/SandboxedProcessService5.java +++ b/src/org/chromium/content/app/SandboxedProcessService5.java @@ -1,4 +1,4 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/src/org/chromium/content/app/SandboxedProcessService6.java b/src/org/chromium/content/app/SandboxedProcessService6.java new file mode 100644 index 0000000..96a5496 --- /dev/null +++ b/src/org/chromium/content/app/SandboxedProcessService6.java @@ -0,0 +1,12 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.app; + +// This is needed to register multiple SandboxedProcess services so that we can have +// more than one sandboxed process. + +public class SandboxedProcessService6 extends SandboxedProcessService { + +} diff --git a/src/org/chromium/content/app/SandboxedProcessService7.java b/src/org/chromium/content/app/SandboxedProcessService7.java new file mode 100644 index 0000000..16b28d5 --- /dev/null +++ b/src/org/chromium/content/app/SandboxedProcessService7.java @@ -0,0 +1,12 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.app; + +// This is needed to register multiple SandboxedProcess services so that we can have +// more than one sandboxed process. + +public class SandboxedProcessService7 extends SandboxedProcessService { + +} diff --git a/src/org/chromium/content/app/SandboxedProcessService8.java b/src/org/chromium/content/app/SandboxedProcessService8.java new file mode 100644 index 0000000..17c1e40 --- /dev/null +++ b/src/org/chromium/content/app/SandboxedProcessService8.java @@ -0,0 +1,12 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.app; + +// This is needed to register multiple SandboxedProcess services so that we can have +// more than one sandboxed process. + +public class SandboxedProcessService8 extends SandboxedProcessService { + +} diff --git a/src/org/chromium/content/app/SandboxedProcessService9.java b/src/org/chromium/content/app/SandboxedProcessService9.java new file mode 100644 index 0000000..91a6f53 --- /dev/null +++ b/src/org/chromium/content/app/SandboxedProcessService9.java @@ -0,0 +1,12 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.app; + +// This is needed to register multiple SandboxedProcess services so that we can have +// more than one sandboxed process. + +public class SandboxedProcessService9 extends SandboxedProcessService { + +} diff --git a/src/org/chromium/content/browser/ActivityContentVideoViewDelegate.java b/src/org/chromium/content/browser/ActivityContentVideoViewClient.java similarity index 66% rename from src/org/chromium/content/browser/ActivityContentVideoViewDelegate.java rename to src/org/chromium/content/browser/ActivityContentVideoViewClient.java index 0af3393..3b5ba7f 100644 --- a/src/org/chromium/content/browser/ActivityContentVideoViewDelegate.java +++ b/src/org/chromium/content/browser/ActivityContentVideoViewClient.java @@ -1,26 +1,26 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.content.browser; import android.app.Activity; -import android.content.Context; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; -import org.chromium.content.browser.ContentVideoViewContextDelegate; +import org.chromium.content.browser.ContentVideoViewClient; /** - * Uses an exisiting Activity to handle displaying video in full screen. + * Uses an existing Activity to handle displaying video in full screen. */ -public class ActivityContentVideoViewDelegate implements ContentVideoViewContextDelegate { +public class ActivityContentVideoViewClient implements ContentVideoViewClient { private Activity mActivity; + private View mView; - public ActivityContentVideoViewDelegate(Activity activity) { + public ActivityContentVideoViewClient(Activity activity) { this.mActivity = activity; } @@ -35,20 +35,24 @@ public void onShowCustomView(View view) { ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER)); + mView = view; } @Override public void onDestroyContentVideoView() { mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + FrameLayout decor = (FrameLayout) mActivity.getWindow().getDecorView(); + decor.removeView(mView); + mView = null; } @Override - public Context getContext() { - return mActivity; + public View getVideoLoadingProgressView() { + return null; } @Override - public View getVideoLoadingProgressView() { + public ContentVideoViewControls createControls() { return null; } } diff --git a/src/org/chromium/content/browser/AndroidBrowserProcess.java b/src/org/chromium/content/browser/AndroidBrowserProcess.java index d4dd6f0..bf046f9 100644 --- a/src/org/chromium/content/browser/AndroidBrowserProcess.java +++ b/src/org/chromium/content/browser/AndroidBrowserProcess.java @@ -1,4 +1,4 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -13,46 +13,25 @@ import org.chromium.content.app.LibraryLoader; import org.chromium.content.common.ProcessInitException; -// NOTE: This file hasn't been fully upstreamed, please don't merge to downstream. @JNINamespace("content") public class AndroidBrowserProcess { - private static final String TAG = "BrowserProcessMain"; // Prevents initializing the process more than once. private static boolean sInitialized = false; - // Computes the actual max renderer processes used. - private static int normalizeMaxRendererProcesses(Context context, int maxRendererProcesses) { - if (maxRendererProcesses == MAX_RENDERERS_AUTOMATIC) { - // We use the device's memory class to decide the maximum renderer - // processes. For the baseline devices the memory class is 16 and we will - // limit it to one render process. For the devices with memory class 24, - // we allow two render processes. - ActivityManager am = (ActivityManager)context.getSystemService( - Context.ACTIVITY_SERVICE); - maxRendererProcesses = Math.max(((am.getMemoryClass() - 8) / 8), 1); - } - if (maxRendererProcesses > MAX_RENDERERS_LIMIT) { - Log.w(TAG, "Excessive maxRendererProcesses value: " + maxRendererProcesses); - return MAX_RENDERERS_LIMIT; - } - return Math.max(0, maxRendererProcesses); - } - - // Automatically decide the number of renderer processes to use based on device memory class. - public static final int MAX_RENDERERS_AUTOMATIC = -1; // Use single-process mode that runs the renderer on a separate thread in the main application. public static final int MAX_RENDERERS_SINGLE_PROCESS = 0; // Cap on the maximum number of renderer processes that can be requested. // This is currently set to account for: - // 6: The maximum number of sandboxed processes we have available + // 13: The maximum number of sandboxed processes we have available // - 1: The regular New Tab Page // - 1: The incognito New Tab Page // - 1: A regular incognito tab + // - 1: Safety buffer (http://crbug.com/251279) public static final int MAX_RENDERERS_LIMIT = - ChildProcessLauncher.MAX_REGISTERED_SANDBOXED_SERVICES - 3; + ChildProcessLauncher.MAX_REGISTERED_SANDBOXED_SERVICES - 4; /** * Initialize the process as a ContentView host. This must be called from the main UI thread. @@ -64,39 +43,36 @@ private static int normalizeMaxRendererProcesses(Context context, int maxRendere * @param maxRendererProcesses Limit on the number of renderers to use. Each tab runs in its own * process until the maximum number of processes is reached. The special value of * MAX_RENDERERS_SINGLE_PROCESS requests single-process mode where the renderer will run in the - * application process in a separate thread. If the special value MAX_RENDERERS_AUTOMATIC is - * used then the number of renderers will be determined based on the device memory class. The - * maximum number of allowed renderers is capped by MAX_RENDERERS_LIMIT. - + * application process in a separate thread. The maximum number of allowed renderers is capped + * by MAX_RENDERERS_LIMIT. + * * @return Whether the process actually needed to be initialized (false if already running). */ public static boolean init(Context context, int maxRendererProcesses) throws ProcessInitException { + assert maxRendererProcesses >= 0; + assert maxRendererProcesses <= MAX_RENDERERS_LIMIT; if (sInitialized) return false; sInitialized = true; + Log.i(TAG, "Initializing chromium process, renderers=" + maxRendererProcesses); - // Normally Main.java will have kicked this off asynchronously for Chrome. But - // other ContentView apps like tests also need them so we make sure we've - // extracted resources here. We can still make it a little async (wait until - // the library is loaded). + // Normally Main.java will have kicked this off asynchronously for Chrome. But other + // ContentView apps like tests also need them so we make sure we've extracted resources + // here. We can still make it a little async (wait until the library is loaded). ResourceExtractor resourceExtractor = ResourceExtractor.get(context); resourceExtractor.startExtractingResources(); - // Normally Main.java will have already loaded the library asynchronously, we only - // need to load it here if we arrived via another flow, e.g. bookmark access & sync setup. + // Normally Main.java will have already loaded the library asynchronously, we only need to + // load it here if we arrived via another flow, e.g. bookmark access & sync setup. LibraryLoader.ensureInitialized(); // TODO(yfriedman): Remove dependency on a command line flag for this. DeviceUtils.addDeviceSpecificUserAgentSwitch(context); Context appContext = context.getApplicationContext(); - - int maxRenderers = normalizeMaxRendererProcesses(appContext, maxRendererProcesses); - Log.i(TAG, "Initializing chromium process, renderers=" + maxRenderers); - // Now we really need to have the resources ready. resourceExtractor.waitForCompletion(); - nativeSetCommandLineFlags(maxRenderers, + nativeSetCommandLineFlags(maxRendererProcesses, nativeIsPluginEnabled() ? getPlugins(context) : null); ContentMain.initApplicationContext(appContext); int result = ContentMain.start(); @@ -112,8 +88,8 @@ public static void initChromiumBrowserProcessForTests(Context context) { resourceExtractor.startExtractingResources(); resourceExtractor.waitForCompletion(); - // Having a single renderer should be sufficient for tests. - // We can't have more than MAX_RENDERERS_LIMIT. + // Having a single renderer should be sufficient for tests. We can't have more than + // MAX_RENDERERS_LIMIT. nativeSetCommandLineFlags(1 /* maxRenderers */, null); } @@ -124,9 +100,8 @@ private static String getPlugins(final Context context) { private static native void nativeSetCommandLineFlags(int maxRenderProcesses, String pluginDescriptor); - // Is this an official build of Chrome? Only native code knows - // for sure. Official build knowledge is needed very early in - // process startup. + // Is this an official build of Chrome? Only native code knows for sure. Official build + // knowledge is needed very early in process startup. private static native boolean nativeIsOfficialBuild(); private static native boolean nativeIsPluginEnabled(); diff --git a/src/org/chromium/content/browser/BrowserStartupConfig.java b/src/org/chromium/content/browser/BrowserStartupConfig.java new file mode 100644 index 0000000..f784c7a --- /dev/null +++ b/src/org/chromium/content/browser/BrowserStartupConfig.java @@ -0,0 +1,43 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +/** + * This class controls how C++ browser main loop is run. + */ +@JNINamespace("content") +public class BrowserStartupConfig { + public interface StartupCallback { + void run(int startupResult); + } + + private static boolean sBrowserMayStartAsynchronously = false; + private static StartupCallback sBrowserStartupCompleteCallback = null; + + @CalledByNative + private static boolean browserMayStartAsynchonously() { + return sBrowserMayStartAsynchronously; + } + + @CalledByNative + private static void browserStartupComplete(int result) { + if(sBrowserStartupCompleteCallback != null) { + sBrowserStartupCompleteCallback.run(result); + } + } + + /** + * Set browser to start asynchronously. May only be called before contentMain.start(). If it + * has been called then contentMain.start() will queue up a series of UI tasks to complete + * browser initialization. + * @param browserStartupCompleteCallback If not null called when browser startup is complete. + */ + public static void setAsync(StartupCallback browserStartupCompleteCallback) { + sBrowserMayStartAsynchronously = true; + sBrowserStartupCompleteCallback = browserStartupCompleteCallback; + } +} diff --git a/src/org/chromium/content/browser/ChildProcessConnection.java b/src/org/chromium/content/browser/ChildProcessConnection.java index cfd5f54..682ce08 100644 --- a/src/org/chromium/content/browser/ChildProcessConnection.java +++ b/src/org/chromium/content/browser/ChildProcessConnection.java @@ -1,4 +1,4 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -28,11 +28,48 @@ import org.chromium.content.common.IChildProcessService; import org.chromium.content.common.TraceEvent; -public class ChildProcessConnection implements ServiceConnection { +/** + * Manages a connection between the browser activity and a child service. The class is responsible + * for estabilishing the connection (start()), closing it (stop()) and increasing the priority of + * the service when it is in active use (between calls to attachAsActive() and detachAsActive()). + */ +public class ChildProcessConnection { + /** + * Used to notify the consumer about disconnection of the service. This callback is provided + * earlier than ConnectionCallbacks below, as a child process might die before the connection is + * fully set up. + */ interface DeathCallback { void onChildProcessDied(int pid); } + /** + * Used to notify the consumer about the connection being established and about out-of-memory + * bindings being bound for the connection. "Out-of-memory" bindings are bindings that raise the + * priority of the service process so that it does not get killed by the OS out-of-memory killer + * during normal operation (yet it still may get killed under drastic memory pressure). + */ + interface ConnectionCallbacks { + /** + * Called when the connection to the service is established. It will be called before any + * calls to onOomBindingsAdded(), onOomBindingRemoved(). + * @param pid Pid of the child process. + * @param oomBindingCount Number of the out-of-memory bindings bound before the connection + * was established. + */ + void onConnected(int pid, int oomBindingCount); + + /** + * Called when a new out-of-memory binding is bound. + */ + void onOomBindingAdded(int pid); + + /** + * Called when an out-of-memory binding is unbound. + */ + void onOomBindingRemoved(int pid); + } + // Names of items placed in the bind intent or connection bundle. public static final String EXTRA_COMMAND_LINE = "com.google.android.apps.chrome.extra.command_line"; @@ -56,15 +93,30 @@ interface DeathCallback { private final Class mServiceClass; // Synchronization: While most internal flow occurs on the UI thread, the public API - // (specifically bind and unbind) may be called from any thread, hence all entry point methods - // into the class are synchronized on the ChildProcessConnection instance to protect access - // to these members. But see also the TODO where AsyncBoundServiceConnection is created. + // (specifically start and stop) may be called from any thread, hence all entry point methods + // into the class are synchronized on the ChildProcessConnection instance to protect access to + // these members. But see also the TODO where AsyncBoundServiceConnection is created. private final Object mUiThreadLock = new Object(); private IChildProcessService mService = null; + // Set to true when the service connect is finished, even if it fails. private boolean mServiceConnectComplete = false; + // Set to true when the service disconnects, as opposed to being properly closed. This happens + // when the process crashes or gets killed by the system out-of-memory killer. + private boolean mServiceDisconnected = false; private int mPID = 0; // Process ID of the corresponding child process. - private HighPriorityConnection mHighPriorityConnection = null; - private int mHighPriorityConnectionCount = 0; + // Initial binding protects the newly spawned process from being killed before it is put to use, + // it is maintained between calls to start() and removeInitialBinding(). + private ChildServiceConnection mInitialBinding = null; + // Strong binding will make the service priority equal to the priority of the activity. We want + // the OS to be able to kill background renderers as it kills other background apps, so strong + // bindings are maintained only for services that are active at the moment (between + // attachAsActive() and detachAsActive()). + private ChildServiceConnection mStrongBinding = null; + // Low priority binding maintained in the entire lifetime of the connection, i.e. between calls + // to start() and stop(). + private ChildServiceConnection mWaivedBinding = null; + // Incremented on attachAsActive(), decremented on detachAsActive(). + private int mAttachAsActiveCount = 0; private static final String TAG = "ChildProcessConnection"; @@ -72,23 +124,109 @@ private static class ConnectionParams { final String[] mCommandLine; final FileDescriptorInfo[] mFilesToBeMapped; final IChildProcessCallback mCallback; - final Runnable mOnConnectionCallback; - ConnectionParams( - String[] commandLine, - FileDescriptorInfo[] filesToBeMapped, - IChildProcessCallback callback, - Runnable onConnectionCallback) { + ConnectionParams(String[] commandLine, FileDescriptorInfo[] filesToBeMapped, + IChildProcessCallback callback) { mCommandLine = commandLine; mFilesToBeMapped = filesToBeMapped; mCallback = callback; - mOnConnectionCallback = onConnectionCallback; } } - // This is only valid while the connection is being established. + // This is set by the consumer of the class in setupConnection() and is later used in + // doSetupConnection(), after which the variable is cleared. Therefore this is only valid while + // the connection is being set up. private ConnectionParams mConnectionParams; - private boolean mIsBound; + + // Callbacks used to notify the consumer about connection events. This is also provided in + // setupConnection(), but remains valid after setup. + private ChildProcessConnection.ConnectionCallbacks mConnectionCallbacks; + + private class ChildServiceConnection implements ServiceConnection { + private boolean mBound = false; + + private final int mBindFlags; + private final boolean mProtectsFromOom; + + public ChildServiceConnection(int bindFlags, boolean protectsFromOom) { + mBindFlags = bindFlags; + mProtectsFromOom = protectsFromOom; + } + + boolean bind(String[] commandLine) { + if (!mBound) { + final Intent intent = createServiceBindIntent(); + if (commandLine != null) { + intent.putExtra(EXTRA_COMMAND_LINE, commandLine); + } + mBound = mContext.bindService(intent, this, mBindFlags); + if (mBound && mProtectsFromOom && mConnectionCallbacks != null) { + mConnectionCallbacks.onOomBindingAdded(getPid()); + } + } + return mBound; + } + + void unbind() { + if (mBound) { + mContext.unbindService(this); + mBound = false; + // When the process crashes, we stop reporting bindings being unbound (so that their + // numbers can be inspected to determine if the process crash could be caused by the + // out-of-memory killing), hence the mServiceDisconnected check below. + if (mProtectsFromOom && mConnectionCallbacks != null && !mServiceDisconnected) { + mConnectionCallbacks.onOomBindingRemoved(getPid()); + } + } + } + + boolean isBound() { + return mBound; + } + + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + synchronized(mUiThreadLock) { + // A flag from the parent class ensures we run the post-connection logic only once + // (instead of once per each ChildServiceConnection). + if (mServiceConnectComplete) { + return; + } + TraceEvent.begin(); + mServiceConnectComplete = true; + mService = IChildProcessService.Stub.asInterface(service); + // Make sure that the connection parameters have already been provided. If not, + // doConnectionSetup() will be called from setupConnection(). + if (mConnectionParams != null) { + doConnectionSetup(); + } + TraceEvent.end(); + } + } + + + // Called on the main thread to notify that the child service did not disconnect gracefully. + @Override + public void onServiceDisconnected(ComponentName className) { + // Ensure that the disconnection logic runs only once (instead of once per each + // ChildServiceConnection). + if (mServiceDisconnected) { + return; + } + mServiceDisconnected = true; + int pid = mPID; // Stash the pid for DeathCallback since stop() will clear it. + boolean disconnectedWhileBeingSetUp = mConnectionParams != null; + Log.w(TAG, "onServiceDisconnected (crash or killed by oom): pid=" + pid); + stop(); // We don't want to auto-restart on crash. Let the browser do that. + if (pid != 0) { + mDeathCallback.onChildProcessDied(pid); + } + // TODO(ppi): does anyone know why we need to do that? + if (disconnectedWhileBeingSetUp && mConnectionCallbacks != null) { + mConnectionCallbacks.onConnected(0, 0); + } + } + } ChildProcessConnection(Context context, int number, boolean inSandbox, ChildProcessConnection.DeathCallback deathCallback, @@ -98,6 +236,11 @@ private static class ConnectionParams { mInSandbox = inSandbox; mDeathCallback = deathCallback; mServiceClass = serviceClass; + mInitialBinding = new ChildServiceConnection(Context.BIND_AUTO_CREATE, true); + mStrongBinding = new ChildServiceConnection( + Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT, true); + mWaivedBinding = new ChildServiceConnection( + Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY, false); } int getServiceNumber() { @@ -122,50 +265,48 @@ private Intent createServiceBindIntent() { } /** - * Bind to an IChildProcessService. This must be followed by a call to setupConnection() - * to setup the connection parameters. (These methods are separated to allow the client - * to pass whatever parameters they have available here, and complete the remainder - * later while reducing the connection setup latency). + * Starts a connection to an IChildProcessService. This must be followed by a call to + * setupConnection() to setup the connection parameters. start() and setupConnection() are + * separate to allow the client to pass whatever parameters they have available here, and + * complete the remainder later while reducing the connection setup latency. * @param commandLine (Optional) Command line for the child process. If omitted, then * the command line parameters must instead be passed to setupConnection(). */ - void bind(String[] commandLine) { + void start(String[] commandLine) { synchronized(mUiThreadLock) { TraceEvent.begin(); assert !ThreadUtils.runningOnUiThread(); - final Intent intent = createServiceBindIntent(); - - if (commandLine != null) { - intent.putExtra(EXTRA_COMMAND_LINE, commandLine); - } - - mIsBound = mContext.bindService(intent, this, Context.BIND_AUTO_CREATE); - if (!mIsBound) { + if (!mInitialBinding.bind(commandLine)) { onBindFailed(); + } else { + mWaivedBinding.bind(null); } TraceEvent.end(); } } - /** Setup a connection previous bound via a call to bind(). - * - * This establishes the parameters that were not already supplied in bind. + /** + * Setups the connection after it was started with start(). This method should be called by the + * consumer of the class to set up additional connection parameters. * @param commandLine (Optional) will be ignored if the command line was already sent in bind() * @param fileToBeMapped a list of file descriptors that should be registered * @param callback Used for status updates regarding this process connection. - * @param onConnectionCallback will be run when the connection is setup and ready to use. + * @param connectionCallbacks will notify the consumer about the connection being established + * and the status of the out-of-memory bindings being bound for the connection. */ void setupConnection( String[] commandLine, FileDescriptorInfo[] filesToBeMapped, - IChildProcessCallback callback, - Runnable onConnectionCallback) { + IChildProcessCallback processCallback, + ConnectionCallbacks connectionCallbacks) { synchronized(mUiThreadLock) { TraceEvent.begin(); assert mConnectionParams == null; - mConnectionParams = new ConnectionParams(commandLine, filesToBeMapped, callback, - onConnectionCallback); + mConnectionCallbacks = connectionCallbacks; + mConnectionParams = new ConnectionParams(commandLine, filesToBeMapped, processCallback); + // Make sure that the service is already connected. If not, doConnectionSetup() will be + // called from onServiceConnected(). if (mServiceConnectComplete) { doConnectionSetup(); } @@ -174,18 +315,16 @@ void setupConnection( } /** - * Unbind the IChildProcessService. It is safe to call this multiple times. + * Terminates the connection to IChildProcessService, closing all bindings. It is safe to call + * this multiple times. */ - void unbind() { + void stop() { synchronized(mUiThreadLock) { - if (mIsBound) { - mContext.unbindService(this); - mIsBound = false; - } + mInitialBinding.unbind(); + mStrongBinding.unbind(); + mWaivedBinding.unbind(); + mAttachAsActiveCount = 0; if (mService != null) { - if (mHighPriorityConnection != null) { - unbindHighPriority(true); - } mService = null; mPID = 0; } @@ -194,20 +333,6 @@ void unbind() { } } - // Called on the main thread to notify that the service is connected. - @Override - public void onServiceConnected(ComponentName className, IBinder service) { - synchronized(mUiThreadLock) { - TraceEvent.begin(); - mServiceConnectComplete = true; - mService = IChildProcessService.Stub.asInterface(service); - if (mConnectionParams != null) { - doConnectionSetup(); - } - TraceEvent.end(); - } - } - // Called on the main thread to notify that the bindService() call failed (returned false). private void onBindFailed() { mServiceConnectComplete = true; @@ -217,17 +342,16 @@ private void onBindFailed() { } /** - * Called when the connection parameters have been set, and a connection has been established - * (as signaled by onServiceConnected), or if the connection failed (mService will be false). + * Called after the connection parameters have been set (in setupConnection()) *and* a + * connection has been established (as signaled by onServiceConnected()) or failed (as signaled + * by onBindFailed(), in this case mService will be null). These two events can happen in any + * order. */ private void doConnectionSetup() { TraceEvent.begin(); assert mServiceConnectComplete && mConnectionParams != null; - // Capture the callback before it is potentially nulled in unbind(). - Runnable onConnectionCallback = mConnectionParams.mOnConnectionCallback; - if (onConnectionCallback == null) { - unbind(); - } else if (mService != null) { + + if (mService != null) { Bundle bundle = new Bundle(); bundle.putStringArray(EXTRA_COMMAND_LINE, mConnectionParams.mCommandLine); @@ -268,7 +392,7 @@ private void doConnectionSetup() { } catch (android.os.RemoteException re) { Log.e(TAG, "Failed to setup connection.", re); } - // We proactivley close the FDs rather than wait for GC & finalizer. + // We proactively close the FDs rather than wait for GC & finalizer. try { for (ParcelFileDescriptor parcelFile : parcelFiles) { if (parcelFile != null) parcelFile.close(); @@ -278,94 +402,89 @@ private void doConnectionSetup() { } } mConnectionParams = null; - if (onConnectionCallback != null) { - onConnectionCallback.run(); + + if (mConnectionCallbacks != null) { + // Number of out-of-memory bindings bound before the connection was set up. + int oomBindingCount = + (mInitialBinding.isBound() ? 1 : 0) + (mStrongBinding.isBound() ? 1 : 0); + mConnectionCallbacks.onConnected(getPid(), oomBindingCount); } TraceEvent.end(); } - // Called on the main thread to notify that the child service did not disconnect gracefully. - @Override - public void onServiceDisconnected(ComponentName className) { - int pid = mPID; // Stash pid & connection callback since unbind() will clear them. - Runnable onConnectionCallback = - mConnectionParams != null ? mConnectionParams.mOnConnectionCallback : null; - Log.w(TAG, "onServiceDisconnected (crash?): pid=" + pid); - unbind(); // We don't want to auto-restart on crash. Let the browser do that. - if (pid != 0) { - mDeathCallback.onChildProcessDied(pid); - } - if (onConnectionCallback != null) { - onConnectionCallback.run(); - } - } + private static final long REMOVE_INITIAL_BINDING_DELAY_MILLIS = 1 * 1000; // One second. /** - * Bind the service with a new high priority connection. This will make the service - * as important as the main process. + * Called to remove the strong binding estabilished when the connection was started. It is safe + * to call this multiple times. The binding is removed after a fixed delay period so that the + * renderer will not be killed immediately after the call. */ - void bindHighPriority() { + void removeInitialBinding() { synchronized(mUiThreadLock) { - if (mService == null) { - Log.w(TAG, "The connection is not bound for " + mPID); + if (!mInitialBinding.isBound()) { + // While it is safe to post and execute the unbinding multiple times, we prefer to + // avoid spamming the message queue. return; } - if (mHighPriorityConnection == null) { - mHighPriorityConnection = new HighPriorityConnection(); - mHighPriorityConnection.bind(); - } - mHighPriorityConnectionCount++; } + ThreadUtils.postOnUiThreadDelayed(new Runnable() { + @Override + public void run() { + synchronized(mUiThreadLock) { + mInitialBinding.unbind(); + } + } + }, REMOVE_INITIAL_BINDING_DELAY_MILLIS); } /** - * Unbind the service as the high priority connection. + * Called when the service becomes active, ie important to the caller. This is handled by + * setting up a binding that will make the service as important as the main process. We allow + * callers to indicate the same connection as active multiple times. Instead of maintaining + * multiple bindings, we count the requests and unbind when the count drops to zero. */ - void unbindHighPriority(boolean force) { + void attachAsActive() { synchronized(mUiThreadLock) { if (mService == null) { Log.w(TAG, "The connection is not bound for " + mPID); return; } - mHighPriorityConnectionCount--; - if (force || (mHighPriorityConnectionCount == 0 && mHighPriorityConnection != null)) { - mHighPriorityConnection.unbind(); - mHighPriorityConnection = null; + if (mAttachAsActiveCount == 0) { + mStrongBinding.bind(null); } + mAttachAsActiveCount++; } } - private class HighPriorityConnection implements ServiceConnection { - - private boolean mHBound = false; - - void bind() { - final Intent intent = createServiceBindIntent(); + private static final long DETACH_AS_ACTIVE_DELAY_MILLIS = 5 * 1000; // Five seconds. - mHBound = mContext.bindService(intent, this, - Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT); - } - - void unbind() { - if (mHBound) { - mContext.unbindService(this); - mHBound = false; + /** + * Called when the service is no longer considered active. Actual binding is removed after a + * fixed delay period so that the renderer will not be killed immediately after the call. + */ + void detachAsActive() { + ThreadUtils.postOnUiThreadDelayed(new Runnable() { + @Override + public void run() { + synchronized(mUiThreadLock) { + if (mService == null) { + Log.w(TAG, "The connection is not bound for " + mPID); + return; + } + assert mAttachAsActiveCount > 0; + mAttachAsActiveCount--; + if (mAttachAsActiveCount == 0) { + mStrongBinding.unbind(); + } + } } - } - - @Override - public void onServiceConnected(ComponentName className, IBinder service) { - } - - @Override - public void onServiceDisconnected(ComponentName className) { - } + }, DETACH_AS_ACTIVE_DELAY_MILLIS); } /** * @return The connection PID, or 0 if not yet connected. */ - public int getPid() { + int getPid() { synchronized(mUiThreadLock) { return mPID; } diff --git a/src/org/chromium/content/browser/ChildProcessLauncher.java b/src/org/chromium/content/browser/ChildProcessLauncher.java index 92669a6..de71a63 100644 --- a/src/org/chromium/content/browser/ChildProcessLauncher.java +++ b/src/org/chromium/content/browser/ChildProcessLauncher.java @@ -1,4 +1,4 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -6,6 +6,7 @@ import android.content.Context; import android.util.Log; +import android.util.SparseIntArray; import android.view.Surface; import java.util.ArrayList; @@ -22,8 +23,7 @@ import org.chromium.content.common.IChildProcessService; /** - * This class provides the method to start/stop ChildProcess called by - * native. + * This class provides the method to start/stop ChildProcess called by native. */ @JNINamespace("content") public class ChildProcessLauncher { @@ -40,23 +40,24 @@ public class ChildProcessLauncher { // The upper limit on the number of simultaneous sandboxed and privileged child service process // instances supported. Each limit must not exceed total number of SandboxedProcessServiceX - // classes and PrivilegedProcessClassX declared in this package, and defined as services in the - // embedding application's manifest file. + // classes and PrivilegedProcessServiceX classes declared in this package and defined as + // services in the embedding application's manifest file. // (See {@link ChildProcessService} for more details on defining the services.) - /* package */ static final int MAX_REGISTERED_SANDBOXED_SERVICES = 6; + /* package */ static final int MAX_REGISTERED_SANDBOXED_SERVICES = 13; /* package */ static final int MAX_REGISTERED_PRIVILEGED_SERVICES = 3; private static class ChildConnectionAllocator { + // Connections to services. Indices of the array correspond to the service numbers. private ChildProcessConnection[] mChildProcessConnections; - // The list of free slots in corresponing Connections. When looking for a free connection, - // the first index in that list should be used. When a connection is freed, its index - // is added to the end of the list. This is so that we avoid immediately reusing a freed - // connection (see bug crbug.com/164069): the framework might keep a service process alive - // when it's been unbound for a short time. If a connection to that same service is bound - // at that point, the process is reused and bad things happen (mostly static variables are - // set when we don't expect them to). - // SHOULD BE ACCESSED WITH THE mConnectionLock. + // The list of free (not bound) service indices. When looking for a free service, the first + // index in that list should be used. When a service is unbound, its index is added to the + // end of the list. This is so that we avoid immediately reusing the freed service (see + // http://crbug.com/164069): the framework might keep a service process alive when it's been + // unbound for a short time. If a new connection to the same service is bound at that point, + // the process is reused and bad things happen (mostly static variables are set when we + // don't expect them to). + // SHOULD BE ACCESSED WITH mConnectionLock. private ArrayList mFreeConnectionIndices; private final Object mConnectionLock = new Object(); @@ -113,8 +114,8 @@ public void free(ChildProcessConnection connection) { } } - // Service class for child process. As the default value it uses - // SandboxedProcessService0 and PrivilegedProcessService0 + // Service class for child process. As the default value it uses SandboxedProcessService0 and + // PrivilegedProcessService0. private static final ChildConnectionAllocator mSandboxedChildConnectionAllocator = new ChildConnectionAllocator(true); private static final ChildConnectionAllocator mPrivilegedChildConnectionAllocator = @@ -122,7 +123,7 @@ public void free(ChildProcessConnection connection) { private static boolean mConnectionAllocated = false; - // Sets service class for sandboxed service and privileged service + // Sets service class for sandboxed service and privileged service. public static void setChildProcessClass( Class sandboxedServiceClass, Class privilegedServiceClass) { @@ -154,7 +155,7 @@ private static ChildProcessConnection allocateBoundConnection(Context context, String[] commandLine, boolean inSandbox) { ChildProcessConnection connection = allocateConnection(context, inSandbox); if (connection != null) { - connection.bind(commandLine); + connection.start(commandLine); } return connection; } @@ -167,13 +168,18 @@ private static void freeConnection(ChildProcessConnection connection) { return; } - // Represents an invalid process handle; same as base/process.h kNullProcessHandle. + // Represents an invalid process handle; same as base/process/process.h kNullProcessHandle. private static final int NULL_PROCESS_HANDLE = 0; // Map from pid to ChildService connection. private static Map mServiceMap = new ConcurrentHashMap(); + // Map from pid to the count of oom bindings. "Oom binding" is a binding that raises the process + // oom priority so that it shouldn't be killed by the OS out-of-memory killer under normal + // conditions (it can still be killed under drastic memory pressure). + private static SparseIntArray sOomBindingCount = new SparseIntArray(); + // A pre-allocated and pre-bound connection ready for connection setup, or null. static ChildProcessConnection mSpareSandboxedConnection = null; @@ -194,9 +200,9 @@ public static IChildProcessService getChildService(int pid) { } /** - * Should be called early in startup so the work needed to spawn the child process can - * be done in parallel to other startup work. Must not be called on the UI thread. - * Spare connection is created in sandboxed child process. + * Should be called early in startup so the work needed to spawn the child process can be done + * in parallel to other startup work. Must not be called on the UI thread. Spare connection is + * created in sandboxed child process. * @param context the application context used for the connection. */ public static void warmUp(Context context) { @@ -223,10 +229,10 @@ private static String getSwitchValue(final String[] commandLine, String switchKe } /** - * Spawns and connects to a child process. May be called on any thread. It will not - * block, but will instead callback to {@link #nativeOnChildProcessStarted} when the - * connection is established. Note this callback will not necessarily be from the same thread - * (currently it always comes from the main thread). + * Spawns and connects to a child process. May be called on any thread. It will not block, but + * will instead callback to {@link #nativeOnChildProcessStarted} when the connection is + * established. Note this callback will not necessarily be from the same thread (currently it + * always comes from the main thread). * * @param context Context used to obtain the application context. * @param commandLine The child process command line argv. @@ -280,25 +286,45 @@ static void start( } final ChildProcessConnection connection = allocatedConnection; Log.d(TAG, "Setting up connection to process: slot=" + connection.getServiceNumber()); - // Note: This runnable will be executed when the child connection is setup. - final Runnable onConnect = new Runnable() { - @Override - public void run() { - final int pid = connection.getPid(); + + ChildProcessConnection.ConnectionCallbacks connectionCallbacks = + new ChildProcessConnection.ConnectionCallbacks() { + public void onConnected(int pid, int oomBindingCount) { Log.d(TAG, "on connect callback, pid=" + pid + " context=" + clientContext); if (pid != NULL_PROCESS_HANDLE) { + sOomBindingCount.put(pid, oomBindingCount); mServiceMap.put(pid, connection); } else { freeConnection(connection); } nativeOnChildProcessStarted(clientContext, pid); } + + public void onOomBindingAdded(int pid) { + if (pid != NULL_PROCESS_HANDLE) { + sOomBindingCount.put(pid, sOomBindingCount.get(pid) + 1); + } + } + + public void onOomBindingRemoved(int pid) { + if (pid != NULL_PROCESS_HANDLE) { + int count = sOomBindingCount.get(pid, -1); + assert count > 0; + count--; + if (count > 0) { + sOomBindingCount.put(pid, count); + } else { + sOomBindingCount.delete(pid); + } + } + } }; + // TODO(sievers): Revisit this as it doesn't correctly handle the utility process // assert callbackType != CALLBACK_FOR_UNKNOWN_PROCESS; - connection.setupConnection( - commandLine, filesToBeMapped, createCallback(callbackType), onConnect); + connection.setupConnection(commandLine, filesToBeMapped, createCallback(callbackType), + connectionCallbacks); } /** @@ -309,30 +335,43 @@ public void run() { @CalledByNative static void stop(int pid) { Log.d(TAG, "stopping child connection: pid=" + pid); - ChildProcessConnection connection = mServiceMap.remove(pid); if (connection == null) { - Log.w(TAG, "Tried to stop non-existent connection to pid: " + pid); + LogPidWarning(pid, "Tried to stop non-existent connection"); return; } - connection.unbind(); + connection.stop(); freeConnection(connection); } /** - * Bind a child process as a high priority process so that it has the same - * priority as the main process. This can be used for the foreground renderer - * process to distinguish it from the the background renderer process. + * Remove the initial child process binding. Child processes are bound with initial binding to + * protect them from getting killed before they are put to use. This method allows to remove the + * binding once it is no longer needed. + */ + static void removeInitialBinding(int pid) { + ChildProcessConnection connection = mServiceMap.get(pid); + if (connection == null) { + LogPidWarning(pid, "Tried to remove a binding for a non-existent connection"); + return; + } + connection.removeInitialBinding(); + } + + /** + * Bind a child process as a high priority process so that it has the same priority as the main + * process. This can be used for the foreground renderer process to distinguish it from the the + * background renderer process. * * @param pid The process handle of the service connection obtained from {@link #start}. */ static void bindAsHighPriority(int pid) { ChildProcessConnection connection = mServiceMap.get(pid); if (connection == null) { - Log.w(TAG, "Tried to bind a non-existent connection to pid: " + pid); + LogPidWarning(pid, "Tried to bind a non-existent connection"); return; } - connection.bindHighPriority(); + connection.attachAsActive(); } /** @@ -343,10 +382,18 @@ static void bindAsHighPriority(int pid) { static void unbindAsHighPriority(int pid) { ChildProcessConnection connection = mServiceMap.get(pid); if (connection == null) { - Log.w(TAG, "Tried to unbind non-existent connection to pid: " + pid); + LogPidWarning(pid, "Tried to unbind non-existent connection"); return; } - connection.unbindHighPriority(false); + connection.detachAsActive(); + } + + /** + * @return True iff the given service process is protected from the out-of-memory killing, or it + * was protected from it when it died. + */ + public static boolean isOomProtected(int pid) { + return sOomBindingCount.get(pid) > 0; } /** @@ -355,17 +402,16 @@ static void unbindAsHighPriority(int pid) { private static IChildProcessCallback createCallback(final int callbackType) { return new IChildProcessCallback.Stub() { /** - * This is called by the remote service regularly to tell us about - * new values. Note that IPC calls are dispatched through a thread - * pool running in each process, so the code executing here will - * NOT be running in our main thread -- so, to update the UI, we need - * to use a Handler. + * This is called by the remote service regularly to tell us about new values. Note that + * IPC calls are dispatched through a thread pool running in each process, so the code + * executing here will NOT be running in our main thread -- so, to update the UI, we + * need to use a Handler. */ @Override public void establishSurfacePeer( int pid, Surface surface, int primaryID, int secondaryID) { - // Do not allow a malicious renderer to connect to a producer. This is only - // used from stream textures managed by the GPU process. + // Do not allow a malicious renderer to connect to a producer. This is only used + // from stream textures managed by the GPU process. if (callbackType != CALLBACK_FOR_GPU_PROCESS) { Log.e(TAG, "Illegal callback for non-GPU process."); return; @@ -387,8 +433,16 @@ public Surface getViewSurface(int surfaceId) { }; }; + private static void LogPidWarning(int pid, String message) { + // This class is effectively a no-op in single process mode, so don't log warnings there. + if (pid > 0 && !nativeIsSingleProcess()) { + Log.w(TAG, message + ", pid=" + pid); + } + } + private static native void nativeOnChildProcessStarted(int clientContext, int pid); private static native Surface nativeGetViewSurface(int surfaceId); private static native void nativeEstablishSurfacePeer( int pid, Surface surface, int primaryID, int secondaryID); + private static native boolean nativeIsSingleProcess(); } diff --git a/src/org/chromium/content/browser/ContentVideoView.java b/src/org/chromium/content/browser/ContentVideoView.java index 9c0e369..d3e760c 100644 --- a/src/org/chromium/content/browser/ContentVideoView.java +++ b/src/org/chromium/content/browser/ContentVideoView.java @@ -24,7 +24,6 @@ import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.MediaController; -import android.widget.MediaController.MediaPlayerControl; import android.widget.ProgressBar; import android.widget.TextView; @@ -32,11 +31,14 @@ import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; +import org.chromium.base.ThreadUtils; import org.chromium.content.common.IChildProcessService; import org.chromium.content.R; @JNINamespace("content") -public class ContentVideoView extends FrameLayout implements MediaPlayerControl, +public class ContentVideoView + extends FrameLayout + implements ContentVideoViewControls.Delegate, SurfaceHolder.Callback, View.OnTouchListener, View.OnKeyListener { private static final String TAG = "ContentVideoView"; @@ -53,11 +55,12 @@ public class ContentVideoView extends FrameLayout implements MediaPlayerControl, private static final int MEDIA_ERROR = 100; private static final int MEDIA_INFO = 200; - /** The video is streamed and its container is not valid for progressive - * playback i.e the video's index (e.g moov atom) is not at the start of the - * file. + /** + * Keep these error codes in sync with the code we defined in + * MediaPlayerListener.java. */ public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 2; + public static final int MEDIA_ERROR_INVALID_CODE = 3; // all possible internal states private static final int STATE_ERROR = -1; @@ -66,28 +69,28 @@ public class ContentVideoView extends FrameLayout implements MediaPlayerControl, private static final int STATE_PAUSED = 2; private static final int STATE_PLAYBACK_COMPLETED = 3; - private SurfaceHolder mSurfaceHolder = null; - private int mVideoWidth = 0; - private int mVideoHeight = 0; + private SurfaceHolder mSurfaceHolder; + private int mVideoWidth; + private int mVideoHeight; private int mCurrentBufferPercentage; private int mDuration; - private MediaController mMediaController = null; + private ContentVideoViewControls mControls; private boolean mCanPause; private boolean mCanSeekBack; private boolean mCanSeekForward; // Native pointer to C++ ContentVideoView object. - private int mNativeContentVideoView = 0; + private int mNativeContentVideoView; // webkit should have prepared the media private int mCurrentState = STATE_IDLE; // Strings for displaying media player errors - static String mPlaybackErrorText; - static String mUnknownErrorText; - static String mErrorButton; - static String mErrorTitle; - static String mVideoLoadingText; + private String mPlaybackErrorText; + private String mUnknownErrorText; + private String mErrorButton; + private String mErrorTitle; + private String mVideoLoadingText; // This view will contain the video. private VideoSurfaceView mVideoSurfaceView; @@ -95,16 +98,9 @@ public class ContentVideoView extends FrameLayout implements MediaPlayerControl, // Progress view when the video is loading. private View mProgressView; - private Surface mSurface = null; + private Surface mSurface; - // There are can be at most 1 fullscreen video - // TODO(qinmin): will change this once we move the creation of this class - // to the host application - private static ContentVideoView sContentVideoView = null; - - // The delegate will follow sContentVideoView. We would need to - // move this to an instance variable if we allow multiple ContentVideoViews. - private static ContentVideoViewContextDelegate sDelegate = null; + private ContentVideoViewClient mClient; private class VideoSurfaceView extends SurfaceView { @@ -136,7 +132,7 @@ private static class ProgressView extends LinearLayout { private ProgressBar mProgressBar; private TextView mTextView; - public ProgressView(Context context) { + public ProgressView(Context context, String videoLoadingText) { super(context); setOrientation(LinearLayout.VERTICAL); setLayoutParams(new LinearLayout.LayoutParams( @@ -144,61 +140,86 @@ public ProgressView(Context context) { LinearLayout.LayoutParams.WRAP_CONTENT)); mProgressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge); mTextView = new TextView(context); - mTextView.setText(mVideoLoadingText); + mTextView.setText(videoLoadingText); addView(mProgressBar); addView(mTextView); } } - private static class FullScreenMediaController extends MediaController { + private static class FullScreenControls implements ContentVideoViewControls { View mVideoView; + MediaController mMediaController; - public FullScreenMediaController(Context context, View video) { - super(context); + public FullScreenControls(Context context, View video) { + mMediaController = new MediaController(context); mVideoView = video; } @Override public void show() { - super.show(); + mMediaController.show(); if (mVideoView != null) { mVideoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); } } + @Override + public void show(int timeout_ms) { + mMediaController.show(timeout_ms); + } + @Override public void hide() { if (mVideoView != null) { mVideoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); } - super.hide(); + mMediaController.hide(); + } + + @Override + public boolean isShowing() { + return mMediaController.isShowing(); + } + + @Override + public void setEnabled(boolean enabled) { + mMediaController.setEnabled(enabled); + } + + @Override + public void setDelegate(Delegate delegate) { + mMediaController.setMediaPlayer(delegate); + } + + @Override + public void setAnchorView(View view) { + mMediaController.setAnchorView(view); } } private Runnable mExitFullscreenRunnable = new Runnable() { @Override public void run() { - destroyContentVideoView(); + exitFullscreen(true); } }; - public ContentVideoView(Context context) { - this(context, 0); - } - - private ContentVideoView(Context context, int nativeContentVideoView) { + private ContentVideoView(Context context, int nativeContentVideoView, + ContentVideoViewClient client) { super(context); - initResources(context); - - if (nativeContentVideoView == 0) return; mNativeContentVideoView = nativeContentVideoView; - + mClient = client; + initResources(context); mCurrentBufferPercentage = 0; mVideoSurfaceView = new VideoSurfaceView(context); + setBackgroundColor(Color.BLACK); + showContentVideoView(); + setVisibility(View.VISIBLE); + mClient.onShowCustomView(this); } - private static void initResources(Context context) { + private void initResources(Context context) { if (mPlaybackErrorText != null) return; mPlaybackErrorText = context.getString( org.chromium.content.R.string.media_player_error_text_invalid_progressive_playback); @@ -212,24 +233,23 @@ private static void initResources(Context context) { org.chromium.content.R.string.media_player_loading_video); } - void showContentVideoView() { + private void showContentVideoView() { FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER); this.addView(mVideoSurfaceView, layoutParams); - View progressView = sDelegate.getVideoLoadingProgressView(); + View progressView = mClient.getVideoLoadingProgressView(); if (progressView != null) { mProgressView = progressView; } else { - mProgressView = new ProgressView(getContext()); + mProgressView = new ProgressView(getContext(), mVideoLoadingText); } this.addView(mProgressView, layoutParams); mVideoSurfaceView.setZOrderOnTop(true); mVideoSurfaceView.setOnKeyListener(this); mVideoSurfaceView.setOnTouchListener(this); mVideoSurfaceView.getHolder().addCallback(this); - mVideoSurfaceView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); mVideoSurfaceView.setFocusable(true); mVideoSurfaceView.setFocusableInTouchMode(true); mVideoSurfaceView.requestFocus(); @@ -242,9 +262,14 @@ public void onMediaPlayerError(int errorType) { return; } + // Ignore some invalid error codes. + if (errorType == MEDIA_ERROR_INVALID_CODE) { + return; + } + mCurrentState = STATE_ERROR; - if (mMediaController != null) { - mMediaController.hide(); + if (mControls != null) { + mControls.hide(); } /* Pop up an error dialog so the user knows that @@ -281,7 +306,7 @@ public void onClick(DialogInterface dialog, int whichButton) { } @CalledByNative - public void onVideoSizeChanged(int width, int height) { + private void onVideoSizeChanged(int width, int height) { mVideoWidth = width; mVideoHeight = height; if (mVideoWidth != 0 && mVideoHeight != 0) { @@ -290,17 +315,17 @@ public void onVideoSizeChanged(int width, int height) { } @CalledByNative - public void onBufferingUpdate(int percent) { + private void onBufferingUpdate(int percent) { mCurrentBufferPercentage = percent; } @CalledByNative - public void onPlaybackComplete() { + private void onPlaybackComplete() { onCompletion(); } @CalledByNative - public void updateMediaMetadata( + private void onUpdateMediaMetadata( int videoWidth, int videoHeight, int duration, @@ -313,31 +338,24 @@ public void updateMediaMetadata( mCanSeekBack = canSeekBack; mCanSeekForward = canSeekForward; mCurrentState = isPlaying() ? STATE_PLAYING : STATE_PAUSED; - if (mMediaController != null) { - mMediaController.setEnabled(true); + if (mControls != null) { + mControls.setEnabled(true); // If paused , should show the controller for ever. if (isPlaying()) - mMediaController.show(); + mControls.show(); else - mMediaController.show(0); + mControls.show(0); } onVideoSizeChanged(videoWidth, videoHeight); } - public void destroyNativeView() { - if (mNativeContentVideoView != 0) { - mNativeContentVideoView = 0; - destroyContentVideoView(); - } - } - @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { mVideoSurfaceView.setFocusable(true); mVideoSurfaceView.setFocusableInTouchMode(true); - if (isInPlaybackState() && mMediaController != null) { - mMediaController.show(); + if (isInPlaybackState() && mControls != null) { + mControls.show(); } } @@ -349,57 +367,57 @@ public void surfaceCreated(SurfaceHolder holder) { @Override public void surfaceDestroyed(SurfaceHolder holder) { - mSurfaceHolder = null; if (mNativeContentVideoView != 0) { - nativeExitFullscreen(mNativeContentVideoView, true); - mNativeContentVideoView = 0; - post(mExitFullscreenRunnable); + nativeSetSurface(mNativeContentVideoView, null); } - removeMediaController(); + mSurfaceHolder = null; + post(mExitFullscreenRunnable); } - public void setMediaController(MediaController controller) { - if (mMediaController != null) { - mMediaController.hide(); + private void setControls(ContentVideoViewControls controls) { + if (mControls != null) { + mControls.hide(); } - mMediaController = controller; - attachMediaController(); + mControls = controls; + attachControls(); } - private void attachMediaController() { - if (mMediaController != null) { - mMediaController.setMediaPlayer(this); - mMediaController.setAnchorView(mVideoSurfaceView); - mMediaController.setEnabled(false); + private void attachControls() { + if (mControls != null) { + mControls.setDelegate(this); + mControls.setAnchorView(mVideoSurfaceView); + mControls.setEnabled(false); } } @CalledByNative - public void openVideo() { + private void openVideo() { if (mSurfaceHolder != null) { mCurrentState = STATE_IDLE; - setMediaController(new FullScreenMediaController(sDelegate.getContext(), this)); - if (mNativeContentVideoView != 0) { - nativeUpdateMediaMetadata(mNativeContentVideoView); - } mCurrentBufferPercentage = 0; + ContentVideoViewControls controls = mClient.createControls(); + if (controls == null) { + controls = new FullScreenControls(getContext(), this); + } + setControls(controls); if (mNativeContentVideoView != 0) { + nativeUpdateMediaMetadata(mNativeContentVideoView); nativeSetSurface(mNativeContentVideoView, - mSurfaceHolder.getSurface()); + mSurfaceHolder.getSurface()); } } } private void onCompletion() { mCurrentState = STATE_PLAYBACK_COMPLETED; - if (mMediaController != null) { - mMediaController.hide(); + if (mControls != null) { + mControls.hide(); } } @Override public boolean onTouch(View v, MotionEvent event) { - if (isInPlaybackState() && mMediaController != null && + if (isInPlaybackState() && mControls != null && event.getAction() == MotionEvent.ACTION_DOWN) { toggleMediaControlsVisiblity(); } @@ -408,7 +426,7 @@ public boolean onTouch(View v, MotionEvent event) { @Override public boolean onTrackballEvent(MotionEvent ev) { - if (isInPlaybackState() && mMediaController != null) { + if (isInPlaybackState() && mControls != null) { toggleMediaControlsVisiblity(); } return false; @@ -424,38 +442,35 @@ public boolean onKey(View v, int keyCode, KeyEvent event) { keyCode != KeyEvent.KEYCODE_MENU && keyCode != KeyEvent.KEYCODE_SEARCH && keyCode != KeyEvent.KEYCODE_ENDCALL; - if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) { + if (isInPlaybackState() && isKeyCodeSupported && mControls != null) { if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { if (isPlaying()) { pause(); - mMediaController.show(); + mControls.show(); } else { start(); - mMediaController.hide(); + mControls.hide(); } return true; } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { if (!isPlaying()) { start(); - mMediaController.hide(); + mControls.hide(); } return true; } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { if (isPlaying()) { pause(); - mMediaController.show(); + mControls.show(); } return true; } else { toggleMediaControlsVisiblity(); } } else if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { - if (mNativeContentVideoView != 0) { - nativeExitFullscreen(mNativeContentVideoView, false); - destroyNativeView(); - } + exitFullscreen(false); return true; } else if (keyCode == KeyEvent.KEYCODE_MENU || keyCode == KeyEvent.KEYCODE_SEARCH) { return true; @@ -464,10 +479,10 @@ public boolean onKey(View v, int keyCode, KeyEvent event) { } private void toggleMediaControlsVisiblity() { - if (mMediaController.isShowing()) { - mMediaController.hide(); + if (mControls.isShowing()) { + mControls.hide(); } else { - mMediaController.show(); + mControls.show(); } } @@ -475,6 +490,7 @@ private boolean isInPlaybackState() { return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE); } + @Override public void start() { if (isInPlaybackState()) { if (mNativeContentVideoView != 0) { @@ -484,6 +500,7 @@ public void start() { } } + @Override public void pause() { if (isInPlaybackState()) { if (isPlaying()) { @@ -496,6 +513,7 @@ public void pause() { } // cache duration as mDuration for faster access + @Override public int getDuration() { if (isInPlaybackState()) { if (mDuration > 0) { @@ -512,6 +530,7 @@ public int getDuration() { return mDuration; } + @Override public int getCurrentPosition() { if (isInPlaybackState() && mNativeContentVideoView != 0) { return nativeGetCurrentPosition(mNativeContentVideoView); @@ -519,27 +538,34 @@ public int getCurrentPosition() { return 0; } + @Override public void seekTo(int msec) { if (mNativeContentVideoView != 0) { nativeSeekTo(mNativeContentVideoView, msec); } } + @Override public boolean isPlaying() { return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView); } + @Override public int getBufferPercentage() { return mCurrentBufferPercentage; } + + @Override public boolean canPause() { return mCanPause; } + @Override public boolean canSeekBackward() { return mCanSeekBack; } + @Override public boolean canSeekForward() { return mCanSeekForward; } @@ -549,28 +575,22 @@ public int getAudioSessionId() { } @CalledByNative - public static ContentVideoView createContentVideoView(int nativeContentVideoView) { - if (sContentVideoView != null) - return sContentVideoView; - - if (sDelegate != null && sDelegate.getContext() != null) { - sContentVideoView = new ContentVideoView(sDelegate.getContext(), - nativeContentVideoView); - - sDelegate.onShowCustomView(sContentVideoView); - sContentVideoView.setBackgroundColor(Color.BLACK); - sContentVideoView.showContentVideoView(); - sContentVideoView.setVisibility(View.VISIBLE); - return sContentVideoView; + private static ContentVideoView createContentVideoView( + Context context, int nativeContentVideoView, ContentVideoViewClient client) { + ThreadUtils.assertOnUiThread(); + // The context needs be Activity to create the ContentVideoView correctly. + if (!(context instanceof Activity)) { + Log.w(TAG, "Wrong type of context, can't create fullscreen video"); + return null; } - return null; + return new ContentVideoView(context, nativeContentVideoView, client); } - public void removeMediaController() { - if (mMediaController != null) { - mMediaController.setEnabled(false); - mMediaController.hide(); - mMediaController = null; + private void removeControls() { + if (mControls != null) { + mControls.setEnabled(false); + mControls.hide(); + mControls = null; } } @@ -581,24 +601,33 @@ public void removeSurfaceView() { mProgressView = null; } - @CalledByNative - public static void destroyContentVideoView() { - sDelegate.onDestroyContentVideoView(); - if (sContentVideoView != null) { - sContentVideoView.removeMediaController(); - sContentVideoView.removeSurfaceView(); - sContentVideoView.setVisibility(View.GONE); + public void exitFullscreen(boolean relaseMediaPlayer) { + destroyContentVideoView(false); + if (mNativeContentVideoView != 0) { + nativeExitFullscreen(mNativeContentVideoView, relaseMediaPlayer); + mNativeContentVideoView = 0; } - sContentVideoView = null; } - public static ContentVideoView getContentVideoView() { - return sContentVideoView; + /** + * This method shall only be called by native and exitFullscreen, + * To exit fullscreen, use exitFullscreen in Java. + */ + @CalledByNative + private void destroyContentVideoView(boolean nativeViewDestroyed) { + if (mVideoSurfaceView != null) { + mClient.onDestroyContentVideoView(); + removeControls(); + removeSurfaceView(); + setVisibility(View.GONE); + } + if (nativeViewDestroyed) { + mNativeContentVideoView = 0; + } } - public static void registerContentVideoViewContextDelegate( - ContentVideoViewContextDelegate delegate) { - sDelegate = delegate; + public static ContentVideoView getContentVideoView() { + return nativeGetSingletonJavaContentVideoView(); } @Override @@ -609,12 +638,13 @@ public boolean onTouchEvent(MotionEvent ev) { @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { - destroyContentVideoView(); + exitFullscreen(false); return true; } return super.onKeyDown(keyCode, event); } + private static native ContentVideoView nativeGetSingletonJavaContentVideoView(); private native void nativeExitFullscreen(int nativeContentVideoView, boolean relaseMediaPlayer); private native int nativeGetCurrentPosition(int nativeContentVideoView); private native int nativeGetDurationInMilliSeconds(int nativeContentVideoView); diff --git a/src/org/chromium/content/browser/ContentVideoViewClient.java b/src/org/chromium/content/browser/ContentVideoViewClient.java new file mode 100644 index 0000000..985f993 --- /dev/null +++ b/src/org/chromium/content/browser/ContentVideoViewClient.java @@ -0,0 +1,43 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser; + +import android.view.View; + +/** + * Main callback class used by ContentVideoView. + * + * This contains the superset of callbacks that must be implemented by the embedder. + * + * onShowCustomView and onDestoryContentVideoView must be implemented, + * getVideoLoadingProgressView() is optional, and may return null if not required. + * + * The implementer is responsible for displaying the Android view when + * {@link #onShowCustomView(View)} is called. + */ +public interface ContentVideoViewClient { + /** + * Called when the video view is ready to be shown. Must be implemented. + * @param view The view to show. + */ + public void onShowCustomView(View view); + + /** + * Called when it's time to destroy the video view. Must be implemented. + */ + public void onDestroyContentVideoView(); + + /** + * Allows the embedder to replace the view indicating that the video is loading. + * If null is returned, the default video loading view is used. + */ + public View getVideoLoadingProgressView(); + + /** + * Allows the embedder to replace the default playback controls by returning a custom + * implementation. If null is returned, the default controls are used. + */ + public ContentVideoViewControls createControls(); +} diff --git a/src/org/chromium/content/browser/ContentVideoViewContextDelegate.java b/src/org/chromium/content/browser/ContentVideoViewContextDelegate.java deleted file mode 100644 index 6eb21b6..0000000 --- a/src/org/chromium/content/browser/ContentVideoViewContextDelegate.java +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package org.chromium.content.browser; - -import android.content.Context; -import android.view.View; - -/** - * Allows customization for clients of the ContentVideoView. - * The implementer is responsible for displaying the Android view when - * {@link #onShowCustomView(View)} is called. - */ -public interface ContentVideoViewContextDelegate { - public void onShowCustomView(View view); - public void onDestroyContentVideoView(); - public Context getContext(); - public View getVideoLoadingProgressView(); -} diff --git a/src/org/chromium/content/browser/ContentVideoViewControls.java b/src/org/chromium/content/browser/ContentVideoViewControls.java new file mode 100644 index 0000000..d075af8 --- /dev/null +++ b/src/org/chromium/content/browser/ContentVideoViewControls.java @@ -0,0 +1,66 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser; + +import android.view.View; +import android.widget.MediaController.MediaPlayerControl; + +/** + * Represents the video controls of the content video view (the fullscreen video player). + * Abstracts ContentVideoView from android.widget.MediaController to allow embedders to + * override the behavior and/or the look of the default controls. + */ +public interface ContentVideoViewControls { + + /** + * The interface that the host of the controls and the video needs to implement. + */ + public interface Delegate extends MediaPlayerControl { + } + + /** + * Show the controls on screen. It will go away + * automatically after 3 seconds of inactivity. + */ + public void show(); + + /** + * Show the controls on screen. It will go away + * automatically after 'timeout' milliseconds of inactivity. + * @param timeout_ms The timeout in milliseconds. Use 0 to show + * the controls until hide() is called. + */ + public void show(int timeout_ms); + + /** + * Remove the controls from the screen. + */ + public void hide(); + + /** + * Check if the controls are shown or hidden at the moment. + */ + public boolean isShowing(); + + /** + * Enable or disable the controls. + */ + public void setEnabled(boolean enabled); + + /** + * Set the media player interface to use to update/implement the controls. + * @param delegate The delegate implementation by the controls host. + */ + public void setDelegate(Delegate delegate); + + /** + * Set the view that acts as the anchor for the control view. + * This can for example be a VideoView, or your Activity's main view. + * When VideoView calls this method, it will use the VideoView's parent + * as the anchor. + * @param view The view to which to anchor the controls when visible. + */ + public void setAnchorView(View view); +} \ No newline at end of file diff --git a/src/org/chromium/content/browser/ContentView.java b/src/org/chromium/content/browser/ContentView.java index 0e6e381..dce2cbf 100644 --- a/src/org/chromium/content/browser/ContentView.java +++ b/src/org/chromium/content/browser/ContentView.java @@ -417,12 +417,6 @@ public boolean drawChild(Canvas canvas, View child, long drawingTime) { return super.drawChild(canvas, child, drawingTime); } - // Needed by ContentViewCore.InternalAccessDelegate - @Override - public void onScrollChanged(int l, int t, int oldl, int oldt) { - super.onScrollChanged(l, t, oldl, oldt); - } - @Override protected void onSizeChanged(int w, int h, int ow, int oh) { TraceEvent.begin(); @@ -483,7 +477,10 @@ public boolean onTouchEvent(MotionEvent event) { */ @Override public boolean onHoverEvent(MotionEvent event) { - return mContentViewCore.onHoverEvent(event); + MotionEvent offset = createOffsetMotionEvent(event); + boolean consumed = mContentViewCore.onHoverEvent(offset); + offset.recycle(); + return consumed; } @Override @@ -491,6 +488,11 @@ public boolean onGenericMotionEvent(MotionEvent event) { return mContentViewCore.onGenericMotionEvent(event); } + @Override + public boolean performLongClick() { + return false; + } + /** * Sets the current amount to offset incoming touch events by. This is used to handle content * moving and not lining up properly with the android input system. diff --git a/src/org/chromium/content/browser/ContentViewClient.java b/src/org/chromium/content/browser/ContentViewClient.java index a9ff266..07bc91f 100644 --- a/src/org/chromium/content/browser/ContentViewClient.java +++ b/src/org/chromium/content/browser/ContentViewClient.java @@ -7,6 +7,7 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; +import android.graphics.RectF; import android.util.Log; import android.view.ActionMode; import android.view.KeyEvent; @@ -58,7 +59,13 @@ public void onOffsetsForFullscreenChanged( float topControlsOffsetYPix, float contentOffsetYPix, float overdrawBottomHeightPix) { } - public void onTabCrash() { + /** + * Notifies the client that the renderer backing the ContentView has crashed. + * @param crashedWhileOomProtected True iff the renderer died while being bound with a high + * priority binding, which indicates that it was probably an actual crash (as opposed to the + * renderer being killed by the OS out-of-memory killer). + */ + public void onRendererCrash(boolean processWasOomProtected) { } public boolean shouldOverrideKeyEvent(KeyEvent event) { @@ -167,6 +174,10 @@ public void onStartContentIntent(Context context, String intentUrl) { public void onExternalVideoSurfaceRequested(int playerId) { } - public void onGeometryChanged(int playerId, float x, float y, float width, float height) { + public void onGeometryChanged(int playerId, RectF rect) { + } + + public ContentVideoViewClient getContentVideoViewClient() { + return null; } } diff --git a/src/org/chromium/content/browser/ContentViewCore.java b/src/org/chromium/content/browser/ContentViewCore.java index 7bbf3ed..95ed270 100644 --- a/src/org/chromium/content/browser/ContentViewCore.java +++ b/src/org/chromium/content/browser/ContentViewCore.java @@ -1,22 +1,28 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.content.browser; import android.app.Activity; +import android.content.ContentResolver; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; +import android.database.ContentObserver; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Rect; +import android.graphics.RectF; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.ResultReceiver; +import android.provider.Settings; +import android.provider.Settings.Secure; import android.text.Editable; import android.util.Log; import android.util.Pair; @@ -30,10 +36,14 @@ import android.view.Window; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; +import android.widget.AbsoluteLayout; import android.widget.FrameLayout; import com.google.common.annotations.VisibleForTesting; @@ -44,9 +54,11 @@ import org.chromium.content.R; import org.chromium.content.browser.ContentViewGestureHandler.MotionEventDelegate; import org.chromium.content.browser.accessibility.AccessibilityInjector; +import org.chromium.content.browser.accessibility.BrowserAccessibilityManager; import org.chromium.content.browser.input.AdapterInputConnection; import org.chromium.content.browser.input.HandleView; import org.chromium.content.browser.input.ImeAdapter; +import org.chromium.content.browser.input.InputMethodManagerWrapper; import org.chromium.content.browser.input.ImeAdapter.AdapterInputConnectionFactory; import org.chromium.content.browser.input.InsertionHandleController; import org.chromium.content.browser.input.SelectPopupDialog; @@ -58,6 +70,8 @@ import org.chromium.ui.gfx.DeviceDisplayInfo; import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -68,7 +82,9 @@ * being tied to the view system. */ @JNINamespace("content") -public class ContentViewCore implements MotionEventDelegate, NavigationClient { + public class ContentViewCore implements MotionEventDelegate, + NavigationClient, + AccessibilityStateChangeListener { /** * Indicates that input events are batched together and delivered just before vsync. */ @@ -79,7 +95,7 @@ public class ContentViewCore implements MotionEventDelegate, NavigationClient { */ public static final int INPUT_EVENTS_DELIVERED_IMMEDIATELY = 0; - private static final String TAG = ContentViewCore.class.getName(); + private static final String TAG = "ContentViewCore"; // Used to avoid enabling zooming in / out if resulting zooming will // produce little visible difference. @@ -152,11 +168,6 @@ public interface InternalAccessDelegate { */ void super_onConfigurationChanged(Configuration newConfig); - /** - * @see View#onScrollChanged(int, int, int, int) - */ - void onScrollChanged(int lPix, int tPix, int oldlPix, int oldtPix); - /** * @see View#awakenScrollBars() */ @@ -172,15 +183,31 @@ public interface InternalAccessDelegate { * An interface that allows the embedder to be notified when the pinch gesture starts and * stops. */ - public interface PinchGestureStateListener { + public interface GestureStateListener { /** * Called when the pinch gesture starts. */ void onPinchGestureStart(); + /** * Called when the pinch gesture ends. */ void onPinchGestureEnd(); + + /** + * Called when the fling gesture is sent. + */ + void onFlingStartGesture(int vx, int vy); + + /** + * Called when the fling cancel gesture is sent. + */ + void onFlingCancelGesture(); + + /** + * Called when a fling event was not handled by the renderer. + */ + void onUnhandledFlingStartEvent(); } /** @@ -191,16 +218,35 @@ public interface ZoomControlsDelegate { * Called when it's reasonable to show zoom controls. */ void invokeZoomPicker(); + /** * Called when zoom controls need to be hidden (e.g. when the view hides). */ void dismissZoomPicker(); + /** * Called when page scale has been changed, so the controls can update their state. */ void updateZoomControls(); } + /** + * An interface that allows the embedder to be notified of changes to the parameters of the + * currently displayed contents. + * These notifications are consistent with respect to the UI thread (the size is the size of + * the contents currently displayed on screen). + */ + public interface UpdateFrameInfoListener { + /** + * Called each time any of the parameters are changed. + * + * @param widthCss The content width in logical (CSS) pixels. + * @param heightCss The content height in logical (CSS) pixels. + * @param pageScaleFactor The page scale. + */ + void onFrameInfoUpdated(float widthCss, float heightCss, float pageScaleFactor); + } + private VSyncManager.Provider mVSyncProvider; private VSyncManager.Listener mVSyncListener; private int mVSyncSubscriberCount; @@ -304,8 +350,12 @@ private void setNeedsAnimate() { private boolean mAttachedToWindow = false; + // Pid of the renderer process backing this ContentViewCore. + private int mPid = 0; + private ContentViewGestureHandler mContentViewGestureHandler; - private PinchGestureStateListener mPinchGestureStateListener; + private GestureStateListener mGestureStateListener; + private UpdateFrameInfoListener mUpdateFrameInfoListener; private ZoomManager mZoomManager; private ZoomControlsDelegate mZoomControlsDelegate; @@ -323,7 +373,7 @@ private void setNeedsAnimate() { private Runnable mDeferredHandleFadeInRunnable; - // Size of the viewport in physical pixels as set from onSizeChanged or setInitialViewportSize. + // Size of the viewport in physical pixels as set from onSizeChanged. private int mViewportWidthPix; private int mViewportHeightPix; private int mPhysicalBackingWidthPix; @@ -353,6 +403,15 @@ private void setNeedsAnimate() { // The AccessibilityInjector that handles loading Accessibility scripts into the web page. private AccessibilityInjector mAccessibilityInjector; + // Handles native accessibility, i.e. without any script injection. + private BrowserAccessibilityManager mBrowserAccessibilityManager; + + // System accessibility service. + private final AccessibilityManager mAccessibilityManager; + + // Allows us to dynamically respond when the accessibility script injection flag changes. + private ContentObserver mAccessibilityScriptInjectionObserver; + // Temporary notification to tell onSizeChanged to focus a form element, // because the OSK was just brought up. private boolean mUnfocusOnNextSizeChanged = false; @@ -374,6 +433,7 @@ private void setNeedsAnimate() { private ViewAndroid mViewAndroid; + /** * Constructs a new ContentViewCore. Embedders must call initialize() after constructing * a ContentViewCore and before using it. @@ -393,11 +453,14 @@ public ContentViewCore(Context context) { mStartHandlePoint = mRenderCoordinates.createNormalizedPoint(); mEndHandlePoint = mRenderCoordinates.createNormalizedPoint(); mInsertionHandlePoint = mRenderCoordinates.createNormalizedPoint(); + mAccessibilityManager = (AccessibilityManager) + getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); } /** * @return The context used for creating this ContentViewCore. */ + @CalledByNative public Context getContext() { return mContext; } @@ -409,23 +472,6 @@ public ViewGroup getContainerView() { return mContainerView; } - /** - * Set initial viewport size parameters, so that the web page can have a reasonable - * size to start before ContentView becomes visible. - * This is useful for a background view that loads the web page before it is shown - * and gets the first onSizeChanged(). - */ - public void setInitialViewportSize(int widthPix, int heightPix, - int offsetXPix, int offsetYPix) { - assert mViewportWidthPix == 0 && mViewportHeightPix == 0 && - mViewportSizeOffsetWidthPix == 0 && mViewportSizeOffsetHeightPix == 0; - mViewportWidthPix = widthPix; - mViewportHeightPix = heightPix; - mViewportSizeOffsetWidthPix = offsetXPix; - mViewportSizeOffsetHeightPix = offsetYPix; - if (mNativeContentViewCore != 0) nativeWasResized(mNativeContentViewCore); - } - /** * Specifies how much smaller the WebKit layout size should be relative to the size of this * view. @@ -463,24 +509,35 @@ public View acquireAnchorView() { } @Override + @SuppressWarnings("deprecation") // AbsoluteLayout.LayoutParams public void setAnchorViewPosition( View view, float x, float y, float width, float height) { assert(view.getParent() == mContainerView); + float scale = (float) DeviceDisplayInfo.create(getContext()).getDIPScale(); // The anchor view should not go outside the bounds of the ContainerView. - int scaledX = Math.round(x * scale); - int scaledWidth = Math.round(width * scale); - if (scaledWidth + scaledX > mContainerView.getWidth()) { - scaledWidth = mContainerView.getWidth() - scaledX; - } - - FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( + int leftMargin = Math.round(x * scale); + int topMargin = Math.round(mRenderCoordinates.getContentOffsetYPix() + y * scale); + // ContentViewCore currently only supports these two container view types. + if (mContainerView instanceof FrameLayout) { + int scaledWidth = Math.round(width * scale); + if (scaledWidth + leftMargin > mContainerView.getWidth()) { + scaledWidth = mContainerView.getWidth() - leftMargin; + } + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( scaledWidth, Math.round(height * scale)); - lp.leftMargin = scaledX; - lp.topMargin = (int) mRenderCoordinates.getContentOffsetYPix() + - Math.round(y * scale); - view.setLayoutParams(lp); + lp.leftMargin = leftMargin; + lp.topMargin = topMargin; + view.setLayoutParams(lp); + } else if (mContainerView instanceof AbsoluteLayout) { + android.widget.AbsoluteLayout.LayoutParams lp = + new android.widget.AbsoluteLayout.LayoutParams((int)width, + (int)height, leftMargin, topMargin); + view.setLayoutParams(lp); + } else { + Log.e(TAG, "Unknown layout " + mContainerView.getClass().getName()); + } } @Override @@ -506,13 +563,13 @@ public AdapterInputConnection getInputConnectionForTest() { } private ImeAdapter createImeAdapter(Context context) { - return new ImeAdapter(context, getSelectionHandleController(), - getInsertionHandleController(), - new ImeAdapter.ViewEmbedder() { + return new ImeAdapter(new InputMethodManagerWrapper(context), + new ImeAdapter.ImeAdapterDelegate() { @Override public void onImeEvent(boolean isFinish) { getContentViewClient().onImeEvent(); if (!isFinish) { + hideHandles(); undoScrollFocusedEditableNodeIntoViewIfNeeded(false); } } @@ -655,7 +712,6 @@ public void initialize(ViewGroup containerView, InternalAccessDelegate internalD initializeContainerView(internalDispatcher, inputEventDeliveryMode); mAccessibilityInjector = AccessibilityInjector.newInstance(this); - mAccessibilityInjector.addOrRemoveAccessibilityApisIfNecessary(); String contentDescription = "Web View"; if (R.string.accessibility_content_view == 0) { @@ -672,6 +728,8 @@ public void didStartLoading(String url) { resetGestureDetectors(); } }; + + mPid = nativeGetCurrentRenderProcessId(mNativeContentViewCore); } @CalledByNative @@ -794,6 +852,16 @@ public void destroy() { mContentSettings = null; mJavaScriptInterfaces.clear(); mRetainedJavaScriptObjects.clear(); + unregisterAccessibilityContentObserver(); + } + + private void unregisterAccessibilityContentObserver() { + if (mAccessibilityScriptInjectionObserver == null) { + return; + } + getContext().getContentResolver().unregisterContentObserver( + mAccessibilityScriptInjectionObserver); + mAccessibilityScriptInjectionObserver = null; } /** @@ -851,12 +919,6 @@ public int getBackgroundColor() { return Color.WHITE; } - public void setBackgroundColor(int color) { - if (mNativeContentViewCore != 0 && getBackgroundColor() != color) { - nativeSetBackgroundColor(mNativeContentViewCore, color); - } - } - @CalledByNative private void onBackgroundColorChanged(int color) { getContentViewClient().onBackgroundColorChanged(color); @@ -944,15 +1006,13 @@ public boolean consumePendingRendererFrame() { } /** - * @return Viewport width in physical pixels as set from onSizeChanged or - * setInitialViewportSize. + * @return Viewport width in physical pixels as set from onSizeChanged. */ @CalledByNative public int getViewportWidthPix() { return mViewportWidthPix; } /** - * @return Viewport height in physical pixels as set from onSizeChanged or - * setInitialViewportSize. + * @return Viewport height in physical pixels as set from onSizeChanged. */ @CalledByNative public int getViewportHeightPix() { return mViewportHeightPix; } @@ -1186,12 +1246,21 @@ private void confirmTouchEvent(int ackResult) { mContentViewGestureHandler.confirmTouchEvent(ackResult); } + @SuppressWarnings("unused") + @CalledByNative + private void unhandledFlingStartEvent() { + if (mGestureStateListener != null) { + mGestureStateListener.onUnhandledFlingStartEvent(); + } + } + @Override public boolean sendGesture(int type, long timeMs, int x, int y, boolean lastInputEventForVSync, Bundle b) { + if (offerGestureToEmbedder(type)) return false; if (mNativeContentViewCore == 0) return false; updateTextHandlesForGesture(type); - updatePinchGestureStateListener(type); + updateGestureStateListener(type, b); if (lastInputEventForVSync && isVSyncNotificationEnabled()) { assert type == ContentViewGestureHandler.GESTURE_SCROLL_BY || type == ContentViewGestureHandler.GESTURE_PINCH_BY; @@ -1260,19 +1329,27 @@ public boolean sendGesture(int type, long timeMs, int x, int y, boolean lastInpu } } - public void setPinchGestureStateListener(PinchGestureStateListener pinchGestureStateListener) { - mPinchGestureStateListener = pinchGestureStateListener; + public void setGestureStateListener(GestureStateListener pinchGestureStateListener) { + mGestureStateListener = pinchGestureStateListener; } - void updatePinchGestureStateListener(int gestureType) { - if (mPinchGestureStateListener == null) return; + void updateGestureStateListener(int gestureType, Bundle b) { + if (mGestureStateListener == null) return; switch (gestureType) { case ContentViewGestureHandler.GESTURE_PINCH_BEGIN: - mPinchGestureStateListener.onPinchGestureStart(); + mGestureStateListener.onPinchGestureStart(); break; case ContentViewGestureHandler.GESTURE_PINCH_END: - mPinchGestureStateListener.onPinchGestureEnd(); + mGestureStateListener.onPinchGestureEnd(); + break; + case ContentViewGestureHandler.GESTURE_FLING_START: + mGestureStateListener.onFlingStartGesture( + b.getInt(ContentViewGestureHandler.VELOCITY_X, 0), + b.getInt(ContentViewGestureHandler.VELOCITY_Y, 0)); + break; + case ContentViewGestureHandler.GESTURE_FLING_CANCEL: + mGestureStateListener.onFlingCancelGesture(); break; default: break; @@ -1308,7 +1385,6 @@ public void onActivityPause() { TraceEvent.begin(); hidePopupDialog(); nativeOnHide(mNativeContentViewCore); - setAccessibilityState(false); TraceEvent.end(); } @@ -1317,7 +1393,7 @@ public void onActivityPause() { */ public void onActivityResume() { nativeOnShow(mNativeContentViewCore); - setAccessibilityState(true); + setAccessibilityState(mAccessibilityManager.isEnabled()); } /** @@ -1325,7 +1401,7 @@ public void onActivityResume() { */ public void onShow() { nativeOnShow(mNativeContentViewCore); - setAccessibilityState(true); + setAccessibilityState(mAccessibilityManager.isEnabled()); } /** @@ -1333,7 +1409,7 @@ public void onShow() { */ public void onHide() { hidePopupDialog(); - setAccessibilityState(false); + setInjectedAccessibility(false); nativeOnHide(mNativeContentViewCore); } @@ -1367,6 +1443,7 @@ private void hidePopupDialog() { void hideSelectActionBar() { if (mActionMode != null) { mActionMode.finish(); + mActionMode = null; } } @@ -1381,12 +1458,15 @@ private void resetGestureDetectors() { public void onAttachedToWindow() { mAttachedToWindow = true; if (mNativeContentViewCore != 0) { - int pid = nativeGetCurrentRenderProcessId(mNativeContentViewCore); - if (pid > 0) { - ChildProcessLauncher.bindAsHighPriority(pid); - } + assert mPid == nativeGetCurrentRenderProcessId(mNativeContentViewCore); + ChildProcessLauncher.bindAsHighPriority(mPid); + // Normally the initial binding is removed in onRenderProcessSwap(), but it is possible + // to construct WebContents and spawn the renderer before passing it to ContentViewCore. + // In this case there will be no onRenderProcessSwap() call and the initial binding will + // be removed here. + ChildProcessLauncher.removeInitialBinding(mPid); } - setAccessibilityState(true); + setAccessibilityState(mAccessibilityManager.isEnabled()); } /** @@ -1396,23 +1476,22 @@ public void onAttachedToWindow() { public void onDetachedFromWindow() { mAttachedToWindow = false; if (mNativeContentViewCore != 0) { - int pid = nativeGetCurrentRenderProcessId(mNativeContentViewCore); - if (pid > 0) { - ChildProcessLauncher.unbindAsHighPriority(pid); - } + assert mPid == nativeGetCurrentRenderProcessId(mNativeContentViewCore); + ChildProcessLauncher.unbindAsHighPriority(mPid); } - setAccessibilityState(false); + setInjectedAccessibility(false); hidePopupDialog(); mZoomControlsDelegate.dismissZoomPicker(); + unregisterAccessibilityContentObserver(); } /** * @see View#onVisibilityChanged(android.view.View, int) */ public void onVisibilityChanged(View changedView, int visibility) { - if (visibility != View.VISIBLE) { - mZoomControlsDelegate.dismissZoomPicker(); - } + if (visibility != View.VISIBLE) { + mZoomControlsDelegate.dismissZoomPicker(); + } } /** @@ -1574,18 +1653,6 @@ public void run() { mScrolledAndZoomedFocusedEditableNode = false; } - /** - * @see View#onFocusedChanged(boolean, int, Rect) - * TODO(benm): Remove once downstream usages have been updated to use single - * parameter version - */ - @Deprecated - @SuppressWarnings("javadoc") - public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { - onFocusChanged(gainFocus); - } - - public void onFocusChanged(boolean gainFocus) { if (!gainFocus) getContentViewClient().onImeStateChangeRequested(false); if (mNativeContentViewCore != 0) nativeSetFocus(mNativeContentViewCore, gainFocus); @@ -1641,6 +1708,9 @@ public boolean dispatchKeyEvent(KeyEvent event) { public boolean onHoverEvent(MotionEvent event) { TraceEvent.begin("onHoverEvent"); mContainerView.removeCallbacks(mFakeMouseMoveRunnable); + if (mBrowserAccessibilityManager != null) { + return mBrowserAccessibilityManager.onHoverEvent(event); + } if (mNativeContentViewCore != 0) { nativeSendMouseMoveEvent(mNativeContentViewCore, event.getEventTime(), event.getX(), event.getY()); @@ -1789,7 +1859,9 @@ public boolean awakenScrollBars(int startDelay, boolean invalidate) { @SuppressWarnings("unused") @CalledByNative private void onTabCrash() { - getContentViewClient().onTabCrash(); + assert mPid != 0; + getContentViewClient().onRendererCrash(ChildProcessLauncher.isOomProtected(mPid)); + mPid = 0; } private void handleTapOrPress( @@ -2101,6 +2173,17 @@ private void temporarilyHideTextHandles() { scheduleTextHandleFadeIn(); } + private boolean allowTextHandleFadeIn() { + if (mContentViewGestureHandler.isNativeScrolling() || + mContentViewGestureHandler.isNativePinching()) { + return false; + } + + if (mPopupZoomer.isShowing()) return false; + + return true; + } + // Cancels any pending fade in and schedules a new one. private void scheduleTextHandleFadeIn() { if (!isInsertionHandleShowing() && !isSelectionHandleShowing()) return; @@ -2109,9 +2192,8 @@ private void scheduleTextHandleFadeIn() { mDeferredHandleFadeInRunnable = new Runnable() { @Override public void run() { - if (mContentViewGestureHandler.isNativeScrolling() || - mContentViewGestureHandler.isNativePinching()) { - // Delay fade in until no longer scrolling or pinching. + if (!allowTextHandleFadeIn()) { + // Delay fade in until it is allowed. scheduleTextHandleFadeIn(); } else { if (isSelectionHandleShowing()) { @@ -2136,6 +2218,10 @@ public void showImeIfNeeded() { if (mNativeContentViewCore != 0) nativeShowImeIfNeeded(mNativeContentViewCore); } + public void setUpdateFrameInfoListener(UpdateFrameInfoListener updateFrameInfoListener) { + mUpdateFrameInfoListener = updateFrameInfoListener; + } + @SuppressWarnings("unused") @CalledByNative private void updateFrameInfo( @@ -2146,8 +2232,8 @@ private void updateFrameInfo( float controlsOffsetYCss, float contentOffsetYCss, float overdrawBottomHeightCss) { TraceEvent.instant("ContentViewCore:updateFrameInfo"); - // Adjust contentWidth/Height to be always at least as big as the actual viewport - // (as set by onSizeChanged or setInitialViewportSize). + // Adjust contentWidth/Height to be always at least as big as + // the actual viewport (as set by onSizeChanged). contentWidth = Math.max(contentWidth, mRenderCoordinates.fromPixToLocalCss(mViewportWidthPix)); contentHeight = Math.max(contentHeight, @@ -2176,14 +2262,6 @@ private void updateFrameInfo( if (needHidePopupZoomer) mPopupZoomer.hide(true); - if (scrollChanged) { - mContainerViewInternals.onScrollChanged( - (int) mRenderCoordinates.fromLocalCssToPix(scrollOffsetX), - (int) mRenderCoordinates.fromLocalCssToPix(scrollOffsetY), - (int) mRenderCoordinates.getScrollXPix(), - (int) mRenderCoordinates.getScrollYPix()); - } - if (pageScaleChanged) { // This function should be called back from native as soon // as the scroll is applied to the backbuffer. We should only @@ -2199,6 +2277,11 @@ private void updateFrameInfo( pageScaleFactor, minPageScaleFactor, maxPageScaleFactor, contentOffsetYPix); + if ((contentSizeChanged || pageScaleChanged) && mUpdateFrameInfoListener != null) { + mUpdateFrameInfoListener.onFrameInfoUpdated( + contentWidth, contentHeight, pageScaleFactor); + } + if (needTemporarilyHideHandles) temporarilyHideTextHandles(); if (needUpdateZoomControls) mZoomControlsDelegate.updateZoomControls(); if (contentOffsetChanged) updateHandleScreenPositions(); @@ -2211,6 +2294,12 @@ private void updateFrameInfo( controlsOffsetPix, contentOffsetYPix, overdrawBottomHeightPix); mPendingRendererFrame = true; + if (mBrowserAccessibilityManager != null) { + mBrowserAccessibilityManager.notifyFrameInfoInitialized(); + } + + // Update geometry for external video surface. + getContentViewClient().onGeometryChanged(-1, null); } @SuppressWarnings("unused") @@ -2265,6 +2354,7 @@ private void showSelectPopup(String[] items, int[] enabled, boolean multiple, private void showDisambiguationPopup(Rect targetRect, Bitmap zoomedBitmap) { mPopupZoomer.setBitmap(zoomedBitmap); mPopupZoomer.show(targetRect); + temporarilyHideTextHandles(); } @SuppressWarnings("unused") @@ -2357,14 +2447,16 @@ private void showPastePopup(int xDip, int yDip) { @SuppressWarnings("unused") @CalledByNative private void onRenderProcessSwap(int oldPid, int newPid) { + assert mPid == oldPid || mPid == newPid; if (mAttachedToWindow && oldPid != newPid) { - if (oldPid > 0) { - ChildProcessLauncher.unbindAsHighPriority(oldPid); - } - if (newPid > 0) { - ChildProcessLauncher.bindAsHighPriority(newPid); - } + ChildProcessLauncher.unbindAsHighPriority(oldPid); + ChildProcessLauncher.bindAsHighPriority(newPid); } + + // We want to remove the initial binding even if the ContentView is not attached, so that + // renderers for ContentViews loading in background do not retain the high priority. + ChildProcessLauncher.removeInitialBinding(newPid); + mPid = newPid; } @SuppressWarnings("unused") @@ -2593,6 +2685,11 @@ private void startContentIntent(String contentUrl) { getContentViewClient().onStartContentIntent(getContext(), contentUrl); } + @Override + public void onAccessibilityStateChanged(boolean enabled) { + setAccessibilityState(enabled); + } + /** * Determines whether or not this ContentViewCore can handle this accessibility action. * @param action The action to perform. @@ -2620,10 +2717,45 @@ public boolean performAccessibilityAction(int action, Bundle arguments) { return false; } + /** + * Set the BrowserAccessibilityManager, used for native accessibility + * (not script injection). This is only set when system accessibility + * has been enabled. + * @param manager The new BrowserAccessibilityManager. + */ + public void setBrowserAccessibilityManager(BrowserAccessibilityManager manager) { + mBrowserAccessibilityManager = manager; + } + + /** + * Get the BrowserAccessibilityManager, used for native accessibility + * (not script injection). This will return null when system accessibility + * is not enabled. + * @return This view's BrowserAccessibilityManager. + */ + public BrowserAccessibilityManager getBrowserAccessibilityManager() { + return mBrowserAccessibilityManager; + } + + /** + * If native accessibility (not script injection) is enabled, and if this is + * running on JellyBean or later, returns an AccessibilityNodeProvider that + * implements native accessibility for this view. Returns null otherwise. + * @return The AccessibilityNodeProvider, if available, or null otherwise. + */ + public AccessibilityNodeProvider getAccessibilityNodeProvider() { + if (mBrowserAccessibilityManager != null) { + return mBrowserAccessibilityManager.getAccessibilityNodeProvider(); + } else { + return null; + } + } + /** * @see View#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo) */ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + // Note: this is only used by the script-injecting accessibility code. mAccessibilityInjector.onInitializeAccessibilityNodeInfo(info); } @@ -2631,6 +2763,7 @@ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { * @see View#onInitializeAccessibilityEvent(AccessibilityEvent) */ public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + // Note: this is only used by the script-injecting accessibility code. event.setClassName(this.getClass().getName()); // Identify where the top-left of the screen currently points to. @@ -2651,6 +2784,46 @@ public void onInitializeAccessibilityEvent(AccessibilityEvent event) { } } + /** + * Returns whether accessibility script injection is enabled on the device + */ + public boolean isDeviceAccessibilityScriptInjectionEnabled() { + try { + if (!mContentSettings.getJavaScriptEnabled()) { + return false; + } + + int result = getContext().checkCallingOrSelfPermission( + android.Manifest.permission.INTERNET); + if (result != PackageManager.PERMISSION_GRANTED) { + return false; + } + + Field field = Settings.Secure.class.getField("ACCESSIBILITY_SCRIPT_INJECTION"); + field.setAccessible(true); + String accessibilityScriptInjection = (String) field.get(null); + ContentResolver contentResolver = getContext().getContentResolver(); + + if (mAccessibilityScriptInjectionObserver == null) { + ContentObserver contentObserver = new ContentObserver(new Handler()) { + public void onChange(boolean selfChange, Uri uri) { + setAccessibilityState(mAccessibilityManager.isEnabled()); + } + }; + contentResolver.registerContentObserver( + Settings.Secure.getUriFor(accessibilityScriptInjection), + false, + contentObserver); + mAccessibilityScriptInjectionObserver = contentObserver; + } + + return Settings.Secure.getInt(contentResolver, accessibilityScriptInjection, 0) == 1; + } catch (NoSuchFieldException e) { + } catch (IllegalAccessException e) { + } + return false; + } + /** * Returns whether or not accessibility injection is being used. */ @@ -2659,10 +2832,40 @@ public boolean isInjectingAccessibilityScript() { } /** - * Enable or disable accessibility features. + * Turns browser accessibility on or off. + * If |state| is |false|, this turns off both native and injected accessibility. + * Otherwise, if accessibility script injection is enabled, this will enable the injected + * accessibility scripts, and if it is disabled this will enable the native accessibility. */ public void setAccessibilityState(boolean state) { - mAccessibilityInjector.setScriptEnabled(state); + boolean injectedAccessibility = false; + boolean nativeAccessibility = false; + if (state) { + if (isDeviceAccessibilityScriptInjectionEnabled()) { + injectedAccessibility = true; + } else { + nativeAccessibility = true; + } + } + setInjectedAccessibility(injectedAccessibility); + setNativeAccessibilityState(nativeAccessibility); + } + + /** + * Enable or disable native accessibility features. + */ + public void setNativeAccessibilityState(boolean enabled) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + nativeSetAccessibilityEnabled(mNativeContentViewCore, enabled); + } + } + + /** + * Enable or disable injected accessibility features + */ + public void setInjectedAccessibility(boolean enabled) { + mAccessibilityInjector.addOrRemoveAccessibilityApisIfNecessary(); + mAccessibilityInjector.setScriptEnabled(enabled); } /** @@ -2803,23 +3006,33 @@ private void animateIfNecessary(long frameTimeMicros) { @CalledByNative private void notifyExternalSurface( int playerId, boolean isRequest, float x, float y, float width, float height) { - RenderCoordinates.NormalizedPoint topLeft = mRenderCoordinates.createNormalizedPoint(); - RenderCoordinates.NormalizedPoint bottomRight = mRenderCoordinates.createNormalizedPoint(); - topLeft.setLocalDip(x * getScale(), y * getScale()); - bottomRight.setLocalDip((x + width) * getScale(), (y + height) * getScale()); - if (isRequest) getContentViewClient().onExternalVideoSurfaceRequested(playerId); - getContentViewClient().onGeometryChanged( - playerId, - topLeft.getXPix(), - topLeft.getYPix(), - bottomRight.getXPix() - topLeft.getXPix(), - bottomRight.getYPix() - topLeft.getYPix()); + getContentViewClient().onGeometryChanged(playerId, new RectF(x, y, x + width, y + height)); + } + + /** + * Offer a subset of gesture events to the embedding View, + * primarily for WebView compatibility. + * + * @param type The type of the event. + * + * @return true if the embedder handled the event. + */ + private boolean offerGestureToEmbedder(int type) { + if (type == ContentViewGestureHandler.GESTURE_LONG_PRESS) { + return mContainerView.performLongClick(); + } + return false; } private native int nativeInit(boolean hardwareAccelerated, int webContentsPtr, int viewAndroidPtr, int windowAndroidPtr); + @CalledByNative + private ContentVideoViewClient getContentVideoViewClient() { + return mContentViewClient.getContentVideoViewClient(); + } + private native void nativeOnJavaContentViewCoreDestroyed(int nativeContentViewCoreImpl); private native void nativeLoadUrl( @@ -2943,8 +3156,6 @@ private native void nativeEvaluateJavaScript(int nativeContentViewCoreImpl, private native int nativeGetBackgroundColor(int nativeContentViewCoreImpl); - private native void nativeSetBackgroundColor(int nativeContentViewCoreImpl, int color); - private native void nativeOnShow(int nativeContentViewCoreImpl); private native void nativeOnHide(int nativeContentViewCoreImpl); @@ -2990,4 +3201,7 @@ private native void nativeAttachExternalVideoSurface( private native void nativeDetachExternalVideoSurface( int nativeContentViewCoreImpl, int playerId); + + private native void nativeSetAccessibilityEnabled( + int nativeContentViewCoreImpl, boolean enabled); } diff --git a/src/org/chromium/content/browser/ContentViewDownloadDelegate.java b/src/org/chromium/content/browser/ContentViewDownloadDelegate.java index 46c3e97..ef2b908 100644 --- a/src/org/chromium/content/browser/ContentViewDownloadDelegate.java +++ b/src/org/chromium/content/browser/ContentViewDownloadDelegate.java @@ -25,19 +25,10 @@ void requestHttpGetDownload(String url, String userAgent, String contentDisposit /** * Notify the host application that a download is started. + * @param filename File name of the downloaded file. + * @param mimeType Mime of the downloaded item. */ - void onDownloadStarted(); - - /** - * Notify the host application that a download is finished. - * @param url The full url to the content that was downloaded. - * @param mimetype The mimetype of downloaded file. - * @param path Path of the downloaded file. - * @param contentLength The file size of the downloaded file (in bytes). - * @param successful Whether the download succeeded - */ - void onDownloadCompleted(String url, String mimetype, String path, - long contentLength, boolean successful); + void onDownloadStarted(String filename, String mimeType); /** * Notify the host application that a download has an extension indicating diff --git a/src/org/chromium/content/browser/ContentViewGestureHandler.java b/src/org/chromium/content/browser/ContentViewGestureHandler.java index f5468c1..eb5ef13 100644 --- a/src/org/chromium/content/browser/ContentViewGestureHandler.java +++ b/src/org/chromium/content/browser/ContentViewGestureHandler.java @@ -6,6 +6,7 @@ import android.content.Context; import android.os.Bundle; +import android.os.Handler; import android.os.SystemClock; import android.util.Log; import android.view.MotionEvent; @@ -26,7 +27,7 @@ */ class ContentViewGestureHandler implements LongPressDelegate { - private static final String TAG = ContentViewGestureHandler.class.toString(); + private static final String TAG = "ContentViewGestureHandler"; /** * Used for GESTURE_FLING_START x velocity */ @@ -110,6 +111,19 @@ class ContentViewGestureHandler implements LongPressDelegate { private int mSingleTapX; private int mSingleTapY; + // Indicate current double tap drag mode state. + private int mDoubleTapDragMode = DOUBLE_TAP_DRAG_MODE_NONE; + + // x, y coordinates for an Anchor on double tap drag zoom. + private float mDoubleTapDragZoomAnchorX; + private float mDoubleTapDragZoomAnchorY; + + // On double tap this will store the y coordinates of the touch. + private float mDoubleTapY; + + // Double tap drag zoom sensitive (speed). + private static final float DOUBLE_TAP_DRAG_ZOOM_SPEED = 0.005f; + // Used to track the last rawX/Y coordinates for moves. This gives absolute scroll distance. // Useful for full screen tracking. private float mLastRawX = 0; @@ -152,11 +166,102 @@ class ContentViewGestureHandler implements LongPressDelegate { static final int INPUT_EVENT_ACK_STATE_NOT_CONSUMED = 2; static final int INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS = 3; - // Return values of sendTouchEventToNative(); + // Return values of sendPendingEventToNative(); static final int EVENT_FORWARDED_TO_NATIVE = 0; static final int EVENT_CONVERTED_TO_CANCEL = 1; static final int EVENT_NOT_FORWARDED = 2; + private final float mPxToDp; + + static final int DOUBLE_TAP_DRAG_MODE_NONE = 0; + static final int DOUBLE_TAP_DRAG_MODE_DETECTION_IN_PROGRESS = 1; + static final int DOUBLE_TAP_DRAG_MODE_ZOOM = 2; + + private class TouchEventTimeoutHandler implements Runnable { + private static final int TOUCH_EVENT_TIMEOUT = 200; + private static final int PENDING_ACK_NONE = 0; + private static final int PENDING_ACK_ORIGINAL_EVENT = 1; + private static final int PENDING_ACK_CANCEL_EVENT = 2; + + private long mEventTime; + private TouchPoint[] mTouchPoints; + private Handler mHandler = new Handler(); + private int mPendingAckState; + + public void start(long eventTime, TouchPoint[] pts) { + assert mTouchPoints == null; + assert mPendingAckState == PENDING_ACK_NONE; + mEventTime = eventTime; + mTouchPoints = pts; + mHandler.postDelayed(this, TOUCH_EVENT_TIMEOUT); + } + + @Override + public void run() { + TraceEvent.begin("TouchEventTimeout"); + while (!mPendingMotionEvents.isEmpty()) { + MotionEvent nextEvent = mPendingMotionEvents.removeFirst(); + processTouchEvent(nextEvent); + recycleEvent(nextEvent); + } + // We are waiting for 2 ACKs: one for the timed-out event, the other for + // the touchcancel event injected when the timed-out event is ACK'ed. + mPendingAckState = PENDING_ACK_ORIGINAL_EVENT; + TraceEvent.end(); + } + + public boolean hasTimeoutEvent() { + return mPendingAckState != PENDING_ACK_NONE; + } + + /** + * @return Whether the ACK is consumed in this method. + */ + public boolean confirmTouchEvent() { + switch (mPendingAckState) { + case PENDING_ACK_NONE: + // The ACK to the original event is received before timeout. + mHandler.removeCallbacks(this); + mTouchPoints = null; + return false; + case PENDING_ACK_ORIGINAL_EVENT: + TraceEvent.instant("TouchEventTimeout:ConfirmOriginalEvent"); + // The ACK to the original event is received after timeout. + // Inject a touchcancel event. + mPendingAckState = PENDING_ACK_CANCEL_EVENT; + mMotionEventDelegate.sendTouchEvent(mEventTime + TOUCH_EVENT_TIMEOUT, + TouchPoint.TOUCH_EVENT_TYPE_CANCEL, mTouchPoints); + mTouchPoints = null; + return true; + case PENDING_ACK_CANCEL_EVENT: + TraceEvent.instant("TouchEventTimeout:ConfirmCancelEvent"); + // The ACK to the injected touchcancel event is received. + mPendingAckState = PENDING_ACK_NONE; + drainAllPendingEventsUntilNextDown(); + return true; + default: + assert false : "Never reached"; + return false; + } + } + + public void mockTimeout() { + assert !hasTimeoutEvent(); + mHandler.removeCallbacks(this); + run(); + } + + /** + * This is for testing only. + * @return Whether a timeout event has been scheduled but not yet run. + */ + public boolean hasScheduledTimeoutEventForTesting() { + return mTouchPoints != null && mPendingAckState == PENDING_ACK_NONE; + } + } + + private TouchEventTimeoutHandler mTouchEventTimeoutHandler = new TouchEventTimeoutHandler(); + /** * This is an interface to handle MotionEvent related communication with the native side also * access some ContentView specific parameters. @@ -216,6 +321,7 @@ boolean sendGesture( mSnapScrollController = new SnapScrollController(context, mZoomManager); mInputEventsDeliveredAtVSync = inputEventDeliveryMode == ContentViewCore.INPUT_EVENTS_DELIVERED_AT_VSYNC; + mPxToDp = 1.0f / context.getResources().getDisplayMetrics().density; initGestureDetectors(context); } @@ -403,15 +509,67 @@ public boolean onSingleTapConfirmed(MotionEvent e) { } @Override - public boolean onDoubleTap(MotionEvent e) { - sendShowPressCancelIfNecessary(e); - sendMotionEventAsGesture(GESTURE_DOUBLE_TAP, e, null); + public boolean onDoubleTapEvent(MotionEvent e) { + switch (e.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + sendShowPressCancelIfNecessary(e); + mDoubleTapDragZoomAnchorX = e.getX(); + mDoubleTapDragZoomAnchorY = e.getY(); + mDoubleTapDragMode = DOUBLE_TAP_DRAG_MODE_DETECTION_IN_PROGRESS; + break; + case MotionEvent.ACTION_MOVE: + if (mDoubleTapDragMode + == DOUBLE_TAP_DRAG_MODE_DETECTION_IN_PROGRESS) { + float distanceX = mDoubleTapDragZoomAnchorX - e.getX(); + float distanceY = mDoubleTapDragZoomAnchorY - e.getY(); + + // Begin double tap drag zoom mode if the move distance is + // further than the threshold. + if (distanceX * distanceX + distanceY * distanceY > + mScaledTouchSlopSquare) { + sendGesture(GESTURE_SCROLL_START, e.getEventTime(), + (int) e.getX(), (int) e.getY(), null); + pinchBegin(e.getEventTime(), + Math.round(mDoubleTapDragZoomAnchorX), + Math.round(mDoubleTapDragZoomAnchorY)); + mDoubleTapDragMode = DOUBLE_TAP_DRAG_MODE_ZOOM; + } + } else if (mDoubleTapDragMode == DOUBLE_TAP_DRAG_MODE_ZOOM) { + mExtraParamBundle.clear(); + sendGesture(GESTURE_SCROLL_BY, e.getEventTime(), + (int) e.getX(), (int) e.getY(), mExtraParamBundle); + + float dy = mDoubleTapY - e.getY(); + pinchBy(e.getEventTime(), + Math.round(mDoubleTapDragZoomAnchorX), + Math.round(mDoubleTapDragZoomAnchorY), + (float) Math.pow(dy < 0 ? + 1.0f - DOUBLE_TAP_DRAG_ZOOM_SPEED : + 1.0f + DOUBLE_TAP_DRAG_ZOOM_SPEED, + Math.abs(dy * mPxToDp))); + } + break; + case MotionEvent.ACTION_UP: + if (mDoubleTapDragMode != DOUBLE_TAP_DRAG_MODE_ZOOM) { + // Normal double tap gesture. + sendMotionEventAsGesture(GESTURE_DOUBLE_TAP, e, null); + } + endDoubleTapDragMode(e); + break; + case MotionEvent.ACTION_CANCEL: + endDoubleTapDragMode(e); + break; + default: + break; + } + mDoubleTapY = e.getY(); return true; } @Override public void onLongPress(MotionEvent e) { - if (!mZoomManager.isScaleGestureDetectionInProgress()) { + if (!mZoomManager.isScaleGestureDetectionInProgress() && + mDoubleTapDragMode == DOUBLE_TAP_DRAG_MODE_NONE) { sendShowPressCancelIfNecessary(e); sendMotionEventAsGesture(GESTURE_LONG_PRESS, e, null); } @@ -502,6 +660,21 @@ void endFling(long timeMs) { } } + /** + * End DOUBLE_TAP_DRAG_MODE_ZOOM by sending GESTURE_SCROLL_END and GESTURE_PINCH_END events. + * @param event A hint event that its x, y, and eventTime will be used for the ending events + * to send. This argument is an optional and can be null. + */ + void endDoubleTapDragMode(MotionEvent event) { + if (mDoubleTapDragMode == DOUBLE_TAP_DRAG_MODE_ZOOM) { + if (event == null) event = obtainActionCancelMotionEvent(); + pinchEnd(event.getEventTime()); + sendGesture(GESTURE_SCROLL_END, event.getEventTime(), + (int) event.getX(), (int) event.getY(), null); + } + mDoubleTapDragMode = DOUBLE_TAP_DRAG_MODE_NONE; + } + // If native thinks scrolling (or fling-scrolling) is going on, tell native // it has ended. private void tellNativeScrollingHasEnded(long timeMs, boolean sendScrollEndEvent) { @@ -569,12 +742,6 @@ void setIgnoreSingleTap(boolean value) { mIgnoreSingleTap = value; } - private float calculateDragAngle(float dx, float dy) { - dx = Math.abs(dx); - dy = Math.abs(dy); - return (float) Math.atan2(dy, dx); - } - private void setClickXAndY(int x, int y) { mSingleTapX = x; mSingleTapY = y; @@ -611,6 +778,8 @@ boolean onTouchEvent(MotionEvent event) { mNoTouchHandlerForGesture = false; mJavaScriptIsConsumingGesture = false; endFling(event.getEventTime()); + } else if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { + endDoubleTapDragMode(null); } if (offerTouchEventToJavaScript(event)) { @@ -676,12 +845,15 @@ private boolean offerTouchEventToJavaScript(MotionEvent event) { return true; } // Avoid flooding the renderer process with move events: if the previous pending - // command is also a move (common case), skip sending this event to the webkit - // side and collapse it into the pending event. + // command is also a move (common case) that has not yet been forwarded, skip sending + // this event to the webkit side and collapse it into the pending event. MotionEvent previousEvent = mPendingMotionEvents.peekLast(); if (previousEvent != null + && previousEvent != mPendingMotionEvents.peekFirst() && previousEvent.getActionMasked() == MotionEvent.ACTION_MOVE && previousEvent.getPointerCount() == event.getPointerCount()) { + TraceEvent.instant("offerTouchEventToJavaScript:EventCoalesced", + "QueueSize = " + mPendingMotionEvents.size()); MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[event.getPointerCount()]; for (int i = 0; i < coords.length; ++i) { @@ -692,39 +864,66 @@ private boolean offerTouchEventToJavaScript(MotionEvent event) { return true; } } - int forward = EVENT_NOT_FORWARDED; if (mPendingMotionEvents.isEmpty()) { - forward = sendTouchEventToNative(event); - } - if (!mPendingMotionEvents.isEmpty() || forward != EVENT_NOT_FORWARDED) { + // Add the event to the pending queue prior to calling sendPendingEventToNative. + // When sending an event to native, the callback to confirmTouchEvent can be + // synchronous or asynchronous and confirmTouchEvent expects the event to be + // in the queue when it is called. + MotionEvent clone = MotionEvent.obtain(event); + mPendingMotionEvents.add(clone); + + int forward = sendPendingEventToNative(); + if (forward == EVENT_NOT_FORWARDED) mPendingMotionEvents.remove(clone); + return forward != EVENT_NOT_FORWARDED; + } else { + TraceEvent.instant("offerTouchEventToJavaScript:EventQueued", + "QueueSize = " + mPendingMotionEvents.size()); // Copy the event, as the original may get mutated after this method returns. MotionEvent clone = MotionEvent.obtain(event); mPendingMotionEvents.add(clone); - // If touch cancel was sent, remember the event. - if (forward == EVENT_CONVERTED_TO_CANCEL) { - mLastCancelledEvent = clone; - } return true; } - return false; } - private int sendTouchEventToNative(MotionEvent event) { + private int sendPendingEventToNative() { + MotionEvent event = mPendingMotionEvents.peekFirst(); + if (event == null) { + assert false : "Cannot send from an empty pending event queue"; + return EVENT_NOT_FORWARDED; + } + + if (mTouchEventTimeoutHandler.hasTimeoutEvent()) return EVENT_NOT_FORWARDED; + TouchPoint[] pts = new TouchPoint[event.getPointerCount()]; int type = TouchPoint.createTouchPoints(event, pts); - if (type != TouchPoint.CONVERSION_ERROR) { - if (!mNativeScrolling && !mPinchInProgress) { - mTouchCancelEventSent = false; - if (mMotionEventDelegate.sendTouchEvent(event.getEventTime(), type, pts)) { - return EVENT_FORWARDED_TO_NATIVE; - } - } else if (!mTouchCancelEventSent) { - mTouchCancelEventSent = true; - if (mMotionEventDelegate.sendTouchEvent(event.getEventTime(), - TouchPoint.TOUCH_EVENT_TYPE_CANCEL, pts)) { - return EVENT_CONVERTED_TO_CANCEL; + if (type == TouchPoint.CONVERSION_ERROR) return EVENT_NOT_FORWARDED; + + if (!mNativeScrolling && !mPinchInProgress) { + mTouchCancelEventSent = false; + + if (mMotionEventDelegate.sendTouchEvent(event.getEventTime(), type, pts)) { + // If confirmTouchEvent() is called synchronously with respect to sendTouchEvent(), + // then |event| will have been recycled. Only start the timer if the sent event has + // not yet been confirmed. + if (event == mPendingMotionEvents.peekFirst() + && event.getAction() != MotionEvent.ACTION_UP + && event.getAction() != MotionEvent.ACTION_CANCEL) { + mTouchEventTimeoutHandler.start(event.getEventTime(), pts); } + return EVENT_FORWARDED_TO_NATIVE; + } + } else if (!mTouchCancelEventSent) { + mTouchCancelEventSent = true; + + MotionEvent previousCancelEvent = mLastCancelledEvent; + mLastCancelledEvent = event; + + if (mMotionEventDelegate.sendTouchEvent(event.getEventTime(), + TouchPoint.TOUCH_EVENT_TYPE_CANCEL, pts)) { + return EVENT_CONVERTED_TO_CANCEL; + } else { + mLastCancelledEvent = previousCancelEvent; } } return EVENT_NOT_FORWARDED; @@ -767,21 +966,30 @@ private boolean processTouchEvent(MotionEvent event) { return handled; } + /** + * For testing to simulate a timeout of a touch event handler. + */ + void mockTouchEventTimeout() { + mTouchEventTimeoutHandler.mockTimeout(); + } + /** * Respond to a MotionEvent being returned from the native side. - * @param handled Whether the MotionEvent was handled on the native side. + * @param ackResult The status acknowledgment code. */ void confirmTouchEvent(int ackResult) { + if (mTouchEventTimeoutHandler.confirmTouchEvent()) return; if (mPendingMotionEvents.isEmpty()) { Log.w(TAG, "confirmTouchEvent with Empty pending list!"); return; } - TraceEvent.begin(); + TraceEvent.begin("confirmTouchEvent"); MotionEvent ackedEvent = mPendingMotionEvents.removeFirst(); - if (ackedEvent.equals(mLastCancelledEvent)) { + if (ackedEvent == mLastCancelledEvent) { // The event is canceled, just drain all the pending events until next // touch down. ackResult = INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS; + TraceEvent.instant("confirmTouchEvent:CanceledEvent"); } switch (ackResult) { case INPUT_EVENT_ACK_STATE_UNKNOWN: @@ -791,15 +999,11 @@ void confirmTouchEvent(int ackResult) { case INPUT_EVENT_ACK_STATE_CONSUMED: mJavaScriptIsConsumingGesture = true; mZoomManager.passTouchEventThrough(ackedEvent); - if (!mPendingMotionEvents.isEmpty()) { - trySendNextEventToNative(mPendingMotionEvents.peekFirst()); - } + trySendPendingEventsToNative(); break; case INPUT_EVENT_ACK_STATE_NOT_CONSUMED: if (!mJavaScriptIsConsumingGesture) processTouchEvent(ackedEvent); - if (!mPendingMotionEvents.isEmpty()) { - trySendNextEventToNative(mPendingMotionEvents.peekFirst()); - } + trySendPendingEventsToNative(); break; case INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS: mNoTouchHandlerForGesture = true; @@ -812,25 +1016,21 @@ void confirmTouchEvent(int ackResult) { mLongPressDetector.cancelLongPressIfNeeded(mPendingMotionEvents.iterator()); - ackedEvent.recycle(); - TraceEvent.end(); + recycleEvent(ackedEvent); + TraceEvent.end("confirmTouchEvent"); } - private void trySendNextEventToNative(MotionEvent nextEvent) { - assert(nextEvent != null); + private void trySendPendingEventsToNative() { + while (!mPendingMotionEvents.isEmpty()) { + int forward = sendPendingEventToNative(); + if (forward != EVENT_NOT_FORWARDED) break; - int forward = sendTouchEventToNative(nextEvent); - if (forward == EVENT_NOT_FORWARDED) { - if (!mJavaScriptIsConsumingGesture) processTouchEvent(nextEvent); - mPendingMotionEvents.removeFirst(); // Even though we missed sending one event to native, as long as we haven't // received INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS, we should keep sending // events on the queue to native. - if (!mPendingMotionEvents.isEmpty()) { - trySendNextEventToNative(mPendingMotionEvents.peekFirst()); - } - } else if (forward == EVENT_CONVERTED_TO_CANCEL) { - mLastCancelledEvent = mPendingMotionEvents.peekFirst(); + MotionEvent event = mPendingMotionEvents.removeFirst(); + if (!mJavaScriptIsConsumingGesture) processTouchEvent(event); + recycleEvent(event); } } @@ -840,14 +1040,21 @@ private void drainAllPendingEventsUntilNextDown() { while (nextEvent != null && nextEvent.getActionMasked() != MotionEvent.ACTION_DOWN) { processTouchEvent(nextEvent); mPendingMotionEvents.removeFirst(); - nextEvent.recycle(); + recycleEvent(nextEvent); nextEvent = mPendingMotionEvents.peekFirst(); } if (nextEvent == null) return; mNoTouchHandlerForGesture = false; - trySendNextEventToNative(nextEvent); + trySendPendingEventsToNative(); + } + + private void recycleEvent(MotionEvent event) { + if (event == mLastCancelledEvent) { + mLastCancelledEvent = null; + } + event.recycle(); } private boolean sendMotionEventAsGesture( @@ -911,7 +1118,7 @@ MotionEvent peekFirstInPendingMotionEventsForTesting() { * @return Whether the motion event is cancelled. */ boolean isEventCancelledForTesting(MotionEvent event) { - return event != null && event.equals(mLastCancelledEvent); + return event != null && event == mLastCancelledEvent; } /** @@ -931,4 +1138,12 @@ void sendShowPressedStateGestureForTesting() { if (mCurrentDownEvent == null) return; mListener.onShowPress(mCurrentDownEvent); } + + /** + * This is for testing only. + * @return Whether a touch timeout event has been scheduled. + */ + boolean hasScheduledTouchTimeoutEventForTesting() { + return mTouchEventTimeoutHandler.hasScheduledTimeoutEventForTesting(); + } } diff --git a/src/org/chromium/content/browser/ContentViewRenderView.java b/src/org/chromium/content/browser/ContentViewRenderView.java index ddd6b28..469c756 100644 --- a/src/org/chromium/content/browser/ContentViewRenderView.java +++ b/src/org/chromium/content/browser/ContentViewRenderView.java @@ -23,6 +23,7 @@ public class ContentViewRenderView extends FrameLayout { // The native side of this object. private int mNativeContentViewRenderView = 0; + private final SurfaceHolder.Callback mSurfaceCallback; private SurfaceView mSurfaceView; private VSyncAdapter mVSyncAdapter; @@ -41,9 +42,10 @@ public ContentViewRenderView(Context context) { assert mNativeContentViewRenderView != 0; mSurfaceView = createSurfaceView(getContext()); - mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { + mSurfaceCallback = new SurfaceHolder.Callback() { @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + assert mNativeContentViewRenderView != 0; nativeSurfaceSetSize(mNativeContentViewRenderView, width, height); if (mCurrentContentView != null) { mCurrentContentView.getContentViewCore().onPhysicalBackingSizeChanged( @@ -53,15 +55,18 @@ public void surfaceChanged(SurfaceHolder holder, int format, int width, int heig @Override public void surfaceCreated(SurfaceHolder holder) { + assert mNativeContentViewRenderView != 0; nativeSurfaceCreated(mNativeContentViewRenderView, holder.getSurface()); onReadyToRender(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { + assert mNativeContentViewRenderView != 0; nativeSurfaceDestroyed(mNativeContentViewRenderView); } - }); + }; + mSurfaceView.getHolder().addCallback(mSurfaceCallback); mVSyncAdapter = new VSyncAdapter(getContext()); addView(mSurfaceView, @@ -124,13 +129,16 @@ void setVSyncListener(VSyncManager.Listener listener) { * native resource can be freed. */ public void destroy() { + mSurfaceView.getHolder().removeCallback(mSurfaceCallback); nativeDestroy(mNativeContentViewRenderView); + mNativeContentViewRenderView = 0; } /** * Makes the passed ContentView the one displayed by this ContentViewRenderView. */ public void setCurrentContentView(ContentView contentView) { + assert mNativeContentViewRenderView != 0; ContentViewCore contentViewCore = contentView.getContentViewCore(); nativeSetCurrentContentView(mNativeContentViewRenderView, contentViewCore.getNativeContentViewCore()); diff --git a/src/org/chromium/content/browser/DeviceMotionAndOrientation.java b/src/org/chromium/content/browser/DeviceMotionAndOrientation.java index ecb9d62..54a0c56 100644 --- a/src/org/chromium/content/browser/DeviceMotionAndOrientation.java +++ b/src/org/chromium/content/browser/DeviceMotionAndOrientation.java @@ -48,7 +48,7 @@ class DeviceMotionAndOrientation implements SensorEventListener { private Object mNativePtrLock = new Object(); // The acceleration vector including gravity expressed in the body frame. - private float[] mAccelerationVector; + private float[] mAccelerationIncludingGravityVector; // The geomagnetic vector expressed in the body frame. private float[] mMagneticFieldVector; @@ -120,6 +120,13 @@ public boolean start(int nativePtr, int eventType, int rateInMilliseconds) { } } + @CalledByNative + public int getNumberActiveDeviceMotionSensors() { + Set deviceMotionSensors = Sets.newHashSet(DEVICE_MOTION_SENSORS); + deviceMotionSensors.removeAll(mActiveSensors); + return DEVICE_MOTION_SENSORS.size() - deviceMotionSensors.size(); + } + /** * Stop listening to sensors for a given event type. Ensures that sensors are not disabled * if they are still in use by a different event type. @@ -174,14 +181,19 @@ void sensorChanged(int type, float[] values) { switch (type) { case Sensor.TYPE_ACCELEROMETER: - if (mAccelerationVector == null) { - mAccelerationVector = new float[3]; + if (mAccelerationIncludingGravityVector == null) { + mAccelerationIncludingGravityVector = new float[3]; } - System.arraycopy(values, 0, mAccelerationVector, 0, - mAccelerationVector.length); + System.arraycopy(values, 0, mAccelerationIncludingGravityVector, + 0, mAccelerationIncludingGravityVector.length); if (mDeviceMotionIsActive) { - gotAccelerationIncludingGravity(mAccelerationVector[0], mAccelerationVector[1], - mAccelerationVector[2]); + gotAccelerationIncludingGravity( + mAccelerationIncludingGravityVector[0], + mAccelerationIncludingGravityVector[1], + mAccelerationIncludingGravityVector[2]); + } + if (mDeviceOrientationIsActive) { + getOrientationUsingGetRotationMatrix(); } break; case Sensor.TYPE_LINEAR_ACCELERATION: @@ -200,19 +212,18 @@ void sensorChanged(int type, float[] values) { } System.arraycopy(values, 0, mMagneticFieldVector, 0, mMagneticFieldVector.length); + if (mDeviceOrientationIsActive) { + getOrientationUsingGetRotationMatrix(); + } break; default: // Unexpected return; } - - if (mDeviceOrientationIsActive) { - getOrientationUsingGetRotationMatrix(); - } } private void getOrientationUsingGetRotationMatrix() { - if (mAccelerationVector == null || mMagneticFieldVector == null) { + if (mAccelerationIncludingGravityVector == null || mMagneticFieldVector == null) { return; } @@ -220,8 +231,8 @@ private void getOrientationUsingGetRotationMatrix() { // The rotation matrix that transforms from the body frame to the earth // frame. float[] deviceRotationMatrix = new float[9]; - if (!SensorManager.getRotationMatrix(deviceRotationMatrix, null, mAccelerationVector, - mMagneticFieldVector)) { + if (!SensorManager.getRotationMatrix(deviceRotationMatrix, null, + mAccelerationIncludingGravityVector, mMagneticFieldVector)) { return; } diff --git a/src/org/chromium/content/browser/DeviceUtils.java b/src/org/chromium/content/browser/DeviceUtils.java index 30bb418..523e2ec 100644 --- a/src/org/chromium/content/browser/DeviceUtils.java +++ b/src/org/chromium/content/browser/DeviceUtils.java @@ -19,16 +19,24 @@ public class DeviceUtils { */ private static final int MINIMUM_TABLET_WIDTH_DP = 600; + private static Boolean sIsTv = null; + private static Boolean sIsTablet = null; + /** * @param context Android's context * @return Whether the app is should treat the device as a tablet for layout. */ public static boolean isTablet(Context context) { - if (isTv(context)) { - return true; + if (sIsTablet == null) { + if (isTv(context)) { + sIsTablet = true; + return sIsTablet; + } + int minimumScreenWidthDp = context.getResources().getConfiguration(). + smallestScreenWidthDp; + sIsTablet = minimumScreenWidthDp >= MINIMUM_TABLET_WIDTH_DP; } - int minimumScreenWidthDp = context.getResources().getConfiguration().smallestScreenWidthDp; - return minimumScreenWidthDp >= MINIMUM_TABLET_WIDTH_DP; + return sIsTablet; } /** @@ -40,11 +48,15 @@ public static boolean isTablet(Context context) { * @return {@code true} if the device should be treated as TV. */ public static boolean isTv(Context context) { - PackageManager manager = context.getPackageManager(); - if (manager != null) { - return manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION); + if (sIsTv == null) { + PackageManager manager = context.getPackageManager(); + if (manager != null) { + sIsTv = manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION); + return sIsTv; + } + sIsTv = false; } - return false; + return sIsTv; } /** diff --git a/src/org/chromium/content/browser/DownloadController.java b/src/org/chromium/content/browser/DownloadController.java index 9577ba1..cac5f97 100644 --- a/src/org/chromium/content/browser/DownloadController.java +++ b/src/org/chromium/content/browser/DownloadController.java @@ -7,16 +7,38 @@ import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; +import android.content.Context; + /** * Java counterpart of android DownloadController. * * Its a singleton class instantiated by the C++ DownloadController. */ @JNINamespace("content") -class DownloadController { +public class DownloadController { private static final String LOGTAG = "DownloadController"; private static DownloadController sInstance; + /** + * Class for notifying the application that download has completed. + */ + public interface DownloadNotificationService { + /** + * Notify the host application that a download is finished. + * @param context Application context. + * @param url The full url to the content that was downloaded. + * @param mimetype The mimetype of downloaded file. + * @param path Path of the downloaded file. + * @param description Description of the downloaded file. + * @param contentLength The file size of the downloaded file (in bytes). + * @param successful Whether the download succeeded. + */ + void onDownloadCompleted(Context context, String url, String mimetype, String path, + String description, long contentLength, boolean successful); + } + + private static DownloadNotificationService sDownloadNotificationService; + @CalledByNative public static DownloadController getInstance() { if (sInstance == null) { @@ -33,6 +55,10 @@ private static ContentViewDownloadDelegate downloadDelegateFromView(ContentViewC return view.getDownloadDelegate(); } + public static void setDownloadNotificationService(DownloadNotificationService service) { + sDownloadNotificationService = service; + } + /** * Notifies the download delegate of a new GET download and passes all the information * needed to download the file. @@ -54,13 +80,16 @@ public void newHttpGetDownload(ContentViewCore view, String url, /** * Notifies the download delegate that a new download has started. This can * be either a POST download or a GET download with authentication. + * @param view ContentViewCore associated with the download item. + * @param filename File name of the downloaded file. + * @param mimeType Mime of the downloaded item. */ @CalledByNative - public void onDownloadStarted(ContentViewCore view) { + public void onDownloadStarted(ContentViewCore view, String filename, String mimeType) { ContentViewDownloadDelegate downloadDelagate = downloadDelegateFromView(view); if (downloadDelagate != null) { - downloadDelagate.onDownloadStarted(); + downloadDelagate.onDownloadStarted(filename, mimeType); } } @@ -69,14 +98,11 @@ public void onDownloadStarted(ContentViewCore view) { * download. This can be either a POST download or a GET download with authentication. */ @CalledByNative - public void onDownloadCompleted(ContentViewCore view, String url, - String contentDisposition, String mimetype, String path, - long contentLength, boolean successful) { - ContentViewDownloadDelegate downloadDelagate = downloadDelegateFromView(view); - - if (downloadDelagate != null) { - downloadDelagate.onDownloadCompleted( - url, mimetype, path, contentLength, successful); + public void onDownloadCompleted(Context context, String url, String mimetype, + String filename, String path, long contentLength, boolean successful) { + if (sDownloadNotificationService != null) { + sDownloadNotificationService.onDownloadCompleted(context, url, mimetype, path, + filename, contentLength, successful); } } diff --git a/src/org/chromium/content/browser/JellyBeanContentView.java b/src/org/chromium/content/browser/JellyBeanContentView.java index 7456ffe..7cca043 100644 --- a/src/org/chromium/content/browser/JellyBeanContentView.java +++ b/src/org/chromium/content/browser/JellyBeanContentView.java @@ -9,6 +9,7 @@ import android.util.AttributeSet; import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; import org.chromium.ui.WindowAndroid; @@ -29,4 +30,14 @@ public boolean performAccessibilityAction(int action, Bundle arguments) { return super.performAccessibilityAction(action, arguments); } + + @Override + public AccessibilityNodeProvider getAccessibilityNodeProvider() { + AccessibilityNodeProvider provider = getContentViewCore().getAccessibilityNodeProvider(); + if (provider != null) { + return provider; + } else { + return super.getAccessibilityNodeProvider(); + } + } } diff --git a/src/org/chromium/content/browser/LocationProvider.java b/src/org/chromium/content/browser/LocationProvider.java index d1c5e5a..8b8eb29 100644 --- a/src/org/chromium/content/browser/LocationProvider.java +++ b/src/org/chromium/content/browser/LocationProvider.java @@ -17,6 +17,7 @@ import org.chromium.base.CalledByNative; import org.chromium.base.ThreadUtils; +import java.util.List; import java.util.concurrent.FutureTask; /** @@ -102,15 +103,19 @@ public void onLocationChanged(Location location) { // possible that we receive callbacks after unregistering. At this point, the // native object will no longer exist. if (mIsRunning) { - nativeNewLocationAvailable(location.getLatitude(), location.getLongitude(), - location.getTime() / 1000.0, - location.hasAltitude(), location.getAltitude(), - location.hasAccuracy(), location.getAccuracy(), - location.hasBearing(), location.getBearing(), - location.hasSpeed(), location.getSpeed()); + updateNewLocation(location); } } + private void updateNewLocation(Location location) { + nativeNewLocationAvailable(location.getLatitude(), location.getLongitude(), + location.getTime() / 1000.0, + location.hasAltitude(), location.getAltitude(), + location.hasAccuracy(), location.getAccuracy(), + location.hasBearing(), location.getBearing(), + location.hasSpeed(), location.getSpeed()); + } + @Override public void onStatusChanged(String provider, int status, Bundle extras) { } @@ -137,6 +142,7 @@ private void ensureLocationManagerCreated() { */ private void registerForLocationUpdates() { ensureLocationManagerCreated(); + if (usePassiveOneShotLocation()) return; assert !mIsRunning; mIsRunning = true; @@ -169,6 +175,35 @@ private void unregisterFromLocationUpdates() { mLocationManager.removeUpdates(this); } } + + private boolean usePassiveOneShotLocation() { + if (!isOnlyPassiveLocationProviderEnabled()) return false; + + // Do not request a location update if the only available location provider is + // the passive one. Make use of the last known location and call + // onLocationChanged directly. + final Location location = mLocationManager.getLastKnownLocation( + LocationManager.PASSIVE_PROVIDER); + if (location != null) { + ThreadUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + updateNewLocation(location); + } + }); + } + return true; + } + + /* + * Checks if the passive location provider is the only provider available + * in the system. + */ + private boolean isOnlyPassiveLocationProviderEnabled() { + List providers = mLocationManager.getProviders(true); + return providers != null && providers.size() == 1 + && providers.get(0).equals(LocationManager.PASSIVE_PROVIDER); + } } // Delegate handling the real work in the main thread. diff --git a/src/org/chromium/content/browser/LongPressDetector.java b/src/org/chromium/content/browser/LongPressDetector.java index d46d485..eab3465 100644 --- a/src/org/chromium/content/browser/LongPressDetector.java +++ b/src/org/chromium/content/browser/LongPressDetector.java @@ -67,6 +67,9 @@ public void handleMessage(Message msg) { * This is an interface to execute the LongPress when it receives the onLongPress message. */ interface LongPressDelegate { + /** + * @param event The event will be recycled after this call has returned. + */ public void onLongPress(MotionEvent event); } @@ -74,9 +77,11 @@ interface LongPressDelegate { * Initiates a LONG_PRESS gesture timer if needed. */ void startLongPressTimerIfNeeded(MotionEvent ev) { - if (!canHandle(ev)) return; + if (ev.getAction() != MotionEvent.ACTION_DOWN) return; - if (mCurrentDownEvent != null) mCurrentDownEvent.recycle(); + // If there is a current down, we do not expect another down event before + // receiving an up event + if (mCurrentDownEvent != null) return; mCurrentDownEvent = MotionEvent.obtain(ev); mLongPressHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime() @@ -84,10 +89,6 @@ void startLongPressTimerIfNeeded(MotionEvent ev) { mInLongPress = false; } - private boolean canHandle(MotionEvent ev) { - return ev.getAction() == MotionEvent.ACTION_DOWN; - } - // Cancel LONG_PRESS timers. void cancelLongPressIfNeeded(MotionEvent ev) { if (!hasPendingMessage() || @@ -103,16 +104,14 @@ void cancelLongPressIfNeeded(MotionEvent ev) { final int deltaY = (int) (y - mCurrentDownEvent.getY()); int distance = (deltaX * deltaX) + (deltaY * deltaY); if (distance > mTouchSlopSquare) { - mInLongPress = false; - mLongPressHandler.removeMessages(LONG_PRESS); + cancelLongPress(); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (mCurrentDownEvent.getDownTime() + TAP_TIMEOUT + LONGPRESS_TIMEOUT > ev.getEventTime()) { - mInLongPress = false; - mLongPressHandler.removeMessages(LONG_PRESS); + cancelLongPress(); } break; default: @@ -135,8 +134,11 @@ void cancelLongPressIfNeeded(Iterator pendingEvents) { } void cancelLongPress() { - if (mLongPressHandler.hasMessages(LONG_PRESS)) { + mInLongPress = false; + if (hasPendingMessage()) { mLongPressHandler.removeMessages(LONG_PRESS); + mCurrentDownEvent.recycle(); + mCurrentDownEvent = null; } } @@ -148,10 +150,12 @@ boolean isInLongPress() { private void dispatchLongPress() { mInLongPress = true; mLongPressDelegate.onLongPress(mCurrentDownEvent); + mCurrentDownEvent.recycle(); + mCurrentDownEvent = null; } boolean hasPendingMessage() { - return mLongPressHandler.hasMessages(LONG_PRESS); + return mCurrentDownEvent != null; } void onOfferTouchEventToJavaScript(MotionEvent event) { diff --git a/src/org/chromium/content/browser/PageTransitionTypes.java b/src/org/chromium/content/browser/PageTransitionTypes.java index f344ad1..ae48610 100644 --- a/src/org/chromium/content/browser/PageTransitionTypes.java +++ b/src/org/chromium/content/browser/PageTransitionTypes.java @@ -1,4 +1,9 @@ + + + + package org.chromium.content.browser; + public class PageTransitionTypes { public static final int PAGE_TRANSITION_LINK = 0; public static final int PAGE_TRANSITION_TYPED = 1; @@ -17,7 +22,7 @@ public class PageTransitionTypes { public static final int PAGE_TRANSITION_FORWARD_BACK = 0x01000000; public static final int PAGE_TRANSITION_FROM_ADDRESS_BAR = 0x02000000; public static final int PAGE_TRANSITION_HOME_PAGE = 0x04000000; -public static final int PAGE_TRANSITION_FROM_INTENT = 0x08000000; +public static final int PAGE_TRANSITION_FROM_API = 0x08000000; public static final int PAGE_TRANSITION_CHAIN_START = 0x10000000; public static final int PAGE_TRANSITION_CHAIN_END = 0x20000000; public static final int PAGE_TRANSITION_CLIENT_REDIRECT = 0x40000000; diff --git a/src/org/chromium/content/browser/PepperPluginManager.java b/src/org/chromium/content/browser/PepperPluginManager.java index ad594b8..dbb7108 100644 --- a/src/org/chromium/content/browser/PepperPluginManager.java +++ b/src/org/chromium/content/browser/PepperPluginManager.java @@ -17,6 +17,8 @@ import java.util.List; +import org.chromium.base.ContextTypes; + /** * {@link PepperPluginManager} collects meta data about plugins from preloaded android apps * that reply to PEPPERPLUGIN intent query. @@ -88,6 +90,11 @@ private static String getPluginDescription(Bundle metaData) { * @return Description string for plugins */ public static String getPlugins(final Context context) { + if (DeviceUtils.isTv(context) && + !ContextTypes.isRunningInWebapp(context)) { + // Chrome-for-tv enables plugins only on webapp mode. + return null; + } StringBuffer ret = new StringBuffer(); PackageManager pm = context.getPackageManager(); List plugins = pm.queryIntentServices( diff --git a/src/org/chromium/content/browser/PowerSaveBlocker.java b/src/org/chromium/content/browser/PowerSaveBlocker.java new file mode 100644 index 0000000..26a9d62 --- /dev/null +++ b/src/org/chromium/content/browser/PowerSaveBlocker.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser; + +import org.chromium.base.CalledByNative; +import org.chromium.ui.WindowAndroid; + +class PowerSaveBlocker { + @CalledByNative + private static void applyBlock(WindowAndroid windowAndroid) { + windowAndroid.keepScreenOn(true); + } + + @CalledByNative + private static void removeBlock(WindowAndroid windowAndroid) { + windowAndroid.keepScreenOn(false); + } +} \ No newline at end of file diff --git a/src/org/chromium/content/browser/SelectActionModeCallback.java b/src/org/chromium/content/browser/SelectActionModeCallback.java index 14bea2b..4ff58bd 100644 --- a/src/org/chromium/content/browser/SelectActionModeCallback.java +++ b/src/org/chromium/content/browser/SelectActionModeCallback.java @@ -104,6 +104,7 @@ protected Context getContext() { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mode.setTitle(null); mode.setSubtitle(null); mEditable = mActionHandler.isSelectionEditable(); createActionMenu(mode, menu); diff --git a/src/org/chromium/content/browser/SmoothScroller.java b/src/org/chromium/content/browser/SmoothScroller.java index f95f82d..5c6c339 100644 --- a/src/org/chromium/content/browser/SmoothScroller.java +++ b/src/org/chromium/content/browser/SmoothScroller.java @@ -44,7 +44,7 @@ public class SmoothScroller { int mouseEventX, int mouseEventY) { mContentViewCore = contentViewCore; mScrollDown = scrollDown; - float scale = mContentViewCore.getScale(); + float scale = mContentViewCore.getRenderCoordinates().getDeviceScaleFactor(); mMouseEventX = mouseEventX * scale; mMouseEventY = mouseEventY * scale; mCurrentY = mMouseEventY; @@ -72,7 +72,8 @@ boolean sendEvent(long time) { break; } case STATE_MOVING: { - double delta = nativeGetScrollDelta(mNativePtr, mContentViewCore.getScale()); + double delta = nativeGetScrollDelta( + mNativePtr, mContentViewCore.getRenderCoordinates().getDeviceScaleFactor()); if (delta != 0) { mCurrentY += mScrollDown ? -delta : delta; diff --git a/src/org/chromium/content/browser/SnapScrollController.java b/src/org/chromium/content/browser/SnapScrollController.java index b457bef..25379e3 100644 --- a/src/org/chromium/content/browser/SnapScrollController.java +++ b/src/org/chromium/content/browser/SnapScrollController.java @@ -13,7 +13,7 @@ * This objects controls the scroll snapping behavior based on scroll updates. */ class SnapScrollController { - private static final String TAG = SnapScrollController.class.toString(); + private static final String TAG = "SnapScrollController"; private static final int SNAP_NONE = 0; private static final int SNAP_HORIZ = 1; private static final int SNAP_VERT = 2; diff --git a/src/org/chromium/content/browser/SpeechRecognition.java b/src/org/chromium/content/browser/SpeechRecognition.java new file mode 100644 index 0000000..ebed46f --- /dev/null +++ b/src/org/chromium/content/browser/SpeechRecognition.java @@ -0,0 +1,295 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Bundle; +import android.speech.RecognitionListener; +import android.speech.RecognitionService; +import android.speech.RecognizerIntent; +import android.speech.SpeechRecognizer; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.content.browser.SpeechRecognitionError; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class uses Android's SpeechRecognizer to perform speech recognition for the Web Speech API + * on Android. Using Android's platform recognizer offers several benefits, like good quality and + * good local fallback when no data connection is available. + */ +@JNINamespace("content") +public class SpeechRecognition { + + // Constants describing the speech recognition provider we depend on. + private static final String PROVIDER_PACKAGE_NAME = "com.google.android.googlequicksearchbox"; + private static final int PROVIDER_MIN_VERSION = 300207030; + + // We track the recognition state to remember what events we need to send when recognition is + // being aborted. Once Android's recognizer is cancelled, its listener won't yield any more + // events, but we still need to call OnSoundEnd and OnAudioEnd if corresponding On*Start were + // called before. + private static final int STATE_IDLE = 0; + private static final int STATE_AWAITING_SPEECH = 1; + private static final int STATE_CAPTURING_SPEECH = 2; + private int mState; + + // The speech recognition provider (if any) matching PROVIDER_PACKAGE_NAME and + // PROVIDER_MIN_VERSION as selected by initialize(). + private static ComponentName mRecognitionProvider; + + private final Context mContext; + private final Intent mIntent; + private final RecognitionListener mListener; + private SpeechRecognizer mRecognizer; + + // Native pointer to C++ SpeechRecognizerImplAndroid. + private int mNativeSpeechRecognizerImplAndroid; + + // Remember if we are using continuous recognition. + private boolean mContinuous; + + // Internal class to handle events from Android's SpeechRecognizer and route them to native. + class Listener implements RecognitionListener { + + @Override + public void onBeginningOfSpeech() { + mState = STATE_CAPTURING_SPEECH; + nativeOnSoundStart(mNativeSpeechRecognizerImplAndroid); + } + + @Override + public void onBufferReceived(byte[] buffer) { } + + @Override + public void onEndOfSpeech() { + // Ignore onEndOfSpeech in continuous mode to let terminate() take care of ending + // events. The Android API documentation is vague as to when onEndOfSpeech is called in + // continuous mode, whereas the Web Speech API defines a stronger semantic on the + // equivalent (onsoundend) event. Thus, the only way to provide a valid onsoundend + // event is to trigger it when the last result is received or the session is aborted. + if (!mContinuous) { + nativeOnSoundEnd(mNativeSpeechRecognizerImplAndroid); + // Since Android doesn't have a dedicated event for when audio capture is finished, + // we fire it after speech has ended. + nativeOnAudioEnd(mNativeSpeechRecognizerImplAndroid); + mState = STATE_IDLE; + } + } + + @Override + public void onError(int error) { + int code = SpeechRecognitionError.NONE; + + // Translate Android SpeechRecognizer errors to Web Speech API errors. + switch(error) { + case SpeechRecognizer.ERROR_AUDIO: + code = SpeechRecognitionError.AUDIO; + break; + case SpeechRecognizer.ERROR_CLIENT: + code = SpeechRecognitionError.ABORTED; + break; + case SpeechRecognizer.ERROR_RECOGNIZER_BUSY: + case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS: + code = SpeechRecognitionError.NOT_ALLOWED; + break; + case SpeechRecognizer.ERROR_NETWORK_TIMEOUT: + case SpeechRecognizer.ERROR_NETWORK: + case SpeechRecognizer.ERROR_SERVER: + code = SpeechRecognitionError.NETWORK; + break; + case SpeechRecognizer.ERROR_NO_MATCH: + code = SpeechRecognitionError.NO_MATCH; + break; + case SpeechRecognizer.ERROR_SPEECH_TIMEOUT: + code = SpeechRecognitionError.NO_SPEECH; + break; + default: + assert false; + return; + } + + terminate(code); + } + + @Override + public void onEvent(int event, Bundle bundle) { } + + @Override + public void onPartialResults(Bundle bundle) { + handleResults(bundle, true); + } + + @Override + public void onReadyForSpeech(Bundle bundle) { + mState = STATE_AWAITING_SPEECH; + nativeOnAudioStart(mNativeSpeechRecognizerImplAndroid); + } + + @Override + public void onResults(Bundle bundle) { + handleResults(bundle, false); + // We assume that onResults is called only once, at the end of a session, thus we + // terminate. If one day the recognition provider changes dictation mode behavior to + // call onResults several times, we should terminate only if (!mContinuous). + terminate(SpeechRecognitionError.NONE); + } + + @Override + public void onRmsChanged(float rms) { } + + private void handleResults(Bundle bundle, boolean provisional) { + if (mContinuous && provisional) { + // In continuous mode, Android's recognizer sends final results as provisional. + provisional = false; + } + + ArrayList list = bundle.getStringArrayList( + SpeechRecognizer.RESULTS_RECOGNITION); + String[] results = list.toArray(new String[list.size()]); + + float[] scores = bundle.getFloatArray(SpeechRecognizer.CONFIDENCE_SCORES); + + nativeOnRecognitionResults(mNativeSpeechRecognizerImplAndroid, + results, + scores, + provisional); + } + } + + // This method must be called before any instance of SpeechRecognition can be created. It will + // query Android's package manager to find a suitable speech recognition provider that supports + // continuous recognition. + public static boolean initialize(Context context) { + if (!SpeechRecognizer.isRecognitionAvailable(context)) + return false; + + PackageManager pm = context.getPackageManager(); + Intent intent = new Intent(RecognitionService.SERVICE_INTERFACE); + final List list = pm.queryIntentServices(intent, PackageManager.GET_SERVICES); + + for (ResolveInfo resolve : list) { + ServiceInfo service = resolve.serviceInfo; + + if (!service.packageName.equals(PROVIDER_PACKAGE_NAME)) + continue; + + int versionCode; + try { + versionCode = pm.getPackageInfo(service.packageName, 0).versionCode; + } catch (NameNotFoundException e) { + continue; + } + + if (versionCode < PROVIDER_MIN_VERSION) + continue; + + mRecognitionProvider = new ComponentName(service.packageName, service.name); + + return true; + } + + // If we reach this point, we failed to find a suitable recognition provider. + return false; + } + + private SpeechRecognition(final Context context, int nativeSpeechRecognizerImplAndroid) { + mContext = context; + mContinuous = false; + mNativeSpeechRecognizerImplAndroid = nativeSpeechRecognizerImplAndroid; + mListener = new Listener(); + mIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + + if (mRecognitionProvider != null) { + mRecognizer = SpeechRecognizer.createSpeechRecognizer(mContext, mRecognitionProvider); + } else { + // It is possible to force-enable the speech recognition web platform feature (using a + // command-line flag) even if initialize() failed to find the PROVIDER_PACKAGE_NAME + // provider, in which case the first available speech recognition provider is used. + // Caveat: Continuous mode may not work as expected with a different provider. + mRecognizer = SpeechRecognizer.createSpeechRecognizer(mContext); + } + + mRecognizer.setRecognitionListener(mListener); + } + + // This function destroys everything when recognition is done, taking care to properly tear + // down by calling On{Sound,Audio}End if corresponding On{Audio,Sound}Start were called. + private void terminate(int error) { + + if (mState != STATE_IDLE) { + if (mState == STATE_CAPTURING_SPEECH) { + nativeOnSoundEnd(mNativeSpeechRecognizerImplAndroid); + } + nativeOnAudioEnd(mNativeSpeechRecognizerImplAndroid); + mState = STATE_IDLE; + } + + if (error != SpeechRecognitionError.NONE) + nativeOnRecognitionError(mNativeSpeechRecognizerImplAndroid, error); + + mRecognizer.destroy(); + mRecognizer = null; + nativeOnRecognitionEnd(mNativeSpeechRecognizerImplAndroid); + mNativeSpeechRecognizerImplAndroid = 0; + } + + @CalledByNative + private static SpeechRecognition createSpeechRecognition( + Context context, int nativeSpeechRecognizerImplAndroid) { + return new SpeechRecognition(context, nativeSpeechRecognizerImplAndroid); + } + + @CalledByNative + private void startRecognition(String language, boolean continuous, boolean interim_results) { + if (mRecognizer == null) + return; + + mContinuous = continuous; + mIntent.putExtra("android.speech.extra.DICTATION_MODE", continuous); + mIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); + mIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, interim_results); + mRecognizer.startListening(mIntent); + } + + @CalledByNative + private void abortRecognition() { + if (mRecognizer == null) + return; + + mRecognizer.cancel(); + terminate(SpeechRecognitionError.ABORTED); + } + + @CalledByNative + private void stopRecognition() { + if (mRecognizer == null) + return; + + mContinuous = false; + mRecognizer.stopListening(); + } + + // Native JNI calls to content/browser/speech/speech_recognizer_impl_android.cc + private native void nativeOnAudioStart(int nativeSpeechRecognizerImplAndroid); + private native void nativeOnSoundStart(int nativeSpeechRecognizerImplAndroid); + private native void nativeOnSoundEnd(int nativeSpeechRecognizerImplAndroid); + private native void nativeOnAudioEnd(int nativeSpeechRecognizerImplAndroid); + private native void nativeOnRecognitionResults(int nativeSpeechRecognizerImplAndroid, + String[] results, + float[] scores, + boolean provisional); + private native void nativeOnRecognitionError(int nativeSpeechRecognizerImplAndroid, int error); + private native void nativeOnRecognitionEnd(int nativeSpeechRecognizerImplAndroid); +} diff --git a/src/org/chromium/content/browser/SpeechRecognitionError.java b/src/org/chromium/content/browser/SpeechRecognitionError.java new file mode 100644 index 0000000..a68d234 --- /dev/null +++ b/src/org/chromium/content/browser/SpeechRecognitionError.java @@ -0,0 +1,35 @@ + + + + +package org.chromium.content.browser; + +public class SpeechRecognitionError { + + + + + +public static final int NONE = 0; + + +public static final int ABORTED = 1; + + +public static final int AUDIO = 2; + + +public static final int NETWORK = 3; + + +public static final int NOT_ALLOWED = 4; + + +public static final int NO_SPEECH = 5; + + +public static final int NO_MATCH = 6; + + +public static final int BAD_GRAMMAR = 7; +} diff --git a/src/org/chromium/content/browser/SpeechRecognitionError.template b/src/org/chromium/content/browser/SpeechRecognitionError.template new file mode 100644 index 0000000..d3a7074 --- /dev/null +++ b/src/org/chromium/content/browser/SpeechRecognitionError.template @@ -0,0 +1,11 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser; + +public class SpeechRecognitionError { +#define DEFINE_SPEECH_RECOGNITION_ERROR(x,y) public static final int x = y; +#include "content/public/common/speech_recognition_error_list.h" +#undef DEFINE_SPEECH_RECOGNITION_ERROR +} diff --git a/src/org/chromium/content/browser/TouchPoint.java b/src/org/chromium/content/browser/TouchPoint.java index 2e44d9e..65c561a 100644 --- a/src/org/chromium/content/browser/TouchPoint.java +++ b/src/org/chromium/content/browser/TouchPoint.java @@ -141,4 +141,15 @@ private static void initializeConstants( TOUCH_POINT_STATE_STATIONARY = touchPointStationary; TOUCH_POINT_STATE_CANCELLED = touchPointCancelled; } + + /** + * Initialize the constants to distinct values if they have not been initialized. + * During pure-Java testing, initializeConstants() may not be called by native code. + * Unit tests should call this method before using the values. + */ + static void initializeConstantsForTesting() { + if (TOUCH_EVENT_TYPE_START == TOUCH_EVENT_TYPE_MOVE) { + initializeConstants(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + } + } } diff --git a/src/org/chromium/content/browser/VSyncMonitor.java b/src/org/chromium/content/browser/VSyncMonitor.java index 961e870..ca08c43 100644 --- a/src/org/chromium/content/browser/VSyncMonitor.java +++ b/src/org/chromium/content/browser/VSyncMonitor.java @@ -21,7 +21,7 @@ * approximation of a vertical sync starting point; see also http://crbug.com/156397. */ public class VSyncMonitor { - private static final String TAG = VSyncMonitor.class.getSimpleName(); + private static final String TAG = "VSyncMonitor"; public interface Listener { /** diff --git a/src/org/chromium/content/browser/VibrationMessageFilter.java b/src/org/chromium/content/browser/VibrationMessageFilter.java new file mode 100644 index 0000000..0e04209 --- /dev/null +++ b/src/org/chromium/content/browser/VibrationMessageFilter.java @@ -0,0 +1,39 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser; + +import android.content.Context; +import android.os.Vibrator; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; + +/** + * This is the implementation of the C++ counterpart VibrationMessageFilter. + */ +@JNINamespace("content") +class VibrationMessageFilter { + + private final Vibrator mVibrator; + + @CalledByNative + private static VibrationMessageFilter create(Context context) { + return new VibrationMessageFilter(context); + } + + @CalledByNative + private void vibrate(long milliseconds) { + mVibrator.vibrate(milliseconds); + } + + @CalledByNative + private void cancelVibration() { + mVibrator.cancel(); + } + + private VibrationMessageFilter(Context context) { + mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + } +} diff --git a/src/org/chromium/content/browser/accessibility/AccessibilityInjector.java b/src/org/chromium/content/browser/accessibility/AccessibilityInjector.java index c0dcf03..4209741 100644 --- a/src/org/chromium/content/browser/accessibility/AccessibilityInjector.java +++ b/src/org/chromium/content/browser/accessibility/AccessibilityInjector.java @@ -40,7 +40,7 @@ * Responsible for accessibility injection and management of a {@link ContentViewCore}. */ public class AccessibilityInjector extends WebContentsObserverAndroid { - private static final String TAG = AccessibilityInjector.class.getSimpleName(); + private static final String TAG = "AccessibilityInjector"; // The ContentView this injector is responsible for managing. protected ContentViewCore mContentViewCore; @@ -130,27 +130,17 @@ public void injectAccessibilityScriptIntoPage() { if (!accessibilityIsAvailable()) return; int axsParameterValue = getAxsUrlParameterValue(); - if (axsParameterValue == ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED) { - try { - Field field = Settings.Secure.class.getField("ACCESSIBILITY_SCRIPT_INJECTION"); - field.setAccessible(true); - String ACCESSIBILITY_SCRIPT_INJECTION = (String) field.get(null); - - boolean onDeviceScriptInjectionEnabled = (Settings.Secure.getInt( - mContentViewCore.getContext().getContentResolver(), - ACCESSIBILITY_SCRIPT_INJECTION, 0) == 1); - String js = getScreenReaderInjectingJs(); - - if (onDeviceScriptInjectionEnabled && js != null && mContentViewCore.isAlive()) { - addOrRemoveAccessibilityApisIfNecessary(); - mContentViewCore.evaluateJavaScript(js, null); - mInjectedScriptEnabled = true; - mScriptInjected = true; - } - } catch (NoSuchFieldException ex) { - } catch (IllegalArgumentException ex) { - } catch (IllegalAccessException ex) { - } + if (axsParameterValue != ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED) { + return; + } + + String js = getScreenReaderInjectingJs(); + if (mContentViewCore.isDeviceAccessibilityScriptInjectionEnabled() && + js != null && mContentViewCore.isAlive()) { + addOrRemoveAccessibilityApisIfNecessary(); + mContentViewCore.evaluateJavaScript(js, null); + mInjectedScriptEnabled = true; + mScriptInjected = true; } } @@ -198,10 +188,11 @@ public boolean accessibilityIsAvailable() { /** * Sets whether or not the script is enabled. If the script is disabled, we also stop any - * we output that is occurring. + * we output that is occurring. If the script has not yet been injected, injects it. * @param enabled Whether or not to enable the script. */ public void setScriptEnabled(boolean enabled) { + if (enabled && !mScriptInjected) injectAccessibilityScriptIntoPage(); if (!accessibilityIsAvailable() || mInjectedScriptEnabled == enabled) return; mInjectedScriptEnabled = enabled; diff --git a/src/org/chromium/content/browser/accessibility/BrowserAccessibilityManager.java b/src/org/chromium/content/browser/accessibility/BrowserAccessibilityManager.java new file mode 100644 index 0000000..f03ba9b --- /dev/null +++ b/src/org/chromium/content/browser/accessibility/BrowserAccessibilityManager.java @@ -0,0 +1,459 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser.accessibility; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Build; +import android.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; +import android.view.inputmethod.InputMethodManager; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.content.browser.ContentViewCore; +import org.chromium.content.browser.RenderCoordinates; + +import java.util.ArrayList; +import java.util.List; + +/** + * Native accessibility for a {@link ContentViewCore}. + * + * This class is safe to load on ICS and can be used to run tests, but + * only the subclass, JellyBeanBrowserAccessibilityManager, actually + * has a AccessibilityNodeProvider implementation needed for native + * accessibility. + */ +@JNINamespace("content") +public class BrowserAccessibilityManager { + private static final String TAG = "BrowserAccessibilityManager"; + + private ContentViewCore mContentViewCore; + private AccessibilityManager mAccessibilityManager; + private RenderCoordinates mRenderCoordinates; + private int mNativeObj; + private int mAccessibilityFocusId; + private int mCurrentHoverId; + private final int[] mTempLocation = new int[2]; + private View mView; + private boolean mUserHasTouchExplored; + private boolean mFrameInfoInitialized; + + // If this is true, enables an experimental feature that focuses the web page after it + // finishes loading. Disabled for now because it can be confusing if the user was + // trying to do something when this happens. + private boolean mFocusPageOnLoad; + + /** + * Create a BrowserAccessibilityManager object, which is owned by the C++ + * BrowserAccessibilityManagerAndroid instance, and connects to the content view. + * @param nativeBrowserAccessibilityManagerAndroid A pointer to the counterpart native + * C++ object that owns this object. + * @param contentViewCore The content view that this object provides accessibility for. + */ + @CalledByNative + private static BrowserAccessibilityManager create(int nativeBrowserAccessibilityManagerAndroid, + ContentViewCore contentViewCore) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + return new JellyBeanBrowserAccessibilityManager( + nativeBrowserAccessibilityManagerAndroid, contentViewCore); + } else { + return new BrowserAccessibilityManager( + nativeBrowserAccessibilityManagerAndroid, contentViewCore); + } + } + + protected BrowserAccessibilityManager(int nativeBrowserAccessibilityManagerAndroid, + ContentViewCore contentViewCore) { + mNativeObj = nativeBrowserAccessibilityManagerAndroid; + mContentViewCore = contentViewCore; + mContentViewCore.setBrowserAccessibilityManager(this); + mAccessibilityFocusId = View.NO_ID; + mCurrentHoverId = View.NO_ID; + mView = mContentViewCore.getContainerView(); + mRenderCoordinates = mContentViewCore.getRenderCoordinates(); + mAccessibilityManager = + (AccessibilityManager) mContentViewCore.getContext() + .getSystemService(Context.ACCESSIBILITY_SERVICE); + } + + @CalledByNative + private void onNativeObjectDestroyed() { + if (mContentViewCore.getBrowserAccessibilityManager() == this) { + mContentViewCore.setBrowserAccessibilityManager(null); + } + mNativeObj = 0; + mContentViewCore = null; + } + + /** + * @return An AccessibilityNodeProvider on JellyBean, and null on previous versions. + */ + public AccessibilityNodeProvider getAccessibilityNodeProvider() { + return null; + } + + /** + * @see AccessibilityNodeProvider#createAccessibilityNodeInfo(int) + */ + protected AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + if (!mAccessibilityManager.isEnabled() || mNativeObj == 0 || !mFrameInfoInitialized) { + return null; + } + + int rootId = nativeGetRootId(mNativeObj); + if (virtualViewId == View.NO_ID) { + virtualViewId = rootId; + } + if (mAccessibilityFocusId == View.NO_ID) { + mAccessibilityFocusId = rootId; + } + + final AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(mView); + info.setPackageName(mContentViewCore.getContext().getPackageName()); + info.setSource(mView, virtualViewId); + + if (nativePopulateAccessibilityNodeInfo(mNativeObj, info, virtualViewId)) { + return info; + } else { + return null; + } + } + + /** + * @see AccessibilityNodeProvider#findAccessibilityNodeInfosByText(String, int) + */ + protected List findAccessibilityNodeInfosByText(String text, + int virtualViewId) { + return new ArrayList(); + } + + /** + * @see AccessibilityNodeProvider#performAction(int, int, Bundle) + */ + protected boolean performAction(int virtualViewId, int action, Bundle arguments) { + if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) { + return false; + } + + switch (action) { + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: + if (mAccessibilityFocusId == virtualViewId) { + return true; + } + + mAccessibilityFocusId = virtualViewId; + sendAccessibilityEvent(mAccessibilityFocusId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + return true; + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + if (mAccessibilityFocusId == virtualViewId) { + mAccessibilityFocusId = View.NO_ID; + } + return true; + case AccessibilityNodeInfo.ACTION_CLICK: + nativeClick(mNativeObj, virtualViewId); + break; + case AccessibilityNodeInfo.ACTION_FOCUS: + nativeFocus(mNativeObj, virtualViewId); + break; + case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: + nativeBlur(mNativeObj); + break; + default: + break; + } + return false; + } + + /** + * @see View#onHoverEvent(MotionEvent) + */ + public boolean onHoverEvent(MotionEvent event) { + if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) { + return false; + } + + if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) return true; + + mUserHasTouchExplored = true; + float x = event.getX(); + float y = event.getY(); + + // Convert to CSS coordinates. + int cssX = (int) (mRenderCoordinates.fromPixToLocalCss(x) + + mRenderCoordinates.getScrollX()); + int cssY = (int) (mRenderCoordinates.fromPixToLocalCss(y) + + mRenderCoordinates.getScrollY()); + int id = nativeHitTest(mNativeObj, cssX, cssY); + if (mCurrentHoverId != id) { + sendAccessibilityEvent(mCurrentHoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + mCurrentHoverId = id; + } + + return true; + } + + /** + * Called by ContentViewCore to notify us when the frame info is initialized, + * the first time, since until that point, we can't use mRenderCoordinates to transform + * web coordinates to screen coordinates. + */ + public void notifyFrameInfoInitialized() { + if (mFrameInfoInitialized) return; + + mFrameInfoInitialized = true; + // (Re-) focus focused element, since we weren't able to create an + // AccessibilityNodeInfo for this element before. + if (mAccessibilityFocusId != View.NO_ID) { + sendAccessibilityEvent(mAccessibilityFocusId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + } + } + + private void sendAccessibilityEvent(int virtualViewId, int eventType) { + if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) return; + + final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + event.setPackageName(mContentViewCore.getContext().getPackageName()); + int rootId = nativeGetRootId(mNativeObj); + if (virtualViewId == rootId) { + virtualViewId = View.NO_ID; + } + event.setSource(mView, virtualViewId); + if (!nativePopulateAccessibilityEvent(mNativeObj, event, virtualViewId, eventType)) return; + + // This is currently needed if we want Android to draw the yellow box around + // the item that has accessibility focus. In practice, this doesn't seem to slow + // things down, because it's only called when the accessibility focus moves. + // TODO(dmazzoni): remove this if/when Android framework fixes bug. + mContentViewCore.getContainerView().postInvalidate(); + + mContentViewCore.getContainerView().requestSendAccessibilityEvent(mView, event); + } + + @CalledByNative + private void handlePageLoaded(int id) { + if (mUserHasTouchExplored) return; + + if (mFocusPageOnLoad) { + // Focus the natively focused node (usually document), + // if this feature is enabled. + mAccessibilityFocusId = id; + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_FOCUSED); + } + } + + @CalledByNative + private void handleFocusChanged(int id) { + if (mAccessibilityFocusId == id) return; + + mAccessibilityFocusId = id; + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_FOCUSED); + } + + @CalledByNative + private void handleCheckStateChanged(int id) { + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_CLICKED); + } + + @CalledByNative + private void handleTextSelectionChanged(int id) { + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED); + } + + @CalledByNative + private void handleEditableTextChanged(int id) { + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); + } + + @CalledByNative + private void handleContentChanged(int id) { + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } + + @CalledByNative + private void handleNavigate() { + mAccessibilityFocusId = View.NO_ID; + mUserHasTouchExplored = false; + mFrameInfoInitialized = false; + } + + @CalledByNative + private void handleScrolledToAnchor(int id) { + if (mAccessibilityFocusId == id) { + return; + } + + mAccessibilityFocusId = id; + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + } + + @CalledByNative + private void announceLiveRegionText(String text) { + mView.announceForAccessibility(text); + } + + @CalledByNative + private void setAccessibilityNodeInfoParent(AccessibilityNodeInfo node, int parentId) { + node.setParent(mView, parentId); + } + + @CalledByNative + private void addAccessibilityNodeInfoChild(AccessibilityNodeInfo node, int child_id) { + node.addChild(mView, child_id); + } + + @CalledByNative + private void setAccessibilityNodeInfoBooleanAttributes(AccessibilityNodeInfo node, + int virtualViewId, boolean checkable, boolean checked, boolean clickable, + boolean enabled, boolean focusable, boolean focused, boolean password, + boolean scrollable, boolean selected, boolean visibleToUser) { + node.setCheckable(checkable); + node.setChecked(checked); + node.setClickable(clickable); + node.setEnabled(enabled); + node.setFocusable(focusable); + node.setFocused(focused); + node.setPassword(password); + node.setScrollable(scrollable); + node.setSelected(selected); + node.setVisibleToUser(visibleToUser); + + if (focusable) { + if (focused) { + node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS); + } else { + node.addAction(AccessibilityNodeInfo.ACTION_FOCUS); + } + } + + if (mAccessibilityFocusId == virtualViewId) { + node.setAccessibilityFocused(true); + node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); + } else { + node.setAccessibilityFocused(false); + node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); + } + + if (clickable) { + node.addAction(AccessibilityNodeInfo.ACTION_CLICK); + } + } + + @CalledByNative + private void setAccessibilityNodeInfoStringAttributes(AccessibilityNodeInfo node, + String className, String contentDescription) { + node.setClassName(className); + node.setContentDescription(contentDescription); + } + + @CalledByNative + private void setAccessibilityNodeInfoLocation(AccessibilityNodeInfo node, + int absoluteLeft, int absoluteTop, int parentRelativeLeft, int parentRelativeTop, + int width, int height, boolean isRootNode) { + // First set the bounds in parent. + Rect boundsInParent = new Rect(parentRelativeLeft, parentRelativeTop, + parentRelativeLeft + width, parentRelativeTop + height); + if (isRootNode) { + // Offset of the web content relative to the View. + boundsInParent.offset(0, (int) mRenderCoordinates.getContentOffsetYPix()); + } + node.setBoundsInParent(boundsInParent); + + // Now set the absolute rect, which requires several transformations. + Rect rect = new Rect(absoluteLeft, absoluteTop, absoluteLeft + width, absoluteTop + height); + + // Offset by the scroll position. + rect.offset(-(int) mRenderCoordinates.getScrollX(), + -(int) mRenderCoordinates.getScrollY()); + + // Convert CSS (web) pixels to Android View pixels + rect.left = (int) mRenderCoordinates.fromLocalCssToPix(rect.left); + rect.top = (int) mRenderCoordinates.fromLocalCssToPix(rect.top); + rect.bottom = (int) mRenderCoordinates.fromLocalCssToPix(rect.bottom); + rect.right = (int) mRenderCoordinates.fromLocalCssToPix(rect.right); + + // Offset by the location of the web content within the view. + rect.offset(0, + (int) mRenderCoordinates.getContentOffsetYPix()); + + // Finally offset by the location of the view within the screen. + final int[] viewLocation = new int[2]; + mView.getLocationOnScreen(viewLocation); + rect.offset(viewLocation[0], viewLocation[1]); + + node.setBoundsInScreen(rect); + } + + @CalledByNative + private void setAccessibilityEventBooleanAttributes(AccessibilityEvent event, + boolean checked, boolean enabled, boolean password, boolean scrollable) { + event.setChecked(checked); + event.setEnabled(enabled); + event.setPassword(password); + event.setScrollable(scrollable); + } + + @CalledByNative + private void setAccessibilityEventClassName(AccessibilityEvent event, String className) { + event.setClassName(className); + } + + @CalledByNative + private void setAccessibilityEventListAttributes(AccessibilityEvent event, + int currentItemIndex, int itemCount) { + event.setCurrentItemIndex(currentItemIndex); + event.setItemCount(itemCount); + } + + @CalledByNative + private void setAccessibilityEventScrollAttributes(AccessibilityEvent event, + int scrollX, int scrollY, int maxScrollX, int maxScrollY) { + event.setScrollX(scrollX); + event.setScrollY(scrollY); + event.setMaxScrollX(maxScrollX); + event.setMaxScrollY(maxScrollY); + } + + @CalledByNative + private void setAccessibilityEventTextChangedAttrs(AccessibilityEvent event, + int fromIndex, int addedCount, int removedCount, String beforeText, String text) { + event.setFromIndex(fromIndex); + event.setAddedCount(addedCount); + event.setRemovedCount(removedCount); + event.setBeforeText(beforeText); + event.getText().add(text); + } + + @CalledByNative + private void setAccessibilityEventSelectionAttrs(AccessibilityEvent event, + int fromIndex, int addedCount, int itemCount, String text) { + event.setFromIndex(fromIndex); + event.setAddedCount(addedCount); + event.setItemCount(itemCount); + event.getText().add(text); + } + + private native int nativeGetRootId(int nativeBrowserAccessibilityManagerAndroid); + private native int nativeHitTest(int nativeBrowserAccessibilityManagerAndroid, int x, int y); + private native boolean nativePopulateAccessibilityNodeInfo( + int nativeBrowserAccessibilityManagerAndroid, AccessibilityNodeInfo info, int id); + private native boolean nativePopulateAccessibilityEvent( + int nativeBrowserAccessibilityManagerAndroid, AccessibilityEvent event, int id, + int eventType); + private native void nativeClick(int nativeBrowserAccessibilityManagerAndroid, int id); + private native void nativeFocus(int nativeBrowserAccessibilityManagerAndroid, int id); + private native void nativeBlur(int nativeBrowserAccessibilityManagerAndroid); +} diff --git a/src/org/chromium/content/browser/accessibility/JellyBeanAccessibilityInjector.java b/src/org/chromium/content/browser/accessibility/JellyBeanAccessibilityInjector.java index 4dc2736..a065870 100644 --- a/src/org/chromium/content/browser/accessibility/JellyBeanAccessibilityInjector.java +++ b/src/org/chromium/content/browser/accessibility/JellyBeanAccessibilityInjector.java @@ -7,7 +7,6 @@ import android.content.Context; import android.os.Bundle; import android.os.SystemClock; -import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; import org.chromium.content.browser.ContentViewCore; diff --git a/src/org/chromium/content/browser/accessibility/JellyBeanBrowserAccessibilityManager.java b/src/org/chromium/content/browser/accessibility/JellyBeanBrowserAccessibilityManager.java new file mode 100644 index 0000000..7165555 --- /dev/null +++ b/src/org/chromium/content/browser/accessibility/JellyBeanBrowserAccessibilityManager.java @@ -0,0 +1,53 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser.accessibility; + +import android.os.Bundle; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; + +import org.chromium.base.JNINamespace; +import org.chromium.content.browser.ContentViewCore; + +import java.util.List; + +/** + * Subclass of BrowserAccessibilityManager for JellyBean that creates an + * AccessibilityNodeProvider and delegates its implementation to this object. + */ +@JNINamespace("content") +public class JellyBeanBrowserAccessibilityManager extends BrowserAccessibilityManager { + private AccessibilityNodeProvider mAccessibilityNodeProvider; + + JellyBeanBrowserAccessibilityManager(int nativeBrowserAccessibilityManagerAndroid, + ContentViewCore contentViewCore) { + super(nativeBrowserAccessibilityManagerAndroid, contentViewCore); + + final BrowserAccessibilityManager delegate = this; + mAccessibilityNodeProvider = new AccessibilityNodeProvider() { + @Override + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + return delegate.createAccessibilityNodeInfo(virtualViewId); + } + + @Override + public List findAccessibilityNodeInfosByText(String text, + int virtualViewId) { + return delegate.findAccessibilityNodeInfosByText(text, virtualViewId); + } + + @Override + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + return delegate.performAction(virtualViewId, action, arguments); + } + }; + } + + @Override + public AccessibilityNodeProvider getAccessibilityNodeProvider() { + return mAccessibilityNodeProvider; + } +} diff --git a/src/org/chromium/content/browser/input/AdapterInputConnection.java b/src/org/chromium/content/browser/input/AdapterInputConnection.java index 75b7fa5..ab8a1fc 100644 --- a/src/org/chromium/content/browser/input/AdapterInputConnection.java +++ b/src/org/chromium/content/browser/input/AdapterInputConnection.java @@ -51,7 +51,8 @@ public class AdapterInputConnection extends BaseInputConnection { mImeAdapter = imeAdapter; mImeAdapter.setInputConnection(this); mSingleLine = true; - outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; + outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN + | EditorInfo.IME_FLAG_NO_EXTRACT_UI; outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; @@ -124,25 +125,24 @@ public void setEditableText(String text, int selectionStart, int selectionEnd, // Non-breaking spaces can cause the IME to get confused. Replace with normal spaces. text = text.replace('\u00A0', ' '); - Editable editable = getEditable(); - - int prevSelectionStart = Selection.getSelectionStart(editable); - int prevSelectionEnd = Selection.getSelectionEnd(editable); - int prevCompositionStart = getComposingSpanStart(editable); - int prevCompositionEnd = getComposingSpanEnd(editable); - String prevText = editable.toString(); - selectionStart = Math.min(selectionStart, text.length()); selectionEnd = Math.min(selectionEnd, text.length()); compositionStart = Math.min(compositionStart, text.length()); compositionEnd = Math.min(compositionEnd, text.length()); + Editable editable = getEditable(); + String prevText = editable.toString(); boolean textUnchanged = prevText.equals(text); if (!textUnchanged) { editable.replace(0, editable.length(), text); } + int prevSelectionStart = Selection.getSelectionStart(editable); + int prevSelectionEnd = Selection.getSelectionEnd(editable); + int prevCompositionStart = getComposingSpanStart(editable); + int prevCompositionEnd = getComposingSpanEnd(editable); + if (prevSelectionStart == selectionStart && prevSelectionEnd == selectionEnd && prevCompositionStart == compositionStart && prevCompositionEnd == compositionEnd) { @@ -311,7 +311,6 @@ public boolean deleteSurroundingText(int leftLength, int rightLength) { @Override public boolean sendKeyEvent(KeyEvent event) { if (DEBUG) Log.w(TAG, "sendKeyEvent [" + event.getAction() + "]"); - mImeAdapter.hideSelectionAndInsertionHandleControllers(); // If this is a key-up, and backspace/del or if the key has a character representation, // need to update the underlying Editable (i.e. the local representation of the text @@ -352,19 +351,9 @@ public boolean finishComposingText() { return true; } - // TODO(aurimas): remove this workaround of changing composition before confirmComposition - // Blink should support keeping the cursor (http://crbug.com/239923) - int selectionStart = Selection.getSelectionStart(editable); - int compositionStart = getComposingSpanStart(editable); super.finishComposingText(); + mImeAdapter.finishComposingText(); - beginBatchEdit(); - if (compositionStart != -1 && compositionStart < selectionStart - && !mImeAdapter.setComposingRegion(compositionStart, selectionStart)) { - return false; - } - if (!mImeAdapter.checkCompositionQueueAndCallNative("", 0, true)) return false; - endBatchEdit(); return true; } diff --git a/src/org/chromium/content/browser/input/DateDialogNormalizer.java b/src/org/chromium/content/browser/input/DateDialogNormalizer.java new file mode 100644 index 0000000..1dbc6c7 --- /dev/null +++ b/src/org/chromium/content/browser/input/DateDialogNormalizer.java @@ -0,0 +1,77 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser.input; + +import android.widget.DatePicker; +import android.widget.DatePicker.OnDateChangedListener; + +import java.util.Calendar; +import java.util.TimeZone; + +/** + * Normalize a date dialog so that it respect min and max. + */ +class DateDialogNormalizer { + + private static void setLimits(DatePicker picker, long min, long max) { + // DatePicker intervals are non inclusive, the DatePicker will throw an + // exception when setting the min/max attribute to the current date + // so make sure this never happens + if (max <= min) { + return; + } + Calendar minCal = trimToDate(min); + Calendar maxCal = trimToDate(max); + int currentYear = picker.getYear(); + int currentMonth = picker.getMonth(); + int currentDayOfMonth = picker.getDayOfMonth(); + picker.updateDate(maxCal.get(Calendar.YEAR), + maxCal.get(Calendar.MONTH), + maxCal.get(Calendar.DAY_OF_MONTH)); + picker.setMinDate(minCal.getTimeInMillis()); + picker.updateDate(minCal.get(Calendar.YEAR), + minCal.get(Calendar.MONTH), + minCal.get(Calendar.DAY_OF_MONTH)); + picker.setMaxDate(maxCal.getTimeInMillis()); + + // Restore the current date, this will keep the min/max settings + // previously set into account. + picker.updateDate(currentYear, currentMonth, currentDayOfMonth); + } + + private static Calendar trimToDate(long time) { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + cal.clear(); + cal.setTimeInMillis(time); + Calendar result = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + result.clear(); + result.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), + 0, 0, 0); + return result; + } + + /** + * Normalizes an existing DateDialogPicker changing the default date if + * needed to comply with the {@code min} and {@code max} attributes. + */ + static void normalize(DatePicker picker, OnDateChangedListener listener, + int year, int month, int day, int hour, int minute, long min, long max) { + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + calendar.clear(); + calendar.set(year, month, day, hour, minute, 0); + if (calendar.getTimeInMillis() < min) { + calendar.clear(); + calendar.setTimeInMillis(min); + } else if (calendar.getTimeInMillis() > max) { + calendar.clear(); + calendar.setTimeInMillis(max); + } + picker.init( + calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH), listener); + + setLimits(picker, min, max); + } +} diff --git a/src/org/chromium/content/browser/input/DateTimeChooserAndroid.java b/src/org/chromium/content/browser/input/DateTimeChooserAndroid.java index f8fca54..0c58217 100644 --- a/src/org/chromium/content/browser/input/DateTimeChooserAndroid.java +++ b/src/org/chromium/content/browser/input/DateTimeChooserAndroid.java @@ -28,10 +28,10 @@ private DateTimeChooserAndroid(Context context, @Override public void replaceDateTime( int dialogType, - int year, int month, int day, int hour, int minute, int second) { + int year, int month, int day, int hour, int minute, int second, int week) { nativeReplaceDateTime(mNativeDateTimeChooserAndroid, dialogType, - year, month, day, hour, minute, second); + year, month, day, hour, minute, second, week); } @Override @@ -42,9 +42,9 @@ public void cancelDateTimeDialog() { } private void showDialog(int dialogType, int year, int month, int monthDay, - int hour, int minute, int second) { + int hour, int minute, int second, int week, double min, double max) { mInputDialogContainer.showDialog(dialogType, year, month, monthDay, - hour, minute, second); + hour, minute, second, week, min, max); } @CalledByNative @@ -52,25 +52,26 @@ private static DateTimeChooserAndroid createDateTimeChooser( ContentViewCore contentViewCore, int nativeDateTimeChooserAndroid, int dialogType, int year, int month, int day, - int hour, int minute, int second) { + int hour, int minute, int second, int week, double min, double max) { DateTimeChooserAndroid chooser = new DateTimeChooserAndroid( contentViewCore.getContext(), nativeDateTimeChooserAndroid); - chooser.showDialog(dialogType, year, month, day, hour, minute, second); + chooser.showDialog(dialogType, year, month, day, hour, minute, second, week, min, max); return chooser; } @CalledByNative private static void initializeDateInputTypes(int textInputTypeDate, int textInputTypeDateTime, int textInputTypeDateTimeLocal, int textInputTypeMonth, - int textInputTypeTime) { - InputDialogContainer.initializeInputTypes(textInputTypeDate, textInputTypeDateTime, - textInputTypeDateTimeLocal, textInputTypeMonth, textInputTypeTime); + int textInputTypeTime, int textInputTypeWeek) { + InputDialogContainer.initializeInputTypes(textInputTypeDate, + textInputTypeDateTime, textInputTypeDateTimeLocal, + textInputTypeMonth, textInputTypeTime, textInputTypeWeek); } private native void nativeReplaceDateTime(int nativeDateTimeChooserAndroid, int dialogType, - int year, int month, int day, int hour, int minute, int second); + int year, int month, int day, int hour, int minute, int second, int week); private native void nativeCancelDialog(int nativeDateTimeChooserAndroid); } diff --git a/src/org/chromium/content/browser/input/DateTimePickerDialog.java b/src/org/chromium/content/browser/input/DateTimePickerDialog.java index 29bca66..4012b22 100644 --- a/src/org/chromium/content/browser/input/DateTimePickerDialog.java +++ b/src/org/chromium/content/browser/input/DateTimePickerDialog.java @@ -9,12 +9,12 @@ import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.os.Build; -import android.os.Bundle; +import android.text.format.Time; import android.view.LayoutInflater; import android.view.View; import android.widget.DatePicker; -import android.widget.TimePicker; import android.widget.DatePicker.OnDateChangedListener; +import android.widget.TimePicker; import android.widget.TimePicker.OnTimeChangedListener; import org.chromium.content.R; @@ -33,6 +33,9 @@ class DateTimePickerDialog extends AlertDialog implements OnClickListener, private final TimePicker mTimePicker; private final OnDateTimeSetListener mCallBack; + private final long mMinTimeMillis; + private final long mMaxTimeMillis; + /** * The callback used to indicate the user is done filling in the date. */ @@ -64,27 +67,12 @@ public DateTimePickerDialog(Context context, int year, int monthOfYear, int dayOfMonth, - int hourOfDay, int minute, boolean is24HourView) { - this(context, 0, callBack, year, monthOfYear, dayOfMonth, - hourOfDay, minute, is24HourView); - } + int hourOfDay, int minute, boolean is24HourView, + long min, long max) { + super(context, 0); - /** - * @param context The context the dialog is to run in. - * @param theme the theme to apply to this dialog - * @param callBack How the parent is notified that the date is set. - * @param year The initial year of the dialog. - * @param monthOfYear The initial month of the dialog. - * @param dayOfMonth The initial day of the dialog. - */ - public DateTimePickerDialog(Context context, - int theme, - OnDateTimeSetListener callBack, - int year, - int monthOfYear, - int dayOfMonth, - int hourOfDay, int minute, boolean is24HourView) { - super(context, theme); + mMinTimeMillis = min; + mMaxTimeMillis = max; mCallBack = callBack; @@ -100,13 +88,16 @@ public DateTimePickerDialog(Context context, View view = inflater.inflate(R.layout.date_time_picker_dialog, null); setView(view); mDatePicker = (DatePicker) view.findViewById(R.id.date_picker); - mDatePicker.init(year, monthOfYear, dayOfMonth, this); + DateDialogNormalizer.normalize(mDatePicker, this, + year, monthOfYear, dayOfMonth, hourOfDay, minute, min, max); mTimePicker = (TimePicker) view.findViewById(R.id.time_picker); mTimePicker.setIs24HourView(is24HourView); mTimePicker.setCurrentHour(hourOfDay); mTimePicker.setCurrentMinute(minute); mTimePicker.setOnTimeChangedListener(this); + onTimeChanged(mTimePicker, mTimePicker.getCurrentHour(), + mTimePicker.getCurrentMinute()); } @Override @@ -138,30 +129,27 @@ protected void onStop() { @Override public void onDateChanged(DatePicker view, int year, int month, int day) { - mDatePicker.init(year, month, day, null); + // Signal a time change so the max/min checks can be applied. + if (mTimePicker != null) { + onTimeChanged(mTimePicker, mTimePicker.getCurrentHour(), + mTimePicker.getCurrentMinute()); + } } @Override public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { - /* do nothing */ - } - - /** - * Gets the {@link DatePicker} contained in this dialog. - * - * @return The DatePicker view. - */ - public DatePicker getDatePicker() { - return mDatePicker; - } - - /** - * Gets the {@link TimePicker} contained in this dialog. - * - * @return The TimePicker view. - */ - public TimePicker getTimePicker() { - return mTimePicker; + Time time = new Time(); + time.set(0, mTimePicker.getCurrentMinute(), + mTimePicker.getCurrentHour(), mDatePicker.getDayOfMonth(), + mDatePicker.getMonth(), mDatePicker.getYear()); + + if (time.toMillis(true) < mMinTimeMillis) { + time.set(mMinTimeMillis); + } else if (time.toMillis(true) > mMaxTimeMillis) { + time.set(mMaxTimeMillis); + } + mTimePicker.setCurrentHour(time.hour); + mTimePicker.setCurrentMinute(time.minute); } /** @@ -177,30 +165,4 @@ public void updateDateTime(int year, int monthOfYear, int dayOfMonth, mTimePicker.setCurrentHour(hourOfDay); mTimePicker.setCurrentMinute(minutOfHour); } - - @Override - public Bundle onSaveInstanceState() { - Bundle state = super.onSaveInstanceState(); - state.putInt(YEAR, mDatePicker.getYear()); - state.putInt(MONTH, mDatePicker.getMonth()); - state.putInt(DAY, mDatePicker.getDayOfMonth()); - state.putInt(HOUR, mTimePicker.getCurrentHour()); - state.putInt(MINUTE, mTimePicker.getCurrentMinute()); - state.putBoolean(IS_24_HOUR, mTimePicker.is24HourView()); - return state; - } - - @Override - public void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - int year = savedInstanceState.getInt(YEAR); - int month = savedInstanceState.getInt(MONTH); - int day = savedInstanceState.getInt(DAY); - mDatePicker.init(year, month, day, this); - int hour = savedInstanceState.getInt(HOUR); - int minute = savedInstanceState.getInt(MINUTE); - mTimePicker.setIs24HourView(savedInstanceState.getBoolean(IS_24_HOUR)); - mTimePicker.setCurrentHour(hour); - mTimePicker.setCurrentMinute(minute); - } } diff --git a/src/org/chromium/content/browser/input/ImeAdapter.java b/src/org/chromium/content/browser/input/ImeAdapter.java index 354b242..dd85bfc 100644 --- a/src/org/chromium/content/browser/input/ImeAdapter.java +++ b/src/org/chromium/content/browser/input/ImeAdapter.java @@ -4,7 +4,6 @@ package org.chromium.content.browser.input; -import android.content.Context; import android.os.Handler; import android.os.ResultReceiver; import android.view.KeyCharacterMap; @@ -40,7 +39,7 @@ */ @JNINamespace("content") public class ImeAdapter { - public interface ViewEmbedder { + public interface ImeAdapterDelegate { /** * @param isFinish whether the event is occurring because input is finished. */ @@ -98,14 +97,11 @@ public void run() { static int sModifierNumLockOn; private int mNativeImeAdapterAndroid; - private final Context mContext; private InputMethodManagerWrapper mInputMethodManagerWrapper; private AdapterInputConnection mInputConnection; - private final ViewEmbedder mViewEmbedder; + private final ImeAdapterDelegate mViewEmbedder; private final Handler mHandler; private DelayedDismissInput mDismissInput = null; - private final SelectionHandleController mSelectionHandleController; - private final InsertionHandleController mInsertionHandleController; private int mTextInputType; private int mInitialSelectionStart; private int mInitialSelectionEnd; @@ -114,17 +110,12 @@ public void run() { boolean mIsShowWithoutHideOutstanding = false; /** - * @param context View context. - * @param selectionHandleController The controller that handles selection. - * @param insertionHandleController The controller that handles insertion. + * @param wrapper InputMethodManagerWrapper that should receive all the call directed to + * InputMethodManager. * @param embedder The view that is used for callbacks from ImeAdapter. */ - public ImeAdapter(Context context, SelectionHandleController selectionHandleController, - InsertionHandleController insertionHandleController, ViewEmbedder embedder) { - mContext = context; - mInputMethodManagerWrapper = new InputMethodManagerWrapper(context); - mSelectionHandleController = selectionHandleController; - mInsertionHandleController = insertionHandleController; + public ImeAdapter(InputMethodManagerWrapper wrapper, ImeAdapterDelegate embedder) { + mInputMethodManagerWrapper = wrapper; mViewEmbedder = embedder; mHandler = new Handler(); } @@ -137,7 +128,7 @@ public AdapterInputConnection get(View view, ImeAdapter imeAdapter, } @VisibleForTesting - protected void setInputMethodManagerWrapper(InputMethodManagerWrapper immw) { + public void setInputMethodManagerWrapper(InputMethodManagerWrapper immw) { mInputMethodManagerWrapper = immw; } @@ -206,11 +197,6 @@ private static int getModifiers(int metaState) { return modifiers; } - void hideSelectionAndInsertionHandleControllers() { - mSelectionHandleController.hideAndDisallowAutomaticShowing(); - mInsertionHandleController.hideAndDisallowAutomaticShowing(); - } - public boolean isActive() { return mInputConnection != null && mInputConnection.isActive(); } @@ -260,7 +246,9 @@ public void attach(int nativeImeAdapter, int textInputType, int selectionStart, mTextInputType = textInputType; mInitialSelectionStart = selectionStart; mInitialSelectionEnd = selectionEnd; - nativeAttachImeAdapter(mNativeImeAdapterAndroid); + if (nativeImeAdapter != 0) { + nativeAttachImeAdapter(mNativeImeAdapterAndroid); + } } /** @@ -351,10 +339,6 @@ boolean checkCompositionQueueAndCallNative(String text, int newCursorPosition, // Committing an empty string finishes the current composition. boolean isFinish = text.isEmpty(); - if (!isFinish) { - mSelectionHandleController.hideAndDisallowAutomaticShowing(); - mInsertionHandleController.hideAndDisallowAutomaticShowing(); - } mViewEmbedder.onImeEvent(isFinish); int keyCode = shouldSendKeyEventWithKeyCode(text); long timeStampMs = System.currentTimeMillis(); @@ -377,6 +361,10 @@ boolean checkCompositionQueueAndCallNative(String text, int newCursorPosition, return true; } + void finishComposingText() { + nativeFinishComposingText(mNativeImeAdapterAndroid); + } + boolean translateAndSendNativeEvents(KeyEvent event) { if (mNativeImeAdapterAndroid == 0) return false; @@ -558,6 +546,8 @@ private native void nativeSetComposingText(int nativeImeAdapterAndroid, String t private native void nativeCommitText(int nativeImeAdapterAndroid, String text); + private native void nativeFinishComposingText(int nativeImeAdapterAndroid); + private native void nativeAttachImeAdapter(int nativeImeAdapterAndroid); private native void nativeSetEditableSelectionOffsets(int nativeImeAdapterAndroid, diff --git a/src/org/chromium/content/browser/input/InputDialogContainer.java b/src/org/chromium/content/browser/input/InputDialogContainer.java index 11189c5..07dd7e0 100644 --- a/src/org/chromium/content/browser/input/InputDialogContainer.java +++ b/src/org/chromium/content/browser/input/InputDialogContainer.java @@ -19,7 +19,7 @@ import android.widget.TimePicker; import org.chromium.content.browser.input.DateTimePickerDialog.OnDateTimeSetListener; -import org.chromium.content.browser.input.MonthPickerDialog.OnMonthSetListener; +import org.chromium.content.browser.input.TwoFieldDatePickerDialog; import org.chromium.content.R; import java.text.ParseException; @@ -32,7 +32,7 @@ public class InputDialogContainer { interface InputActionDelegate { void cancelDateTimeDialog(); void replaceDateTime(int dialogType, - int year, int month, int day, int hour, int minute, int second); + int year, int month, int day, int hour, int minute, int second, int week); } // Default values used in Time representations of selected date/time before formatting. @@ -42,6 +42,7 @@ void replaceDateTime(int dialogType, private static final int MONTHDAY_DEFAULT = 1; private static final int HOUR_DEFAULT = 0; private static final int MINUTE_DEFAULT = 0; + private static final int WEEK_DEFAULT = 0; // Date formats as accepted by Time.format. private static final String HTML_DATE_FORMAT = "%Y-%m-%d"; @@ -51,12 +52,14 @@ void replaceDateTime(int dialogType, private static final String HTML_DATE_TIME_FORMAT = "%Y-%m-%dT%H:%MZ"; private static final String HTML_DATE_TIME_LOCAL_FORMAT = "%Y-%m-%dT%H:%M"; private static final String HTML_MONTH_FORMAT = "%Y-%m"; + private static final String HTML_WEEK_FORMAT = "%Y-%w"; private static int sTextInputTypeDate; private static int sTextInputTypeDateTime; private static int sTextInputTypeDateTimeLocal; private static int sTextInputTypeMonth; private static int sTextInputTypeTime; + private static int sTextInputTypeWeek; private Context mContext; @@ -66,19 +69,22 @@ void replaceDateTime(int dialogType, private AlertDialog mDialog; private InputActionDelegate mInputActionDelegate; - static void initializeInputTypes(int textInputTypeDate, int textInputTypeDateTime, - int textInputTypeDateTimeLocal, int textInputTypeMonth, int textInputTypeTime) { + static void initializeInputTypes(int textInputTypeDate, + int textInputTypeDateTime, int textInputTypeDateTimeLocal, + int textInputTypeMonth, int textInputTypeTime, + int textInputTypeWeek) { sTextInputTypeDate = textInputTypeDate; sTextInputTypeDateTime = textInputTypeDateTime; sTextInputTypeDateTimeLocal = textInputTypeDateTimeLocal; sTextInputTypeMonth = textInputTypeMonth; sTextInputTypeTime = textInputTypeTime; + sTextInputTypeWeek = textInputTypeWeek; } static boolean isDialogInputType(int type) { return type == sTextInputTypeDate || type == sTextInputTypeTime || type == sTextInputTypeDateTime || type == sTextInputTypeDateTimeLocal - || type == sTextInputTypeMonth; + || type == sTextInputTypeMonth || type == sTextInputTypeWeek; } InputDialogContainer(Context context, InputActionDelegate inputActionDelegate) { @@ -93,7 +99,7 @@ private Time normalizeTime(int year, int month, int monthDay, minute == 0 && second == 0) { Calendar cal = Calendar.getInstance(); result.set(cal.get(Calendar.SECOND), cal.get(Calendar.MINUTE), - cal.get(Calendar.HOUR), cal.get(Calendar.DATE), + cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.DATE), cal.get(Calendar.MONTH), cal.get(Calendar.YEAR)); } else { result.set(second, minute, hour, monthDay, month, year); @@ -102,26 +108,48 @@ private Time normalizeTime(int year, int month, int monthDay, } void showDialog(final int dialogType, int year, int month, int monthDay, - int hour, int minute, int second) { + int hour, int minute, int second, int week, double min, double max) { if (isDialogShowing()) mDialog.dismiss(); + // Java Date dialogs like longs but Blink prefers doubles.. + // Both parameters mean different things depending on the type + // For input type=month min and max come as number on months since 1970 + // For other types (including type=time) they are just milliseconds since 1970 + // In any case the cast here is safe given the above restrictions. + long minTime = (long) min; + long maxTime = (long) max; + Time time = normalizeTime(year, month, monthDay, hour, minute, second); if (dialogType == sTextInputTypeDate) { - mDialog = new DatePickerDialog(mContext, new DateListener(dialogType), - time.year, time.month, time.monthDay); - mDialog.setTitle(mContext.getText(R.string.date_picker_dialog_title)); + DatePickerDialog dialog = new DatePickerDialog(mContext, + new DateListener(dialogType), time.year, time.month, time.monthDay); + DateDialogNormalizer.normalize(dialog.getDatePicker(), dialog, + time.year, time.month, time.monthDay, 0, 0, minTime, maxTime); + + dialog.setTitle(mContext.getText(R.string.date_picker_dialog_title)); + mDialog = dialog; } else if (dialogType == sTextInputTypeTime) { - mDialog = new TimePickerDialog(mContext, new TimeListener(dialogType), - time.hour, time.minute, DateFormat.is24HourFormat(mContext)); + mDialog = TimeDialog.create(mContext, new TimeListener(dialogType), + time.hour, time.minute, DateFormat.is24HourFormat(mContext), + minTime, maxTime); } else if (dialogType == sTextInputTypeDateTime || dialogType == sTextInputTypeDateTimeLocal) { mDialog = new DateTimePickerDialog(mContext, new DateTimeListener(dialogType), time.year, time.month, time.monthDay, - time.hour, time.minute, DateFormat.is24HourFormat(mContext)); + time.hour, time.minute, DateFormat.is24HourFormat(mContext), + minTime, maxTime); } else if (dialogType == sTextInputTypeMonth) { - mDialog = new MonthPickerDialog(mContext, new MonthListener(dialogType), - time.year, time.month); + mDialog = new MonthPickerDialog(mContext, new MonthOrWeekListener(dialogType), + time.year, time.month, minTime, maxTime); + } else if (dialogType == sTextInputTypeWeek) { + if (week == 0) { + Calendar cal = Calendar.getInstance(); + year = WeekPicker.getISOWeekYearForDate(cal); + week = WeekPicker.getWeekForDate(cal); + } + mDialog = new WeekPickerDialog(mContext, new MonthOrWeekListener(dialogType), + year, week, minTime, maxTime); } mDialog.setButton(DialogInterface.BUTTON_POSITIVE, @@ -144,7 +172,7 @@ public void onClick(DialogInterface dialog, int which) { @Override public void onClick(DialogInterface dialog, int which) { mDialogAlreadyDismissed = true; - mInputActionDelegate.replaceDateTime(dialogType, 0, 0, 0, 0, 0, 0); + mInputActionDelegate.replaceDateTime(dialogType, 0, 0, 0, 0, 0, 0, 0); } }); @@ -171,7 +199,8 @@ private class DateListener implements OnDateSetListener { public void onDateSet(DatePicker view, int year, int month, int monthDay) { if (!mDialogAlreadyDismissed) { setFieldDateTimeValue(mDialogType, - year, month, monthDay, HOUR_DEFAULT, MINUTE_DEFAULT, + year, month, monthDay, + HOUR_DEFAULT, MINUTE_DEFAULT, WEEK_DEFAULT, HTML_DATE_FORMAT); } } @@ -189,7 +218,7 @@ public void onTimeSet(TimePicker view, int hourOfDay, int minute) { if (!mDialogAlreadyDismissed) { setFieldDateTimeValue(mDialogType, YEAR_DEFAULT, MONTH_DEFAULT, MONTHDAY_DEFAULT, - hourOfDay, minute, HTML_TIME_FORMAT); + hourOfDay, minute, WEEK_DEFAULT, HTML_TIME_FORMAT); } } } @@ -208,36 +237,43 @@ public void onDateTimeSet(DatePicker dateView, TimePicker timeView, int year, int month, int monthDay, int hourOfDay, int minute) { if (!mDialogAlreadyDismissed) { - setFieldDateTimeValue(mDialogType, year, month, monthDay, hourOfDay, minute, + setFieldDateTimeValue(mDialogType, year, month, monthDay, + hourOfDay, minute, WEEK_DEFAULT, mLocal ? HTML_DATE_TIME_LOCAL_FORMAT : HTML_DATE_TIME_FORMAT); } } } - private class MonthListener implements OnMonthSetListener { + private class MonthOrWeekListener implements TwoFieldDatePickerDialog.OnValueSetListener { private final int mDialogType; - MonthListener(int dialogType) { + MonthOrWeekListener(int dialogType) { mDialogType = dialogType; } @Override - public void onMonthSet(MonthPicker view, int year, int month) { + public void onValueSet(int year, int positionInYear) { if (!mDialogAlreadyDismissed) { - setFieldDateTimeValue(mDialogType, year, month, MONTHDAY_DEFAULT, - HOUR_DEFAULT, MINUTE_DEFAULT, HTML_MONTH_FORMAT); + if (mDialogType == sTextInputTypeMonth) { + setFieldDateTimeValue(mDialogType, year, positionInYear, MONTHDAY_DEFAULT, + HOUR_DEFAULT, MINUTE_DEFAULT, WEEK_DEFAULT, + HTML_MONTH_FORMAT); + } else { + setFieldDateTimeValue(mDialogType, year, MONTH_DEFAULT, MONTHDAY_DEFAULT, + HOUR_DEFAULT, MINUTE_DEFAULT, positionInYear, HTML_WEEK_FORMAT); + } } } } private void setFieldDateTimeValue(int dialogType, int year, int month, int monthDay, int hourOfDay, - int minute, String dateFormat) { + int minute, int week, String dateFormat) { // Prevents more than one callback being sent to the native // side when the dialog triggers multiple events. mDialogAlreadyDismissed = true; mInputActionDelegate.replaceDateTime(dialogType, - year, month, monthDay, hourOfDay, minute, 0 /* second */); + year, month, monthDay, hourOfDay, minute, 0 /* second */, week); } } diff --git a/src/org/chromium/content/browser/input/InputMethodManagerWrapper.java b/src/org/chromium/content/browser/input/InputMethodManagerWrapper.java index 34b8b4e..becc019 100644 --- a/src/org/chromium/content/browser/input/InputMethodManagerWrapper.java +++ b/src/org/chromium/content/browser/input/InputMethodManagerWrapper.java @@ -13,7 +13,7 @@ /** * Wrapper around Android's InputMethodManager */ -class InputMethodManagerWrapper { +public class InputMethodManagerWrapper { private final Context mContext; public InputMethodManagerWrapper(Context context) { diff --git a/src/org/chromium/content/browser/input/MonthPicker.java b/src/org/chromium/content/browser/input/MonthPicker.java index 01e636e..3e06d75 100644 --- a/src/org/chromium/content/browser/input/MonthPicker.java +++ b/src/org/chromium/content/browser/input/MonthPicker.java @@ -5,466 +5,105 @@ package org.chromium.content.browser.input; import android.content.Context; -import android.content.res.Configuration; -import android.os.Parcel; -import android.os.Parcelable; -import android.text.format.DateUtils; -import android.util.AttributeSet; -import android.util.SparseArray; -import android.view.LayoutInflater; -import android.view.accessibility.AccessibilityEvent; -import android.widget.DatePicker; -import android.widget.FrameLayout; -import android.widget.NumberPicker; -import android.widget.NumberPicker.OnValueChangeListener; import java.text.DateFormatSymbols; import java.util.Arrays; import java.util.Calendar; import java.util.Locale; -import java.util.TimeZone; import org.chromium.content.R; -// This class is heavily based on android.widget.DatePicker. -public class MonthPicker extends FrameLayout { - - private static final int DEFAULT_START_YEAR = 1900; - - private static final int DEFAULT_END_YEAR = 2100; - - private static final boolean DEFAULT_ENABLED_STATE = true; - - private final NumberPicker mMonthSpinner; - - private final NumberPicker mYearSpinner; - - private Locale mCurrentLocale; - - private OnMonthChangedListener mMonthChangedListener; +public class MonthPicker extends TwoFieldDatePicker { + private static final int MONTHS_NUMBER = 12; private String[] mShortMonths; - private int mNumberOfMonths; - - private Calendar mMinDate; + public MonthPicker(Context context, long minValue, long maxValue) { + super(context, minValue, maxValue); - private Calendar mMaxDate; - - private Calendar mCurrentDate; - - private boolean mIsEnabled = DEFAULT_ENABLED_STATE; - - /** - * The callback used to indicate the user changes\d the date. - */ - public interface OnMonthChangedListener { - - /** - * Called upon a date change. - * - * @param view The view associated with this listener. - * @param year The year that was set. - * @param monthOfYear The month that was set (0-11) for compatibility - * with {@link java.util.Calendar}. - */ - void onMonthChanged(MonthPicker view, int year, int monthOfYear); - } - - public MonthPicker(Context context) { - this(context, null); - } - - public MonthPicker(Context context, AttributeSet attrs) { - this(context, attrs, android.R.attr.datePickerStyle); - } - - public MonthPicker(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + getPositionInYearSpinner().setContentDescription( + getResources().getString(R.string.accessibility_date_picker_month)); // initialization based on locale - setCurrentLocale(Locale.getDefault()); - - int startYear = DEFAULT_START_YEAR; - int endYear = DEFAULT_END_YEAR; - - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.month_picker, this, true); - - OnValueChangeListener onChangeListener = new OnValueChangeListener() { - @Override - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - Calendar tempDate = getCalendarForLocale(null, mCurrentLocale); - tempDate.setTimeInMillis(mCurrentDate.getTimeInMillis()); - - // take care of wrapping of days and months to update greater fields - if (picker == mMonthSpinner) { - if (oldVal == 11 && newVal == 0) { - tempDate.add(Calendar.MONTH, 1); - } else if (oldVal == 0 && newVal == 11) { - tempDate.add(Calendar.MONTH, -1); - } else { - tempDate.add(Calendar.MONTH, newVal - oldVal); - } - } else if (picker == mYearSpinner) { - tempDate.set(Calendar.YEAR, newVal); - } else { - throw new IllegalArgumentException(); - } - - // now set the date to the adjusted one - setDate(tempDate.get(Calendar.YEAR), tempDate.get(Calendar.MONTH)); - updateSpinners(); - notifyDateChanged(); - } - }; - - // month - mMonthSpinner = (NumberPicker) findViewById(R.id.month); - mMonthSpinner.setMinValue(0); - mMonthSpinner.setMaxValue(mNumberOfMonths - 1); - mMonthSpinner.setDisplayedValues(mShortMonths); - mMonthSpinner.setOnLongPressUpdateInterval(200); - mMonthSpinner.setOnValueChangedListener(onChangeListener); - - // year - mYearSpinner = (NumberPicker) findViewById(R.id.year); - mYearSpinner.setOnLongPressUpdateInterval(100); - mYearSpinner.setOnValueChangedListener(onChangeListener); - - Calendar tempDate = getCalendarForLocale(null, mCurrentLocale); - tempDate.set(startYear, 0, 1); - - setMinDate(tempDate.getTimeInMillis()); - tempDate.set(endYear, 11, 31); - setMaxDate(tempDate.getTimeInMillis()); + mShortMonths = + DateFormatSymbols.getInstance(Locale.getDefault()).getShortMonths(); // initialize to current date - mCurrentDate.setTimeInMillis(System.currentTimeMillis()); - init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), null); - } - - /** - * Gets the minimal date supported by this {@link DatePicker} in - * milliseconds since January 1, 1970 00:00:00 in - * {@link TimeZone#getDefault()} time zone. - *

- * Note: The default minimal date is 01/01/1900. - *

- * - * @return The minimal supported date. - */ - public long getMinDate() { - return mMinDate.getTimeInMillis(); - } - - /** - * Sets the minimal date supported by this {@link NumberPicker} in - * milliseconds since January 1, 1970 00:00:00 in - * {@link TimeZone#getDefault()} time zone. - * - * @param minDate The minimal supported date. - */ - public void setMinDate(long minDate) { - Calendar tempDate = getCalendarForLocale(null, mCurrentLocale); - tempDate.setTimeInMillis(minDate); - if (tempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) - && tempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) { - return; - } - mMinDate.setTimeInMillis(minDate); - if (mCurrentDate.before(mMinDate)) { - mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); - } - updateSpinners(); - } - - /** - * Gets the maximal date supported by this {@link DatePicker} in - * milliseconds since January 1, 1970 00:00:00 in - * {@link TimeZone#getDefault()} time zone. - *

- * Note: The default maximal date is 12/31/2100. - *

- * - * @return The maximal supported date. - */ - public long getMaxDate() { - return mMaxDate.getTimeInMillis(); - } - - /** - * Sets the maximal date supported by this {@link DatePicker} in - * milliseconds since January 1, 1970 00:00:00 in - * {@link TimeZone#getDefault()} time zone. - * - * @param maxDate The maximal supported date. - */ - public void setMaxDate(long maxDate) { - Calendar tempDate = getCalendarForLocale(null, mCurrentLocale); - tempDate.setTimeInMillis(maxDate); - if (tempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR) - && tempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) { - return; - } - mMaxDate.setTimeInMillis(maxDate); - if (mCurrentDate.after(mMaxDate)) { - mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); - } - updateSpinners(); - } - - @Override - public void setEnabled(boolean enabled) { - if (mIsEnabled == enabled) { - return; - } - super.setEnabled(enabled); - mMonthSpinner.setEnabled(enabled); - mYearSpinner.setEnabled(enabled); - mIsEnabled = enabled; + Calendar cal = Calendar.getInstance(); + init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), null); } @Override - public boolean isEnabled() { - return mIsEnabled; + protected Calendar createDateFromValue(long value) { + int year = (int)Math.min(value / 12 + 1970, Integer.MAX_VALUE); + int month = (int) (value % 12); + Calendar cal = Calendar.getInstance(); + cal.clear(); + cal.set(year, month, 1); + return cal; } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - onPopulateAccessibilityEvent(event); - return true; - } - - @Override - public void onPopulateAccessibilityEvent(AccessibilityEvent event) { - super.onPopulateAccessibilityEvent(event); - - final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; - String selectedDateUtterance = DateUtils.formatDateTime(getContext(), - mCurrentDate.getTimeInMillis(), flags); - event.getText().add(selectedDateUtterance); - } - - @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - setCurrentLocale(newConfig.locale); - } - - /** - * Sets the current locale. - * - * @param locale The current locale. - */ - private void setCurrentLocale(Locale locale) { - if (locale.equals(mCurrentLocale)) { - return; - } - - mCurrentLocale = locale; - mMinDate = getCalendarForLocale(mMinDate, locale); - mMaxDate = getCalendarForLocale(mMaxDate, locale); - mCurrentDate = getCalendarForLocale(mCurrentDate, locale); - - mShortMonths = - DateFormatSymbols.getInstance(mCurrentLocale).getShortMonths(); - mNumberOfMonths = mShortMonths.length; - } - - /** - * Gets a calendar for locale bootstrapped with the value of a given calendar. - * - * @param oldCalendar The old calendar. - * @param locale The locale. - */ - private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { - if (oldCalendar == null) { - return Calendar.getInstance(locale); + protected void setCurrentDate(int year, int month) { + Calendar date = Calendar.getInstance(); + date.set(year, month, 1); + if (date.before(getMinDate())) { + setCurrentDate(getMinDate()); + } else if (date.after(getMaxDate())) { + setCurrentDate(getMaxDate()); } else { - final long currentTimeMillis = oldCalendar.getTimeInMillis(); - Calendar newCalendar = Calendar.getInstance(locale); - newCalendar.setTimeInMillis(currentTimeMillis); - return newCalendar; - } - } - - /** - * Updates the current date. - * - * @param year The year. - * @param month The month which is starting from zero. - */ - public void updateMonth(int year, int month) { - if (!isNewDate(year, month)) { - return; + setCurrentDate(date); } - setDate(year, month); - updateSpinners(); - notifyDateChanged(); } - // Override so we are in complete control of save / restore for this widget. @Override - protected void dispatchRestoreInstanceState(SparseArray container) { - dispatchThawSelfOnly(container); - } - - @Override - protected Parcelable onSaveInstanceState() { - Parcelable superState = super.onSaveInstanceState(); - return new SavedState(superState, getYear(), getMonth()); - } - - @Override - protected void onRestoreInstanceState(Parcelable state) { - SavedState ss = (SavedState) state; - super.onRestoreInstanceState(ss.getSuperState()); - setDate(ss.mYear, ss.mMonth); - updateSpinners(); - } - - /** - * Initialize the state. If the provided values designate an inconsistent - * date the values are normalized before updating the spinners. - * - * @param year The initial year. - * @param monthOfYear The initial month starting from zero. - * @param onMonthChangedListener How user is notified date is changed by - * user, can be null. - */ - public void init(int year, int monthOfYear, OnMonthChangedListener onMonthChangedListener) { - setDate(year, monthOfYear); - updateSpinners(); - mMonthChangedListener = onMonthChangedListener; - } - - private boolean isNewDate(int year, int month) { - return (mCurrentDate.get(Calendar.YEAR) != year - || mCurrentDate.get(Calendar.MONTH) != month); - } - - private void setDate(int year, int month) { - mCurrentDate.set(year, month, 1); - if (mCurrentDate.before(mMinDate)) { - mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); - } else if (mCurrentDate.after(mMaxDate)) { - mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); - } - } - - private void updateSpinners() { - // set the spinner ranges respecting the min and max dates - if (mCurrentDate.equals(mMinDate)) { - mMonthSpinner.setDisplayedValues(null); - mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH)); - mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH)); - mMonthSpinner.setWrapSelectorWheel(false); - } else if (mCurrentDate.equals(mMaxDate)) { - mMonthSpinner.setDisplayedValues(null); - mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH)); - mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH)); - mMonthSpinner.setWrapSelectorWheel(false); - } else { - mMonthSpinner.setDisplayedValues(null); - mMonthSpinner.setMinValue(0); - mMonthSpinner.setMaxValue(11); - mMonthSpinner.setWrapSelectorWheel(true); - } + protected void updateSpinners() { + super.updateSpinners(); // make sure the month names are a zero based array // with the months in the month spinner String[] displayedValues = Arrays.copyOfRange(mShortMonths, - mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1); - mMonthSpinner.setDisplayedValues(displayedValues); - - // year spinner range does not change based on the current date - mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR)); - mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR)); - mYearSpinner.setWrapSelectorWheel(false); - - // set the spinner values - mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR)); - mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH)); - } - - /** - * @return The selected year. - */ - public int getYear() { - return mCurrentDate.get(Calendar.YEAR); + getPositionInYearSpinner().getMinValue(), + getPositionInYearSpinner().getMaxValue() + 1); + getPositionInYearSpinner().setDisplayedValues(displayedValues); } /** * @return The selected month. */ public int getMonth() { - return mCurrentDate.get(Calendar.MONTH); + return getCurrentDate().get(Calendar.MONTH); } - /** - * @return The selected day of month. - */ - public int getDayOfMonth() { - return mCurrentDate.get(Calendar.DAY_OF_MONTH); + @Override + public int getPositionInYear() { + return getMonth(); } - /** - * Notifies the listener, if such, for a change in the selected date. - */ - private void notifyDateChanged() { - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - if (mMonthChangedListener != null) { - mMonthChangedListener.onMonthChanged(this, getYear(), getMonth()); - } + @Override + protected int getMaxYear() { + return getMaxDate().get(Calendar.YEAR); } - /** - * Class for managing state storing/restoring. - */ - private static class SavedState extends BaseSavedState { - - private final int mYear; + @Override + protected int getMinYear() { + return getMinDate().get(Calendar.YEAR); + } - private final int mMonth; - /** - * Constructor called from {@link DatePicker#onSaveInstanceState()} - */ - private SavedState(Parcelable superState, int year, int month) { - super(superState); - mYear = year; - mMonth = month; - } - - /** - * Constructor called from {@link #CREATOR} - */ - private SavedState(Parcel in) { - super(in); - mYear = in.readInt(); - mMonth = in.readInt(); + @Override + protected int getMaxPositionInYear() { + if (getYear() == getMaxDate().get(Calendar.YEAR)) { + return getMaxDate().get(Calendar.MONTH); } + return MONTHS_NUMBER - 1; + } - @Override - public void writeToParcel(Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeInt(mYear); - dest.writeInt(mMonth); + @Override + protected int getMinPositionInYear() { + if (getYear() == getMinDate().get(Calendar.YEAR)) { + return getMinDate().get(Calendar.MONTH); } - - @SuppressWarnings("all") - // suppress unused and hiding - public static final Parcelable.Creator CREATOR = new Creator() { - - public SavedState createFromParcel(Parcel in) { - return new SavedState(in); - } - - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; + return 0; } } diff --git a/src/org/chromium/content/browser/input/MonthPickerDialog.java b/src/org/chromium/content/browser/input/MonthPickerDialog.java index 354810e..24a4e8b 100644 --- a/src/org/chromium/content/browser/input/MonthPickerDialog.java +++ b/src/org/chromium/content/browser/input/MonthPickerDialog.java @@ -4,39 +4,11 @@ package org.chromium.content.browser.input; -import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.os.Build; -import android.os.Bundle; -import android.view.View; -import org.chromium.content.browser.input.MonthPicker.OnMonthChangedListener; import org.chromium.content.R; -public class MonthPickerDialog extends AlertDialog implements OnClickListener, - OnMonthChangedListener { - - private static final String YEAR = "year"; - private static final String MONTH = "month"; - - private final MonthPicker mMonthPicker; - private final OnMonthSetListener mCallBack; - - /** - * The callback used to indicate the user is done filling in the date. - */ - public interface OnMonthSetListener { - - /** - * @param view The view associated with this listener. - * @param year The year that was set. - * @param monthOfYear The month that was set (0-11) for compatibility - * with {@link java.util.Calendar}. - */ - void onMonthSet(MonthPicker view, int year, int monthOfYear); - } +public class MonthPickerDialog extends TwoFieldDatePickerDialog { /** * @param context The context the dialog is to run in. @@ -44,69 +16,24 @@ public interface OnMonthSetListener { * @param year The initial year of the dialog. * @param monthOfYear The initial month of the dialog. */ - public MonthPickerDialog(Context context, - OnMonthSetListener callBack, - int year, - int monthOfYear) { - this(context, 0, callBack, year, monthOfYear); - } - - /** - * @param context The context the dialog is to run in. - * @param theme the theme to apply to this dialog - * @param callBack How the parent is notified that the date is set. - * @param year The initial year of the dialog. - * @param monthOfYear The initial month of the dialog. - */ - public MonthPickerDialog(Context context, - int theme, - OnMonthSetListener callBack, - int year, - int monthOfYear) { - super(context, theme); - - mCallBack = callBack; - - setButton(BUTTON_POSITIVE, context.getText( - R.string.date_picker_dialog_set), this); - setButton(BUTTON_NEGATIVE, context.getText(android.R.string.cancel), - (OnClickListener) null); - setIcon(0); + public MonthPickerDialog(Context context, OnValueSetListener callBack, + int year, int monthOfYear, long minMonth, long maxMonth) { + super(context, callBack, year, monthOfYear, minMonth, maxMonth); setTitle(R.string.month_picker_dialog_title); - - mMonthPicker = new MonthPicker(context); - setView(mMonthPicker); - mMonthPicker.init(year, monthOfYear, this); } @Override - public void onClick(DialogInterface dialog, int which) { - tryNotifyMonthSet(); - } - - private void tryNotifyMonthSet() { - if (mCallBack != null) { - mMonthPicker.clearFocus(); - mCallBack.onMonthSet(mMonthPicker, mMonthPicker.getYear(), - mMonthPicker.getMonth()); - } + protected TwoFieldDatePicker createPicker(Context context, long minValue, long maxValue) { + return new MonthPicker(context, minValue, maxValue); } @Override - protected void onStop() { - if (Build.VERSION.SDK_INT >= 16) { - // The default behavior of dialogs changed in JellyBean and onwards. - // Dismissing a dialog (by pressing back for example) - // applies the chosen date. This code is added here so that the custom - // pickers behave the same as the internal DatePickerDialog. - tryNotifyMonthSet(); + protected void tryNotifyDateSet() { + if (mCallBack != null) { + MonthPicker picker = getMonthPicker(); + picker.clearFocus(); + mCallBack.onValueSet(picker.getYear(), picker.getMonth()); } - super.onStop(); - } - - @Override - public void onMonthChanged(MonthPicker view, int year, int month) { - mMonthPicker.init(year, month, null); } /** @@ -115,32 +42,6 @@ public void onMonthChanged(MonthPicker view, int year, int month) { * @return The calendar view. */ public MonthPicker getMonthPicker() { - return mMonthPicker; - } - - /** - * Sets the current date. - * - * @param year The date year. - * @param monthOfYear The date month. - */ - public void updateDate(int year, int monthOfYear) { - mMonthPicker.updateMonth(year, monthOfYear); - } - - @Override - public Bundle onSaveInstanceState() { - Bundle state = super.onSaveInstanceState(); - state.putInt(YEAR, mMonthPicker.getYear()); - state.putInt(MONTH, mMonthPicker.getMonth()); - return state; - } - - @Override - public void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - int year = savedInstanceState.getInt(YEAR); - int month = savedInstanceState.getInt(MONTH); - mMonthPicker.init(year, month, this); + return (MonthPicker) mPicker; } } diff --git a/src/org/chromium/content/browser/input/SelectionHandleController.java b/src/org/chromium/content/browser/input/SelectionHandleController.java index f65c6a5..77b5c76 100644 --- a/src/org/chromium/content/browser/input/SelectionHandleController.java +++ b/src/org/chromium/content/browser/input/SelectionHandleController.java @@ -12,7 +12,7 @@ public abstract class SelectionHandleController implements CursorController { // The following constants match the ones in - // third_party/WebKit/Source/WebKit/chromium/public/WebTextDirection.h + // third_party/WebKit/public/web/WebTextDirection.h private static final int TEXT_DIRECTION_DEFAULT = 0; private static final int TEXT_DIRECTION_LTR = 1; private static final int TEXT_DIRECTION_RTL = 2; diff --git a/src/org/chromium/content/browser/input/TimeDialog.java b/src/org/chromium/content/browser/input/TimeDialog.java new file mode 100644 index 0000000..60d196f --- /dev/null +++ b/src/org/chromium/content/browser/input/TimeDialog.java @@ -0,0 +1,70 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser.input; + +import android.app.TimePickerDialog; +import android.content.Context; +import android.text.format.Time; +import android.widget.TimePicker; + +/** + * Wrapper on {@code TimePickerDialog} to control min and max times. + */ +public class TimeDialog extends TimePickerDialog { + + private Time mMinTime; + private Time mMaxTime; + + public static TimeDialog create(Context context, OnTimeSetListener callBack, + int hour, int minute, boolean is24HourView, long min, long max) { + Time time = getBoundedTime(hour, minute, min, max); + return new TimeDialog(context, callBack, time.hour, time.minute, + is24HourView, min, max); + } + + private TimeDialog( + Context context, OnTimeSetListener callBack, + int hourOfDay, int minute, boolean is24HourView, long min, long max) { + super(context, callBack, hourOfDay, minute, is24HourView); + if (min >= max) { + mMinTime = getTimeForHourAndMinute(0, 0); + mMaxTime = getTimeForHourAndMinute(23, 59); + } else { + mMinTime = getTimeForMillis(min); + mMaxTime = getTimeForMillis(max); + } + } + + @Override + public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { + Time time = getBoundedTime(hourOfDay, minute, + mMinTime.toMillis(true), mMaxTime.toMillis(true)); + super.onTimeChanged(view, time.hour, time.minute); + updateTime(time.hour, time.minute); + } + + private static Time getBoundedTime(int hour, int minute, + long min, long max) { + Time time = getTimeForHourAndMinute(hour, minute); + if (time.toMillis(true) < min) { + return getTimeForMillis(min); + } else if (time.toMillis(true) > max) { + return getTimeForMillis(max); + } + return time; + } + + private static Time getTimeForMillis(long millis) { + Time time = new Time("GMT"); + time.set(millis); + return time; + } + + private static Time getTimeForHourAndMinute(int hour, int minute) { + Time time = new Time("GMT"); + time.set(0, minute, hour, 1, 0, 1970); + return time; + } +} diff --git a/src/org/chromium/content/browser/input/TwoFieldDatePicker.java b/src/org/chromium/content/browser/input/TwoFieldDatePicker.java new file mode 100644 index 0000000..4eaa0b7 --- /dev/null +++ b/src/org/chromium/content/browser/input/TwoFieldDatePicker.java @@ -0,0 +1,246 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser.input; + +import android.content.Context; +import android.view.LayoutInflater; +import android.widget.NumberPicker; +import android.widget.NumberPicker.OnValueChangeListener; +import android.text.format.DateUtils; +import android.view.accessibility.AccessibilityEvent; +import android.widget.FrameLayout; +import android.widget.NumberPicker; + +import java.util.Calendar; + +import org.chromium.content.R; + +// This class is heavily based on android.widget.DatePicker. +public abstract class TwoFieldDatePicker extends FrameLayout { + + private NumberPicker mPositionInYearSpinner; + + private NumberPicker mYearSpinner; + + private OnMonthOrWeekChangedListener mMonthOrWeekChangedListener; + + // It'd be nice to use android.text.Time like in other Dialogs but + // it suffers from the 2038 effect so it would prevent us from + // having dates over 2038. + private Calendar mMinDate; + + private Calendar mMaxDate; + + private Calendar mCurrentDate; + + /** + * The callback used to indicate the user changes\d the date. + */ + public interface OnMonthOrWeekChangedListener { + + /** + * Called upon a date change. + * + * @param view The view associated with this listener. + * @param year The year that was set. + * @param positionInYear The month or week in year. + */ + void onMonthOrWeekChanged(TwoFieldDatePicker view, int year, int positionInYear); + } + + public TwoFieldDatePicker(Context context, long minValue, long maxValue) { + super(context, null, android.R.attr.datePickerStyle); + + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.two_field_date_picker, this, true); + + OnValueChangeListener onChangeListener = new OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + int year = getYear(); + int positionInYear = getPositionInYear(); + // take care of wrapping of days and months to update greater fields + if (picker == mPositionInYearSpinner) { + positionInYear = newVal; + if (oldVal == picker.getMaxValue() && newVal == picker.getMinValue()) { + year += 1; + } else if (oldVal == picker.getMinValue() && newVal == picker.getMaxValue()) { + year -=1; + } + } else if (picker == mYearSpinner) { + year = newVal; + } else { + throw new IllegalArgumentException(); + } + + // now set the date to the adjusted one + setCurrentDate(year, positionInYear); + updateSpinners(); + notifyDateChanged(); + } + }; + + mCurrentDate = Calendar.getInstance(); + if (minValue >= maxValue) { + mMinDate = Calendar.getInstance(); + mMinDate.set(0, 0, 1); + mMaxDate = Calendar.getInstance(); + mMaxDate.set(9999, 0, 1); + } else { + mMinDate = createDateFromValue(minValue); + mMaxDate = createDateFromValue(maxValue); + } + + // month + mPositionInYearSpinner = (NumberPicker) findViewById(R.id.position_in_year); + mPositionInYearSpinner.setOnLongPressUpdateInterval(200); + mPositionInYearSpinner.setOnValueChangedListener(onChangeListener); + + // year + mYearSpinner = (NumberPicker) findViewById(R.id.year); + mYearSpinner.setOnLongPressUpdateInterval(100); + mYearSpinner.setOnValueChangedListener(onChangeListener); + } + + /** + * Initialize the state. If the provided values designate an inconsistent + * date the values are normalized before updating the spinners. + * + * @param year The initial year. + * @param positionInYear The initial month starting from zero or week in year. + * @param onMonthChangedListener How user is notified date is changed by + * user, can be null. + */ + public void init(int year, int positionInYear, + OnMonthOrWeekChangedListener onMonthOrWeekChangedListener) { + setCurrentDate(year, positionInYear); + updateSpinners(); + mMonthOrWeekChangedListener = onMonthOrWeekChangedListener; + } + + public boolean isNewDate(int year, int positionInYear) { + return (getYear() != year || getPositionInYear() != positionInYear); + } + + /** + * Subclasses know the semantics of @value, and need to return + * a Calendar corresponding to it. + */ + protected abstract Calendar createDateFromValue(long value); + + /** + * Updates the current date. + * + * @param year The year. + * @param positionInYear The month or week in year. + */ + public void updateDate(int year, int positionInYear) { + if (!isNewDate(year, positionInYear)) { + return; + } + setCurrentDate(year, positionInYear); + updateSpinners(); + notifyDateChanged(); + } + + /** + * Subclasses know the semantics of @positionInYear, and need to update @mCurrentDate to the + * appropriate date. + */ + protected abstract void setCurrentDate(int year, int positionInYear); + + protected void setCurrentDate(Calendar date) { + mCurrentDate = date; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + onPopulateAccessibilityEvent(event); + return true; + } + + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + + final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; + String selectedDateUtterance = DateUtils.formatDateTime(getContext(), + mCurrentDate.getTimeInMillis(), flags); + event.getText().add(selectedDateUtterance); + } + + /** + * @return The selected year. + */ + public int getYear() { + return mCurrentDate.get(Calendar.YEAR); + } + + /** + * @return The selected month or week. + */ + public abstract int getPositionInYear(); + + protected abstract int getMaxYear(); + + protected abstract int getMinYear(); + + protected abstract int getMaxPositionInYear(); + + protected abstract int getMinPositionInYear(); + + protected Calendar getMaxDate() { + return mMaxDate; + } + + protected Calendar getMinDate() { + return mMinDate; + } + + protected Calendar getCurrentDate() { + return mCurrentDate; + } + + protected NumberPicker getPositionInYearSpinner() { + return mPositionInYearSpinner; + } + + protected NumberPicker getYearSpinner() { + return mYearSpinner; + } + + /** + * This method should be subclassed to update the spinners based on mCurrentDate. + */ + protected void updateSpinners() { + mPositionInYearSpinner.setDisplayedValues(null); + + // set the spinner ranges respecting the min and max dates + mPositionInYearSpinner.setMinValue(getMinPositionInYear()); + mPositionInYearSpinner.setMaxValue(getMaxPositionInYear()); + mPositionInYearSpinner.setWrapSelectorWheel( + !mCurrentDate.equals(mMinDate) && !mCurrentDate.equals(mMaxDate)); + + // year spinner range does not change based on the current date + mYearSpinner.setMinValue(getMinYear()); + mYearSpinner.setMaxValue(getMaxYear()); + mYearSpinner.setWrapSelectorWheel(false); + + // set the spinner values + mYearSpinner.setValue(getYear()); + mPositionInYearSpinner.setValue(getPositionInYear()); + } + + /** + * Notifies the listener, if such, for a change in the selected date. + */ + protected void notifyDateChanged() { + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + if (mMonthOrWeekChangedListener != null) { + mMonthOrWeekChangedListener.onMonthOrWeekChanged(this, getYear(), getPositionInYear()); + } + } +} diff --git a/src/org/chromium/content/browser/input/TwoFieldDatePickerDialog.java b/src/org/chromium/content/browser/input/TwoFieldDatePickerDialog.java new file mode 100644 index 0000000..4dc3fa4 --- /dev/null +++ b/src/org/chromium/content/browser/input/TwoFieldDatePickerDialog.java @@ -0,0 +1,124 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser.input; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.os.Build; +import android.os.Bundle; +import android.view.View; + +import org.chromium.content.browser.input.TwoFieldDatePicker.OnMonthOrWeekChangedListener; +import org.chromium.content.R; + +public abstract class TwoFieldDatePickerDialog extends AlertDialog implements OnClickListener, + OnMonthOrWeekChangedListener { + + private static final String YEAR = "year"; + private static final String POSITION_IN_YEAR = "position_in_year"; + + protected final TwoFieldDatePicker mPicker; + protected final OnValueSetListener mCallBack; + + /** + * The callback used to indicate the user is done filling in the date. + */ + public interface OnValueSetListener { + + /** + * @param year The year that was set. + * @param positionInYear The week in year. + * with {@link java.util.Calendar}. + */ + void onValueSet(int year, int positionInYear); + } + + /** + * @param context The context the dialog is to run in. + * @param callBack How the parent is notified that the date is set. + * @param year The initial year of the dialog. + * @param weekOfYear The initial week of the dialog. + */ + public TwoFieldDatePickerDialog(Context context, + OnValueSetListener callBack, + int year, + int positionInYear, + long minValue, + long maxValue) { + this(context, 0, callBack, year, positionInYear, minValue, maxValue); + } + + /** + * @param context The context the dialog is to run in. + * @param theme the theme to apply to this dialog + * @param callBack How the parent is notified that the date is set. + * @param year The initial year of the dialog. + * @param weekOfYear The initial week of the dialog. + */ + public TwoFieldDatePickerDialog(Context context, + int theme, + OnValueSetListener callBack, + int year, + int positionInYear, + long minValue, + long maxValue) { + super(context, theme); + + mCallBack = callBack; + + setButton(BUTTON_POSITIVE, context.getText( + R.string.date_picker_dialog_set), this); + setButton(BUTTON_NEGATIVE, context.getText(android.R.string.cancel), + (OnClickListener) null); + setIcon(0); + + mPicker = createPicker(context, minValue, maxValue); + setView(mPicker); + mPicker.init(year, positionInYear, this); + } + + protected TwoFieldDatePicker createPicker(Context context, long minValue, long maxValue) { + return null; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + tryNotifyDateSet(); + } + + /** + * Notifies the listener, if such, that a date has been set. + */ + protected abstract void tryNotifyDateSet(); + + @Override + protected void onStop() { + if (Build.VERSION.SDK_INT >= 16) { + // The default behavior of dialogs changed in JellyBean and onwards. + // Dismissing a dialog (by pressing back for example) + // applies the chosen date. This code is added here so that the custom + // pickers behave the same as the internal DatePickerDialog. + tryNotifyDateSet(); + } + super.onStop(); + } + + @Override + public void onMonthOrWeekChanged(TwoFieldDatePicker view, int year, int positionInYear) { + mPicker.init(year, positionInYear, null); + } + + /** + * Sets the current date. + * + * @param year The date week year. + * @param weekOfYear The date week. + */ + public void updateDate(int year, int weekOfYear) { + mPicker.updateDate(year, weekOfYear); + } +} diff --git a/src/org/chromium/content/browser/input/WeekPicker.java b/src/org/chromium/content/browser/input/WeekPicker.java new file mode 100644 index 0000000..117793b --- /dev/null +++ b/src/org/chromium/content/browser/input/WeekPicker.java @@ -0,0 +1,130 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser.input; + +import android.content.Context; + +import java.util.Calendar; + +import org.chromium.content.R; + +// This class is heavily based on android.widget.DatePicker. +public class WeekPicker extends TwoFieldDatePicker { + + public WeekPicker(Context context, long minValue, long maxValue) { + super(context, minValue, maxValue); + + getPositionInYearSpinner().setContentDescription( + getResources().getString(R.string.accessibility_date_picker_week)); + + // initialize to current date + Calendar cal = Calendar.getInstance(); + cal.setFirstDayOfWeek(Calendar.MONDAY); + cal.setMinimalDaysInFirstWeek(4); + cal.setTimeInMillis(System.currentTimeMillis()); + init(getISOWeekYearForDate(cal), getWeekForDate(cal), null); + } + + private Calendar createDateFromWeek(int year, int week) { + Calendar date = Calendar.getInstance(); + date.clear(); + date.setFirstDayOfWeek(Calendar.MONDAY); + date.setMinimalDaysInFirstWeek(4); + date.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); + date.set(Calendar.YEAR, year); + date.set(Calendar.WEEK_OF_YEAR, week); + return date; + } + + @Override + protected Calendar createDateFromValue(long value) { + Calendar date = Calendar.getInstance(); + date.clear(); + date.setFirstDayOfWeek(Calendar.MONDAY); + date.setMinimalDaysInFirstWeek(4); + date.setTimeInMillis(value); + return date; + } + + public static int getISOWeekYearForDate(Calendar date) { + int year = date.get(Calendar.YEAR); + int month = date.get(Calendar.MONTH); + int week = date.get(Calendar.WEEK_OF_YEAR); + if (month == 0 && week > 51) { + year--; + } else if (month == 11 && week == 1) { + year++; + } + return year; + } + + public static int getWeekForDate(Calendar date) { + return date.get(Calendar.WEEK_OF_YEAR); + } + + @Override + protected void setCurrentDate(int year, int week) { + Calendar date = createDateFromWeek(year, week); + if (date.before(getMinDate())) { + setCurrentDate(getMinDate()); + } else if (date.after(getMaxDate())) { + setCurrentDate(getMaxDate()); + } else { + setCurrentDate(date); + } + } + + private int getNumberOfWeeks() { + // Create a date in the middle of the year, where the week year matches the year. + Calendar date = createDateFromWeek(getYear(), 20); + return date.getActualMaximum(Calendar.WEEK_OF_YEAR); + } + + /** + * @return The selected year. + */ + @Override + public int getYear() { + return getISOWeekYearForDate(getCurrentDate()); + } + + /** + * @return The selected week. + */ + public int getWeek() { + return getWeekForDate(getCurrentDate()); + } + + @Override + public int getPositionInYear() { + return getWeek(); + } + + @Override + protected int getMaxYear() { + return getISOWeekYearForDate(getMaxDate()); + } + + @Override + protected int getMinYear() { + return getISOWeekYearForDate(getMinDate()); + } + + @Override + protected int getMaxPositionInYear() { + if (getYear() == getISOWeekYearForDate(getMaxDate())) { + return getWeekForDate(getMaxDate()); + } + return getNumberOfWeeks(); + } + + @Override + protected int getMinPositionInYear() { + if (getYear() == getISOWeekYearForDate(getMinDate())) { + return getWeekForDate(getMinDate()); + } + return 1; + } +} diff --git a/src/org/chromium/content/browser/input/WeekPickerDialog.java b/src/org/chromium/content/browser/input/WeekPickerDialog.java new file mode 100644 index 0000000..83db991 --- /dev/null +++ b/src/org/chromium/content/browser/input/WeekPickerDialog.java @@ -0,0 +1,65 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser.input; + +import android.content.Context; + +import org.chromium.content.R; + +public class WeekPickerDialog extends TwoFieldDatePickerDialog { + + /** + * @param context The context the dialog is to run in. + * @param callBack How the parent is notified that the date is set. + * @param year The initial year of the dialog. + * @param weekOfYear The initial week of the dialog. + */ + public WeekPickerDialog(Context context, + OnValueSetListener callBack, + int year, int weekOfYear, + long minValue, long maxValue) { + this(context, 0, callBack, year, weekOfYear, minValue, maxValue); + } + + /** + * @param context The context the dialog is to run in. + * @param theme the theme to apply to this dialog + * @param callBack How the parent is notified that the date is set. + * @param year The initial year of the dialog. + * @param weekOfYear The initial week of the dialog. + */ + public WeekPickerDialog(Context context, + int theme, + OnValueSetListener callBack, + int year, + int weekOfYear, + long minValue, long maxValue) { + super(context, theme, callBack, year, weekOfYear, minValue, maxValue); + setTitle(R.string.week_picker_dialog_title); + } + + @Override + protected TwoFieldDatePicker createPicker(Context context, long minValue, long maxValue) { + return new WeekPicker(context, minValue, maxValue); + } + + @Override + protected void tryNotifyDateSet() { + if (mCallBack != null) { + WeekPicker picker = getWeekPicker(); + picker.clearFocus(); + mCallBack.onValueSet(picker.getYear(), picker.getWeek()); + } + } + + /** + * Gets the {@link WeekPicker} contained in this dialog. + * + * @return The calendar view. + */ + public WeekPicker getWeekPicker() { + return (WeekPicker) mPicker; + } +} diff --git a/src/org/chromium/content/common/CommandLine.java b/src/org/chromium/content/common/CommandLine.java index 83cb348..ce00449 100644 --- a/src/org/chromium/content/common/CommandLine.java +++ b/src/org/chromium/content/common/CommandLine.java @@ -70,6 +70,9 @@ public abstract class CommandLine { // Native switch - chrome_switches::kEnableInstantExtendedAPI public static final String ENABLE_INSTANT_EXTENDED_API = "enable-instant-extended-api"; + // Native switch - content_switches::kEnableSpeechRecognition + public static final String ENABLE_SPEECH_RECOGNITION = "enable-speech-recognition"; + // Public abstract interface, implemented in derived classes. // All these methods reflect their native-side counterparts. /** @@ -162,16 +165,9 @@ public static void init(String[] args) { * @param file The fully qualified command line file. */ public static void initFromFile(String file) { - char[] buffer = new char[0]; - try { - // Arbitrary clamp of 8k on the amount of file we read in. - buffer = readUtf8FileFully(file, 8 * 1024); - } catch (FileNotFoundException e) { - // Ignore: having a command line file is optional. - } catch (IOException e) { - Log.w(TAG, "error reading command line file " + file + e); - } - init(tokenizeQuotedAruments(buffer)); + // Arbitrary clamp of 8k on the amount of file we read in. + char[] buffer = readUtf8FileFully(file, 8 * 1024); + init(buffer == null ? null : tokenizeQuotedAruments(buffer)); } /** @@ -257,29 +253,43 @@ private static void setInstance(CommandLine commandLine) { /** * @param fileName the file to read in. - * @param sizeLimit cap on the file size: will throw an exception if exceeded - * @return Array of chars read from the file - * @throws FileNotFoundException file does not exceed - * @throws IOException error encountered accessing the file + * @param sizeLimit cap on the file size. + * @return Array of chars read from the file, or null if the file cannot be read + * or if its length exceeds |sizeLimit|. */ - private static char[] readUtf8FileFully(String fileName, int sizeLimit) throws - FileNotFoundException, IOException { + private static char[] readUtf8FileFully(String fileName, int sizeLimit) { Reader reader = null; + File f = new File(fileName); + long fileLength = f.length(); + + if (fileLength == 0) { + return null; + } + + if (fileLength > sizeLimit) { + Log.w(TAG, "File " + fileName + " length " + fileLength + " exceeds limit " + + sizeLimit); + return null; + } + try { - File f = new File(fileName); - if (f.length() > sizeLimit) { - throw new IOException("File " + fileName + " length " + f.length() + - " exceeds limit " + sizeLimit); - } - char[] buffer = new char[(int) f.length()]; + char[] buffer = new char[(int) fileLength]; reader = new InputStreamReader(new FileInputStream(f), "UTF-8"); int charsRead = reader.read(buffer); // Debug check that we've exhausted the input stream (will fail e.g. if the // file grew after we inspected its length). assert !reader.ready(); return charsRead < buffer.length ? Arrays.copyOfRange(buffer, 0, charsRead) : buffer; + } catch (FileNotFoundException e) { + return null; + } catch (IOException e) { + return null; } finally { - if (reader != null) reader.close(); + try { + if (reader != null) reader.close(); + } catch (IOException e) { + Log.e(TAG, "Unable to close file reader.", e); + } } } diff --git a/src/org/chromium/content/common/ResultCodes.java b/src/org/chromium/content/common/ResultCodes.java index 567b949..07641b7 100644 --- a/src/org/chromium/content/common/ResultCodes.java +++ b/src/org/chromium/content/common/ResultCodes.java @@ -1,4 +1,9 @@ + + + + package org.chromium.content.common; + public class ResultCodes { public static final int RESULT_CODE_NORMAL_EXIT = 0; public static final int RESULT_CODE_KILLED = 1; diff --git a/src/org/chromium/content/common/TopControlsState.java b/src/org/chromium/content/common/TopControlsState.java new file mode 100644 index 0000000..5638e5c --- /dev/null +++ b/src/org/chromium/content/common/TopControlsState.java @@ -0,0 +1,17 @@ + + + + +package org.chromium.content.common; + +public class TopControlsState { + + + + + + +public static final int SHOWN = 1; +public static final int HIDDEN = 2; +public static final int BOTH = 3; +} diff --git a/src/org/chromium/content/common/TopControlsState.template b/src/org/chromium/content/common/TopControlsState.template new file mode 100644 index 0000000..97a3f17 --- /dev/null +++ b/src/org/chromium/content/common/TopControlsState.template @@ -0,0 +1,12 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.common; + +public class TopControlsState { +#define DEFINE_TOP_CONTROLS_STATE(name, value) public static final int name = value; +#include "content/public/common/top_controls_state_list.h" +#undef DEFINE_TOP_CONTROLS_STATE +} + diff --git a/src/org/chromium/content/common/TraceEvent.java b/src/org/chromium/content/common/TraceEvent.java index d606bb7..85954e3 100644 --- a/src/org/chromium/content/common/TraceEvent.java +++ b/src/org/chromium/content/common/TraceEvent.java @@ -6,10 +6,11 @@ import android.os.Build; import android.os.Looper; +import android.os.MessageQueue; +import android.os.SystemClock; import android.util.Log; import android.util.Printer; -import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -22,19 +23,139 @@ // be ignored. (Perhaps we could devise to buffer them up in future?). public class TraceEvent { - private static boolean sEnabled = false; + private static volatile boolean sEnabled = false; + + /** + * A class that records, traces and logs statistics about the main Looper. + * The output of this class can be used in a number of interesting ways: + *

+ *

  1. + * When using chrometrace, there will be a near-continuous line of + * measurements showing both event dispatches as well as idles; + *
  2. + * Logging messages are output for events that run too long on the + * event dispatcher, making it easy to identify problematic areas; + *
  3. + * Statistics are output whenever there is an idle after a non-trivial + * amount of activity, allowing information to be gathered about task + * density and execution cadence on the Looper; + *
+ *

+ * The class attaches itself as an idle handler to the main Looper, and + * monitors the execution of events and idle notifications. Task counters + * accumulate between idle notifications and get reset when a new idle + * notification is received. + */ + private final static class LooperMonitor implements Printer, MessageQueue.IdleHandler { + // Tags for dumping to logcat or TraceEvent + private final static String TAG = "TraceEvent.LooperMonitor"; + private final static String IDLE_EVENT_NAME = "Looper.queueIdle"; + private final static String DISPATCH_EVENT_NAME = + "Looper.dispatchMessage"; + + // Calculation constants + private final static long FRAME_DURATION_MILLIS = 1000L / 60L; // 60 FPS + // A reasonable threshold for defining a Looper event as "long running" + private final static long MIN_INTERESTING_DURATION_MILLIS = + FRAME_DURATION_MILLIS; + // A reasonable threshold for a "burst" of tasks on the Looper + private final static long MIN_INTERESTING_BURST_DURATION_MILLIS = + MIN_INTERESTING_DURATION_MILLIS * 3; + + // Stats tracking + private long mLastIdleStartedAt = 0L; + private long mLastWorkStartedAt = 0L; + private int mNumTasksSeen = 0; + private int mNumIdlesSeen = 0; + private int mNumTasksSinceLastIdle = 0; + + // State + private boolean mIdleMonitorAttached = false; - private static class LooperTracePrinter implements Printer { - private static final String NAME = "Looper.dispatchMessage"; @Override - public void println(String line) { - if (line.startsWith(">>>>>")) { - TraceEvent.begin(NAME, line); + public void println(final String line) { + if (line.startsWith(">")) { + begin(line); } else { - assert line.startsWith("<<<<<"); - TraceEvent.end(NAME); + assert line.startsWith("<"); + end(line); + } + } + + // Called from within the begin/end methods only. + // This method can only execute on the looper thread, because that is + // the only thread that is permitted to call Looper.myqueue(). + private final void syncIdleMonitoring() { + if (sEnabled && !mIdleMonitorAttached) { + // approximate start time for computational purposes + mLastIdleStartedAt = SystemClock.elapsedRealtime(); + Looper.myQueue().addIdleHandler( + LooperMonitor.getInstance()); + mIdleMonitorAttached = true; + Log.v(TAG, "attached idle handler"); + } else if (mIdleMonitorAttached && !sEnabled) { + Looper.myQueue().removeIdleHandler( + LooperMonitor.getInstance()); + mIdleMonitorAttached = false; + Log.v(TAG, "detached idle handler"); } } + + private final void begin(final String line) { + // Close-out any prior 'idle' period before starting new task. + if (mNumTasksSinceLastIdle == 0) { + TraceEvent.end(IDLE_EVENT_NAME); + } + TraceEvent.begin(DISPATCH_EVENT_NAME, line); + mLastWorkStartedAt = SystemClock.elapsedRealtime(); + syncIdleMonitoring(); + } + + private final void end(final String line) { + final long elapsed = SystemClock.elapsedRealtime() + - mLastWorkStartedAt; + if (elapsed > MIN_INTERESTING_DURATION_MILLIS) { + traceAndLog(Log.WARN, "observed a task that took " + + elapsed + "ms: " + line); + } + TraceEvent.end(DISPATCH_EVENT_NAME); + syncIdleMonitoring(); + mNumTasksSeen++; + mNumTasksSinceLastIdle++; + } + + private static void traceAndLog(int level, String message) { + TraceEvent.instant("TraceEvent.LooperMonitor:IdleStats", message); + Log.println(level, TAG, message); + } + + @Override + public final boolean queueIdle() { + final long now = SystemClock.elapsedRealtime(); + if (mLastIdleStartedAt == 0) mLastIdleStartedAt = now; + final long elapsed = now - mLastIdleStartedAt; + mNumIdlesSeen++; + TraceEvent.begin(IDLE_EVENT_NAME, mNumTasksSinceLastIdle + " tasks since last idle."); + if (elapsed > MIN_INTERESTING_BURST_DURATION_MILLIS) { + // Dump stats + String statsString = mNumTasksSeen + " tasks and " + + mNumIdlesSeen + " idles processed so far, " + + mNumTasksSinceLastIdle + " tasks bursted and " + + elapsed + "ms elapsed since last idle"; + traceAndLog(Log.DEBUG, statsString); + } + mLastIdleStartedAt = now; + mNumTasksSinceLastIdle = 0; + return true; // stay installed + } + + // Holder for monitor avoids unnecessary construction on non-debug runs + private final static class Holder { + private final static LooperMonitor sInstance = new LooperMonitor(); + } + public final static LooperMonitor getInstance() { + return Holder.sInstance; + } } private static long sTraceTagView; @@ -112,7 +233,8 @@ public static void setEnabledToMatchNative() { public static synchronized void setEnabled(boolean enabled) { if (sEnabled == enabled) return; sEnabled = enabled; - Looper.getMainLooper().setMessageLogging(enabled ? new LooperTracePrinter() : null); + Looper.getMainLooper().setMessageLogging( + enabled ? LooperMonitor.getInstance() : null); } /** diff --git a/src/org/chromium/media/AudioManagerAndroid.java b/src/org/chromium/media/AudioManagerAndroid.java index 2ed09bd..a7afdae 100644 --- a/src/org/chromium/media/AudioManagerAndroid.java +++ b/src/org/chromium/media/AudioManagerAndroid.java @@ -8,7 +8,11 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.media.AudioFormat; import android.media.AudioManager; +import android.media.AudioRecord; +import android.media.AudioTrack; import android.os.Build; import android.util.Log; @@ -17,10 +21,15 @@ @JNINamespace("media") class AudioManagerAndroid { - private static final String TAG = AudioManagerAndroid.class.getSimpleName(); + private static final String TAG = "AudioManagerAndroid"; + // Most of Google lead devices use 44.1K as the default sampling rate, 44.1K // is also widely used on other android devices. private static final int DEFAULT_SAMPLING_RATE = 44100; + // Randomly picked up frame size which is close to return value on N4. + // Return this default value when + // getProperty(PROPERTY_OUTPUT_FRAMES_PER_BUFFER) fails. + private static final int DEFAULT_FRAME_PER_BUFFER = 256; private final AudioManager mAudioManager; private final Context mContext; @@ -32,6 +41,9 @@ class AudioManagerAndroid { public void setMode(int mode) { try { mAudioManager.setMode(mode); + if (mode == AudioManager.MODE_IN_COMMUNICATION) { + mAudioManager.setSpeakerphoneOn(true); + } } catch (SecurityException e) { Log.e(TAG, "setMode exception: " + e.getMessage()); logDeviceInfo(); @@ -81,8 +93,14 @@ public void unregisterHeadsetReceiver() { mAudioManager.setSpeakerphoneOn(mOriginalSpeakerStatus); } + private void logDeviceInfo() { + Log.i(TAG, "Manufacturer:" + Build.MANUFACTURER + + " Board: " + Build.BOARD + " Device: " + Build.DEVICE + + " Model: " + Build.MODEL + " PRODUCT: " + Build.PRODUCT); + } + @CalledByNative - public int getNativeOutputSampleRate() { + private int getNativeOutputSampleRate() { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { String sampleRateString = mAudioManager.getProperty( AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); @@ -93,9 +111,58 @@ public int getNativeOutputSampleRate() { } } - private void logDeviceInfo() { - Log.i(TAG, "Manufacturer:" + Build.MANUFACTURER + - " Board: " + Build.BOARD + " Device: " + Build.DEVICE + - " Model: " + Build.MODEL + " PRODUCT: " + Build.PRODUCT); + /** + * Returns the minimum frame size required for audio input. + * + * @param sampleRate sampling rate + * @param channels number of channels + */ + @CalledByNative + private static int getMinInputFrameSize(int sampleRate, int channels) { + int channelConfig; + if (channels == 1) { + channelConfig = AudioFormat.CHANNEL_IN_MONO; + } else if (channels == 2) { + channelConfig = AudioFormat.CHANNEL_IN_STEREO; + } else { + return -1; + } + return AudioRecord.getMinBufferSize( + sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT) / 2 / channels; } + + /** + * Returns the minimum frame size required for audio output. + * + * @param sampleRate sampling rate + * @param channels number of channels + */ + @CalledByNative + private static int getMinOutputFrameSize(int sampleRate, int channels) { + int channelConfig; + if (channels == 1) { + channelConfig = AudioFormat.CHANNEL_OUT_MONO; + } else if (channels == 2) { + channelConfig = AudioFormat.CHANNEL_OUT_STEREO; + } else { + return -1; + } + return AudioTrack.getMinBufferSize( + sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT) / 2 / channels; + } + + @CalledByNative + private boolean isAudioLowLatencySupported() { + return mContext.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_AUDIO_LOW_LATENCY); + } + + @CalledByNative + private int getAudioLowLatencyOutputFrameSize() { + String framesPerBuffer = + mAudioManager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); + return (framesPerBuffer == null ? + DEFAULT_FRAME_PER_BUFFER : Integer.parseInt(framesPerBuffer)); + } + } diff --git a/src/org/chromium/media/MediaCodecBridge.java b/src/org/chromium/media/MediaCodecBridge.java index 9e7894c..ed5d947 100644 --- a/src/org/chromium/media/MediaCodecBridge.java +++ b/src/org/chromium/media/MediaCodecBridge.java @@ -27,12 +27,25 @@ class MediaCodecBridge { private static final String TAG = "MediaCodecBridge"; + // Error code for MediaCodecBridge. Keep this value in sync with + // INFO_MEDIA_CODEC_ERROR in media_codec_bridge.h. + private static final int MEDIA_CODEC_ERROR = -1000; + + // After a flush(), dequeueOutputBuffer() can often produce empty presentation timestamps + // for several frames. As a result, the player may find that the time does not increase + // after decoding a frame. To detect this, we check whether the presentation timestamp from + // dequeueOutputBuffer() is larger than input_timestamp - MAX_PRESENTATION_TIMESTAMP_SHIFT_US + // after a flush. And we set the presentation timestamp from dequeueOutputBuffer() to be + // non-decreasing for the remaining frames. + private static final long MAX_PRESENTATION_TIMESTAMP_SHIFT_US = 100000; + private ByteBuffer[] mInputBuffers; private ByteBuffer[] mOutputBuffers; private MediaCodec mMediaCodec; - private AudioTrack mAudioTrack; + private boolean mFlushed; + private long mLastPresentationTimeUs; private static class DequeueOutputResult { private final int mIndex; @@ -41,8 +54,8 @@ private static class DequeueOutputResult { private final long mPresentationTimeMicroseconds; private final int mNumBytes; - private DequeueOutputResult( - int index, int flags, int offset, long presentationTimeMicroseconds, int numBytes) { + private DequeueOutputResult(int index, int flags, int offset, + long presentationTimeMicroseconds, int numBytes) { mIndex = index; mFlags = flags; mOffset = offset; @@ -68,6 +81,8 @@ private DequeueOutputResult( private MediaCodecBridge(String mime) { mMediaCodec = MediaCodec.createDecoderByType(mime); + mLastPresentationTimeUs = 0; + mFlushed = true; } @CalledByNative @@ -91,12 +106,18 @@ private void start() { @CalledByNative private int dequeueInputBuffer(long timeoutUs) { - return mMediaCodec.dequeueInputBuffer(timeoutUs); + try { + return mMediaCodec.dequeueInputBuffer(timeoutUs); + } catch(Exception e) { + Log.e(TAG, "Cannot dequeue Input buffer " + e.toString()); + } + return MEDIA_CODEC_ERROR; } @CalledByNative private void flush() { mMediaCodec.flush(); + mFlushed = true; if (mAudioTrack != null) { mAudioTrack.flush(); } @@ -111,8 +132,13 @@ private void stop() { } @CalledByNative - private MediaFormat getOutputFormat() { - return mMediaCodec.getOutputFormat(); + private int getOutputHeight() { + return mMediaCodec.getOutputFormat().getInteger(MediaFormat.KEY_HEIGHT); + } + + @CalledByNative + private int getOutputWidth() { + return mMediaCodec.getOutputFormat().getInteger(MediaFormat.KEY_WIDTH); } @CalledByNative @@ -128,7 +154,27 @@ private ByteBuffer getOutputBuffer(int index) { @CalledByNative private void queueInputBuffer( int index, int offset, int size, long presentationTimeUs, int flags) { - mMediaCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags); + resetLastPresentationTimeIfNeeded(presentationTimeUs); + try { + mMediaCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags); + } catch(IllegalStateException e) { + Log.e(TAG, "Failed to queue input buffer " + e.toString()); + } + } + + @CalledByNative + private void queueSecureInputBuffer( + int index, int offset, byte[] iv, byte[] keyId, int[] numBytesOfClearData, + int[] numBytesOfEncryptedData, int numSubSamples, long presentationTimeUs) { + resetLastPresentationTimeIfNeeded(presentationTimeUs); + try { + MediaCodec.CryptoInfo cryptoInfo = new MediaCodec.CryptoInfo(); + cryptoInfo.set(numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, + keyId, iv, MediaCodec.CRYPTO_MODE_AES_CTR); + mMediaCodec.queueSecureInputBuffer(index, offset, cryptoInfo, presentationTimeUs, 0); + } catch(IllegalStateException e) { + Log.e(TAG, "Failed to queue secure input buffer " + e.toString()); + } } @CalledByNative @@ -144,31 +190,85 @@ private void getOutputBuffers() { @CalledByNative private DequeueOutputResult dequeueOutputBuffer(long timeoutUs) { MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); - int index = mMediaCodec.dequeueOutputBuffer(info, timeoutUs); + int index = MEDIA_CODEC_ERROR; + try { + index = mMediaCodec.dequeueOutputBuffer(info, timeoutUs); + if (info.presentationTimeUs < mLastPresentationTimeUs) { + // TODO(qinmin): return a special code through DequeueOutputResult + // to notify the native code the the frame has a wrong presentation + // timestamp and should be skipped. + info.presentationTimeUs = mLastPresentationTimeUs; + } + mLastPresentationTimeUs = info.presentationTimeUs; + } catch (IllegalStateException e) { + Log.e(TAG, "Cannot dequeue output buffer " + e.toString()); + } return new DequeueOutputResult( index, info.flags, info.offset, info.presentationTimeUs, info.size); } @CalledByNative - private void configureVideo(MediaFormat format, Surface surface, MediaCrypto crypto, + private boolean configureVideo(MediaFormat format, Surface surface, MediaCrypto crypto, int flags) { - mMediaCodec.configure(format, surface, crypto, flags); + try { + mMediaCodec.configure(format, surface, crypto, flags); + return true; + } catch (IllegalStateException e) { + Log.e(TAG, "Cannot configure the video codec " + e.toString()); + } + return false; } @CalledByNative - private void configureAudio(MediaFormat format, MediaCrypto crypto, int flags, + private static MediaFormat createAudioFormat(String mime, int SampleRate, int ChannelCount) { + return MediaFormat.createAudioFormat(mime, SampleRate, ChannelCount); + } + + @CalledByNative + private static MediaFormat createVideoFormat(String mime, int width, int height) { + return MediaFormat.createVideoFormat(mime, width, height); + } + + @CalledByNative + private static void setCodecSpecificData(MediaFormat format, int index, ByteBuffer bytes) { + String name = null; + if (index == 0) { + name = "csd-0"; + } else if (index == 1) { + name = "csd-1"; + } + if (name != null) { + format.setByteBuffer(name, bytes); + } + } + + @CalledByNative + private static void setFrameHasADTSHeader(MediaFormat format) { + format.setInteger(MediaFormat.KEY_IS_ADTS, 1); + } + + @CalledByNative + private boolean configureAudio(MediaFormat format, MediaCrypto crypto, int flags, boolean playAudio) { - mMediaCodec.configure(format, null, crypto, flags); - if (playAudio) { - int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); - int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); - int channelConfig = (channelCount == 1) ? AudioFormat.CHANNEL_OUT_MONO : - AudioFormat.CHANNEL_OUT_STEREO; - int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, - AudioFormat.ENCODING_PCM_16BIT); - mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, - AudioFormat.ENCODING_PCM_16BIT, minBufferSize, AudioTrack.MODE_STREAM); + try { + mMediaCodec.configure(format, null, crypto, flags); + if (playAudio) { + int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); + int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); + int channelConfig = (channelCount == 1) ? AudioFormat.CHANNEL_OUT_MONO : + AudioFormat.CHANNEL_OUT_STEREO; + // Using 16bit PCM for output. Keep this value in sync with + // kBytesPerAudioOutputSample in media_codec_bridge.cc. + int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, + AudioFormat.ENCODING_PCM_16BIT); + mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, + AudioFormat.ENCODING_PCM_16BIT, minBufferSize, AudioTrack.MODE_STREAM); + } + return true; + } catch (IllegalStateException e) { + Log.e(TAG, "Cannot configure the audio codec " + e.toString()); } + return false; } @CalledByNative @@ -184,4 +284,19 @@ private void playOutputBuffer(byte[] buf) { } } } + + @CalledByNative + private void setVolume(double volume) { + if (mAudioTrack != null) { + mAudioTrack.setStereoVolume((float) volume, (float) volume); + } + } + + private void resetLastPresentationTimeIfNeeded(long presentationTimeUs) { + if (mFlushed) { + mLastPresentationTimeUs = + Math.max(presentationTimeUs - MAX_PRESENTATION_TIMESTAMP_SHIFT_US, 0); + mFlushed = false; + } + } } diff --git a/src/org/chromium/media/MediaPlayerBridge.java b/src/org/chromium/media/MediaPlayerBridge.java index 7274418..4b0a1aa 100644 --- a/src/org/chromium/media/MediaPlayerBridge.java +++ b/src/org/chromium/media/MediaPlayerBridge.java @@ -9,20 +9,142 @@ import android.net.Uri; import android.text.TextUtils; import android.util.Log; +import android.view.Surface; import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; +import java.io.IOException; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; +import java.util.Map; +// A wrapper around android.media.MediaPlayer that allows the native code to use it. +// See media/base/android/media_player_bridge.cc for the corresponding native code. @JNINamespace("media") -class MediaPlayerBridge { +public class MediaPlayerBridge { private static final String TAG = "MediaPlayerBridge"; + // Local player to forward this to. We don't initialize it here since the subclass might not + // want it. + private MediaPlayer mPlayer; + + @CalledByNative + private static MediaPlayerBridge create() { + return new MediaPlayerBridge(); + } + + protected MediaPlayer getLocalPlayer() { + if (mPlayer == null) { + mPlayer = new MediaPlayer(); + } + return mPlayer; + } + + @CalledByNative + protected void setSurface(Surface surface) { + getLocalPlayer().setSurface(surface); + } + + @CalledByNative + protected void prepareAsync() throws IllegalStateException { + getLocalPlayer().prepareAsync(); + } + + @CalledByNative + protected boolean isPlaying() { + return getLocalPlayer().isPlaying(); + } + + @CalledByNative + protected int getVideoWidth() { + return getLocalPlayer().getVideoWidth(); + } + + @CalledByNative + protected int getVideoHeight() { + return getLocalPlayer().getVideoHeight(); + } + + @CalledByNative + protected int getCurrentPosition() { + return getLocalPlayer().getCurrentPosition(); + } + + @CalledByNative + protected int getDuration() { + return getLocalPlayer().getDuration(); + } + + @CalledByNative + protected void release() { + getLocalPlayer().release(); + } + + @CalledByNative + protected void setVolume(double volume) { + getLocalPlayer().setVolume((float) volume, (float) volume); + } + + @CalledByNative + protected void start() { + getLocalPlayer().start(); + } + + @CalledByNative + protected void pause() { + getLocalPlayer().pause(); + } + + @CalledByNative + protected void seekTo(int msec) throws IllegalStateException { + getLocalPlayer().seekTo(msec); + } + + @CalledByNative + protected boolean setDataSource( + Context context, String url, String cookies, boolean hideUrlLog) { + Uri uri = Uri.parse(url); + HashMap headersMap = new HashMap(); + if (hideUrlLog) + headersMap.put("x-hide-urls-from-log", "true"); + if (!TextUtils.isEmpty(cookies)) + headersMap.put("Cookie", cookies); + try { + getLocalPlayer().setDataSource(context, uri, headersMap); + return true; + } catch (Exception e) { + return false; + } + } + + protected void setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener listener) { + getLocalPlayer().setOnBufferingUpdateListener(listener); + } + + protected void setOnCompletionListener(MediaPlayer.OnCompletionListener listener) { + getLocalPlayer().setOnCompletionListener(listener); + } + + protected void setOnErrorListener(MediaPlayer.OnErrorListener listener) { + getLocalPlayer().setOnErrorListener(listener); + } + + protected void setOnPreparedListener(MediaPlayer.OnPreparedListener listener) { + getLocalPlayer().setOnPreparedListener(listener); + } + + protected void setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener listener) { + getLocalPlayer().setOnSeekCompleteListener(listener); + } + + protected void setOnVideoSizeChangedListener(MediaPlayer.OnVideoSizeChangedListener listener) { + getLocalPlayer().setOnVideoSizeChangedListener(listener); + } + private static class AllowedOperations { private final boolean mCanPause; private final boolean mCanSeekForward; @@ -45,23 +167,6 @@ private AllowedOperations(boolean canPause, boolean canSeekForward, private boolean canSeekBackward() { return mCanSeekBackward; } } - @CalledByNative - private static boolean setDataSource(MediaPlayer player, Context context, String url, - String cookies, boolean hideUrlLog) { - Uri uri = Uri.parse(url); - HashMap headersMap = new HashMap(); - if (hideUrlLog) - headersMap.put("x-hide-urls-from-log", "true"); - if (!TextUtils.isEmpty(cookies)) - headersMap.put("Cookie", cookies); - try { - player.setDataSource(context, uri, headersMap); - return true; - } catch (Exception e) { - return false; - } - } - /** * Returns an AllowedOperations object to show all the operations that are * allowed on the media player. diff --git a/src/org/chromium/media/MediaPlayerListener.java b/src/org/chromium/media/MediaPlayerListener.java index ba080c8..3c68589 100644 --- a/src/org/chromium/media/MediaPlayerListener.java +++ b/src/org/chromium/media/MediaPlayerListener.java @@ -124,19 +124,15 @@ public void releaseResources() { @CalledByNative private static MediaPlayerListener create(int nativeMediaPlayerListener, - Context context, MediaPlayer mediaPlayer) { + Context context, MediaPlayerBridge mediaPlayerBridge) { final MediaPlayerListener listener = new MediaPlayerListener(nativeMediaPlayerListener, context); - mediaPlayer.setOnBufferingUpdateListener(listener); - mediaPlayer.setOnCompletionListener(listener); - mediaPlayer.setOnErrorListener(listener); - mediaPlayer.setOnPreparedListener(listener); - mediaPlayer.setOnSeekCompleteListener(listener); - mediaPlayer.setOnVideoSizeChangedListener(listener); - if (PackageManager.PERMISSION_GRANTED == - context.checkCallingOrSelfPermission(permission.WAKE_LOCK)) { - mediaPlayer.setWakeMode(context, android.os.PowerManager.FULL_WAKE_LOCK); - } + mediaPlayerBridge.setOnBufferingUpdateListener(listener); + mediaPlayerBridge.setOnCompletionListener(listener); + mediaPlayerBridge.setOnErrorListener(listener); + mediaPlayerBridge.setOnPreparedListener(listener); + mediaPlayerBridge.setOnSeekCompleteListener(listener); + mediaPlayerBridge.setOnVideoSizeChangedListener(listener); AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); am.requestAudioFocus( @@ -144,7 +140,7 @@ private static MediaPlayerListener create(int nativeMediaPlayerListener, AudioManager.STREAM_MUSIC, // Request permanent focus. - AudioManager.AUDIOFOCUS_GAIN); + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); return listener; } diff --git a/src/org/chromium/media/VideoCapture.java b/src/org/chromium/media/VideoCapture.java index 150f3b6..f055f35 100644 --- a/src/org/chromium/media/VideoCapture.java +++ b/src/org/chromium/media/VideoCapture.java @@ -31,9 +31,35 @@ static class CaptureCapability { public int mDesiredFps = 0; } + // Some devices with OS older than JELLY_BEAN don't support YV12 format correctly. + // Some devices don't support YV12 format correctly even with JELLY_BEAN or newer OS. + // To work around the issues on those devices, we'd have to request NV21. + // This is a temporary hack till device manufacturers fix the problem or + // we don't need to support those devices any more. + private static class DeviceImageFormatHack { + private static final String[] sBUGGY_DEVICE_LIST = { + "SAMSUNG-SGH-I747", + }; + + static int getImageFormat() { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) { + return ImageFormat.NV21; + } + + for (String buggyDevice : sBUGGY_DEVICE_LIST) { + if (buggyDevice.contentEquals(android.os.Build.MODEL)) { + return ImageFormat.NV21; + } + } + + return ImageFormat.YV12; + } + } + private Camera mCamera; public ReentrantLock mPreviewBufferLock = new ReentrantLock(); - private int mPixelFormat = ImageFormat.YV12; + private int mImageFormat = ImageFormat.YV12; + private byte[] mColorPlane = null; private Context mContext = null; // True when native code has started capture. private boolean mIsRunning = false; @@ -87,26 +113,29 @@ public boolean allocate(int width, int height, int frameRate) { // Calculate fps. List listFpsRange = parameters.getSupportedPreviewFpsRange(); + if (listFpsRange.size() == 0) { + Log.e(TAG, "allocate: no fps range found"); + return false; + } int frameRateInMs = frameRate * 1000; - boolean fpsIsSupported = false; - int fpsMin = 0; - int fpsMax = 0; Iterator itFpsRange = listFpsRange.iterator(); + int[] fpsRange = (int[])itFpsRange.next(); + // Use the first range as default. + int fpsMin = fpsRange[0]; + int fpsMax = fpsRange[1]; + int newFrameRate = (fpsMin + 999) / 1000; while (itFpsRange.hasNext()) { - int[] fpsRange = (int[])itFpsRange.next(); + fpsRange = (int[])itFpsRange.next(); if (fpsRange[0] <= frameRateInMs && frameRateInMs <= fpsRange[1]) { - fpsIsSupported = true; fpsMin = fpsRange[0]; fpsMax = fpsRange[1]; + newFrameRate = frameRate; break; } } - - if (!fpsIsSupported) { - Log.e(TAG, "allocate: fps " + frameRate + " is not supported"); - return false; - } + frameRate = newFrameRate; + Log.d(TAG, "allocate: fps set to " + frameRate); mCurrentCapability = new CaptureCapability(); mCurrentCapability.mDesiredFps = frameRate; @@ -144,8 +173,10 @@ public boolean allocate(int width, int height, int frameRate) { Log.d(TAG, "allocate: matched width=" + matchedWidth + ", height=" + matchedHeight); + calculateImageFormat(matchedWidth, matchedHeight); + parameters.setPreviewSize(matchedWidth, matchedHeight); - parameters.setPreviewFormat(mPixelFormat); + parameters.setPreviewFormat(mImageFormat); parameters.setPreviewFpsRange(fpsMin, fpsMax); mCamera.setParameters(parameters); @@ -171,7 +202,7 @@ public boolean allocate(int width, int height, int frameRate) { mCamera.setPreviewTexture(mSurfaceTexture); int bufSize = matchedWidth * matchedHeight * - ImageFormat.getBitsPerPixel(mPixelFormat) / 8; + ImageFormat.getBitsPerPixel(mImageFormat) / 8; for (int i = 0; i < NUM_CAPTURE_BUFFERS; i++) { byte[] buffer = new byte[bufSize]; mCamera.addCallbackBuffer(buffer); @@ -283,11 +314,14 @@ public void onPreviewFrame(byte[] data, Camera camera) { if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) { rotation = (mCameraOrientation + rotation) % 360; rotation = (360 - rotation) % 360; - flipHorizontal = (rotation == 180 || rotation == 0); - flipVertical = !flipHorizontal; + flipHorizontal = (rotation == 270 || rotation == 90); + flipVertical = flipHorizontal; } else { rotation = (mCameraOrientation - rotation + 360) % 360; } + if (mImageFormat == ImageFormat.NV21) { + convertNV21ToYV12(data); + } nativeOnFrameAvailable(mNativeVideoCaptureDeviceAndroid, data, mExpectedFrameSize, rotation, flipVertical, flipHorizontal); @@ -374,4 +408,22 @@ private int getDeviceOrientation() { } return orientation; } + + private void calculateImageFormat(int width, int height) { + mImageFormat = DeviceImageFormatHack.getImageFormat(); + if (mImageFormat == ImageFormat.NV21) { + mColorPlane = new byte[width * height / 4]; + } + } + + private void convertNV21ToYV12(byte[] data) { + final int ySize = mCurrentCapability.mWidth * mCurrentCapability.mHeight; + final int uvSize = ySize / 4; + for (int i = 0; i < uvSize; i++) { + final int index = ySize + i * 2; + data[ySize + i] = data[index]; + mColorPlane[i] = data[index + 1]; + } + System.arraycopy(mColorPlane, 0, data, ySize + uvSize, uvSize); + } } diff --git a/src/org/chromium/media/WebAudioMediaCodecBridge.java b/src/org/chromium/media/WebAudioMediaCodecBridge.java index 2559693..ac752d4 100644 --- a/src/org/chromium/media/WebAudioMediaCodecBridge.java +++ b/src/org/chromium/media/WebAudioMediaCodecBridge.java @@ -10,10 +10,11 @@ import android.media.MediaCodec.BufferInfo; import android.media.MediaExtractor; import android.media.MediaFormat; +import android.os.ParcelFileDescriptor; import android.util.Log; +import java.io.File; import java.nio.ByteBuffer; -import android.os.ParcelFileDescriptor; import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; @@ -25,6 +26,13 @@ class WebAudioMediaCodecBridge { // TODO(rtoy): What is the correct timeout value for reading // from a file in memory? static final long TIMEOUT_MICROSECONDS = 500; + @CalledByNative + private static String CreateTempFile(Context ctx) throws java.io.IOException { + File outputDirectory = ctx.getCacheDir(); + File outputFile = File.createTempFile("webaudio", ".dat", outputDirectory); + return outputFile.getAbsolutePath(); + } + @CalledByNative private static boolean decodeAudioFile(Context ctx, int nativeMediaCodecBridge, diff --git a/src/org/chromium/net/AndroidNetworkLibrary.java b/src/org/chromium/net/AndroidNetworkLibrary.java index bc82bb1..95752cc 100644 --- a/src/org/chromium/net/AndroidNetworkLibrary.java +++ b/src/org/chromium/net/AndroidNetworkLibrary.java @@ -30,7 +30,7 @@ */ class AndroidNetworkLibrary { - private static final String TAG = AndroidNetworkLibrary.class.getName(); + private static final String TAG = "AndroidNetworkLibrary"; /** * Stores the key pair through the CertInstaller activity. diff --git a/src/org/chromium/net/CertVerifyResultAndroid.java b/src/org/chromium/net/CertVerifyResultAndroid.java index 2363f9d..3eb7b78 100644 --- a/src/org/chromium/net/CertVerifyResultAndroid.java +++ b/src/org/chromium/net/CertVerifyResultAndroid.java @@ -1,10 +1,32 @@ + + + + package org.chromium.net; + public class CertVerifyResultAndroid { + + + + public static final int VERIFY_OK = 0; + + public static final int VERIFY_FAILED = -1; + + public static final int VERIFY_NO_TRUSTED_ROOT = -2; + + public static final int VERIFY_EXPIRED = -3; + + public static final int VERIFY_NOT_YET_VALID = -4; + + public static final int VERIFY_UNABLE_TO_PARSE = -5; + + + public static final int VERIFY_INCORRECT_KEY_USAGE = -6; } diff --git a/src/org/chromium/net/CertificateMimeType.java b/src/org/chromium/net/CertificateMimeType.java index 95d38fa..bddac2f 100644 --- a/src/org/chromium/net/CertificateMimeType.java +++ b/src/org/chromium/net/CertificateMimeType.java @@ -1,5 +1,12 @@ + + + + package org.chromium.net; + public class CertificateMimeType { + + public static final int UNKNOWN = 0; public static final int X509_USER_CERT = 1; public static final int X509_CA_CERT = 2; diff --git a/src/org/chromium/net/NetError.java b/src/org/chromium/net/NetError.java index 63fa793..cabbff2 100644 --- a/src/org/chromium/net/NetError.java +++ b/src/org/chromium/net/NetError.java @@ -1,4 +1,9 @@ + + + + package org.chromium.net; + public class NetError { public static final int ERR_IO_PENDING = -1; public static final int ERR_FAILED = -2; @@ -74,6 +79,9 @@ public class NetError { public static final int ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN = -150; public static final int ERR_CLIENT_AUTH_CERT_TYPE_UNSUPPORTED = -151; public static final int ERR_ORIGIN_BOUND_CERT_GENERATION_TYPE_MISMATCH = -152; +public static final int ERR_SSL_DECRYPT_ERROR_ALERT = -153; +public static final int ERR_WS_THROTTLE_QUEUE_TOO_LARGE = -154; +public static final int ERR_TOO_MANY_SOCKET_STREAMS = -155; public static final int ERR_CERT_COMMON_NAME_INVALID = -200; public static final int ERR_CERT_DATE_INVALID = -201; public static final int ERR_CERT_AUTHORITY_INVALID = -202; @@ -129,6 +137,7 @@ public class NetError { public static final int ERR_CONTENT_LENGTH_MISMATCH = -354; public static final int ERR_INCOMPLETE_CHUNKED_ENCODING = -355; public static final int ERR_QUIC_PROTOCOL_ERROR = -356; +public static final int ERR_RESPONSE_HEADERS_TRUNCATED = -357; public static final int ERR_CACHE_MISS = -400; public static final int ERR_CACHE_READ_FAILURE = -401; public static final int ERR_CACHE_WRITE_FAILURE = -402; @@ -160,6 +169,7 @@ public class NetError { public static final int ERR_KEY_GENERATION_FAILED = -710; public static final int ERR_ORIGIN_BOUND_CERT_GENERATION_FAILED = -711; public static final int ERR_PRIVATE_KEY_EXPORT_FAILED = -712; +public static final int ERR_SELF_SIGNED_CERT_GENERATION_FAILED = -713; public static final int ERR_DNS_MALFORMED_RESPONSE = -800; public static final int ERR_DNS_SERVER_REQUIRES_TCP = -801; public static final int ERR_DNS_SERVER_FAILED = -802; diff --git a/src/org/chromium/net/PrivateKeyType.java b/src/org/chromium/net/PrivateKeyType.java index 671829c..8c4c33a 100644 --- a/src/org/chromium/net/PrivateKeyType.java +++ b/src/org/chromium/net/PrivateKeyType.java @@ -1,5 +1,11 @@ + + + + package org.chromium.net; + public class PrivateKeyType { + public static final int RSA = 0; public static final int DSA = 1; public static final int ECDSA = 2; diff --git a/src/org/chromium/net/X509Util.java b/src/org/chromium/net/X509Util.java index 1c0c045..30007ca 100644 --- a/src/org/chromium/net/X509Util.java +++ b/src/org/chromium/net/X509Util.java @@ -27,7 +27,7 @@ public class X509Util { - private static final String TAG = X509Util.class.getName(); + private static final String TAG = "X509Util"; private static CertificateFactory sCertificateFactory; diff --git a/src/org/chromium/ui/ColorPickerAdvanced.java b/src/org/chromium/ui/ColorPickerAdvanced.java new file mode 100644 index 0000000..91800ea --- /dev/null +++ b/src/org/chromium/ui/ColorPickerAdvanced.java @@ -0,0 +1,252 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; + +import android.content.Context; +import android.graphics.Color; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; + +/** + * Represents a more advanced way for the user to choose a color, based on selecting each of + * the Hue, Saturation and Value attributes. + */ +public class ColorPickerAdvanced extends LinearLayout implements OnSeekBarChangeListener { + private static final int HUE_SEEK_BAR_MAX = 360; + + private static final int HUE_COLOR_COUNT = 7; + + private static final int SATURATION_SEEK_BAR_MAX = 100; + + private static final int SATURATION_COLOR_COUNT = 2; + + private static final int VALUE_SEEK_BAR_MAX = 100; + + private static final int VALUE_COLOR_COUNT = 2; + + ColorPickerAdvancedComponent mHueDetails; + + ColorPickerAdvancedComponent mSaturationDetails; + + ColorPickerAdvancedComponent mValueDetails; + + private OnColorChangedListener mOnColorChangedListener; + + private int mCurrentColor; + + private final float[] mCurrentHsvValues = new float[3]; + + public ColorPickerAdvanced(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ColorPickerAdvanced(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + public ColorPickerAdvanced(Context context) { + super(context); + init(); + } + + /** + * Initializes all the views and variables in the advanced view. + */ + private void init() { + setOrientation(LinearLayout.VERTICAL); + + mHueDetails = createAndAddNewGradient(R.string.color_picker_hue, + HUE_SEEK_BAR_MAX, this); + mSaturationDetails = createAndAddNewGradient(R.string.color_picker_saturation, + SATURATION_SEEK_BAR_MAX, this); + mValueDetails = createAndAddNewGradient(R.string.color_picker_value, + VALUE_SEEK_BAR_MAX, this); + + refreshGradientComponents(); + } + + /** + * Creates a new GradientDetails object from the parameters provided, initializes it, + * and adds it to this advanced view. + * + * @param textResourceId The text to display for the label. + * @param seekBarMax The maximum value of the seek bar for the gradient. + * @param seekBarListener Object listening to when the user changes the seek bar. + * + * @return A new GradientDetails object initialized with the given parameters. + */ + public ColorPickerAdvancedComponent createAndAddNewGradient(int textResourceId, + int seekBarMax, + OnSeekBarChangeListener seekBarListener) { + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View newComponent = inflater.inflate(R.layout.color_picker_advanced_component, null); + addView(newComponent); + + return new ColorPickerAdvancedComponent(newComponent, + textResourceId, + seekBarMax, + seekBarListener); + } + + /** + * Sets the listener for when the user changes the color. + * + * @param onColorChangedListener The object listening for the change in color. + */ + public void setListener(OnColorChangedListener onColorChangedListener) { + mOnColorChangedListener = onColorChangedListener; + } + + /** + * @return The color the user has currently chosen. + */ + public int getColor() { + return mCurrentColor; + } + + /** + * Sets the color that the user has currently chosen. + * + * @param color The currently chosen color. + */ + public void setColor(int color) { + mCurrentColor = color; + Color.colorToHSV(mCurrentColor, mCurrentHsvValues); + refreshGradientComponents(); + } + + /** + * Notifies the listener, if there is one, of a change in the selected color. + */ + private void notifyColorChanged() { + if (mOnColorChangedListener != null) { + mOnColorChangedListener.onColorChanged(getColor()); + } + } + + /** + * Callback for when a slider is updated on the advanced view. + * + * @param seekBar The color slider that was updated. + * @param progress The new value of the color slider. + * @param fromUser Whether it was the user the changed the value, or whether + * we were setting it up. + */ + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + mCurrentHsvValues[0] = mHueDetails.getValue(); + mCurrentHsvValues[1] = mSaturationDetails.getValue() / 100.0f; + mCurrentHsvValues[2] = mValueDetails.getValue() / 100.0f; + + mCurrentColor = Color.HSVToColor(mCurrentHsvValues); + + updateHueGradient(); + updateSaturationGradient(); + updateValueGradient(); + + notifyColorChanged(); + } + } + + /** + * Updates only the hue gradient display with the hue value for the + * currently selected color. + */ + private void updateHueGradient() { + float[] tempHsvValues = new float[3]; + tempHsvValues[1] = mCurrentHsvValues[1]; + tempHsvValues[2] = mCurrentHsvValues[2]; + + int[] newColors = new int[HUE_COLOR_COUNT]; + + for (int i = 0; i < HUE_COLOR_COUNT; ++i) { + tempHsvValues[0] = i * 60.0f; + newColors[i] = Color.HSVToColor(tempHsvValues); + } + mHueDetails.setGradientColors(newColors); + } + + /** + * Updates only the saturation gradient display with the saturation value + * for the currently selected color. + */ + private void updateSaturationGradient() { + float[] tempHsvValues = new float[3]; + tempHsvValues[0] = mCurrentHsvValues[0]; + tempHsvValues[1] = 0.0f; + tempHsvValues[2] = mCurrentHsvValues[2]; + + int[] newColors = new int[SATURATION_COLOR_COUNT]; + + newColors[0] = Color.HSVToColor(tempHsvValues); + + tempHsvValues[1] = 1.0f; + newColors[1] = Color.HSVToColor(tempHsvValues); + mSaturationDetails.setGradientColors(newColors); + } + + /** + * Updates only the Value gradient display with the Value amount for + * the currently selected color. + */ + private void updateValueGradient() { + float[] tempHsvValues = new float[3]; + tempHsvValues[0] = mCurrentHsvValues[0]; + tempHsvValues[1] = mCurrentHsvValues[1]; + tempHsvValues[2] = 0.0f; + + int[] newColors = new int[VALUE_COLOR_COUNT]; + + newColors[0] = Color.HSVToColor(tempHsvValues); + + tempHsvValues[2] = 1.0f; + newColors[1] = Color.HSVToColor(tempHsvValues); + mValueDetails.setGradientColors(newColors); + } + + /** + * Updates all the gradient displays to show the currently selected color. + */ + private void refreshGradientComponents() { + // Round and bound the saturation value. + int saturationValue = Math.round(mCurrentHsvValues[1] * 100.0f); + saturationValue = Math.min(saturationValue, SATURATION_SEEK_BAR_MAX); + saturationValue = Math.max(saturationValue, 0); + + // Round and bound the Value amount. + int valueValue = Math.round(mCurrentHsvValues[2] * 100.0f); + valueValue = Math.min(valueValue, VALUE_SEEK_BAR_MAX); + valueValue = Math.max(valueValue, 0); + + // Don't need to round the hue value since its possible values match the seek bar + // range directly. + mHueDetails.setValue(mCurrentHsvValues[0]); + mSaturationDetails.setValue(saturationValue); + mValueDetails.setValue(valueValue); + + updateHueGradient(); + updateSaturationGradient(); + updateValueGradient(); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // Do nothing. + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + // Do nothing. + } +} diff --git a/src/org/chromium/ui/ColorPickerAdvancedComponent.java b/src/org/chromium/ui/ColorPickerAdvancedComponent.java new file mode 100644 index 0000000..1e679d5 --- /dev/null +++ b/src/org/chromium/ui/ColorPickerAdvancedComponent.java @@ -0,0 +1,94 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; + +import android.content.Context; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.GradientDrawable.Orientation; +import android.os.Build; +import android.view.View; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; +import android.widget.TextView; +import org.chromium.base.ApiCompatibilityUtils; + +/** + * Encapsulates a single gradient view of the HSV color display, including its label, gradient + * view and seek bar. + * + * Mirrors a "color_picker_advanced_component" layout. + */ +public class ColorPickerAdvancedComponent { + // The view that displays the gradient. + private final View mGradientView; + // The seek bar that allows the user to change the value of this component. + private final SeekBar mSeekBar; + // The set of colors to interpolate the gradient through. + private int[] mGradientColors; + // The Drawable that represents the gradient. + private GradientDrawable mGradientDrawable; + // The text label for the component. + private final TextView mText; + + /** + * Initializes the views. + * + * @param rootView View that contains all the content, such as the label, gradient view, etc. + * @param textResourceId The resource ID of the text to show on the label. + * @param seekBarMax The range of the seek bar. + * @param seekBarListener The listener for when the seek bar value changes. + */ + ColorPickerAdvancedComponent(final View rootView, + final int textResourceId, + final int seekBarMax, + final OnSeekBarChangeListener seekBarListener) { + mGradientView = rootView.findViewById(R.id.gradient); + mText = (TextView) rootView.findViewById(R.id.text); + mText.setText(textResourceId); + mGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, null); + mSeekBar = (SeekBar) rootView.findViewById(R.id.seek_bar); + mSeekBar.setOnSeekBarChangeListener(seekBarListener); + mSeekBar.setMax(seekBarMax); + // Setting the thumb offset means the seek bar thumb can move all the way to each end + // of the gradient view. + Context context = rootView.getContext(); + int offset = context.getResources() + .getDrawable(R.drawable.color_picker_advanced_select_handle) + .getIntrinsicWidth(); + mSeekBar.setThumbOffset(offset / 2); + } + + /** + * @return The value represented by this component, maintained by the seek bar progress. + */ + public float getValue() { + return mSeekBar.getProgress(); + } + + /** + * Sets the value of the component (by setting the seek bar value). + * + * @param newValue The value to give the component. + */ + public void setValue(float newValue) { + mSeekBar.setProgress((int) newValue); + } + + /** + * Sets the colors for the gradient view to interpolate through. + * + * @param newColors The set of colors representing the interpolation points for the gradient. + */ + public void setGradientColors(int[] newColors) { + mGradientColors = newColors.clone(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + Orientation currentOrientation = Orientation.LEFT_RIGHT; + mGradientDrawable = new GradientDrawable(currentOrientation, mGradientColors); + } else { + mGradientDrawable.setColors(mGradientColors); + } + ApiCompatibilityUtils.setBackgroundForView(mGradientView, mGradientDrawable); + } +} diff --git a/src/org/chromium/ui/ColorPickerDialog.java b/src/org/chromium/ui/ColorPickerDialog.java index d743a5c..58f1e0a 100644 --- a/src/org/chromium/ui/ColorPickerDialog.java +++ b/src/org/chromium/ui/ColorPickerDialog.java @@ -4,209 +4,163 @@ package org.chromium.ui; +import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.RectF; -import android.graphics.Shader; -import android.graphics.SweepGradient; -import android.os.Bundle; -import android.view.MotionEvent; +import android.view.LayoutInflater; import android.view.View; +import android.widget.Button; +import android.widget.TextView; /** - * UI for the color chooser that shows on the Android platform as a result - * of <input type=color > form element. - * - *

Note that this UI is only temporary and will be replaced once the UI - * design in - * https://code.google.com/p/chromium/issues/detail?id=162491 is finalized + * UI for the color chooser that shows on the Android platform as a result of + * <input type=color > form element. */ -public class ColorPickerDialog extends Dialog { +public class ColorPickerDialog extends AlertDialog implements OnColorChangedListener { + private final ColorPickerAdvanced mAdvancedColorPicker; - public interface OnColorChangedListener { - void colorChanged(int color); - } + private final ColorPickerSimple mSimpleColorPicker; - private OnColorChangedListener mListener; - private int mInitialColor; - - private static class ColorPickerView extends View { - private static final int CENTER_RADIUS = 32; - private static final int DIALOG_HEIGHT = 200; - private static final int BOUNDING_BOX_EDGE = 100; - private static final float PI = 3.1415926f; - - private final Paint mPaint; - private final Paint mCenterPaint; - private final int[] mColors; - private final OnColorChangedListener mListener; - private boolean mTrackingCenter; - private boolean mHighlightCenter; - - private int center_x = -1; - private int center_y = -1; - - ColorPickerView(Context c, OnColorChangedListener listener, int color) { - super(c); - mListener = listener; - mColors = new int[] { - 0xFFFF0000, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFF00FF00, - 0xFFFFFF00, 0xFFFF0000 - }; - Shader shader = new SweepGradient(0, 0, mColors, null); - - mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mPaint.setShader(shader); - mPaint.setStyle(Paint.Style.STROKE); - mPaint.setStrokeWidth(32); - - mCenterPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mCenterPaint.setColor(color); - mCenterPaint.setStrokeWidth(5); - } + private final Button mMoreButton; - @Override - protected void onDraw(Canvas canvas) { - if (center_x == -1) { - center_x = getWidth() / 2; - } - if (center_y == -1) { - center_y = getHeight() / 2; - } + // The view up in the corner that shows the user the color they've currently selected. + private final View mCurrentColorView; - float r = BOUNDING_BOX_EDGE - mPaint.getStrokeWidth() * 0.5f; + private final OnColorChangedListener mListener; - canvas.translate(center_x, center_y); + private final int mInitialColor; - canvas.drawOval(new RectF(-r, -r, r, r), mPaint); - canvas.drawCircle(0, 0, CENTER_RADIUS, mCenterPaint); + private int mCurrentColor; - if (mTrackingCenter) { - int color = mCenterPaint.getColor(); - mCenterPaint.setStyle(Paint.Style.STROKE); + /** + * @param context The context the dialog is to run in. + * @param theme The theme to display the dialog in. + * @param listener The object to notify when the color is set. + * @param color The initial color to set. + */ + public ColorPickerDialog(Context context, OnColorChangedListener listener, int color) { + super(context, 0); - if (mHighlightCenter) { - mCenterPaint.setAlpha(0xFF); - } else { - mCenterPaint.setAlpha(0x80); - } - canvas.drawCircle(0, 0, - CENTER_RADIUS + mCenterPaint.getStrokeWidth(), - mCenterPaint); + mListener = listener; + mInitialColor = color; + mCurrentColor = mInitialColor; + + // Initialize title + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View title = inflater.inflate(R.layout.color_picker_dialog_title, null); + setCustomTitle(title); + + mCurrentColorView = title.findViewById(R.id.selected_color_view); + + TextView titleText = (TextView) title.findViewById(R.id.title); + titleText.setText(R.string.color_picker_dialog_title); + + // Initialize Set/Cancel buttons + String positiveButtonText = context.getString(R.string.color_picker_button_set); + setButton(BUTTON_POSITIVE, positiveButtonText, + new Dialog.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + tryNotifyColorSet(mCurrentColor); + } + }); + + // Note that with the color picker there's not really any such thing as + // "cancelled". + // The color picker flow only finishes when we return a color, so we + // have to always + // return something. The concept of "cancelled" in this case just means + // returning + // the color that we were initialized with. + String negativeButtonText = context.getString(R.string.color_picker_button_cancel); + setButton(BUTTON_NEGATIVE, negativeButtonText, + new Dialog.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + tryNotifyColorSet(mInitialColor); + } + }); - mCenterPaint.setStyle(Paint.Style.FILL); - mCenterPaint.setColor(color); + setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface arg0) { + tryNotifyColorSet(mInitialColor); } - } + }); - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - setMeasuredDimension(widthMeasureSpec, DIALOG_HEIGHT); - } - - private static int interpolate(int low, int high, float interPolant) { - return low + java.lang.Math.round(interPolant * (high - low)); - } + // Initialize main content view + View content = inflater.inflate(R.layout.color_picker_dialog_content, null); + setView(content); - static int interpolateColor(int colors[], float x, float y) { - float angle = (float)java.lang.Math.atan2(y, x); - float unit = angle / (2 * PI); - if (unit < 0) { - unit += 1; - } - if (unit <= 0) { - return colors[0]; - } - if (unit >= 1) { - return colors[colors.length - 1]; + // Initialize More button. + mMoreButton = (Button) content.findViewById(R.id.more_colors_button); + mMoreButton.setOnClickListener(new Button.OnClickListener() { + @Override + public void onClick(View v) { + showAdvancedView(); } + }); - float p = unit * (colors.length - 1); - int i = (int)p; - p -= i; - - // Now p is just the fractional part [0...1) and i is the index. - int c0 = colors[i]; - int c1 = colors[i+1]; - int a = interpolate(Color.alpha(c0), Color.alpha(c1), p); - int r = interpolate(Color.red(c0), Color.red(c1), p); - int g = interpolate(Color.green(c0), Color.green(c1), p); - int b = interpolate(Color.blue(c0), Color.blue(c1), p); + // Initialize advanced color view (hidden initially). + mAdvancedColorPicker = + (ColorPickerAdvanced) content.findViewById(R.id.color_picker_advanced); + mAdvancedColorPicker.setVisibility(View.GONE); - return Color.argb(a, r, g, b); - } + // Initialize simple color view (default view). + mSimpleColorPicker = (ColorPickerSimple) content.findViewById(R.id.color_picker_simple); + mSimpleColorPicker.init(this); - @Override - public boolean onTouchEvent(MotionEvent event) { - float x = event.getX() - center_x; - float y = event.getY() - center_y; - - // equivalent to sqrt(x * x + y * y) <= CENTER_RADIUS but cheaper - boolean inCenter = (x * x + y * y) <= (CENTER_RADIUS * CENTER_RADIUS); - - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - mTrackingCenter = inCenter; - if (inCenter) { - mHighlightCenter = true; - invalidate(); - break; - } - case MotionEvent.ACTION_MOVE: - if (mTrackingCenter) { - if (mHighlightCenter != inCenter) { - mHighlightCenter = inCenter; - invalidate(); - } - } else { - mCenterPaint.setColor(interpolateColor(mColors, x, y)); - invalidate(); - } - break; - case MotionEvent.ACTION_UP: - if (mTrackingCenter) { - if (inCenter) { - mListener.colorChanged(mCenterPaint.getColor()); - } - - // Draw without the halo surrounding the central circle. - mTrackingCenter = false; - invalidate(); - } - break; - default: - break; - } - return true; - } + updateCurrentColor(mInitialColor); } - public ColorPickerDialog(Context context, - OnColorChangedListener listener, - int initialColor) { - super(context); - - mListener = listener; - mInitialColor = initialColor; + /** + * Listens to the ColorPicker for when the user has changed the selected color, and + * updates the current color (the color shown in the title) accordingly. + * + * @param color The new color chosen by the user. + */ + @Override + public void onColorChanged(int color) { + updateCurrentColor(color); + } - setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface arg0) { - mListener.colorChanged(mInitialColor); - } - }); + /** + * Hides the simple view (the default) and shows the advanced one instead, hiding the + * "More" button at the same time. + */ + private void showAdvancedView() { + // Only need to hide the borders, not the Views themselves, since the Views are + // contained within the borders. + View buttonBorder = findViewById(R.id.more_colors_button_border); + buttonBorder.setVisibility(View.GONE); + + View simpleViewBorder = findViewById(R.id.color_picker_simple_border); + simpleViewBorder.setVisibility(View.GONE); + + mAdvancedColorPicker.setVisibility(View.VISIBLE); + mAdvancedColorPicker.setListener(this); + mAdvancedColorPicker.setColor(mCurrentColor); } - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(new ColorPickerView(getContext(), mListener, mInitialColor)); + /** + * Tries to notify any listeners that the color has been set. + */ + private void tryNotifyColorSet(int color) { + if (mListener != null) { + mListener.onColorChanged(color); + } + } - // TODO(miguelg): Internationalization - setTitle("Select Color"); + /** + * Updates the internal cache of the currently selected color, updating the colorful little + * box in the title at the same time. + */ + private void updateCurrentColor(int color) { + mCurrentColor = color; + if (mCurrentColorView != null) { + mCurrentColorView.setBackgroundColor(color); + } } } diff --git a/src/org/chromium/ui/ColorPickerMoreButton.java b/src/org/chromium/ui/ColorPickerMoreButton.java new file mode 100644 index 0000000..a982afd --- /dev/null +++ b/src/org/chromium/ui/ColorPickerMoreButton.java @@ -0,0 +1,56 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.util.AttributeSet; +import android.widget.Button; + +/** + * Simple class that draws a white border around a button, purely for a UI change. + */ +public class ColorPickerMoreButton extends Button { + + // A cache for the paint used to draw the border, so it doesn't have to be created in + // every onDraw() call. + private Paint mBorderPaint; + + public ColorPickerMoreButton(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ColorPickerMoreButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + /** + * Sets up the paint to use for drawing the border. + */ + public void init() { + mBorderPaint = new Paint(); + mBorderPaint.setStyle(Paint.Style.STROKE); + mBorderPaint.setColor(Color.WHITE); + // Set the width to one pixel. + mBorderPaint.setStrokeWidth(1.0f); + // And make sure the border doesn't bleed into the outside. + mBorderPaint.setAntiAlias(false); + } + + /** + * Draws the border around the edge of the button. + * + * @param canvas The canvas to draw on. + */ + @Override + protected void onDraw(Canvas canvas) { + canvas.drawRect(0.5f, 0.5f, getWidth() - 1.5f, getHeight() - 1.5f, mBorderPaint); + super.onDraw(canvas); + } +} diff --git a/src/org/chromium/ui/ColorPickerSimple.java b/src/org/chromium/ui/ColorPickerSimple.java new file mode 100644 index 0000000..979405a --- /dev/null +++ b/src/org/chromium/ui/ColorPickerSimple.java @@ -0,0 +1,189 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +package org.chromium.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + + +/** + * Draws a grid of (predefined) colors and allows the user to choose one of + * those colors. + */ +public class ColorPickerSimple extends View { + private static final int ROW_COUNT = 2; + + private static final int COLUMN_COUNT = 4; + + private static final int GRID_CELL_COUNT = ROW_COUNT * COLUMN_COUNT; + + private static final int[] COLORS = { Color.RED, + Color.CYAN, + Color.BLUE, + Color.GREEN, + Color.MAGENTA, + Color.YELLOW, + Color.BLACK, + Color.WHITE + }; + + private Paint mBorderPaint; + + private Rect[] mBounds; + + private Paint[] mPaints; + + private OnColorChangedListener mOnColorTouchedListener; + + private int mLastTouchedXPosition; + + private int mLastTouchedYPosition; + + public ColorPickerSimple(Context context) { + super(context); + } + + public ColorPickerSimple(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ColorPickerSimple(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Initializes the listener and precalculates the grid and color positions. + * + * @param onColorChangedListener The listener that gets notified when the user touches + * a color. + */ + public void init(OnColorChangedListener onColorChangedListener) { + mOnColorTouchedListener = onColorChangedListener; + + // This will get calculated when the layout size is updated. + mBounds = null; + + mPaints = new Paint[GRID_CELL_COUNT]; + for (int i = 0; i < GRID_CELL_COUNT; ++i) { + Paint newPaint = new Paint(); + newPaint.setColor(COLORS[i]); + mPaints[i] = newPaint; + } + + mBorderPaint = new Paint(); + int borderColor = getContext().getResources().getColor(R.color.color_picker_border_color); + mBorderPaint.setColor(borderColor); + + // Responds to the user touching the grid and works out which color has been chosen as + // a result, depending on the X,Y coordinate. Note that we respond to the click event + // here, but the onClick() method doesn't provide us with the X,Y coordinates, so we + // track them in onTouchEvent() below. This way the grid reacts properly to touch events + // whereas if we put this onClick() code in onTouchEvent below then we get some strange + // interactions with the ScrollView in the parent ColorPickerDialog. + setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mOnColorTouchedListener != null && getWidth() > 0 && getHeight() > 0) { + int column = mLastTouchedXPosition * COLUMN_COUNT / getWidth(); + int row = mLastTouchedYPosition * ROW_COUNT / getHeight(); + + int colorIndex = (row * COLUMN_COUNT) + column; + if (colorIndex >= 0 && colorIndex < COLORS.length) { + mOnColorTouchedListener.onColorChanged(COLORS[colorIndex]); + } + } + } + }); + } + + /** + * Draws the grid of colors, based on the rectangles calculated in onSizeChanged(). + * Also draws borders in between the colored rectangles. + * + * @param canvas The canvas the colors are drawn onto. + */ + @Override + public void onDraw(Canvas canvas) { + if (mBounds == null || mPaints == null) { + return; + } + + canvas.drawColor(Color.WHITE); + + // Draw the actual colored rectangles. + for (int i = 0; i < GRID_CELL_COUNT; ++i) { + canvas.drawRect(mBounds[i], mPaints[i]); + } + + // Draw 1px borders between the rows. + for (int i = 0; i < ROW_COUNT - 1; ++i) { + canvas.drawLine(0, + mBounds[i * COLUMN_COUNT].bottom + 1, + getWidth(), + mBounds[i * COLUMN_COUNT].bottom + 1, + mBorderPaint); + } + + // Draw 1px borders between the columns. + for (int j = 0; j < COLUMN_COUNT - 1; ++j) { + canvas.drawLine(mBounds[j].right + 1, + 0, + mBounds[j].right + 1, + getHeight(), + mBorderPaint); + } + } + + /** + * Stores the X,Y coordinates of the touch so that we can use them in the onClick() listener + * above to work out where the click was on the grid. + * + * @param event The MotionEvent the X,Y coordinates are retrieved from. + */ + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mLastTouchedXPosition = (int) event.getX(); + mLastTouchedYPosition = (int) event.getY(); + } + return super.onTouchEvent(event); + } + + /** + * Recalculates the color grid with the new sizes. + */ + @Override + protected void onSizeChanged(int width, int height, int oldw, int oldh) { + calculateGrid(width, height); + } + + /** + * Calculates the sizes and positions of the cells in the grid, splitting + * them up as evenly as possible. Leaves 3 pixels between each cell so that + * we can draw a border between them as well, and leaves a pixel around the + * edge. + */ + private void calculateGrid(final int width, final int height) { + mBounds = new Rect[GRID_CELL_COUNT]; + + for (int i = 0; i < ROW_COUNT; ++i) { + for (int j = 0; j < COLUMN_COUNT; ++j) { + int left = j * (width + 1) / COLUMN_COUNT + 1; + int right = (j + 1) * (width + 1) / COLUMN_COUNT - 2; + + int top = i * (height + 1) / ROW_COUNT + 1; + int bottom = (i + 1) * (height + 1) / ROW_COUNT - 2; + + Rect rect = new Rect(left, top, right, bottom); + mBounds[(i * COLUMN_COUNT) + j] = rect; + } + } + } +} diff --git a/src/org/chromium/ui/OnColorChangedListener.java b/src/org/chromium/ui/OnColorChangedListener.java new file mode 100644 index 0000000..4caa3cf --- /dev/null +++ b/src/org/chromium/ui/OnColorChangedListener.java @@ -0,0 +1,17 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +package org.chromium.ui; + +/** + * The callback used to indicate the user changed the color. + */ +public interface OnColorChangedListener { + + /** + * Called upon a color change. + * + * @param color The color that was set. + */ + void onColorChanged(int color); +} \ No newline at end of file diff --git a/src/org/chromium/ui/R.java b/src/org/chromium/ui/R.java index 53f60cc..c722c14 100644 --- a/src/org/chromium/ui/R.java +++ b/src/org/chromium/ui/R.java @@ -18,5 +18,42 @@ public final class R { public static final class string { public static int low_memory_error; public static int opening_file_error; + public static int color_picker_button_more; + public static int color_picker_hue; + public static int color_picker_saturation; + public static int color_picker_value; + public static int color_picker_button_set; + public static int color_picker_button_cancel; + public static int color_picker_dialog_title; + } + public static final class id { + public static int autofill_label; + public static int autofill_popup_window; + public static int autofill_sublabel; + public static int selected_color_view; + public static int title; + public static int more_colors_button; + public static int color_picker_advanced; + public static int color_picker_simple; + public static int more_colors_button_border; + public static int color_picker_simple_border; + public static int gradient; + public static int text; + public static int seek_bar; + } + public static final class layout { + public static int autofill_text; + public static int color_picker_dialog_title; + public static int color_picker_dialog_content; + public static int color_picker_advanced_component; + } + public static final class drawable { + public static int color_picker_advanced_select_handle; + } + public static final class style { + public static int AutofillPopupWindow; + } + public static final class color { + public static int color_picker_border_color; } } diff --git a/src/org/chromium/ui/SelectFileDialog.java b/src/org/chromium/ui/SelectFileDialog.java index a9b117b..9aa11e4 100644 --- a/src/org/chromium/ui/SelectFileDialog.java +++ b/src/org/chromium/ui/SelectFileDialog.java @@ -11,6 +11,7 @@ import android.net.Uri; import android.os.Environment; import android.provider.MediaStore; +import android.text.TextUtils; import java.io.File; import java.util.ArrayList; @@ -34,15 +35,11 @@ class SelectFileDialog implements WindowAndroid.IntentCallback{ private static final String ALL_VIDEO_TYPES = VIDEO_TYPE + "*"; private static final String ALL_AUDIO_TYPES = AUDIO_TYPE + "*"; private static final String ANY_TYPES = "*/*"; - private static final String CAPTURE_CAMERA = "camera"; - private static final String CAPTURE_CAMCORDER = "camcorder"; - private static final String CAPTURE_MICROPHONE = "microphone"; - private static final String CAPTURE_FILESYSTEM = "filesystem"; private static final String CAPTURE_IMAGE_DIRECTORY = "browser-photos"; private final int mNativeSelectFileDialog; private List mFileTypes; - private String mCapture; // May be null if no capture parameter was set. + private boolean mCapture; private Uri mCameraOutputUri; private SelectFileDialog(int nativeSelectFileDialog) { @@ -56,7 +53,7 @@ private SelectFileDialog(int nativeSelectFileDialog) { * @param window The WindowAndroid that can show intents */ @CalledByNative - private void selectFile(String[] fileTypes, String capture, WindowAndroid window) { + private void selectFile(String[] fileTypes, boolean capture, WindowAndroid window) { mFileTypes = new ArrayList(Arrays.asList(fileTypes)); mCapture = capture; @@ -69,9 +66,9 @@ private void selectFile(String[] fileTypes, String capture, WindowAndroid window MediaStore.Audio.Media.RECORD_SOUND_ACTION); String lowMemoryError = window.getContext().getString(R.string.low_memory_error); - // Quick check - if a capture parameter other than filesystem (the default) is specified we - // should just launch the appropriate intent. Otherwise build up a chooser based on the - // accept type and then display that to the user. + // Quick check - if the |capture| parameter is set and |fileTypes| has the appropriate MIME + // type, we should just launch the appropriate intent. Otherwise build up a chooser based on + // the accept type and then display that to the user. if (captureCamera()) { if (window.showIntent(camera, this, lowMemoryError)) return; } else if (captureCamcorder()) { @@ -89,19 +86,19 @@ private void selectFile(String[] fileTypes, String capture, WindowAndroid window // chooser above. if (shouldShowImageTypes()) { extraIntents.add(camera); - getContentIntent.setType("image/*"); + getContentIntent.setType(ALL_IMAGE_TYPES); } else if (shouldShowVideoTypes()) { extraIntents.add(camcorder); - getContentIntent.setType("video/*"); + getContentIntent.setType(ALL_VIDEO_TYPES); } else if (shouldShowAudioTypes()) { extraIntents.add(soundRecorder); - getContentIntent.setType("audio/*"); + getContentIntent.setType(ALL_AUDIO_TYPES); } } if (extraIntents.isEmpty()) { // We couldn't resolve an accept type, so fallback to a generic chooser. - getContentIntent.setType("*/*"); + getContentIntent.setType(ANY_TYPES); extraIntents.add(camera); extraIntents.add(camcorder); extraIntents.add(soundRecorder); @@ -181,8 +178,7 @@ public void onIntentCompleted(WindowAndroid window, int resultCode, } if (!success) { onFileNotSelected(); - String openingFileError = window.getContext().getString(R.string.opening_file_error); - window.showError(openingFileError); + window.showError(R.string.opening_file_error); } } @@ -215,22 +211,20 @@ private boolean shouldShowAudioTypes() { return shouldShowTypes(ALL_AUDIO_TYPES, AUDIO_TYPE); } + private boolean acceptsSpecificType(String type) { + return mFileTypes.size() == 1 && TextUtils.equals(mFileTypes.get(0), type); + } + private boolean captureCamera() { - return shouldShowImageTypes() && mCapture != null && mCapture.startsWith(CAPTURE_CAMERA); + return mCapture && acceptsSpecificType(ALL_IMAGE_TYPES); } private boolean captureCamcorder() { - return shouldShowVideoTypes() && mCapture != null && - mCapture.startsWith(CAPTURE_CAMCORDER); + return mCapture && acceptsSpecificType(ALL_VIDEO_TYPES); } private boolean captureMicrophone() { - return shouldShowAudioTypes() && mCapture != null && - mCapture.startsWith(CAPTURE_MICROPHONE); - } - - private boolean captureFilesystem() { - return mCapture != null && mCapture.startsWith(CAPTURE_FILESYSTEM); + return mCapture && acceptsSpecificType(ALL_AUDIO_TYPES); } private boolean acceptSpecificType(String accept) { diff --git a/src/org/chromium/ui/WindowAndroid.java b/src/org/chromium/ui/WindowAndroid.java index 58716d8..477e5e6 100644 --- a/src/org/chromium/ui/WindowAndroid.java +++ b/src/org/chromium/ui/WindowAndroid.java @@ -11,6 +11,7 @@ import android.content.Intent; import android.os.Bundle; import android.util.SparseArray; +import android.view.WindowManager; import android.widget.Toast; import java.util.HashMap; @@ -80,6 +81,14 @@ public void showError(String error) { } } + /** + * Displays an error message from the given resource id. + * @param resId The error message string's resource id. + */ + public void showError(int resId) { + showError(mActivity.getString(resId)); + } + /** * Displays an error message for a nonexistent callback. * @param error The error message string to be displayed. @@ -99,6 +108,7 @@ public void sendBroadcast(Intent intent) { * TODO(nileshagrawal): Stop returning Activity Context crbug.com/233440. * @return Activity context. */ + @Deprecated public Context getContext() { return mActivity; } @@ -190,6 +200,14 @@ public int getNativePointer() { return mNativeWindowAndroid; } + public void keepScreenOn(boolean screenOn) { + if (screenOn) { + mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + private native int nativeInit(); private native void nativeDestroy(int nativeWindowAndroid); diff --git a/src/org/chromium/ui/autofill/AutofillListAdapter.java b/src/org/chromium/ui/autofill/AutofillListAdapter.java new file mode 100644 index 0000000..62203dc --- /dev/null +++ b/src/org/chromium/ui/autofill/AutofillListAdapter.java @@ -0,0 +1,54 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + + +package org.chromium.ui.autofill; + +import android.content.Context; +import android.text.TextUtils; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import org.chromium.ui.R; + +import java.util.ArrayList; + +/** + * Autofill suggestion adapter for AutofillWindow. + */ +public class AutofillListAdapter extends ArrayAdapter { + private Context mContext; + + AutofillListAdapter(Context context, ArrayList objects) { + super(context, R.layout.autofill_text, objects); + mContext = context; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View layout = convertView; + if (convertView == null) { + LayoutInflater inflater = + (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + layout = inflater.inflate(R.layout.autofill_text, null); + } + TextView labelView = (TextView) layout.findViewById(R.id.autofill_label); + labelView.setText(getItem(position).mLabel); + + TextView sublabelView = (TextView) layout.findViewById(R.id.autofill_sublabel); + CharSequence sublabel = getItem(position).mSublabel; + if (TextUtils.isEmpty(sublabel)) { + sublabelView.setVisibility(View.GONE); + } else { + sublabelView.setText(sublabel); + sublabelView.setVisibility(View.VISIBLE); + } + + return layout; + } +} diff --git a/src/org/chromium/ui/autofill/AutofillPopup.java b/src/org/chromium/ui/autofill/AutofillPopup.java new file mode 100644 index 0000000..0483b02 --- /dev/null +++ b/src/org/chromium/ui/autofill/AutofillPopup.java @@ -0,0 +1,214 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.autofill; + +import android.content.Context; +import android.graphics.Paint; +import android.graphics.Rect; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnLayoutChangeListener; +import android.widget.AdapterView; +import android.widget.ListPopupWindow; +import android.widget.TextView; + +import java.util.ArrayList; + +import org.chromium.ui.R; +import org.chromium.ui.ViewAndroidDelegate; + +/** + * The Autofill suggestion popup that lists relevant suggestions. + */ +public class AutofillPopup extends ListPopupWindow implements AdapterView.OnItemClickListener { + + /** + * Constants defining types of Autofill suggestion entries. + * Has to be kept in sync with enum in WebAutofillClient.h + * + * Not supported: MenuItemIDWarningMessage, MenuItemIDSeparator, MenuItemIDClearForm, and + * MenuItemIDAutofillOptions. + */ + private static final int ITEM_ID_AUTOCOMPLETE_ENTRY = 0; + private static final int ITEM_ID_PASSWORD_ENTRY = -2; + private static final int ITEM_ID_DATA_LIST_ENTRY = -6; + + private static final int TEXT_PADDING_DP = 30; + + private final AutofillPopupDelegate mAutofillCallback; + private final Context mContext; + private final ViewAndroidDelegate mViewAndroidDelegate; + private View mAnchorView; + private float mAnchorWidth; + private float mAnchorHeight; + private float mAnchorX; + private float mAnchorY; + private Paint mLabelViewPaint; + private Paint mSublabelViewPaint; + private OnLayoutChangeListener mLayoutChangeListener; + + /** + * An interface to handle the touch interaction with an AutofillPopup object. + */ + public interface AutofillPopupDelegate { + /** + * Requests the controller to hide AutofillPopup. + */ + public void requestHide(); + + /** + * Handles the selection of an Autofill suggestion from an AutofillPopup. + * @param listIndex The index of the selected Autofill suggestion. + */ + public void suggestionSelected(int listIndex); + } + + /** + * Creates an AutofillWindow with specified parameters. + * @param context Application context. + * @param viewAndroidDelegate View delegate used to add and remove views. + * @param autofillCallback A object that handles the calls to the native AutofillPopupView. + */ + public AutofillPopup(Context context, ViewAndroidDelegate viewAndroidDelegate, + AutofillPopupDelegate autofillCallback) { + super(context, null, 0, R.style.AutofillPopupWindow); + mContext = context; + mViewAndroidDelegate = viewAndroidDelegate ; + mAutofillCallback = autofillCallback; + + setOnItemClickListener(this); + + mAnchorView = mViewAndroidDelegate.acquireAnchorView(); + mAnchorView.setId(R.id.autofill_popup_window); + mAnchorView.setTag(this); + + mViewAndroidDelegate.setAnchorViewPosition(mAnchorView, mAnchorX, mAnchorY, mAnchorWidth, + mAnchorHeight); + + mLayoutChangeListener = new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (v == mAnchorView) AutofillPopup.this.show(); + } + }; + + mAnchorView.addOnLayoutChangeListener(mLayoutChangeListener); + setAnchorView(mAnchorView); + } + + @Override + public void show() { + // An ugly hack to keep the popup from expanding on top of the keyboard. + setInputMethodMode(INPUT_METHOD_NEEDED); + super.show(); + } + + /** + * Sets the location and the size of the anchor view that the AutofillPopup will use to attach + * itself. + * @param x X coordinate of the top left corner of the anchor view. + * @param y Y coordinate of the top left corner of the anchor view. + * @param width The width of the anchor view. + * @param height The height of the anchor view. + */ + public void setAnchorRect(float x, float y, float width, float height) { + mAnchorWidth = width; + mAnchorHeight = height; + mAnchorX = x; + mAnchorY = y; + if (mAnchorView != null) { + mViewAndroidDelegate.setAnchorViewPosition(mAnchorView, mAnchorX, mAnchorY, + mAnchorWidth, mAnchorHeight); + } + } + + /** + * Sets the Autofill suggestions to display in the popup and shows the popup. + * @param suggestions Autofill suggestion data. + */ + public void show(AutofillSuggestion[] suggestions) { + // Remove the AutofillSuggestions with IDs that are not supported by Android + ArrayList cleanedData = new ArrayList(); + for (int i = 0; i < suggestions.length; i++) { + int itemId = suggestions[i].mUniqueId; + if (itemId > 0 || itemId == ITEM_ID_AUTOCOMPLETE_ENTRY || + itemId == ITEM_ID_PASSWORD_ENTRY || itemId == ITEM_ID_DATA_LIST_ENTRY) { + cleanedData.add(suggestions[i]); + } + } + setAdapter(new AutofillListAdapter(mContext, cleanedData)); + // Once the mAnchorRect is resized and placed correctly, it will show the Autofill popup. + mAnchorWidth = Math.max(getDesiredWidth(cleanedData), mAnchorWidth); + mViewAndroidDelegate.setAnchorViewPosition(mAnchorView, mAnchorX, mAnchorY, mAnchorWidth, + mAnchorHeight); + } + + /** + * Overrides the default dismiss behavior to request the controller to dismiss the view. + */ + @Override + public void dismiss() { + mAutofillCallback.requestHide(); + } + + /** + * Hides the popup and removes the anchor view from the ContainerView. + */ + public void hide() { + super.dismiss(); + mAnchorView.removeOnLayoutChangeListener(mLayoutChangeListener); + mAnchorView.setTag(null); + mViewAndroidDelegate.releaseAnchorView(mAnchorView); + } + + /** + * Get desired popup window width by calculating the maximum text length from Autofill data. + * @param data Autofill suggestion data. + * @return The popup window width in DIP. + */ + private float getDesiredWidth(ArrayList data) { + if (mLabelViewPaint == null || mSublabelViewPaint == null) { + LayoutInflater inflater = + (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View layout = inflater.inflate(R.layout.autofill_text, null); + TextView labelView = (TextView) layout.findViewById(R.id.autofill_label); + mLabelViewPaint = labelView.getPaint(); + TextView sublabelView = (TextView) layout.findViewById(R.id.autofill_sublabel); + mSublabelViewPaint = sublabelView.getPaint(); + } + + float maxTextWidth = 0; + Rect bounds = new Rect(); + for (int i = 0; i < data.size(); ++i) { + bounds.setEmpty(); + String label = data.get(i).mLabel; + if (!TextUtils.isEmpty(label)) { + mLabelViewPaint.getTextBounds(label, 0, label.length(), bounds); + } + float labelWidth = bounds.width(); + + bounds.setEmpty(); + String sublabel = data.get(i).mSublabel; + if (!TextUtils.isEmpty(sublabel)) { + mSublabelViewPaint.getTextBounds(sublabel, 0, sublabel.length(), bounds); + } + + float localMax = Math.max(labelWidth, bounds.width()); + maxTextWidth = Math.max(maxTextWidth, localMax); + } + // Scale it down to make it unscaled by screen density. + maxTextWidth = maxTextWidth / mContext.getResources().getDisplayMetrics().density; + // Adding padding. + return maxTextWidth + TEXT_PADDING_DP; + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + mAutofillCallback.suggestionSelected(position); + } + +} diff --git a/src/org/chromium/ui/autofill/AutofillSuggestion.java b/src/org/chromium/ui/autofill/AutofillSuggestion.java new file mode 100644 index 0000000..51669ea --- /dev/null +++ b/src/org/chromium/ui/autofill/AutofillSuggestion.java @@ -0,0 +1,26 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.autofill; + +/** + * Autofill suggestion container used to store information needed for each Autofill popup entry. + */ +public class AutofillSuggestion { + final String mLabel; + final String mSublabel; + final int mUniqueId; + + /** + * Constructs a Autofill suggestion container. + * @param name The name of the Autofill suggestion. + * @param label The describing label of the Autofill suggestion. + * @param uniqueId The unique id used to identify the Autofill suggestion. + */ + public AutofillSuggestion(String name, String label, int uniqueId) { + mLabel = name; + mSublabel = label; + mUniqueId = uniqueId; + } +} \ No newline at end of file diff --git a/src/org/chromium/ui/gfx/BitmapHelper.java b/src/org/chromium/ui/gfx/BitmapHelper.java index 5e237f7..97511a1 100644 --- a/src/org/chromium/ui/gfx/BitmapHelper.java +++ b/src/org/chromium/ui/gfx/BitmapHelper.java @@ -17,11 +17,53 @@ public static Bitmap createBitmap(int width, int height) { return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); } + /** + * Decode and sample down a bitmap resource to the requested width and height. + * + * @param name The resource name of the bitmap to decode. + * @param reqWidth The requested width of the resulting bitmap. + * @param reqHeight The requested height of the resulting bitmap. + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions. + * that are equal to or greater than the requested width and height. + */ @CalledByNative - public static Bitmap decodeDrawableResource(String name) { + private static Bitmap decodeDrawableResource(String name, + int reqWidth, + int reqHeight) { Resources res = Resources.getSystem(); - int resource_id = res.getIdentifier(name, null, null); + int resId = res.getIdentifier(name, null, null); - return BitmapFactory.decodeResource(res, resource_id); + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeResource(res, resId, options); + + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + + options.inJustDecodeBounds = false; + return BitmapFactory.decodeResource(res, resId, options); + } + + // http://developer.android.com/training/displaying-bitmaps/load-bitmap.html + private static int calculateInSampleSize(BitmapFactory.Options options, + int reqWidth, + int reqHeight) { + // Raw height and width of image + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + + if (height > reqHeight || width > reqWidth) { + + // Calculate ratios of height and width to requested height and width + final int heightRatio = Math.round((float) height / (float) reqHeight); + final int widthRatio = Math.round((float) width / (float) reqWidth); + + // Choose the smallest ratio as inSampleSize value, this will guarantee + // a final image with both dimensions larger than or equal to the + // requested height and width. + inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; + } + + return inSampleSize; } } diff --git a/src/org/chromium/ui/gfx/DeviceDisplayInfo.java b/src/org/chromium/ui/gfx/DeviceDisplayInfo.java index b133931..088c9c2 100644 --- a/src/org/chromium/ui/gfx/DeviceDisplayInfo.java +++ b/src/org/chromium/ui/gfx/DeviceDisplayInfo.java @@ -7,7 +7,6 @@ import android.content.Context; import android.graphics.PixelFormat; import android.os.Build; -import android.telephony.TelephonyManager; import android.util.DisplayMetrics; import android.view.Display; import android.view.WindowManager; @@ -25,11 +24,13 @@ @JNINamespace("gfx") public class DeviceDisplayInfo { - private WindowManager mWinManager; + + private final Context mAppContext; + private final WindowManager mWinManager; private DeviceDisplayInfo(Context context) { - Context appContext = context.getApplicationContext(); - mWinManager = (WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE); + mAppContext = context.getApplicationContext(); + mWinManager = (WindowManager) mAppContext.getSystemService(Context.WINDOW_SERVICE); } /** @@ -114,24 +115,12 @@ public double getDIPScale() { return getMetrics().density; } - /** - * @return Display refresh rate in frames per second. - */ - @CalledByNative - public double getRefreshRate() { - double result = getDisplay().getRefreshRate(); - // Sanity check. - return (result >= 61 || result < 30) ? 0 : result; - } - private Display getDisplay() { return mWinManager.getDefaultDisplay(); } private DisplayMetrics getMetrics() { - DisplayMetrics metrics = new DisplayMetrics(); - getDisplay().getMetrics(metrics); - return metrics; + return mAppContext.getResources().getDisplayMetrics(); } /** diff --git a/src/us/costan/chrome/ChromeView.java b/src/us/costan/chrome/ChromeView.java index 3d5f724..b2e70f5 100644 --- a/src/us/costan/chrome/ChromeView.java +++ b/src/us/costan/chrome/ChromeView.java @@ -27,6 +27,7 @@ import android.os.Bundle; import android.os.Message; import android.util.AttributeSet; +import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; @@ -60,6 +61,7 @@ public class ChromeView extends FrameLayout { public ChromeView(Context context) { this(context, null); + Log.i("chromeview", "ChromeView ctor"); } /** Constructor for inflating via XML. */ @@ -78,6 +80,7 @@ public ChromeView(Context context, AttributeSet attrs) { } catch(ClassCastException e) { // Hope that hardware acceleration is enabled. + Log.e("chromeview", e.getMessage()); } SharedPreferences sharedPreferences = context.getSharedPreferences( @@ -1164,5 +1167,22 @@ public boolean requestDrawGL(Canvas canvas) { } } } + @Override + public void overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, int maxOverScrollX, + int maxOverScrollY, boolean isTouchEvent) { + + // FIXME (davis): no clue if this is right + ChromeView.super.overScrollBy( deltaX, deltaY, scrollX, scrollY, + scrollRangeX, scrollRangeY, maxOverScrollX, + maxOverScrollY, isTouchEvent); + + } + @Override + public void super_scrollTo(int scrollX, int scrollY) { + + // FIXME (davis): no clue if this is right + + } } }