diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..a4763d1 --- /dev/null +++ b/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fef33df --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Files for the dex VM. +*.dex + +# Java class files. +*.class + +# Generated files. +bin/ +gen/ + +# Local configuration file (sdk path, etc). +local.properties + +# Vim. +*.sw* diff --git a/.project b/.project new file mode 100644 index 0000000..643a2a9 --- /dev/null +++ b/.project @@ -0,0 +1,33 @@ + + + chromeview + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..9bf2876 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,281 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_assignment=0 +org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 +org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_after_package=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +org.eclipse.jdt.core.formatter.blank_lines_before_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 +org.eclipse.jdt.core.formatter.blank_lines_before_package=0 +org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 +org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false +org.eclipse.jdt.core.formatter.comment.format_block_comments=true +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.comment.format_html=true +org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true +org.eclipse.jdt.core.formatter.comment.format_line_comments=true +org.eclipse.jdt.core.formatter.comment.format_source_code=true +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true +org.eclipse.jdt.core.formatter.comment.indent_root_tags=true +org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert +org.eclipse.jdt.core.formatter.comment.line_length=80 +org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true +org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true +org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.compact_else_if=true +org.eclipse.jdt.core.formatter.continuation_indentation=2 +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 +org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off +org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true +org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_empty_lines=false +org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false +org.eclipse.jdt.core.formatter.indentation.size=2 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert +org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert +org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.join_lines_in_comments=true +org.eclipse.jdt.core.formatter.join_wrapped_lines=true +org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.lineSplit=80 +org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false +org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.tabulation.char=space +org.eclipse.jdt.core.formatter.tabulation.size=2 +org.eclipse.jdt.core.formatter.use_on_off_tags=false +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false +org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true +org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 0000000..998a281 --- /dev/null +++ b/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +formatter_profile=_NetMap +formatter_settings_version=12 diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..45f529c --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0777126 --- /dev/null +++ b/README.md @@ -0,0 +1,179 @@ +# ChromeView + +ChormeView works like Android's WebView, but is backed by the latest Chromium +code. + + +## Why ChromeView + +ChromeView lets you ship your own Chromium code, instead of using whatever +version comes with your user's Android image. This gives your application +early access to the newest features in Chromium, and removes the variability +due to different WebView implementations in different versions of Android. + + +## Setting Up + +This section explains how to set up your Android project to use ChromeView. + +### Get the Code + +Check out the repository in your Eclipse workspace, and make your project use +ChromeView as a library. In Eclipse, right-click your project directory, select +`Properties`, choose the `Android` category, and click on the `Add` button in +the `Library section`. + +### Copy Data + +Copy `assets/webviewchromium.pak` to your project's `assets` directory. +[Star this bug](https://code.google.com/p/android/issues/detail?id=35748) if +you agree that this is annoying. + +In your `Application` subclass, call `ChromeView.initialize` and pass it the +application's context. For example, + +### Initialize Chromium + +```java +import us.costan.chrome.ChromeView; +import android.app.Application; + +public class MyApplication extends Application { + @Override + public void onCreate() { + super.onCreate(); + ChromeView.initialize(this); + } +} +``` + +Now you can use ChromeView in the same contexts as you would use WebView. + +### Star some bugs + +If you use this project and want to help move it along, please star the +following bugs. + +* [crbug.com/113088](http://crbug.com/113088) +* [crbug.com/234907](http://crbug.com/234907) and +* [this Android bug](https://code.google.com/p/android/issues/detail?id=35748) + + +## Usage + +To access ChromeView in the graphical layout editor, go to the `Palette`, +expand the `Custom and Library Views` section, and click the `Refresh` button. + +ChromeView supports most of the WebView methods. For example, + +```java +ChromeView chromeView = (ChromeView)findViewById(R.id.gameUiView); +chromeView.getSettings().setJavaScriptEnabled(true); +chromeView.loadUrl("http://www.google.com"); +``` + +### JavaScript + +ChromeView's `addJavaScriptInterface` exposes public methods that are annotated +with `@ChromeJavascriptInterface`. This is because WebView's +`@JavascriptInterface` is only available on Android 4.2 and above, but +ChromeView targets 4.0 and 4.1 as well. + +```java +import us.costan.chrome.ChromeJavascriptInterface; + +public class JsBindings { + @ChromeJavascriptInterface + public String getHello() { + return "Hello world"; + } +} + +chromeView.addJavascriptInterface(new JsBindings(), "AndroidBindings"); +``` + +### Cookies + +ChromeCookieManager is ChromeView's equivalent of CookieManager. + +```java +ChromeCookieManager.getInstance().getCookie("https://www.google.com"); +``` + +### Faster Development + +To speed up the application launch on real devices, remove the `libs/x86` +directory. When developing on Atom devices, remove the ARM directory instead. + +Remember to `git checkout -- .` and get the library back before building a +release APK. + +### Internet Access + +If your application manifest doesn't specify the +[INTERNET permission](http://developer.android.com/reference/android/Manifest.permission.html#INTERNET), +the Chromium code behind ChromeView silentely blocks all network requests. This +is mentioned here because it can be hard to debug. + + +## Building + +The bulk of this project is Chromium source code and build products. With the +appropriate infrastructure, the Chromium bits can be easily updated. + +[crbuild/vm-build.md](crbuild/vm-build.md) contains step-by-step instructions +for setting up a VM and building the Chromium for Android components used by +ChromeView. + +Once Chromium has been successfully built, running +[crbuild/update.sh](crbuild/update.sh) will copy the relevant bits from the +build VM into the ChromeView source tree. + + +## Issues + +Attempting to scroll the view (by swiping a finger across the screen) does not +update the displayed image. However, internally, the view is scrolled. This can +be seen by displaying a stack of buttons and trying to click on the topmost +one. This issue makes ChromeView mostly unusable in production. + +The core issue is that the integration is done via `AwContent` in the +`android_webview` directory of the Chromium source tree, which is experimental +and not intended for embedding use. The "right" way of doing this is to embed +a `ContentView` from the `content` directory, or a `Shell` in `content/shell`. +Unfortunately, these components' APIs don't match WebView nearly as well as +AwContent, and they're much harder to integrate. Pull requests or a fork would +be welcome. + +This repository is rebased often, because the large files in `lib/` would +result in a huge repository if new commits were created for each build. The +large files are Chromium build products. + + +## Contributing + +Please don't hesitate to send your Pull Requests! + +Please don't send pull requests including the binary assets or code extracted +from Android (`assets/`, `libs/`, `src/com/googlecode/` and `src/org/android`). +If your Pull Request requires updated Android bits, mention that in the PR +description, and I will rebuild the Android bits. + + +## Copyright and License + +The directories below contain code from the +[The Chromium Project](http://www.chromium.org/), which is subject to the +copyright and license on the project site. + +* `assets/` +* `libs/` +* `src/com/googlecode` +* `src/org/chromium` + +Some of the source code in `src/us/costan/chrome` has been derived from the +Android source code, and is therefore covered by the +[Android project licenses](http://source.android.com/source/licenses.html). + +The rest of the code is Copyright 2013, Victor Costan, and available under the +MIT license. diff --git a/assets/webviewchromium.pak b/assets/webviewchromium.pak new file mode 100644 index 0000000..2ac39bd Binary files /dev/null and b/assets/webviewchromium.pak differ diff --git a/crbuild/update.sh b/crbuild/update.sh new file mode 100755 index 0000000..84f51eb --- /dev/null +++ b/crbuild/update.sh @@ -0,0 +1,59 @@ +# Updates this project with the Chrome build files. +# This script assumes the Chrome build VM is up at crbuild.local + +# Clean up. +rm -r assets/* +rm -r libs/* +rm -r src/com/googlecode +rm -r 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 + +# 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 +rm libs/**/gdbserver +scp -r crbuild@crbuild.local: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/ + +# 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/ + +# Strip a ContentView file that's not supposed to be here. +rm src/org/chromium/content/common/common.aidl + +# Get rid of the .svn directory in eyesfree. +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/ + +# Generated files. +scp -r crbuild@crbuild.local: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/ + +# android_webview generated sources. Must come after all the other sources. +scp -r crbuild@crbuild.local:chromium/src/android_webview/java/generated_src/* src/ diff --git a/crbuild/vm-build.md b/crbuild/vm-build.md new file mode 100644 index 0000000..9e25539 --- /dev/null +++ b/crbuild/vm-build.md @@ -0,0 +1,126 @@ +# VM Setup Instructions + +This library's repository includes some Chromium files that are painful to +build. Chromium's build process is a bit fussy, and the Android target is even +more fussy, so the least painful way of getting it done is to set up a VM with +the exact software that the build process was designed for. + +This document contains step-by-step instructions for setting up the build VM +and building the files used in this library. + + +## VM Building + +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/ + * Get the `64-bit PC (AMD64) server install image` + +2. Set up a VirtualBox VM. + * Name: ChromeWebView + * Type: Linux + * Version: Ubuntu 64-bit + * RAM: 4096Mb + * Disk: VDI, dynamic, 48Gb + +3. Change the settings (Machine > Settings in the VirtualBox menu) + * System > Processor > Processor(s): 4 (number of CPU cores on the machine) + * Audio > uncheck Enable Audio + * Network > Adapter 1 > Advanced > Adapter Type: virtio-net + * Network > Adapter 2 + * check Enable network adapter + * Attached to > Host-only Adapter + * Advanced > Adapter Type: virtio-net + * Ports > USB > uncheck Enable USB 2.0 (EHCI) Controller + +4. Start VM and set up the server. + * Select the Ubuntu ISO downloaded earlier. + * Start a server installation, providing default answers, except: + * Hostname: crbuild + * Full name: crbuild + * Username: crbuild + * Password: crbuild + * Confirm using a weak password + * Encrypt home directory: no + * Partitioning: Guided - use entire disk (no LVM or encryption) + * Software to install: OpenSSH server + +6. After the VM restarts, set up networking. + * Log in using the VM console. + * Open /etc/network/interfaces in a text editor (sudo vim ...) + * Duplicate the "primary network interface" section + * In the duplicate section, replace-all eth0 with eth1, primary with + secondary + * Save the file. + * `sudo apt-get install -y avahi-daemon` + * `sudo reboot` + +7. Prepare to SSH into the VM. + * If you don't have an ssh key + * `ssh-keygen -t rsa` + * press Enter all the way (default key type, no passphrase) + + ```bash + ssh-copy-id crbuild@crbuild.local + ssh crbuild@crbuild.local + ``` + +8. Get the Oracle Java 6 JDK. + * Go to http://www.oracle.com/technetwork/java/javase/downloads/index.html + * Search for "Java SE 6", click on JDK + * Click on the radio button for accepting the license + * Download the Linux x86 non-RPM file (jdk-6uNN-linux-x64.bin) + + ```bash + exit # Get out of the VM ssh session, run this on the host. + scp ~/Downloads/jdk-6u*-linux-x64.bin crbuild@crbuild.local:~/jdk6.bin + ``` + +9. Set up the VM builds target platform(s). Choosing more than one platform + will result in no incremental building being done. + + ```bash + # ssh crbuild@crbuild.local + touch ~/.build_arm + touch ~/.build_x86 + ``` + +10. Optionally set the path of the directory that will hold the Chromium source + code to. This is only used the first time the setup script runs, so it only + needs to be set once. + + ```bash + export CHROMIUM_DIR=/mnt/chromium + ``` + + +## VM Setup + +The setup script will complete the work done above. The script is idempotent, +so it can be ran to bring a VM's software up to date. + +```bash +# ssh crbuild@crbuild.local +curl -fLsS https://github.com/pwnall/chromeview/raw/master/crbuild/vm-setup.sh | sh +``` + +If the script fails, the steps in [vm-setup.sh](crbuild/vm-setup.sh) can be +copy-pasted one by one in the VM's ssh console. Please open an issue or pull +request if the script fails. + +If this is the first time the Chromium source code is downloaded and +`$CHROMIUM_DIR` is defined, the directory at that path will be created and +symlinked into `~/chromium`. The source code can be moved around, as long as +the symlink is updated. + + +## Building + +The build script will update the Chromium source code and do a build. For +infrequent builds, the setup script above should be ran before a build. + +```bash +# ssh crbuild@crbuild.local +curl -fLsS https://github.com/pwnall/chromeview/raw/master/crbuild/vm-build.sh | sh +``` diff --git a/crbuild/vm-build.sh b/crbuild/vm-build.sh new file mode 100644 index 0000000..2239bb4 --- /dev/null +++ b/crbuild/vm-build.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# Builds the Chromium bits needed by ChromeView. + +set -o errexit # Stop the script on the first error. +set -o nounset # Catch un-initialized variables. + +cd ~/chromium/ +# https://code.google.com/p/chromium/wiki/UsingGit +gclient sync --jobs 16 +cd ~/chromium/src + +if [ -f ~/.build_android ] ; then + . build/android/envsetup.sh --target-arch=arm + android_gyp + ninja -C out/Release -k0 -j$CPUS libwebviewchromium android_webview_apk \ + content_shell_apk chromium_testshell +fi + +if [ -f ~/.build_x86 ] ; then + . build/android/envsetup.sh --target-arch=x86 + android_gyp + ninja -C out/Release -k0 -j$CPUS libwebviewchromium android_webview_apk \ + content_shell_apk chromium_testshell +fi diff --git a/crbuild/vm-setup.sh b/crbuild/vm-setup.sh new file mode 100755 index 0000000..76a57f3 --- /dev/null +++ b/crbuild/vm-setup.sh @@ -0,0 +1,112 @@ +#!/bin/sh +# Idempotent VM setup / upgrade script. + +set -o errexit # Stop the script on the first error. +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" + +# Sun JDK 6. +if [ ! -f /usr/bin/javac ] ; then + if [ ! -f ~/jdk6.bin ] ; then + echo 'Please download the Linux x86 non-RPM JDK6 as jdk6.bin from' + echo 'http://www.oracle.com/technetwork/java/javase/downloads/index.html' + exit 1 + fi + + sudo mkdir -p /usr/lib/jvm + cd /usr/lib/jvm + sudo /bin/sh ~/jdk6.bin -noregister + rm ~/jdk6.bin + sudo update-alternatives --install /usr/bin/javac javac \ + /usr/lib/jvm/jdk1.6.0_*/bin/javac 50000 + sudo update-alternatives --config javac + sudo update-alternatives --install /usr/bin/java java \ + /usr/lib/jvm/jdk1.6.0_*/bin/java 50000 + sudo update-alternatives --config java + sudo update-alternatives --install /usr/bin/javaws javaws \ + /usr/lib/jvm/jdk1.6.0_*/bin/javaws 50000 + sudo update-alternatives --config javaws + sudo update-alternatives --install /usr/bin/javap javap \ + /usr/lib/jvm/jdk1.6.0_*/bin/javap 50000 + sudo update-alternatives --config javap + sudo update-alternatives --install /usr/bin/jar jar \ + /usr/lib/jvm/jdk1.6.0_*/bin/jar 50000 + sudo update-alternatives --config jar + sudo update-alternatives --install /usr/bin/jarsigner jarsigner \ + /usr/lib/jvm/jdk1.6.0_*/bin/jarsigner 50000 + sudo update-alternatives --config jarsigner + cd ~/ +fi + +# Update all system packages. +sudo apt-get update -qq +sudo apt-get -y dist-upgrade + +# debconf-get-selections is useful for figuring out debconf defaults. +sudo apt-get install -y debconf-utils + +# Quiet all package installation prompts. +sudo debconf-set-selections <<'END' +debconf debconf/frontend select Noninteractive +debconf debconf/priority select critical +END + +# Git. +sudo apt-get install -y git + +# Depot tools. +# http://dev.chromium.org/developers/how-tos/install-depot-tools +cd ~ +if [ -d ~/depot_tools ] ; then + cd ~/depot_tools + git pull origin master + cd ~ +fi +if [ ! -d ~/depot_tools ] ; then + git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git +fi +if ! grep -q 'export PATH=$PATH:$HOME/depot_tools' ~/.bashrc ; then + echo 'export PATH=$PATH:$HOME/depot_tools' >> ~/.bashrc + export PATH=$PATH:$HOME/depot_tools +fi + +# Subversion and git-svn. +sudo apt-get install -y git-svn subversion + +# Chromium source. +# https://code.google.com/p/chromium/wiki/UsingGit +# http://dev.chromium.org/developers/how-tos/get-the-code +if [ ! -d ~/chromium ] ; then + if [ ! -z $CHROMIUM_DIR ] ; then + sudo mkdir -p "$CHROMIUM_DIR" + sudo chown $USER "$CHROMIUM_DIR" + chmod 0755 "$CHROMIUM_DIR" + ln -s "$CHROMIUM_DIR" ~/chromium + fi + if [ -z "$CHROMIUM_DIR" ] ; then + mkdir -p ~/chromium + fi +fi +cd ~/chromium +if [ ! -f .gclient ] ; then + ~/depot_tools/fetch android --nosvn=True || \ + echo "Ignore the error above if this is a first-time setup" +fi +if ! grep '"safesync_url":' .gclient ; then + echo -n '' # Noop + # TODO(pwnall): do this right + # echo '"safesync_url": "https://chromium-status.appspot.com/git-lkgr"' >> .gclient +fi +cd ~/chromium/src +sudo ./build/install-build-deps-android.sh +sudo ./build/install-build-deps.sh --no-syms --lib32 --arm --no-prompt +gclient runhooks + +# Chromium build setup. +# https://code.google.com/p/chromium/wiki/LinuxBuildInstructions +# https://code.google.com/p/chromium/wiki/AndroidBuildInstructions + +# Chromium build depedenecies not covered by the Chromium scripts. +sudo apt-get install -y ia32-libs libc6-dev-i386 g++-multilib diff --git a/libs/armeabi-v7a/libwebviewchromium.so b/libs/armeabi-v7a/libwebviewchromium.so new file mode 100755 index 0000000..c476be5 Binary files /dev/null and b/libs/armeabi-v7a/libwebviewchromium.so differ diff --git a/libs/guava_javalib.jar b/libs/guava_javalib.jar new file mode 100644 index 0000000..24b4e2e Binary files /dev/null and b/libs/guava_javalib.jar differ diff --git a/libs/jsr_305_javalib.jar b/libs/jsr_305_javalib.jar new file mode 100644 index 0000000..a2a6691 Binary files /dev/null and b/libs/jsr_305_javalib.jar differ diff --git a/libs/x86/libwebviewchromium.so b/libs/x86/libwebviewchromium.so new file mode 100755 index 0000000..4213ac2 Binary files /dev/null and b/libs/x86/libwebviewchromium.so differ diff --git a/proguard-project.txt b/proguard-project.txt new file mode 100644 index 0000000..f2fe155 --- /dev/null +++ b/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/project.properties b/project.properties new file mode 100644 index 0000000..484dab0 --- /dev/null +++ b/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-17 +android.library=true diff --git a/res/raw/blank_html.html b/res/raw/blank_html.html new file mode 100644 index 0000000..989fbfc --- /dev/null +++ b/res/raw/blank_html.html @@ -0,0 +1,5 @@ + + + This page intentionally left blank. + + diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 0000000..2334481 --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,5 @@ + + + + text/plain + \ No newline at end of file diff --git a/src/com/googlecode/eyesfree/braille/display/BrailleDisplayProperties.aidl b/src/com/googlecode/eyesfree/braille/display/BrailleDisplayProperties.aidl new file mode 100644 index 0000000..5a7f410 --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/display/BrailleDisplayProperties.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.display; + +parcelable BrailleDisplayProperties; diff --git a/src/com/googlecode/eyesfree/braille/display/BrailleDisplayProperties.java b/src/com/googlecode/eyesfree/braille/display/BrailleDisplayProperties.java new file mode 100644 index 0000000..606476f --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/display/BrailleDisplayProperties.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.display; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Properties of a braille display such as dimensions and keyboard + * configuration. + */ +public class BrailleDisplayProperties implements Parcelable { + private final int mNumTextCells; + private final int mNumStatusCells; + private final BrailleKeyBinding[] mKeyBindings; + private final Map mFriendlyKeyNames; + + public BrailleDisplayProperties(int numTextCells, int numStatusCells, + BrailleKeyBinding[] keyBindings, + Map friendlyKeyNames) { + mNumTextCells = numTextCells; + mNumStatusCells = numStatusCells; + mKeyBindings = keyBindings; + mFriendlyKeyNames = friendlyKeyNames; + } + + /** + * Returns the number of cells on the main display intended for display of + * text or other content. + */ + public int getNumTextCells() { + return mNumTextCells; + } + + /** + * Returns the number of status cells that are separated from the main + * display. This value will be {@code 0} for displays without any separate + * status cells. + */ + public int getNumStatusCells() { + return mNumStatusCells; + } + + /** + * Returns the list of key bindings for this display. + */ + public BrailleKeyBinding[] getKeyBindings() { + return mKeyBindings; + } + + /** + * Returns an unmodifiable map mapping key names in {@link BrailleKeyBinding} + * objects to localized user-friendly key names. + */ + public Map getFriendlyKeyNames() { + return mFriendlyKeyNames; + } + + @Override + public String toString() { + return String.format( + "BrailleDisplayProperties [numTextCells: %d, numStatusCells: %d, " + + "keyBindings: %d]", + mNumTextCells, mNumStatusCells, mKeyBindings.length); + } + + // For Parcelable support. + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public BrailleDisplayProperties createFromParcel(Parcel in) { + return new BrailleDisplayProperties(in); + } + + @Override + public BrailleDisplayProperties[] newArray(int size) { + return new BrailleDisplayProperties[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mNumTextCells); + out.writeInt(mNumStatusCells); + out.writeTypedArray(mKeyBindings, flags); + out.writeInt(mFriendlyKeyNames.size()); + for (Map.Entry entry : mFriendlyKeyNames.entrySet()) { + out.writeString(entry.getKey()); + out.writeString(entry.getValue()); + } + } + + private BrailleDisplayProperties(Parcel in) { + mNumTextCells = in.readInt(); + mNumStatusCells = in.readInt(); + mKeyBindings = in.createTypedArray(BrailleKeyBinding.CREATOR); + int size = in.readInt(); + Map names = new HashMap(size); + for (int i = 0; i < size; ++i) { + names.put(in.readString(), in.readString()); + } + mFriendlyKeyNames = Collections.unmodifiableMap(names); + } +} diff --git a/src/com/googlecode/eyesfree/braille/display/BrailleInputEvent.aidl b/src/com/googlecode/eyesfree/braille/display/BrailleInputEvent.aidl new file mode 100644 index 0000000..f64c080 --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/display/BrailleInputEvent.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.display; + +parcelable BrailleInputEvent; diff --git a/src/com/googlecode/eyesfree/braille/display/BrailleInputEvent.java b/src/com/googlecode/eyesfree/braille/display/BrailleInputEvent.java new file mode 100644 index 0000000..1c2ffb4 --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/display/BrailleInputEvent.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.display; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.SparseArray; + +import java.util.HashMap; + +/** + * An input event, originating from a braille display. + * + * An event contains a command that is a high-level representation of the + * key or key combination that was pressed on the display such as a + * navigation key or braille keyboard combination. For some commands, there is + * also an integer argument that contains additional information. + */ +public class BrailleInputEvent implements Parcelable { + + // Movement commands. + + /** Keyboard command: Used when there is no actual command. */ + public static final int CMD_NONE = -1; + + /** Keyboard command: Navigate upwards. */ + public static final int CMD_NAV_LINE_PREVIOUS = 1; + /** Keyboard command: Navigate downwards. */ + public static final int CMD_NAV_LINE_NEXT = 2; + /** Keyboard command: Navigate left one item. */ + public static final int CMD_NAV_ITEM_PREVIOUS = 3; + /** Keyboard command: Navigate right one item. */ + public static final int CMD_NAV_ITEM_NEXT = 4; + /** Keyboard command: Navigate one display window to the left. */ + public static final int CMD_NAV_PAN_LEFT = 5; + /** Keyboard command: Navigate one display window to the right. */ + public static final int CMD_NAV_PAN_RIGHT = 6; + /** Keyboard command: Navigate to the top or beginning. */ + public static final int CMD_NAV_TOP = 7; + /** Keyboard command: Navigate to the bottom or end. */ + public static final int CMD_NAV_BOTTOM = 8; + + // Activation commands. + + /** Keyboard command: Activate the currently selected/focused item. */ + public static final int CMD_ACTIVATE_CURRENT = 20; + + // Scrolling. + + /** Keyboard command: Scroll backward. */ + public static final int CMD_SCROLL_BACKWARD = 30; + /** Keyboard command: Scroll forward. */ + public static final int CMD_SCROLL_FORWARD = 31; + + // Selection commands. + + /** Keyboard command: Set the start ot the selection. */ + public static final int CMD_SELECTION_START = 40; + /** Keyboard command: Set the end of the selection. */ + public static final int CMD_SELECTION_END = 41; + /** Keyboard command: Select all content of the current field. */ + public static final int CMD_SELECTION_SELECT_ALL = 42; + /** Keyboard command: Cut the content of the selection. */ + public static final int CMD_SELECTION_CUT = 43; + /** Keyboard command: Copy the current selection. */ + public static final int CMD_SELECTION_COPY = 44; + /** + * Keyboard command: Paste the content of the clipboard at the current + * insertion point. + */ + public static final int CMD_SELECTION_PASTE = 45; + + /** + * Keyboard command: Primary routing key pressed, typically + * used to move the insertion point or click/tap on the item + * under the key. + * The argument is the zero-based position, relative to the first cell + * on the display, of the cell that is closed to the key that + * was pressed. + */ + public static final int CMD_ROUTE = 50; + + // Braille keyboard input. + + /** + * Keyboard command: A key combination was pressed on the braille + * keyboard. + * The argument contains the dots that were pressed as a bitmask. + */ + public static final int CMD_BRAILLE_KEY = 60; + + // Editing keys. + + /** Keyboard command: Enter key. */ + public static final int CMD_KEY_ENTER = 70; + /** Keyboard command: Delete backward. */ + public static final int CMD_KEY_DEL = 71; + /** Keyboard command: Delete forward. */ + public static final int CMD_KEY_FORWARD_DEL = 72; + + // Glboal navigation keys. + + /** Keyboard command: Back button. */ + public static final int CMD_GLOBAL_BACK = 90; + /** Keyboard command: Home button. */ + public static final int CMD_GLOBAL_HOME = 91; + /** Keyboard command: Recent apps button. */ + public static final int CMD_GLOBAL_RECENTS = 92; + /** Keyboard command: Show notificaitons. */ + public static final int CMD_GLOBAL_NOTIFICATIONS = 93; + + // Miscelanous commands. + + /** Keyboard command: Invoke keyboard help. */ + public static final int CMD_HELP = 100; + + // Meanings of the argument to a command. + + /** This command doesn't have an argument. */ + public static final int ARGUMENT_NONE = 0; + /** + * The lower order bits of the arguemnt to this command represent braille + * dots. Dot 1 is represented by the rightmost bit and so on until dot 8, + * which is represented by bit 7, counted from the right. + */ + public static final int ARGUMENT_DOTS = 1; + /** + * The argument represents a 0-based position on the display counted from + * the leftmost cell. + */ + public static final int ARGUMENT_POSITION = 2; + + private static final SparseArray CMD_NAMES = + new SparseArray(); + private static final HashMap NAMES_TO_CMDS + = new HashMap(); + static { + CMD_NAMES.append(CMD_NAV_LINE_PREVIOUS, "CMD_NAV_LINE_PREVIOUS"); + CMD_NAMES.append(CMD_NAV_LINE_NEXT, "CMD_NAV_LINE_NEXT"); + CMD_NAMES.append(CMD_NAV_ITEM_PREVIOUS, "CMD_NAV_ITEM_PREVIOUS"); + CMD_NAMES.append(CMD_NAV_ITEM_NEXT, "CMD_NAV_ITEM_NEXT"); + CMD_NAMES.append(CMD_NAV_PAN_LEFT, "CMD_NAV_PAN_LEFT"); + CMD_NAMES.append(CMD_NAV_PAN_RIGHT, "CMD_NAV_PAN_RIGHT"); + CMD_NAMES.append(CMD_NAV_TOP, "CMD_NAV_TOP"); + CMD_NAMES.append(CMD_NAV_BOTTOM, "CMD_NAV_BOTTOM"); + CMD_NAMES.append(CMD_ACTIVATE_CURRENT, "CMD_ACTIVATE_CURRENT"); + CMD_NAMES.append(CMD_SCROLL_BACKWARD, "CMD_SCROLL_BACKWARD"); + CMD_NAMES.append(CMD_SCROLL_FORWARD, "CMD_SCROLL_FORWARD"); + CMD_NAMES.append(CMD_SELECTION_START, "CMD_SELECTION_START"); + CMD_NAMES.append(CMD_SELECTION_END, "CMD_SELECTION_END"); + CMD_NAMES.append(CMD_SELECTION_SELECT_ALL, "CMD_SELECTION_SELECT_ALL"); + CMD_NAMES.append(CMD_SELECTION_CUT, "CMD_SELECTION_CUT"); + CMD_NAMES.append(CMD_SELECTION_COPY, "CMD_SELECTION_COPY"); + CMD_NAMES.append(CMD_SELECTION_PASTE, "CMD_SELECTION_PASTE"); + CMD_NAMES.append(CMD_ROUTE, "CMD_ROUTE"); + CMD_NAMES.append(CMD_BRAILLE_KEY, "CMD_BRAILLE_KEY"); + CMD_NAMES.append(CMD_KEY_ENTER, "CMD_KEY_ENTER"); + CMD_NAMES.append(CMD_KEY_DEL, "CMD_KEY_DEL"); + CMD_NAMES.append(CMD_KEY_FORWARD_DEL, "CMD_KEY_FORWARD_DEL"); + CMD_NAMES.append(CMD_GLOBAL_BACK, "CMD_GLOBAL_BACK"); + CMD_NAMES.append(CMD_GLOBAL_HOME, "CMD_GLOBAL_HOME"); + CMD_NAMES.append(CMD_GLOBAL_RECENTS, "CMD_GLOBAL_RECENTS"); + CMD_NAMES.append(CMD_GLOBAL_NOTIFICATIONS, "CMD_GLOBAL_NOTIFICATIONS"); + CMD_NAMES.append(CMD_HELP, "CMD_HELP"); + for (int i = 0; i < CMD_NAMES.size(); ++i) { + NAMES_TO_CMDS.put(CMD_NAMES.valueAt(i), + CMD_NAMES.keyAt(i)); + } + } + + private final int mCommand; + private final int mArgument; + private final long mEventTime; + + public BrailleInputEvent(int command, int argument, long eventTime) { + mCommand = command; + mArgument = argument; + mEventTime = eventTime; + } + + /** + * Returns the keyboard command that this event represents. + */ + public int getCommand() { + return mCommand; + } + + /** + * Returns the command-specific argument of the event, or zero if the + * command doesn't have an argument. See the individual command constants + * for more details. + */ + public int getArgument() { + return mArgument; + } + + /** + * Returns the approximate time when this event happened as + * returned by {@link android.os.SystemClock#uptimeMillis}. + */ + public long getEventTime() { + return mEventTime; + } + + /** + * Returns a string representation of {@code command}, or the string + * {@code (unknown)} if the command is unknown. + */ + public static String commandToString(int command) { + String ret = CMD_NAMES.get(command); + return ret != null ? ret : "(unknown)"; + } + + /** + * Returns the command corresponding to {@code commandName}, or + * {@link #CMD_NONE} if the name doesn't match any existing command. + */ + public static int stringToCommand(String commandName) { + Integer command = NAMES_TO_CMDS.get(commandName); + if (command == null) { + return CMD_NONE; + } + return command; + } + + /** + * Returns the type of argument for the given {@code command}. + */ + public static int argumentType(int command) { + switch (command) { + case CMD_SELECTION_START: + case CMD_SELECTION_END: + case CMD_ROUTE: + return ARGUMENT_POSITION; + case CMD_BRAILLE_KEY: + return ARGUMENT_DOTS; + default: + return ARGUMENT_NONE; + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("BrailleInputEvent {"); + sb.append("amd="); + sb.append(commandToString(mCommand)); + sb.append(", arg="); + sb.append(mArgument); + sb.append("}"); + return sb.toString(); + } + + // For Parcelable support. + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public BrailleInputEvent createFromParcel(Parcel in) { + return new BrailleInputEvent(in); + } + + @Override + public BrailleInputEvent[] newArray(int size) { + return new BrailleInputEvent[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mCommand); + out.writeInt(mArgument); + out.writeLong(mEventTime); + } + + private BrailleInputEvent(Parcel in) { + mCommand = in.readInt(); + mArgument = in.readInt(); + mEventTime = in.readLong(); + } +} diff --git a/src/com/googlecode/eyesfree/braille/display/BrailleKeyBinding.java b/src/com/googlecode/eyesfree/braille/display/BrailleKeyBinding.java new file mode 100644 index 0000000..92b58d0 --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/display/BrailleKeyBinding.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.display; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Represents a binding between a combination of braille device keys and a + * command as declared in {@link BrailleInputEvent}. + */ +public class BrailleKeyBinding implements Parcelable { + private int mCommand; + private String[] mKeyNames; + + public BrailleKeyBinding() { + } + + public BrailleKeyBinding(int command, String[] keyNames) { + mCommand = command; + mKeyNames = keyNames; + } + + /** + * Sets the command for this binding. + */ + public BrailleKeyBinding setCommand(int command) { + mCommand = command; + return this; + } + + /** + * Sets the key names for this binding. + */ + public BrailleKeyBinding setKeyNames(String[] keyNames) { + mKeyNames = keyNames; + return this; + } + + /** + * Returns the command for this key binding. + * @see {@link BrailleInputEvent}. + */ + public int getCommand() { + return mCommand; + } + + /** + * Returns the list of device-specific keys that, when pressed + * at the same time, will yield the command of this key binding. + */ + public String[] getKeyNames() { + return mKeyNames; + } + + // For Parcelable support. + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public BrailleKeyBinding createFromParcel(Parcel in) { + return new BrailleKeyBinding(in); + } + + @Override + public BrailleKeyBinding[] newArray(int size) { + return new BrailleKeyBinding[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mCommand); + out.writeStringArray(mKeyNames); + } + + private BrailleKeyBinding(Parcel in) { + mCommand = in.readInt(); + mKeyNames = in.createStringArray(); + } +} diff --git a/src/com/googlecode/eyesfree/braille/display/Display.java b/src/com/googlecode/eyesfree/braille/display/Display.java new file mode 100644 index 0000000..54a57a2 --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/display/Display.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.display; + +import android.os.Message; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +/** + * A client for the braille display service. + */ +public class Display { + private static final String LOG_TAG = Display.class.getSimpleName(); + /** Service name used for connecting to the service. */ + public static final String ACTION_DISPLAY_SERVICE = + "com.googlecode.eyesfree.braille.service.ACTION_DISPLAY_SERVICE"; + + /** Initial value, which is never reported to the listener. */ + private static final int STATE_UNKNOWN = -2; + public static final int STATE_ERROR = -1; + public static final int STATE_NOT_CONNECTED = 0; + public static final int STATE_CONNECTED = 1; + + private final OnConnectionStateChangeListener + mConnectionStateChangeListener; + private final Context mContext; + private final DisplayHandler mHandler; + private volatile OnInputEventListener mInputEventListener; + private static final Intent mServiceIntent = + new Intent(ACTION_DISPLAY_SERVICE); + private Connection mConnection; + private int currentConnectionState = STATE_UNKNOWN; + private BrailleDisplayProperties mDisplayProperties; + private ServiceCallback mServiceCallback = new ServiceCallback(); + /** + * Delay before the first rebind attempt on bind error or service + * disconnect. + */ + private static final int REBIND_DELAY_MILLIS = 500; + private static final int MAX_REBIND_ATTEMPTS = 5; + private int mNumFailedBinds = 0; + + /** + * A callback interface to get informed about connection state changes. + */ + public interface OnConnectionStateChangeListener { + void onConnectionStateChanged(int state); + } + + /** + * A callback interface for input from the braille display. + */ + public interface OnInputEventListener { + void onInputEvent(BrailleInputEvent inputEvent); + } + + /** + * Constructs an instance and connects to the braille display service. + * The current thread must have an {@link android.os.Looper} associated + * with it. Callbacks from this object will all be executed on the + * current thread. Connection state will be reported to {@code listener). + */ + public Display(Context context, OnConnectionStateChangeListener listener) { + this(context, listener, null); + } + + /** + * Constructs an instance and connects to the braille display service. + * Callbacks from this object will all be executed on the thread + * associated with {@code handler}. If {@code handler} is {@code null}, + * the current thread must have an {@link android.os.Looper} associated + * with it, which will then be used to execute callbacks. Connection + * state will be reported to {@code listener). + */ + public Display(Context context, OnConnectionStateChangeListener listener, + Handler handler) { + mContext = context; + mConnectionStateChangeListener = listener; + if (handler == null) { + mHandler = new DisplayHandler(); + } else { + mHandler = new DisplayHandler(handler); + } + + doBindService(); + } + + /** + * Sets a {@code listener} for input events. {@code listener} can be + * {@code null} to remove a previously set listener. + */ + public void setOnInputEventListener(OnInputEventListener listener) { + mInputEventListener = listener; + } + + /** + * Returns the display properties, or {@code null} if not connected + * to a display. + */ + public BrailleDisplayProperties getDisplayProperties() { + return mDisplayProperties; + } + + /** + * Displays a given dots configuration on the braille display. + * @param patterns Dots configuration to be displayed. + */ + public void displayDots(byte[] patterns) { + IBrailleService localService = getBrailleService(); + if (localService != null) { + try { + localService.displayDots(patterns); + } catch (RemoteException ex) { + Log.e(LOG_TAG, "Error in displayDots", ex); + } + } else { + Log.v(LOG_TAG, "Error in displayDots: service not connected"); + } + } + + /** + * Unbinds from the braille display service and deallocates any + * resources. This method should be called when the braille display + * is no longer in use by this client. + */ + public void shutdown() { + doUnbindService(); + } + + // NOTE: The methods in this class will be executed in the main + // application thread. + private class Connection implements ServiceConnection { + private volatile IBrailleService mService; + + @Override + public void onServiceConnected(ComponentName className, + IBinder binder) { + Log.i(LOG_TAG, "Connected to braille service"); + IBrailleService localService = + IBrailleService.Stub.asInterface(binder); + try { + localService.registerCallback(mServiceCallback); + mService = localService; + synchronized (mHandler) { + mNumFailedBinds = 0; + } + } catch (RemoteException e) { + // In this case the service has crashed before we could even do + // anything with it. + Log.e(LOG_TAG, "Failed to register callback on service", e); + // We should get a disconnected call and the rebind + // and failure reporting happens in that handler. + } + } + + @Override + public void onServiceDisconnected(ComponentName className) { + mService = null; + Log.e(LOG_TAG, "Disconnected from braille service"); + // Report display disconnected for now, this will turn into a + // connected state or error state depending on how the retrying + // goes. + mHandler.reportConnectionState(STATE_NOT_CONNECTED, null); + mHandler.scheduleRebind(); + } + } + + // NOTE: The methods of this class will be executed in the IPC + // thread pool and not on the main application thread. + private class ServiceCallback extends IBrailleServiceCallback.Stub { + @Override + public void onDisplayConnected( + BrailleDisplayProperties displayProperties) { + mHandler.reportConnectionState(STATE_CONNECTED, displayProperties); + } + + @Override + public void onDisplayDisconnected() { + mHandler.reportConnectionState(STATE_NOT_CONNECTED, null); + } + + @Override + public void onInput(BrailleInputEvent inputEvent) { + mHandler.reportInputEvent(inputEvent); + } + } + + private void doBindService() { + Connection localConnection = new Connection(); + if (!mContext.bindService(mServiceIntent, localConnection, + Context.BIND_AUTO_CREATE)) { + Log.e(LOG_TAG, "Failed to bind Service"); + mHandler.scheduleRebind(); + return; + } + mConnection = localConnection; + Log.i(LOG_TAG, "Bound to braille service"); + } + + private void doUnbindService() { + IBrailleService localService = getBrailleService(); + if (localService != null) { + try { + localService.unregisterCallback(mServiceCallback); + } catch (RemoteException e) { + // Nothing to do if the service can't be reached. + } + } + if (mConnection != null) { + mContext.unbindService(mConnection); + mConnection = null; + } + } + + private IBrailleService getBrailleService() { + Connection localConnection = mConnection; + if (localConnection != null) { + return localConnection.mService; + } + return null; + } + + private class DisplayHandler extends Handler { + private static final int MSG_REPORT_CONNECTION_STATE = 1; + private static final int MSG_REPORT_INPUT_EVENT = 2; + private static final int MSG_REBIND_SERVICE = 3; + + public DisplayHandler() { + } + + public DisplayHandler(Handler handler) { + super(handler.getLooper()); + } + + public void reportConnectionState(final int newState, + final BrailleDisplayProperties displayProperties) { + obtainMessage(MSG_REPORT_CONNECTION_STATE, newState, 0, + displayProperties) + .sendToTarget(); + } + + public void reportInputEvent(BrailleInputEvent event) { + obtainMessage(MSG_REPORT_INPUT_EVENT, event).sendToTarget(); + } + + public void scheduleRebind() { + synchronized (this) { + if (mNumFailedBinds < MAX_REBIND_ATTEMPTS) { + int delay = REBIND_DELAY_MILLIS << mNumFailedBinds; + sendEmptyMessageDelayed(MSG_REBIND_SERVICE, delay); + ++mNumFailedBinds; + Log.w(LOG_TAG, String.format( + "Will rebind to braille service in %d ms.", delay)); + } else { + reportConnectionState(STATE_ERROR, null); + } + } + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_REPORT_CONNECTION_STATE: + handleReportConnectionState(msg.arg1, + (BrailleDisplayProperties) msg.obj); + break; + case MSG_REPORT_INPUT_EVENT: + handleReportInputEvent((BrailleInputEvent) msg.obj); + break; + case MSG_REBIND_SERVICE: + handleRebindService(); + break; + } + } + + private void handleReportConnectionState(int newState, + BrailleDisplayProperties displayProperties) { + mDisplayProperties = displayProperties; + if (newState != currentConnectionState + && mConnectionStateChangeListener != null) { + mConnectionStateChangeListener.onConnectionStateChanged( + newState); + } + currentConnectionState = newState; + } + + private void handleReportInputEvent(BrailleInputEvent event) { + OnInputEventListener localListener = mInputEventListener; + if (localListener != null) { + localListener.onInputEvent(event); + } + } + + private void handleRebindService() { + if (mConnection != null) { + doUnbindService(); + } + doBindService(); + } + } +} diff --git a/src/com/googlecode/eyesfree/braille/display/IBrailleService.aidl b/src/com/googlecode/eyesfree/braille/display/IBrailleService.aidl new file mode 100644 index 0000000..2b478bb --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/display/IBrailleService.aidl @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.display; + +import com.googlecode.eyesfree.braille.display.IBrailleServiceCallback; + +/** + * Interface for clients to talk to the braille display service. + */ +interface IBrailleService { + /** + * Register a callback for the {@code callingApp} which will receive + * certain braille display related events. + */ + boolean registerCallback(in IBrailleServiceCallback callback); + + /** + * Unregister a previously registered callback for the {@code callingApp}. + */ + oneway void unregisterCallback(in IBrailleServiceCallback callback); + + /** + * Updates the main cells of the connected braille display + * with a given dot {@code pattern}. + * + * @return {@code true} on success and {@code false} otherwise. + */ + void displayDots(in byte[] patterns); +} diff --git a/src/com/googlecode/eyesfree/braille/display/IBrailleServiceCallback.aidl b/src/com/googlecode/eyesfree/braille/display/IBrailleServiceCallback.aidl new file mode 100644 index 0000000..545d1ad --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/display/IBrailleServiceCallback.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.display; + +import com.googlecode.eyesfree.braille.display.BrailleDisplayProperties; +import com.googlecode.eyesfree.braille.display.BrailleInputEvent; + +/** + * Callback interface that a braille display client can expose to + * get information about various braille display events. + */ +interface IBrailleServiceCallback { + void onDisplayConnected(in BrailleDisplayProperties displayProperties); + void onDisplayDisconnected(); + void onInput(in BrailleInputEvent inputEvent); +} diff --git a/src/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.aidl b/src/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.aidl new file mode 100644 index 0000000..770c283 --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.selfbraille; + +import com.googlecode.eyesfree.braille.selfbraille.WriteData; + +/** + * Interface for a client to control braille output for a part of the + * accessibility node tree. + */ +interface ISelfBrailleService { + void write(IBinder clientToken, in WriteData writeData); + oneway void disconnect(IBinder clientToken); +} diff --git a/src/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java b/src/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java new file mode 100644 index 0000000..e4a363a --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.selfbraille; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Client-side interface to the self brailling interface. + * + * Threading: Instances of this object should be created and shut down + * in a thread with a {@link Looper} associated with it. Other methods may + * be called on any thread. + */ +public class SelfBrailleClient { + private static final String LOG_TAG = + SelfBrailleClient.class.getSimpleName(); + private static final String ACTION_SELF_BRAILLE_SERVICE = + "com.googlecode.eyesfree.braille.service.ACTION_SELF_BRAILLE_SERVICE"; + private static final String BRAILLE_BACK_PACKAGE = + "com.googlecode.eyesfree.brailleback"; + private static final Intent mServiceIntent = + new Intent(ACTION_SELF_BRAILLE_SERVICE) + .setPackage(BRAILLE_BACK_PACKAGE); + /** + * SHA-1 hash value of the Eyes-Free release key certificate, used to sign + * BrailleBack. It was generated from the keystore with: + * $ keytool -exportcert -keystore -alias android.keystore \ + * > cert + * $ keytool -printcert -file cert + */ + // The typecasts are to silence a compiler warning about loss of precision + private static final byte[] EYES_FREE_CERT_SHA1 = new byte[] { + (byte) 0x9B, (byte) 0x42, (byte) 0x4C, (byte) 0x2D, + (byte) 0x27, (byte) 0xAD, (byte) 0x51, (byte) 0xA4, + (byte) 0x2A, (byte) 0x33, (byte) 0x7E, (byte) 0x0B, + (byte) 0xB6, (byte) 0x99, (byte) 0x1C, (byte) 0x76, + (byte) 0xEC, (byte) 0xA4, (byte) 0x44, (byte) 0x61 + }; + /** + * Delay before the first rebind attempt on bind error or service + * disconnect. + */ + private static final int REBIND_DELAY_MILLIS = 500; + private static final int MAX_REBIND_ATTEMPTS = 5; + + private final Binder mIdentity = new Binder(); + private final Context mContext; + private final boolean mAllowDebugService; + private final SelfBrailleHandler mHandler = new SelfBrailleHandler(); + private boolean mShutdown = false; + + /** + * Written in handler thread, read in any thread calling methods on the + * object. + */ + private volatile Connection mConnection; + /** Protected by synchronizing on mHandler. */ + private int mNumFailedBinds = 0; + + /** + * Constructs an instance of this class. {@code context} is used to bind + * to the self braille service. The current thread must have a Looper + * associated with it. If {@code allowDebugService} is true, this instance + * will connect to a BrailleBack service without requiring it to be signed + * by the release key used to sign BrailleBack. + */ + public SelfBrailleClient(Context context, boolean allowDebugService) { + mContext = context; + mAllowDebugService = allowDebugService; + doBindService(); + } + + /** + * Shuts this instance down, deallocating any global resources it is using. + * This method must be called on the same thread that created this object. + */ + public void shutdown() { + mShutdown = true; + doUnbindService(); + } + + public void write(WriteData writeData) { + writeData.validate(); + ISelfBrailleService localService = getSelfBrailleService(); + if (localService != null) { + try { + localService.write(mIdentity, writeData); + } catch (RemoteException ex) { + Log.e(LOG_TAG, "Self braille write failed", ex); + } + } + } + + private void doBindService() { + Connection localConnection = new Connection(); + if (!mContext.bindService(mServiceIntent, localConnection, + Context.BIND_AUTO_CREATE)) { + Log.e(LOG_TAG, "Failed to bind to service"); + mHandler.scheduleRebind(); + return; + } + mConnection = localConnection; + Log.i(LOG_TAG, "Bound to self braille service"); + } + + private void doUnbindService() { + if (mConnection != null) { + ISelfBrailleService localService = getSelfBrailleService(); + if (localService != null) { + try { + localService.disconnect(mIdentity); + } catch (RemoteException ex) { + // Nothing to do. + } + } + mContext.unbindService(mConnection); + mConnection = null; + } + } + + private ISelfBrailleService getSelfBrailleService() { + Connection localConnection = mConnection; + if (localConnection != null) { + return localConnection.mService; + } + return null; + } + + private boolean verifyPackage() { + PackageManager pm = mContext.getPackageManager(); + PackageInfo pi; + try { + pi = pm.getPackageInfo(BRAILLE_BACK_PACKAGE, + PackageManager.GET_SIGNATURES); + } catch (PackageManager.NameNotFoundException ex) { + Log.w(LOG_TAG, "Can't verify package " + BRAILLE_BACK_PACKAGE, + ex); + return false; + } + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException ex) { + Log.e(LOG_TAG, "SHA-1 not supported", ex); + return false; + } + // Check if any of the certificates match our hash. + for (Signature signature : pi.signatures) { + digest.update(signature.toByteArray()); + if (MessageDigest.isEqual(EYES_FREE_CERT_SHA1, digest.digest())) { + return true; + } + digest.reset(); + } + if (mAllowDebugService) { + Log.w(LOG_TAG, String.format( + "*** %s connected to BrailleBack with invalid (debug?) " + + "signature ***", + mContext.getPackageName())); + return true; + } + return false; + } + private class Connection implements ServiceConnection { + // Read in application threads, written in main thread. + private volatile ISelfBrailleService mService; + + @Override + public void onServiceConnected(ComponentName className, + IBinder binder) { + if (!verifyPackage()) { + Log.w(LOG_TAG, String.format("Service certificate mismatch " + + "for %s, dropping connection", + BRAILLE_BACK_PACKAGE)); + mHandler.unbindService(); + return; + } + Log.i(LOG_TAG, "Connected to self braille service"); + mService = ISelfBrailleService.Stub.asInterface(binder); + synchronized (mHandler) { + mNumFailedBinds = 0; + } + } + + @Override + public void onServiceDisconnected(ComponentName className) { + Log.e(LOG_TAG, "Disconnected from self braille service"); + mService = null; + // Retry by rebinding. + mHandler.scheduleRebind(); + } + } + + private class SelfBrailleHandler extends Handler { + private static final int MSG_REBIND_SERVICE = 1; + private static final int MSG_UNBIND_SERVICE = 2; + + public void scheduleRebind() { + synchronized (this) { + if (mNumFailedBinds < MAX_REBIND_ATTEMPTS) { + int delay = REBIND_DELAY_MILLIS << mNumFailedBinds; + sendEmptyMessageDelayed(MSG_REBIND_SERVICE, delay); + ++mNumFailedBinds; + } + } + } + + public void unbindService() { + sendEmptyMessage(MSG_UNBIND_SERVICE); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_REBIND_SERVICE: + handleRebindService(); + break; + case MSG_UNBIND_SERVICE: + handleUnbindService(); + break; + } + } + + private void handleRebindService() { + if (mShutdown) { + return; + } + if (mConnection != null) { + doUnbindService(); + } + doBindService(); + } + + private void handleUnbindService() { + doUnbindService(); + } + } +} diff --git a/src/com/googlecode/eyesfree/braille/selfbraille/WriteData.aidl b/src/com/googlecode/eyesfree/braille/selfbraille/WriteData.aidl new file mode 100644 index 0000000..b02ec85 --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/selfbraille/WriteData.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.selfbraille; + +parcelable WriteData; diff --git a/src/com/googlecode/eyesfree/braille/selfbraille/WriteData.java b/src/com/googlecode/eyesfree/braille/selfbraille/WriteData.java new file mode 100644 index 0000000..3c16502 --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/selfbraille/WriteData.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.selfbraille; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * Represents what should be shown on the braille display for a + * part of the accessibility node tree. + */ +public class WriteData implements Parcelable { + + private static final String PROP_SELECTION_START = "selectionStart"; + private static final String PROP_SELECTION_END = "selectionEnd"; + + private AccessibilityNodeInfo mAccessibilityNodeInfo; + private CharSequence mText; + private Bundle mProperties = Bundle.EMPTY; + + /** + * Returns a new {@link WriteData} instance for the given {@code view}. + */ + public static WriteData forView(View view) { + AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(view); + WriteData writeData = new WriteData(); + writeData.mAccessibilityNodeInfo = node; + return writeData; + } + + public AccessibilityNodeInfo getAccessibilityNodeInfo() { + return mAccessibilityNodeInfo; + } + + /** + * Sets the text to be displayed when the accessibility node associated + * with this instance has focus. If this method is not called (or + * {@code text} is {@code null}), this client relinquishes control over + * this node. + */ + public WriteData setText(CharSequence text) { + mText = text; + return this; + } + + public CharSequence getText() { + return mText; + } + + /** + * Sets the start position in the text of a text selection or cursor that + * should be marked on the display. A negative value (the default) means + * no selection will be added. + */ + public WriteData setSelectionStart(int v) { + writableProperties().putInt(PROP_SELECTION_START, v); + return this; + } + + /** + * @see {@link #setSelectionStart}. + */ + public int getSelectionStart() { + return mProperties.getInt(PROP_SELECTION_START, -1); + } + + /** + * Sets the end of the text selection to be marked on the display. This + * value should only be non-negative if the selection start is + * non-negative. If this value is <= the selection start, the selection + * is a cursor. Otherwise, the selection covers the range from + * start(inclusive) to end (exclusive). + * + * @see {@link android.text.Selection}. + */ + public WriteData setSelectionEnd(int v) { + writableProperties().putInt(PROP_SELECTION_END, v); + return this; + } + + /** + * @see {@link #setSelectionEnd}. + */ + public int getSelectionEnd() { + return mProperties.getInt(PROP_SELECTION_END, -1); + } + + private Bundle writableProperties() { + if (mProperties == Bundle.EMPTY) { + mProperties = new Bundle(); + } + return mProperties; + } + + /** + * Checks constraints on the fields that must be satisfied before sending + * this instance to the self braille service. + * @throws IllegalStateException + */ + public void validate() throws IllegalStateException { + if (mAccessibilityNodeInfo == null) { + throw new IllegalStateException( + "Accessibility node info can't be null"); + } + int selectionStart = getSelectionStart(); + int selectionEnd = getSelectionEnd(); + if (mText == null) { + if (selectionStart > 0 || selectionEnd > 0) { + throw new IllegalStateException( + "Selection can't be set without text"); + } + } else { + if (selectionStart < 0 && selectionEnd >= 0) { + throw new IllegalStateException( + "Selection end without start"); + } + int textLength = mText.length(); + if (selectionStart > textLength || selectionEnd > textLength) { + throw new IllegalStateException("Selection out of bounds"); + } + } + } + + // For Parcelable support. + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public WriteData createFromParcel(Parcel in) { + return new WriteData(in); + } + + @Override + public WriteData[] newArray(int size) { + return new WriteData[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + * Note: The {@link AccessibilityNodeInfo} will be + * recycled by this method, don't try to use this more than once. + */ + @Override + public void writeToParcel(Parcel out, int flags) { + mAccessibilityNodeInfo.writeToParcel(out, flags); + // The above call recycles the node, so make sure we don't use it + // anymore. + mAccessibilityNodeInfo = null; + out.writeString(mText.toString()); + out.writeBundle(mProperties); + } + + private WriteData() { + } + + private WriteData(Parcel in) { + mAccessibilityNodeInfo = + AccessibilityNodeInfo.CREATOR.createFromParcel(in); + mText = in.readString(); + mProperties = in.readBundle(); + } +} diff --git a/src/com/googlecode/eyesfree/braille/translate/BrailleTranslator.java b/src/com/googlecode/eyesfree/braille/translate/BrailleTranslator.java new file mode 100644 index 0000000..e7ee9cb --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/translate/BrailleTranslator.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.translate; + +/** + * Translates from text to braille and the other way according to a + * particular translation table. + */ +public interface BrailleTranslator { + /** + * Translates a string into the corresponding dot patterns and returns the + * resulting byte array. Returns {@code null} on error. + */ + byte[] translate(String text); + + /** + * Translates the braille {@code cells} into the corresponding text, which + * is returned. Returns {@code null} on error. + */ + String backTranslate(byte[] cells); +} diff --git a/src/com/googlecode/eyesfree/braille/translate/ITranslatorService.aidl b/src/com/googlecode/eyesfree/braille/translate/ITranslatorService.aidl new file mode 100644 index 0000000..1ccab87 --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/translate/ITranslatorService.aidl @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.translate; + +import com.googlecode.eyesfree.braille.translate.ITranslatorServiceCallback; + +interface ITranslatorService { + /** + * Sets a callback to be called when the service is ready to translate. + * Using any of the other methods in this interface before the + * callback is called with a successful status will return + * failure. + */ + void setCallback(ITranslatorServiceCallback callback); + + /** + * Makes sure that the given table string is valid and that the + * table compiles. + */ + boolean checkTable(String tableName); + + /** + * Translates text into braille according to the give tableName. + * Returns null on fatal translation errors. + */ + byte[] translate(String text, String tableName); + + /** + * Translates braille cells into text according to the given table + * name. Returns null on fatal translation errors. + */ + String backTranslate(in byte[] cells, String tableName); +} diff --git a/src/com/googlecode/eyesfree/braille/translate/ITranslatorServiceCallback.aidl b/src/com/googlecode/eyesfree/braille/translate/ITranslatorServiceCallback.aidl new file mode 100644 index 0000000..91c74cb --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/translate/ITranslatorServiceCallback.aidl @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.translate; + +oneway interface ITranslatorServiceCallback { + void onInit(int status); +} diff --git a/src/com/googlecode/eyesfree/braille/translate/TranslatorManager.java b/src/com/googlecode/eyesfree/braille/translate/TranslatorManager.java new file mode 100644 index 0000000..841a041 --- /dev/null +++ b/src/com/googlecode/eyesfree/braille/translate/TranslatorManager.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.braille.translate; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; + +/** + * Client-side interface to the central braille translator service. + * + * This class can be used to retrieve {@link BrailleTranslator} instances for + * performing translation between text and braille cells. + * + * Typically, an instance of this class is created at application + * initialization time and destroyed using the {@link destroy()} method when + * the application is about to be destroyed. It is recommended that the + * instance is destroyed and recreated if braille translation is not going to + * be need for a long period of time. + * + * Threading:
+ * The object must be destroyed on the same thread it was created. + * Other methods may be called from any thread. + */ +public class TranslatorManager { + private static final String LOG_TAG = + TranslatorManager.class.getSimpleName(); + private static final String ACTION_TRANSLATOR_SERVICE = + "com.googlecode.eyesfree.braille.service.ACTION_TRANSLATOR_SERVICE"; + private static final Intent mServiceIntent = + new Intent(ACTION_TRANSLATOR_SERVICE); + /** + * Delay before the first rebind attempt on bind error or service + * disconnect. + */ + private static final int REBIND_DELAY_MILLIS = 500; + private static final int MAX_REBIND_ATTEMPTS = 5; + public static final int ERROR = -1; + public static final int SUCCESS = 0; + + /** + * A callback interface to get notified when the translation + * manager is ready to be used, or an error occurred during + * initialization. + */ + public interface OnInitListener { + /** + * Called exactly once when it has been determined that the + * translation service is either ready to be used ({@code SUCCESS}) + * or the service is not available {@code ERROR}. + */ + public void onInit(int status); + } + + private final Context mContext; + private final TranslatorManagerHandler mHandler = + new TranslatorManagerHandler(); + private final ServiceCallback mServiceCallback = new ServiceCallback(); + + private OnInitListener mOnInitListener; + private Connection mConnection; + private int mNumFailedBinds = 0; + + /** + * Constructs an instance. {@code context} is used to bind to the + * translator service. The other methods of this class should not be + * called (they will fail) until {@code onInitListener.onInit()} + * is called. + */ + public TranslatorManager(Context context, OnInitListener onInitListener) { + mContext = context; + mOnInitListener = onInitListener; + doBindService(); + } + + /** + * Destroys this instance, deallocating any global resources it is using. + * Any {@link BrailleTranslator} objects that were created using this + * object are invalid after this call. + */ + public void destroy() { + doUnbindService(); + mHandler.destroy(); + } + + /** + * Returns a new {@link BrailleTranslator} for the translation + * table specified by {@code tableName}. + */ + // TODO: Document how to discover valid table names. + public BrailleTranslator getTranslator(String tableName) { + ITranslatorService localService = getTranslatorService(); + if (localService != null) { + try { + if (localService.checkTable(tableName)) { + return new BrailleTranslatorImpl(tableName); + } + } catch (RemoteException ex) { + Log.e(LOG_TAG, "Error in getTranslator", ex); + } + } + return null; + } + + private void doBindService() { + Connection localConnection = new Connection(); + if (!mContext.bindService(mServiceIntent, localConnection, + Context.BIND_AUTO_CREATE)) { + Log.e(LOG_TAG, "Failed to bind to service"); + mHandler.scheduleRebind(); + return; + } + mConnection = localConnection; + Log.i(LOG_TAG, "Bound to translator service"); + } + + private void doUnbindService() { + if (mConnection != null) { + mContext.unbindService(mConnection); + mConnection = null; + } + } + + private ITranslatorService getTranslatorService() { + Connection localConnection = mConnection; + if (localConnection != null) { + return localConnection.mService; + } + return null; + } + + private class Connection implements ServiceConnection { + // Read in application threads, written in main thread. + private volatile ITranslatorService mService; + + @Override + public void onServiceConnected(ComponentName className, + IBinder binder) { + Log.i(LOG_TAG, "Connected to translation service"); + ITranslatorService localService = + ITranslatorService.Stub.asInterface(binder); + try { + localService.setCallback(mServiceCallback); + mService = localService; + synchronized (mHandler) { + mNumFailedBinds = 0; + } + } catch (RemoteException ex) { + // Service went away, rely on disconnect handler to + // schedule a rebind. + Log.e(LOG_TAG, "Error when setting callback", ex); + } + } + + @Override + public void onServiceDisconnected(ComponentName className) { + Log.e(LOG_TAG, "Disconnected from translator service"); + mService = null; + // Retry by rebinding, and finally call the onInit if aplicable. + mHandler.scheduleRebind(); + } + } + + private class BrailleTranslatorImpl implements BrailleTranslator { + private final String mTable; + + public BrailleTranslatorImpl(String table) { + mTable = table; + } + + @Override + public byte[] translate(String text) { + ITranslatorService localService = getTranslatorService(); + if (localService != null) { + try { + return localService.translate(text, mTable); + } catch (RemoteException ex) { + Log.e(LOG_TAG, "Error in translate", ex); + } + } + return null; + } + + @Override + public String backTranslate(byte[] cells) { + ITranslatorService localService = getTranslatorService(); + if (localService != null) { + try { + return localService.backTranslate(cells, mTable); + } catch (RemoteException ex) { + Log.e(LOG_TAG, "Error in backTranslate", ex); + } + } + return null; + } + } + + private class ServiceCallback extends ITranslatorServiceCallback.Stub { + @Override + public void onInit(int status) { + mHandler.onInit(status); + } + } + + private class TranslatorManagerHandler extends Handler { + private static final int MSG_ON_INIT = 1; + private static final int MSG_REBIND_SERVICE = 2; + + public void onInit(int status) { + obtainMessage(MSG_ON_INIT, status, 0).sendToTarget(); + } + + public void destroy() { + mOnInitListener = null; + // Cacnel outstanding messages, most importantly + // scheduled rebinds. + removeCallbacksAndMessages(null); + } + + public void scheduleRebind() { + synchronized (this) { + if (mNumFailedBinds < MAX_REBIND_ATTEMPTS) { + int delay = REBIND_DELAY_MILLIS << mNumFailedBinds; + sendEmptyMessageDelayed(MSG_REBIND_SERVICE, delay); + ++mNumFailedBinds; + } else { + onInit(ERROR); + } + } + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_ON_INIT: + handleOnInit(msg.arg1); + break; + case MSG_REBIND_SERVICE: + handleRebindService(); + break; + } + } + + private void handleOnInit(int status) { + if (mOnInitListener != null) { + mOnInitListener.onInit(status); + mOnInitListener = null; + } + } + + private void handleRebindService() { + if (mConnection != null) { + doUnbindService(); + } + doBindService(); + } + } +} diff --git a/src/org/chromium/android_webview/AndroidProtocolHandler.java b/src/org/chromium/android_webview/AndroidProtocolHandler.java new file mode 100644 index 0000000..4c7ca07 --- /dev/null +++ b/src/org/chromium/android_webview/AndroidProtocolHandler.java @@ -0,0 +1,235 @@ +// 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.res.AssetManager; +import android.net.Uri; +import android.util.Log; +import android.util.TypedValue; + +import java.io.InputStream; +import java.io.IOException; +import java.net.URLConnection; +import java.util.List; + +import org.chromium.base.CalledByNativeUnchecked; +import org.chromium.base.JNINamespace; + +/** + * Implements the Java side of Android URL protocol jobs. + * See android_protocol_handler.cc. + */ +@JNINamespace("android_webview") +public class AndroidProtocolHandler { + private static final String TAG = "AndroidProtocolHandler"; + + // Supported URL schemes. This needs to be kept in sync with + // clank/native/framework/chrome/url_request_android_job.cc. + private static final String FILE_SCHEME = "file"; + private static final String CONTENT_SCHEME = "content"; + + /** + * Open an InputStream for an Android resource. + * @param context The context manager. + * @param url The url to load. + * @return An InputStream to the Android resource. + */ + // TODO(bulach): this should have either a throw clause, or + // handle the exception in the java side rather than the native side. + @CalledByNativeUnchecked + public static InputStream open(Context context, String url) { + Uri uri = verifyUrl(url); + if (uri == null) { + return null; + } + String path = uri.getPath(); + if (uri.getScheme().equals(FILE_SCHEME)) { + if (path.startsWith(nativeGetAndroidAssetPath())) { + return openAsset(context, uri); + } else if (path.startsWith(nativeGetAndroidResourcePath())) { + return openResource(context, uri); + } + } else if (uri.getScheme().equals(CONTENT_SCHEME)) { + return openContent(context, uri); + } + return null; + } + + private static int getFieldId(Context context, String assetType, String assetName) + throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { + Class d = context.getClassLoader() + .loadClass(context.getPackageName() + ".R$" + assetType); + java.lang.reflect.Field field = d.getField(assetName); + int id = field.getInt(null); + return id; + } + + private static int getValueType(Context context, int field_id) { + TypedValue value = new TypedValue(); + context.getResources().getValue(field_id, value, true); + return value.type; + } + + private static InputStream openResource(Context context, Uri uri) { + assert(uri.getScheme().equals(FILE_SCHEME)); + assert(uri.getPath() != null); + assert(uri.getPath().startsWith(nativeGetAndroidResourcePath())); + // The path must be of the form "/android_res/asset_type/asset_name.ext". + List pathSegments = uri.getPathSegments(); + if (pathSegments.size() != 3) { + Log.e(TAG, "Incorrect resource path: " + uri); + return null; + } + String assetPath = pathSegments.get(0); + String assetType = pathSegments.get(1); + String assetName = pathSegments.get(2); + if (!("/" + assetPath + "/").equals(nativeGetAndroidResourcePath())) { + Log.e(TAG, "Resource path does not start with " + nativeGetAndroidResourcePath() + + ": " + uri); + return null; + } + // Drop the file extension. + assetName = assetName.split("\\.")[0]; + try { + // Use the application context for resolving the resource package name so that we do + // not use the browser's own resources. Note that if 'context' here belongs to the + // test suite, it does not have a separate application context. In that case we use + // the original context object directly. + if (context.getApplicationContext() != null) { + context = context.getApplicationContext(); + } + int field_id = getFieldId(context, assetType, assetName); + int value_type = getValueType(context, field_id); + if (value_type == TypedValue.TYPE_STRING) { + return context.getResources().openRawResource(field_id); + } else { + Log.e(TAG, "Asset not of type string: " + uri); + return null; + } + } catch (ClassNotFoundException e) { + Log.e(TAG, "Unable to open resource URL: " + uri, e); + return null; + } catch (NoSuchFieldException e) { + Log.e(TAG, "Unable to open resource URL: " + uri, e); + return null; + } catch (IllegalAccessException e) { + Log.e(TAG, "Unable to open resource URL: " + uri, e); + return null; + } + } + + private static InputStream openAsset(Context context, Uri uri) { + assert(uri.getScheme().equals(FILE_SCHEME)); + assert(uri.getPath() != null); + assert(uri.getPath().startsWith(nativeGetAndroidAssetPath())); + String path = uri.getPath().replaceFirst(nativeGetAndroidAssetPath(), ""); + try { + AssetManager assets = context.getAssets(); + return assets.open(path, AssetManager.ACCESS_STREAMING); + } catch (IOException e) { + Log.e(TAG, "Unable to open asset URL: " + uri); + return null; + } + } + + private static InputStream openContent(Context context, Uri uri) { + assert(uri.getScheme().equals(CONTENT_SCHEME)); + try { + // We strip the query parameters before opening the stream to + // ensure that the URL we try to load exactly matches the URL + // we have permission to read. + Uri baseUri = stripQueryParameters(uri); + return context.getContentResolver().openInputStream(baseUri); + } catch (Exception e) { + Log.e(TAG, "Unable to open content URL: " + uri); + return null; + } + } + + /** + * Determine the mime type for an Android resource. + * @param context The context manager. + * @param stream The opened input stream which to examine. + * @param url The url from which the stream was opened. + * @return The mime type or null if the type is unknown. + */ + // TODO(bulach): this should have either a throw clause, or + // handle the exception in the java side rather than the native side. + @CalledByNativeUnchecked + public static String getMimeType(Context context, InputStream stream, String url) { + Uri uri = verifyUrl(url); + if (uri == null) { + return null; + } + String path = uri.getPath(); + // The content URL type can be queried directly. + if (uri.getScheme().equals(CONTENT_SCHEME)) { + return context.getContentResolver().getType(uri); + // Asset files may have a known extension. + } else if (uri.getScheme().equals(FILE_SCHEME) && + path.startsWith(nativeGetAndroidAssetPath())) { + String mimeType = URLConnection.guessContentTypeFromName(path); + if (mimeType != null) { + return mimeType; + } + } + // Fall back to sniffing the type from the stream. + try { + return URLConnection.guessContentTypeFromStream(stream); + } catch (IOException e) { + return null; + } + } + + /** + * Make sure the given string URL is correctly formed and parse it into a Uri. + * @return a Uri instance, or null if the URL was invalid. + */ + private static Uri verifyUrl(String url) { + if (url == null) { + return null; + } + Uri uri = Uri.parse(url); + if (uri == null) { + Log.e(TAG, "Malformed URL: " + url); + return null; + } + String path = uri.getPath(); + if (path == null || path.length() == 0) { + Log.e(TAG, "URL does not have a path: " + url); + return null; + } + return uri; + } + + /** + * Remove query parameters from a Uri. + * @param uri The input uri. + * @return The given uri without query parameters. + */ + private static Uri stripQueryParameters(Uri uri) { + assert(uri.getAuthority() != null); + assert(uri.getPath() != null); + Uri.Builder builder = new Uri.Builder(); + builder.scheme(uri.getScheme()); + builder.encodedAuthority(uri.getAuthority()); + builder.encodedPath(uri.getPath()); + return builder.build(); + } + + /** + * Set the context to be used for resolving resource queries. + * @param context Context to be used, or null for the default application + * context. + */ + public static void setResourceContextForTesting(Context context) { + nativeSetResourceContextForTesting(context); + } + + private static native void nativeSetResourceContextForTesting(Context context); + private static native String nativeGetAndroidAssetPath(); + private static native String nativeGetAndroidResourcePath(); +} diff --git a/src/org/chromium/android_webview/AwBrowserContext.java b/src/org/chromium/android_webview/AwBrowserContext.java new file mode 100644 index 0000000..2536d7b --- /dev/null +++ b/src/org/chromium/android_webview/AwBrowserContext.java @@ -0,0 +1,66 @@ +// 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.android_webview; + +import android.content.SharedPreferences; + +import org.chromium.content.browser.ContentViewStatics; + +/** + * Java side of the Browser Context: contains all the java side objects needed to host one + * browing session (i.e. profile). + * Note that due to running in single process mode, and limitations on renderer process only + * being able to use a single browser context, currently there can only be one AwBrowserContext + * instance, so at this point the class mostly exists for conceptual clarity. + * + * Obtain the default (singleton) instance with AwBrowserProcess.getDefaultBrowserContext(). + */ +public class AwBrowserContext { + + private SharedPreferences mSharedPreferences; + + private AwGeolocationPermissions mGeolocationPermissions; + private AwCookieManager mCookieManager; + private AwFormDatabase mFormDatabase; + + public AwBrowserContext(SharedPreferences sharedPreferences) { + mSharedPreferences = sharedPreferences; + } + + public AwGeolocationPermissions getGeolocationPermissions() { + if (mGeolocationPermissions == null) { + mGeolocationPermissions = new AwGeolocationPermissions(mSharedPreferences); + } + return mGeolocationPermissions; + } + + public AwCookieManager getCookieManager() { + if (mCookieManager == null) { + mCookieManager = new AwCookieManager(); + } + return mCookieManager; + } + + public AwFormDatabase getFormDatabase() { + if (mFormDatabase == null) { + mFormDatabase = new AwFormDatabase(); + } + return mFormDatabase; + } + + /** + * @see android.webkit.WebView#pauseTimers() + */ + public void pauseTimers() { + ContentViewStatics.setWebKitSharedTimersSuspended(true); + } + + /** + * @see android.webkit.WebView#resumeTimers() + */ + public void resumeTimers() { + ContentViewStatics.setWebKitSharedTimersSuspended(false); + } +} \ No newline at end of file diff --git a/src/org/chromium/android_webview/AwBrowserProcess.java b/src/org/chromium/android_webview/AwBrowserProcess.java new file mode 100644 index 0000000..7c3ab76 --- /dev/null +++ b/src/org/chromium/android_webview/AwBrowserProcess.java @@ -0,0 +1,59 @@ +// 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.SharedPreferences; + +import org.chromium.base.PathUtils; +import org.chromium.base.ThreadUtils; +import org.chromium.content.app.LibraryLoader; +import org.chromium.content.browser.AndroidBrowserProcess; +import org.chromium.content.common.ProcessInitException; + +/** + * Wrapper for the steps needed to initialize the java and native sides of webview chromium. + */ +public abstract class AwBrowserProcess { + private static final String PRIVATE_DATA_DIRECTORY_SUFFIX = "webview"; + + /** + * Loads the native library, and performs basic static construction of objects needed + * to run webview in this process. Does not create threads; safe to call from zygote. + * Note: it is up to the caller to ensure this is only called once. + */ + public static void loadLibrary() { + PathUtils.setPrivateDataDirectorySuffix(PRIVATE_DATA_DIRECTORY_SUFFIX); + try { + LibraryLoader.loadNow(); + } catch (ProcessInitException e) { + throw new RuntimeException("Cannot load WebView", e); + } + } + + /** + * Starts the chromium browser process running within this process. Creates threads + * and performs other per-app resource allocations; must not be called from zygote. + * Note: it is up to the caller to ensure this is only called once. + * @param context The Android application context + */ + public static void start(final Context context) { + // We must post to the UI thread to cover the case that the user + // has invoked Chromium startup by using the (thread-safe) + // CookieManager rather than creating a WebView. + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + try { + LibraryLoader.ensureInitialized(); + AndroidBrowserProcess.init(context, + AndroidBrowserProcess.MAX_RENDERERS_SINGLE_PROCESS); + } catch (ProcessInitException e) { + throw new RuntimeException("Cannot initialize WebView", e); + } + } + }); + } +} diff --git a/src/org/chromium/android_webview/AwContentVideoViewDelegate.java b/src/org/chromium/android_webview/AwContentVideoViewDelegate.java new file mode 100644 index 0000000..6a82e62 --- /dev/null +++ b/src/org/chromium/android_webview/AwContentVideoViewDelegate.java @@ -0,0 +1,52 @@ +// 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 new file mode 100644 index 0000000..499a42c --- /dev/null +++ b/src/org/chromium/android_webview/AwContents.java @@ -0,0 +1,1494 @@ +// 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.android_webview; + +import android.annotation.SuppressLint; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Picture; +import android.graphics.Rect; +import android.net.http.SslCertificate; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Message; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.webkit.GeolocationPermissions; +import android.webkit.ValueCallback; +import com.google.common.annotations.VisibleForTesting; +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.base.ThreadUtils; +import org.chromium.content.browser.ContentSettings; +import org.chromium.content.browser.ContentVideoView; +import org.chromium.content.browser.ContentViewClient; +import org.chromium.content.browser.ContentViewCore; +import org.chromium.content.browser.ContentViewStatics; +import org.chromium.content.browser.LoadUrlParams; +import org.chromium.content.browser.NavigationHistory; +import org.chromium.content.browser.PageTransitionTypes; +import org.chromium.content.common.CleanupReference; +import org.chromium.components.navigation_interception.InterceptNavigationDelegate; +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; + +/** + * Exposes the native AwContents class, and together these classes wrap the ContentViewCore + * and Browser components that are required to implement Android WebView API. This is the + * primary entry point for the WebViewProvider implementation; it holds a 1:1 object + * relationship with application WebView instances. + * (We define this class independent of the hidden WebViewProvider interfaces, to allow + * continuous build & test in the open source SDK-based tree). + */ +@JNINamespace("android_webview") +public class AwContents { + private static final String TAG = AwContents.class.getSimpleName(); + + private static final String WEB_ARCHIVE_EXTENSION = ".mht"; + + /** + * WebKit hit test related data strcutre. These are used to implement + * getHitTestResult, requestFocusNodeHref, requestImageRef methods in WebView. + * All values should be updated together. The native counterpart is + * AwHitTestData. + */ + public static class HitTestData { + // Used in getHitTestResult. + public int hitTestResultType; + public String hitTestResultExtraData; + + // Used in requestFocusNodeHref (all three) and requestImageRef (only imgSrc). + public String href; + public String anchorText; + public String imgSrc; + } + + /** + * Interface that consumers of {@link AwContents} must implement to allow the proper + * dispatching of view methods through the containing view. + */ + public interface InternalAccessDelegate extends ContentViewCore.InternalAccessDelegate { + /** + * @see View#setMeasuredDimension(int, int) + */ + void setMeasuredDimension(int measuredWidth, int measuredHeight); + + /** + * Requests a callback on the native DrawGL method (see getAwDrawGLFunction) + * if called from within onDraw, |canvas| will be non-null and hardware accelerated. + * otherwise, |canvas| will be null, and the container view itself will be hardware + * accelerated. + * + * @return false indicates the GL draw request was not accepted, and the caller + * should fallback to the SW path. + */ + boolean requestDrawGL(Canvas canvas); + } + + private int mNativeAwContents; + private AwBrowserContext mBrowserContext; + private 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 AwLayoutSizer mLayoutSizer; + private AwZoomControls mZoomControls; + // This can be accessed on any thread after construction. See AwContentsIoThreadClient. + private final AwSettings mSettings; + private boolean mIsPaused; + private Bitmap mFavicon; + private boolean mHasRequestedVisitedHistoryFromClient; + // TODO(boliu): This should be in a global context, not per webview. + private final double mDIPScale; + + // Must call nativeUpdateLastHitTestData first to update this before use. + private final HitTestData mPossiblyStaleHitTestData; + + private DefaultVideoPosterRequestHandler mDefaultVideoPosterRequestHandler; + + private boolean mNewPictureInvalidationOnly; + + private Rect mGlobalVisibleBounds; + private int mLastGlobalVisibleWidth; + private int mLastGlobalVisibleHeight; + + private boolean mContainerViewFocused; + private boolean mWindowFocused; + + private static final class DestroyRunnable implements Runnable { + private int mNativeAwContents; + private DestroyRunnable(int nativeAwContents) { + mNativeAwContents = nativeAwContents; + } + @Override + public void run() { + nativeDestroy(mNativeAwContents); + } + } + + private CleanupReference mCleanupReference; + + //-------------------------------------------------------------------------------------------- + private class IoThreadClientImpl implements AwContentsIoThreadClient { + // All methods are called on the IO thread. + + @Override + public int getCacheMode() { + return mSettings.getCacheMode(); + } + + @Override + public InterceptedRequestData shouldInterceptRequest(final String url, + boolean isMainFrame) { + InterceptedRequestData interceptedRequestData; + // Return the response directly if the url is default video poster url. + interceptedRequestData = mDefaultVideoPosterRequestHandler.shouldInterceptRequest(url); + if (interceptedRequestData != null) return interceptedRequestData; + + interceptedRequestData = mContentsClient.shouldInterceptRequest(url); + + if (interceptedRequestData == null) { + mContentsClient.getCallbackHelper().postOnLoadResource(url); + } + + if (isMainFrame && interceptedRequestData != null && + interceptedRequestData.getData() == null) { + // In this case the intercepted URLRequest job will simulate an empty response + // which doesn't trigger the onReceivedError callback. For WebViewClassic + // compatibility we synthesize that callback. http://crbug.com/180950 + mContentsClient.getCallbackHelper().postOnReceivedError( + ErrorCodeConversionHelper.ERROR_UNKNOWN, + null /* filled in by the glue layer */, url); + } + return interceptedRequestData; + } + + @Override + public boolean shouldBlockContentUrls() { + return !mSettings.getAllowContentAccess(); + } + + @Override + public boolean shouldBlockFileUrls() { + return !mSettings.getAllowFileAccess(); + } + + @Override + public boolean shouldBlockNetworkLoads() { + return mSettings.getBlockNetworkLoads(); + } + + @Override + public void onDownloadStart(String url, + String userAgent, + String contentDisposition, + String mimeType, + long contentLength) { + mContentsClient.getCallbackHelper().postOnDownloadStart(url, userAgent, + contentDisposition, mimeType, contentLength); + } + + @Override + public void newLoginRequest(String realm, String account, String args) { + mContentsClient.getCallbackHelper().postOnReceivedLoginRequest(realm, account, args); + } + } + + //-------------------------------------------------------------------------------------------- + private class InterceptNavigationDelegateImpl implements InterceptNavigationDelegate { + private String mLastLoadUrlAddress; + + public void onUrlLoadRequested(String url) { + mLastLoadUrlAddress = url; + } + + @Override + public boolean shouldIgnoreNavigation(NavigationParams navigationParams) { + final String url = navigationParams.url; + boolean ignoreNavigation = false; + if (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; + + // If the embedder requested the load of a certain URL via the loadUrl API, then we + // do not offer it to AwContentsClient.shouldOverrideUrlLoading. + // The embedder is also not allowed to intercept POST requests because of + // crbug.com/155250. + } else if (!navigationParams.isPost) { + ignoreNavigation = mContentsClient.shouldOverrideUrlLoading(url); + } + + // The existing contract is that shouldOverrideUrlLoading callbacks are delivered before + // onPageStarted callbacks; third party apps depend on this behavior. + // Using a ResouceThrottle to implement the navigation interception feature results in + // the WebContentsObserver.didStartLoading callback happening before the + // ResourceThrottle has a chance to run. + // To preserve the ordering the onPageStarted callback is synthesized from the + // shouldOverrideUrlLoading, and only if the navigation was not ignored (this + // balances out with the onPageFinished callback, which is suppressed in the + // AwContentsClient if the navigation was ignored). + if (!ignoreNavigation) { + // The shouldOverrideUrlLoading call might have resulted in posting messages to the + // UI thread. Using sendMessage here (instead of calling onPageStarted directly) + // will allow those to run in order. + mContentsClient.getCallbackHelper().postOnPageStarted(url); + } + + return ignoreNavigation; + } + } + + //-------------------------------------------------------------------------------------------- + private class AwLayoutSizerDelegate implements AwLayoutSizer.Delegate { + @Override + public void requestLayout() { + mContainerView.requestLayout(); + } + + @Override + public void setMeasuredDimension(int measuredWidth, int measuredHeight) { + mInternalAccessAdapter.setMeasuredDimension(measuredWidth, measuredHeight); + } + } + + //-------------------------------------------------------------------------------------------- + private class AwPinchGestureStateListener implements ContentViewCore.PinchGestureStateListener { + @Override + public void onPinchGestureStart() { + // While it's possible to re-layout the view during a pinch gesture, the effect is very + // janky (especially that the page scale update notification comes from the renderer + // main thread, not from the impl thread, so it's usually out of sync with what's on + // screen). It's also quite expensive to do a re-layout, so we simply postpone + // re-layout for the duration of the gesture. This is compatible with what + // WebViewClassic does. + mLayoutSizer.freezeLayoutRequests(); + } + + 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(); + } + }; + + private ScrollChangeListener mScrollChangeListener; + + /** + * @param browserContext the browsing context to associate this view contents with. + * @param containerView the view-hierarchy item this object will be bound to. + * @param internalAccessAdapter to access private methods on containerView. + * @param contentsClient will receive API callbacks from this WebView Contents + * @param isAccessFromFileURLsGrantedByDefault passed to AwSettings. + * + * This constructor uses the default view sizing policy. + */ + public AwContents(AwBrowserContext browserContext, ViewGroup containerView, + InternalAccessDelegate internalAccessAdapter, AwContentsClient contentsClient, + boolean isAccessFromFileURLsGrantedByDefault) { + this(browserContext, containerView, internalAccessAdapter, contentsClient, + isAccessFromFileURLsGrantedByDefault, new AwLayoutSizer()); + } + + private static ContentViewCore createAndInitializeContentViewCore(ViewGroup containerView, + InternalAccessDelegate internalDispatcher, int nativeWebContents, + ContentViewCore.PinchGestureStateListener pinchGestureStateListener, + ContentViewClient contentViewClient, + ContentViewCore.ZoomControlsDelegate zoomControlsDelegate) { + ContentViewCore contentViewCore = new ContentViewCore(containerView.getContext()); + // Note INPUT_EVENTS_DELIVERED_IMMEDIATELY is passed to avoid triggering vsync in the + // compositor, not because input events are delivered immediately. + contentViewCore.initialize(containerView, internalDispatcher, nativeWebContents, null, + ContentViewCore.INPUT_EVENTS_DELIVERED_IMMEDIATELY); + contentViewCore.setPinchGestureStateListener(pinchGestureStateListener); + contentViewCore.setContentViewClient(contentViewClient); + contentViewCore.setZoomControlsDelegate(zoomControlsDelegate); + return contentViewCore; + } + + /** + * @param layoutSizer the AwLayoutSizer instance implementing the sizing policy for the view. + * + * This version of the constructor is used in test code to inject test versions of the above + * documented classes + */ + public AwContents(AwBrowserContext browserContext, ViewGroup containerView, + InternalAccessDelegate internalAccessAdapter, AwContentsClient contentsClient, + boolean isAccessFromFileURLsGrantedByDefault, AwLayoutSizer layoutSizer) { + 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); + mLayoutSizer = layoutSizer; + mLayoutSizer.setDelegate(new AwLayoutSizerDelegate()); + mLayoutSizer.setDIPScale(mDIPScale); + mWebContentsDelegate = new AwWebContentsDelegateAdapter(contentsClient, + mLayoutSizer.getPreferredSizeChangedListener()); + mNativeAwContents = nativeInit(mWebContentsDelegate, mContentsClientBridge); + mContentsClient = contentsClient; + mCleanupReference = new CleanupReference(this, new DestroyRunnable(mNativeAwContents)); + + int nativeWebContents = nativeGetWebContents(mNativeAwContents); + mZoomControls = new AwZoomControls(this); + mContentViewCore = createAndInitializeContentViewCore( + containerView, internalAccessAdapter, nativeWebContents, + new AwPinchGestureStateListener(), mContentsClient.getContentViewClient(), + mZoomControls); + mContentsClient.installWebContentsObserver(mContentViewCore); + + 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()); + + 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); + } + } + + @VisibleForTesting + public ContentViewCore getContentViewCore() { + return mContentViewCore; + } + + // Can be called from any thread. + public AwSettings getSettings() { + return mSettings; + } + + public void setIoThreadClient(AwContentsIoThreadClient ioThreadClient) { + mIoThreadClient = ioThreadClient; + nativeSetIoThreadClient(mNativeAwContents, mIoThreadClient); + } + + private void setInterceptNavigationDelegate(InterceptNavigationDelegateImpl delegate) { + mInterceptNavigationDelegate = delegate; + nativeSetInterceptNavigationDelegate(mNativeAwContents, delegate); + } + + 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(); + mNativeAwContents = 0; + } + + public static void setAwDrawSWFunctionTable(int functionTablePointer) { + nativeSetAwDrawSWFunctionTable(functionTablePointer); + } + + public static void setAwDrawGLFunctionTable(int functionTablePointer) { + nativeSetAwDrawGLFunctionTable(functionTablePointer); + } + + public static int getAwDrawGLFunction() { + return nativeGetAwDrawGLFunction(); + } + + 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); + } + + public void onDraw(Canvas canvas) { + if (mNativeAwContents == 0) return; + if (canvas.isHardwareAccelerated() && + nativePrepareDrawGL(mNativeAwContents, + mContainerView.getScrollX(), mContainerView.getScrollY()) && + mInternalAccessAdapter.requestDrawGL(canvas)) { + 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)); + } + } + + @SuppressLint("WrongCall") + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mLayoutSizer.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + public int getContentHeightCss() { + return (int) Math.ceil(mContentViewCore.getContentHeightCss()); + } + + public int getContentWidthCss() { + return (int) Math.ceil(mContentViewCore.getContentWidthCss()); + } + + public Picture capturePicture() { + return nativeCapturePicture(mNativeAwContents); + } + + /** + * 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; + 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); + } + + public void findNext(boolean forward) { + if (mNativeAwContents == 0) return; + nativeFindNext(mNativeAwContents, forward); + } + + public void clearMatches() { + if (mNativeAwContents == 0) return; + nativeClearMatches(mNativeAwContents); + } + + /** + * @return load progress of the WebContents. + */ + public int getMostRecentProgress() { + // WebContentsDelegateAndroid conveniently caches the most recent notified value for us. + return mWebContentsDelegate.getMostRecentProgress(); + } + + public Bitmap getFavicon() { + return mFavicon; + } + + private void requestVisitedHistoryFromClient() { + ValueCallback callback = new ValueCallback() { + @Override + public void onReceiveValue(final String[] value) { + ThreadUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + if (mNativeAwContents == 0) return; + nativeAddVisitedLinks(mNativeAwContents, value); + } + }); + } + }; + mContentsClient.getVisitedHistory(callback); + } + + /** + * Load url without fixing up the url string. Consumers of ContentView are responsible for + * ensuring the URL passed in is properly formatted (i.e. the scheme has been added if left + * off during user input). + * + * @param pararms Parameters for this load. + */ + public void loadUrl(LoadUrlParams params) { + if (params.getLoadUrlType() == LoadUrlParams.LOAD_TYPE_DATA && + !params.isBaseUrlDataScheme()) { + // This allows data URLs with a non-data base URL access to file:///android_asset/ and + // file:///android_res/ URLs. If AwSettings.getAllowFileAccess permits, it will also + // allow access to file:// URLs (subject to OS level permission checks). + params.setCanLoadLocalResources(true); + } + + // If we are reloading the same url, then set transition type as reload. + if (params.getUrl() != null && + params.getUrl().equals(mContentViewCore.getUrl()) && + params.getTransitionType() == PageTransitionTypes.PAGE_TRANSITION_LINK) { + params.setTransitionType(PageTransitionTypes.PAGE_TRANSITION_RELOAD); + } + + // For WebView, always use the user agent override, which is set + // every time the user agent in AwSettings is modified. + params.setOverrideUserAgent(LoadUrlParams.UA_OVERRIDE_TRUE); + + mContentViewCore.loadUrl(params); + + suppressInterceptionForThisNavigation(); + + // The behavior of WebViewClassic uses the populateVisitedLinks callback in WebKit. + // Chromium does not use this use code path and the best emulation of this behavior to call + // request visited links once on the first URL load of the WebView. + if (!mHasRequestedVisitedHistoryFromClient) { + mHasRequestedVisitedHistoryFromClient = true; + requestVisitedHistoryFromClient(); + } + } + + private void suppressInterceptionForThisNavigation() { + if (mInterceptNavigationDelegate != null) { + // getUrl returns a sanitized address in the same format that will be used for + // callbacks, so it's safe to use string comparison as an equality check later on. + mInterceptNavigationDelegate.onUrlLoadRequested(mContentViewCore.getUrl()); + } + } + + /** + * Get the URL of the current page. + * + * @return The URL of the current page or null if it's empty. + */ + public String getUrl() { + String url = mContentViewCore.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()) { + nativeFocusFirstNode(mNativeAwContents); + } + } + + public boolean isMultiTouchZoomSupported() { + return mSettings.supportsMultiTouchZoom(); + } + + public View getZoomControlsForTest() { + return mZoomControls.getZoomControlsViewForTest(); + } + + //-------------------------------------------------------------------------------------------- + // WebView[Provider] method implementations (where not provided by ContentViewCore) + //-------------------------------------------------------------------------------------------- + + /** + * @see ContentViewCore#getContentSettings() + */ + public ContentSettings getContentSettings() { + return mContentViewCore.getContentSettings(); + } + + /** + * @see ContentViewCore#computeHorizontalScrollRange() + */ + public int computeHorizontalScrollRange() { + return mContentViewCore.computeHorizontalScrollRange(); + } + + /** + * @see ContentViewCore#computeHorizontalScrollOffset() + */ + public int computeHorizontalScrollOffset() { + return mContentViewCore.computeHorizontalScrollOffset(); + } + + /** + * @see ContentViewCore#computeVerticalScrollRange() + */ + public int computeVerticalScrollRange() { + return mContentViewCore.computeVerticalScrollRange(); + } + + /** + * @see ContentViewCore#computeVerticalScrollOffset() + */ + public int computeVerticalScrollOffset() { + return mContentViewCore.computeVerticalScrollOffset(); + } + + /** + * @see ContentViewCore#computeVerticalScrollExtent() + */ + public int computeVerticalScrollExtent() { + return mContentViewCore.computeVerticalScrollExtent(); + } + + /** + * @see android.webkit.WebView#stopLoading() + */ + public void stopLoading() { + mContentViewCore.stopLoading(); + } + + /** + * @see android.webkit.WebView#reload() + */ + public void reload() { + mContentViewCore.reload(); + } + + /** + * @see android.webkit.WebView#canGoBack() + */ + public boolean canGoBack() { + return mContentViewCore.canGoBack(); + } + + /** + * @see android.webkit.WebView#goBack() + */ + public void goBack() { + mContentViewCore.goBack(); + + suppressInterceptionForThisNavigation(); + } + + /** + * @see android.webkit.WebView#canGoForward() + */ + public boolean canGoForward() { + return mContentViewCore.canGoForward(); + } + + /** + * @see android.webkit.WebView#goForward() + */ + public void goForward() { + mContentViewCore.goForward(); + + suppressInterceptionForThisNavigation(); + } + + /** + * @see android.webkit.WebView#canGoBackOrForward(int) + */ + public boolean canGoBackOrForward(int steps) { + return mContentViewCore.canGoToOffset(steps); + } + + /** + * @see android.webkit.WebView#goBackOrForward(int) + */ + public void goBackOrForward(int steps) { + mContentViewCore.goToOffset(steps); + + suppressInterceptionForThisNavigation(); + } + + /** + * @see android.webkit.WebView#pauseTimers() + */ + // TODO(kristianm): Remove + public void pauseTimers() { + ContentViewStatics.setWebKitSharedTimersSuspended(true); + } + + /** + * @see android.webkit.WebView#resumeTimers() + */ + // TODO(kristianm): Remove + public void resumeTimers() { + ContentViewStatics.setWebKitSharedTimersSuspended(false); + } + + /** + * @see android.webkit.WebView#onPause() + */ + public void onPause() { + mIsPaused = true; + mContentViewCore.onHide(); + } + + /** + * @see android.webkit.WebView#onResume() + */ + public void onResume() { + mContentViewCore.onShow(); + mIsPaused = false; + } + + /** + * @see android.webkit.WebView#isPaused() + */ + public boolean isPaused() { + return mIsPaused; + } + + /** + * @see android.webkit.WebView#onCreateInputConnection(EditorInfo) + */ + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return mContentViewCore.onCreateInputConnection(outAttrs); + } + + /** + * @see android.webkit.WebView#onKeyUp(int, KeyEvent) + */ + public boolean onKeyUp(int keyCode, KeyEvent event) { + return mContentViewCore.onKeyUp(keyCode, event); + } + + /** + * @see android.webkit.WebView#dispatchKeyEvent(KeyEvent) + */ + public boolean dispatchKeyEvent(KeyEvent event) { + return mContentViewCore.dispatchKeyEvent(event); + } + + /** + * Clears the resource cache. Note that the cache is per-application, so this will clear the + * cache for all WebViews used. + * + * @param includeDiskFiles if false, only the RAM cache is cleared + */ + public void clearCache(boolean includeDiskFiles) { + if (mNativeAwContents == 0) return; + nativeClearCache(mNativeAwContents, includeDiskFiles); + } + + public void documentHasImages(Message message) { + if (mNativeAwContents == 0) return; + nativeDocumentHasImages(mNativeAwContents, message); + } + + public void saveWebArchive( + final String basename, boolean autoname, final ValueCallback callback) { + if (!autoname) { + saveWebArchiveInternal(basename, callback); + return; + } + // If auto-generating the file name, handle the name generation on a background thread + // as it will require I/O access for checking whether previous files existed. + new AsyncTask() { + @Override + protected String doInBackground(Void... params) { + return generateArchiveAutoNamePath(getOriginalUrl(), basename); + } + + @Override + protected void onPostExecute(String result) { + saveWebArchiveInternal(result, callback); + } + }.execute(); + } + + public String getOriginalUrl() { + NavigationHistory history = mContentViewCore.getNavigationHistory(); + int currentIndex = history.getCurrentEntryIndex(); + if (currentIndex >= 0 && currentIndex < history.getEntryCount()) { + return history.getEntryAtIndex(currentIndex).getOriginalUrl(); + } + return null; + } + + /** + * @see ContentViewCore#getNavigationHistory() + */ + public NavigationHistory getNavigationHistory() { + return mContentViewCore.getNavigationHistory(); + } + + /** + * @see android.webkit.WebView#getTitle() + */ + public String getTitle() { + return mContentViewCore.getTitle(); + } + + /** + * @see android.webkit.WebView#clearHistory() + */ + public void clearHistory() { + mContentViewCore.clearHistory(); + } + + public String[] getHttpAuthUsernamePassword(String host, String realm) { + return HttpAuthDatabase.getInstance(mContentViewCore.getContext()) + .getHttpAuthUsernamePassword(host, realm); + } + + public void setHttpAuthUsernamePassword(String host, String realm, String username, + String password) { + HttpAuthDatabase.getInstance(mContentViewCore.getContext()) + .setHttpAuthUsernamePassword(host, realm, username, password); + } + + /** + * @see android.webkit.WebView#getCertificate() + */ + public SslCertificate getCertificate() { + if (mNativeAwContents == 0) return null; + return SslUtil.getCertificateFromDerBytes(nativeGetCertificate(mNativeAwContents)); + } + + /** + * @see android.webkit.WebView#clearSslPreferences() + */ + public void clearSslPreferences() { + mContentViewCore.clearSslPreferences(); + } + + /** + * Method to return all hit test values relevant to public WebView API. + * Note that this expose more data than needed for WebView.getHitTestResult. + * Unsafely returning reference to mutable internal object to avoid excessive + * garbage allocation on repeated calls. + */ + public HitTestData getLastHitTestResult() { + if (mNativeAwContents == 0) return null; + nativeUpdateLastHitTestData(mNativeAwContents); + return mPossiblyStaleHitTestData; + } + + /** + * @see android.webkit.WebView#requestFocusNodeHref() + */ + public void requestFocusNodeHref(Message msg) { + if (msg == null || mNativeAwContents == 0) return; + + nativeUpdateLastHitTestData(mNativeAwContents); + Bundle data = msg.getData(); + data.putString("url", mPossiblyStaleHitTestData.href); + data.putString("title", mPossiblyStaleHitTestData.anchorText); + data.putString("src", mPossiblyStaleHitTestData.imgSrc); + msg.setData(data); + msg.sendToTarget(); + } + + /** + * @see android.webkit.WebView#requestImageRef() + */ + public void requestImageRef(Message msg) { + if (msg == null || mNativeAwContents == 0) return; + + nativeUpdateLastHitTestData(mNativeAwContents); + Bundle data = msg.getData(); + data.putString("url", mPossiblyStaleHitTestData.imgSrc); + msg.setData(data); + msg.sendToTarget(); + } + + /** + * @see android.webkit.WebView#getScale() + * + * Please note that the scale returned is the page scale multiplied by + * the screen density factor. See CTS WebViewTest.testSetInitialScale. + */ + public float getScale() { + return (float)(mContentViewCore.getScale() * mDIPScale); + } + + /** + * @see android.webkit.WebView#flingScroll(int, int) + */ + public void flingScroll(int vx, int vy) { + mContentViewCore.flingScroll(vx, vy); + } + + /** + * @see android.webkit.WebView#pageUp(boolean) + */ + public boolean pageUp(boolean top) { + return mContentViewCore.pageUp(top); + } + + /** + * @see android.webkit.WebView#pageDown(boolean) + */ + public boolean pageDown(boolean bottom) { + return mContentViewCore.pageDown(bottom); + } + + /** + * @see android.webkit.WebView#canZoomIn() + */ + public boolean canZoomIn() { + return mContentViewCore.canZoomIn(); + } + + /** + * @see android.webkit.WebView#canZoomOut() + */ + public boolean canZoomOut() { + return mContentViewCore.canZoomOut(); + } + + /** + * @see android.webkit.WebView#zoomIn() + */ + public boolean zoomIn() { + return mContentViewCore.zoomIn(); + } + + /** + * @see android.webkit.WebView#zoomOut() + */ + public boolean zoomOut() { + return mContentViewCore.zoomOut(); + } + + /** + * @see android.webkit.WebView#invokeZoomPicker() + */ + public void invokeZoomPicker() { + mContentViewCore.invokeZoomPicker(); + } + + //-------------------------------------------------------------------------------------------- + // View and ViewGroup method implementations + //-------------------------------------------------------------------------------------------- + + /** + * @see android.webkit.View#onTouchEvent() + */ + public boolean onTouchEvent(MotionEvent event) { + if (mNativeAwContents == 0) return false; + boolean rv = mContentViewCore.onTouchEvent(event); + + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + int actionIndex = event.getActionIndex(); + + // Note this will trigger IPC back to browser even if nothing is hit. + nativeRequestNewHitTestDataAt(mNativeAwContents, + (int)Math.round(event.getX(actionIndex) / mDIPScale), + (int)Math.round(event.getY(actionIndex) / mDIPScale)); + } + + return rv; + } + + /** + * @see android.view.View#onHoverEvent() + */ + public boolean onHoverEvent(MotionEvent event) { + return mContentViewCore.onHoverEvent(event); + } + + /** + * @see android.view.View#onGenericMotionEvent() + */ + public boolean onGenericMotionEvent(MotionEvent event) { + return mContentViewCore.onGenericMotionEvent(event); + } + + /** + * @see android.view.View#onConfigurationChanged() + */ + public void onConfigurationChanged(Configuration newConfig) { + mContentViewCore.onConfigurationChanged(newConfig); + } + + /** + * @see android.view.View#onAttachedToWindow() + */ + public void onAttachedToWindow() { + if (mScrollChangeListener == null) { + mScrollChangeListener = new ScrollChangeListener(); + } + mContainerView.getViewTreeObserver().addOnScrollChangedListener(mScrollChangeListener); + + 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() { + if (mNativeAwContents != 0) { + nativeOnDetachedFromWindow(mNativeAwContents); + } + + if (mScrollChangeListener != null) { + mContainerView.getViewTreeObserver().removeOnScrollChangedListener( + mScrollChangeListener); + mScrollChangeListener = null; + } + + mContentViewCore.onDetachedFromWindow(); + } + + /** + * @see android.view.View#onWindowFocusChanged() + */ + public void onWindowFocusChanged(boolean hasWindowFocus) { + mWindowFocused = hasWindowFocus; + mContentViewCore.onFocusChanged(mContainerViewFocused && mWindowFocused); + } + + /** + * @see android.view.View#onFocusChanged() + */ + public void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + mContainerViewFocused = focused; + mContentViewCore.onFocusChanged(mContainerViewFocused && mWindowFocused); + } + + /** + * @see android.view.View#onSizeChanged() + */ + public void onSizeChanged(int w, int h, int ow, int oh) { + if (mNativeAwContents == 0) return; + updatePhysicalBackingSizeIfNeeded(); + mContentViewCore.onSizeChanged(w, h, ow, oh); + nativeOnSizeChanged(mNativeAwContents, w, h, ow, oh); + } + + /** + * @see android.view.View#onVisibilityChanged() + */ + public void onVisibilityChanged(View changedView, int visibility) { + updateVisiblityState(); + } + + /** + * @see android.view.View#onWindowVisibilityChanged() + */ + public void onWindowVisibilityChanged(int visibility) { + updateVisiblityState(); + } + + private void updateVisiblityState() { + if (mNativeAwContents == 0 || mIsPaused) return; + boolean windowVisible = mContainerView.getWindowVisibility() == View.VISIBLE; + boolean viewVisible = mContainerView.getVisibility() == View.VISIBLE; + nativeSetWindowViewVisibility(mNativeAwContents, windowVisible, viewVisible); + + if (viewVisible) { + mContentViewCore.onShow(); + } else { + mContentViewCore.onHide(); + } + } + + + /** + * Key for opaque state in bundle. Note this is only public for tests. + */ + public static final String SAVE_RESTORE_STATE_KEY = "WEBVIEW_CHROMIUM_STATE"; + + /** + * Save the state of this AwContents into provided Bundle. + * @return False if saving state failed. + */ + public boolean saveState(Bundle outState) { + if (outState == null) return false; + + byte[] state = nativeGetOpaqueState(mNativeAwContents); + if (state == null) return false; + + outState.putByteArray(SAVE_RESTORE_STATE_KEY, state); + return true; + } + + /** + * Restore the state of this AwContents into provided Bundle. + * @param inState Must be a bundle returned by saveState. + * @return False if restoring state failed. + */ + public boolean restoreState(Bundle inState) { + if (inState == null) return false; + + byte[] state = inState.getByteArray(SAVE_RESTORE_STATE_KEY); + if (state == null) return false; + + boolean result = nativeRestoreFromOpaqueState(mNativeAwContents, state); + + // The onUpdateTitle callback normally happens when a page is loaded, + // but is optimized out in the restoreState case because the title is + // already restored. See WebContentsImpl::UpdateTitleForEntry. So we + // call the callback explicitly here. + if (result) mContentsClient.onReceivedTitle(mContentViewCore.getTitle()); + + return result; + } + + /** + * @see ContentViewCore#addPossiblyUnsafeJavascriptInterface(Object, String, Class) + */ + public void addPossiblyUnsafeJavascriptInterface(Object object, String name, + Class requiredAnnotation) { + mContentViewCore.addPossiblyUnsafeJavascriptInterface(object, name, requiredAnnotation); + } + + /** + * @see android.webkit.WebView#removeJavascriptInterface(String) + */ + public void removeJavascriptInterface(String interfaceName) { + mContentViewCore.removeJavascriptInterface(interfaceName); + } + + /** + * @see android.webkit.WebView#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo) + */ + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + mContentViewCore.onInitializeAccessibilityNodeInfo(info); + } + + /** + * @see android.webkit.WebView#onInitializeAccessibilityEvent(AccessibilityEvent) + */ + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + mContentViewCore.onInitializeAccessibilityEvent(event); + } + + public boolean supportsAccessibilityAction(int action) { + return mContentViewCore.supportsAccessibilityAction(action); + } + + /** + * @see android.webkit.WebView#performAccessibilityAction(int, Bundle) + */ + public boolean performAccessibilityAction(int action, Bundle arguments) { + return mContentViewCore.performAccessibilityAction(action, arguments); + } + + //-------------------------------------------------------------------------------------------- + // Methods called from native via JNI + //-------------------------------------------------------------------------------------------- + + @CalledByNative + private static void onDocumentHasImagesResponse(boolean result, Message message) { + message.arg1 = result ? 1 : 0; + message.sendToTarget(); + } + + @CalledByNative + private void onReceivedTouchIconUrl(String url, boolean precomposed) { + mContentsClient.onReceivedTouchIconUrl(url, precomposed); + } + + @CalledByNative + private void onReceivedIcon(Bitmap bitmap) { + mContentsClient.onReceivedIcon(bitmap); + mFavicon = bitmap; + } + + /** Callback for generateMHTML. */ + @CalledByNative + private static void generateMHTMLCallback( + String path, long size, ValueCallback callback) { + if (callback == null) return; + callback.onReceiveValue(size < 0 ? null : path); + } + + @CalledByNative + private void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) { + mContentsClient.onReceivedHttpAuthRequest(handler, host, realm); + } + + private class AwGeolocationCallback implements GeolocationPermissions.Callback { + + @Override + public void invoke(final String origin, final boolean allow, final boolean retain) { + ThreadUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + if (retain) { + if (allow) { + mBrowserContext.getGeolocationPermissions().allow(origin); + } else { + mBrowserContext.getGeolocationPermissions().deny(origin); + } + } + nativeInvokeGeolocationCallback(mNativeAwContents, allow, origin); + } + }); + } + } + + @CalledByNative + private void onGeolocationPermissionsShowPrompt(String origin) { + AwGeolocationPermissions permissions = mBrowserContext.getGeolocationPermissions(); + // Reject if geoloaction is disabled, or the origin has a retained deny + if (!mSettings.getGeolocationEnabled()) { + nativeInvokeGeolocationCallback(mNativeAwContents, false, origin); + return; + } + // Allow if the origin has a retained allow + if (permissions.hasOrigin(origin)) { + nativeInvokeGeolocationCallback(mNativeAwContents, permissions.isOriginAllowed(origin), + origin); + return; + } + mContentsClient.onGeolocationPermissionsShowPrompt( + origin, new AwGeolocationCallback()); + } + + @CalledByNative + private void onGeolocationPermissionsHidePrompt() { + mContentsClient.onGeolocationPermissionsHidePrompt(); + } + + @CalledByNative + public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, + boolean isDoneCounting) { + mContentsClient.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting); + } + + @CalledByNative + public void onNewPicture() { + mContentsClient.onNewPicture(mNewPictureInvalidationOnly ? null : capturePicture()); + } + + // Called as a result of nativeUpdateLastHitTestData. + @CalledByNative + private void updateHitTestData( + int type, String extra, String href, String anchorText, String imgSrc) { + mPossiblyStaleHitTestData.hitTestResultType = type; + mPossiblyStaleHitTestData.hitTestResultExtraData = extra; + mPossiblyStaleHitTestData.href = href; + mPossiblyStaleHitTestData.anchorText = anchorText; + mPossiblyStaleHitTestData.imgSrc = imgSrc; + } + + @CalledByNative + private void requestProcessMode() { + mInternalAccessAdapter.requestDrawGL(null); + } + + @CalledByNative + private void invalidate() { + mContainerView.invalidate(); + } + + @CalledByNative + private boolean performLongClick() { + return mContainerView.performLongClick(); + } + + @CalledByNative + private int[] getLocationOnScreen() { + int[] result = new int[2]; + mContainerView.getLocationOnScreen(result); + return result; + } + + @CalledByNative + private void onPageScaleFactorChanged(float pageScaleFactor) { + // This change notification comes from the renderer thread, not from the cc/ impl thread. + mLayoutSizer.onPageScaleChanged(pageScaleFactor); + } + + // ------------------------------------------------------------------------------------------- + // Helper methods + // ------------------------------------------------------------------------------------------- + + private void saveWebArchiveInternal(String path, final ValueCallback callback) { + if (path == null || mNativeAwContents == 0) { + ThreadUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + callback.onReceiveValue(null); + } + }); + } else { + nativeGenerateMHTML(mNativeAwContents, path, callback); + } + } + + /** + * Try to generate a pathname for saving an MHTML archive. This roughly follows WebView's + * autoname logic. + */ + private static String generateArchiveAutoNamePath(String originalUrl, String baseName) { + String name = null; + if (originalUrl != null && !originalUrl.isEmpty()) { + try { + String path = new URL(originalUrl).getPath(); + int lastSlash = path.lastIndexOf('/'); + if (lastSlash > 0) { + name = path.substring(lastSlash + 1); + } else { + name = path; + } + } catch (MalformedURLException e) { + // If it fails parsing the URL, we'll just rely on the default name below. + } + } + + if (TextUtils.isEmpty(name)) name = "index"; + + String testName = baseName + name + WEB_ARCHIVE_EXTENSION; + if (!new File(testName).exists()) return testName; + + for (int i = 1; i < 100; i++) { + testName = baseName + name + "-" + i + WEB_ARCHIVE_EXTENSION; + if (!new File(testName).exists()) return testName; + } + + Log.e(TAG, "Unable to auto generate archive name for path: " + baseName); + return null; + } + + //-------------------------------------------------------------------------------------------- + // Native methods + //-------------------------------------------------------------------------------------------- + + private native int nativeInit(AwWebContentsDelegate webViewWebContentsDelegate, + AwContentsClientBridge contentsClientBridge); + 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 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 void nativeFindAllAsync(int nativeAwContents, String searchString); + private native void nativeFindNext(int nativeAwContents, boolean forward); + private native void nativeClearMatches(int nativeAwContents); + private native void nativeClearCache(int nativeAwContents, boolean includeDiskFiles); + private native byte[] nativeGetCertificate(int nativeAwContents); + + // Coordinates in desity independent pixels. + private native void nativeRequestNewHitTestDataAt(int nativeAwContents, int x, int y); + 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 nativeOnAttachedToWindow(int nativeAwContents, int w, int h); + private native void nativeOnDetachedFromWindow(int nativeAwContents); + + // Returns null if save state fails. + private native byte[] nativeGetOpaqueState(int nativeAwContents); + + // 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 void nativeFocusFirstNode(int nativeAwContents); + + 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 void nativeEnableOnNewPicture(int nativeAwContents, boolean enabled); + + private native void nativeInvokeGeolocationCallback( + int nativeAwContents, boolean value, String requestingFrame); +} diff --git a/src/org/chromium/android_webview/AwContentsClient.java b/src/org/chromium/android_webview/AwContentsClient.java new file mode 100644 index 0000000..70cd68c --- /dev/null +++ b/src/org/chromium/android_webview/AwContentsClient.java @@ -0,0 +1,240 @@ +// 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.pm.ActivityInfo; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Picture; +import android.graphics.Rect; +import android.graphics.RectF; +import android.net.http.SslError; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.webkit.ConsoleMessage; +import android.webkit.GeolocationPermissions; +import android.webkit.SslErrorHandler; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; + +import org.chromium.content.browser.ContentViewClient; +import org.chromium.content.browser.ContentViewCore; +import org.chromium.content.browser.WebContentsObserverAndroid; +import org.chromium.net.NetError; + +/** + * Base-class that an AwContents embedder derives from to receive callbacks. + * This extends ContentViewClient, as in many cases we want to pass-thru ContentViewCore + * callbacks right to our embedder, and this setup facilities that. + * For any other callbacks we need to make transformations of (e.g. adapt parameters + * or perform filtering) we can provide final overrides for methods here, and then introduce + * new abstract methods that the our own client must implement. + * i.e.: all methods in this class should either be final, or abstract. + */ +public abstract class AwContentsClient { + + private static final String TAG = "AwContentsClient"; + private final AwContentsClientCallbackHelper mCallbackHelper = + new AwContentsClientCallbackHelper(this); + + private AwWebContentsObserver mWebContentsObserver; + + private AwContentViewClient mContentViewClient = new AwContentViewClient(); + + private double mDIPScale; + + class AwWebContentsObserver extends WebContentsObserverAndroid { + public AwWebContentsObserver(ContentViewCore contentViewCore) { + super(contentViewCore); + } + + @Override + public void didStopLoading(String url) { + AwContentsClient.this.onPageFinished(url); + } + + @Override + public void didFailLoad(boolean isProvisionalLoad, + boolean isMainFrame, int errorCode, String description, String failingUrl) { + if (errorCode == NetError.ERR_ABORTED) { + // This error code is generated for the following reasons: + // - WebView.stopLoading is called, + // - the navigation is intercepted by the embedder via shouldOverrideNavigation. + // + // The Android WebView does not notify the embedder of these situations using this + // error code with the WebViewClient.onReceivedError callback. + return; + } + if (!isMainFrame) { + // The Android WebView does not notify the embedder of sub-frame failures. + return; + } + AwContentsClient.this.onReceivedError( + ErrorCodeConversionHelper.convertErrorCode(errorCode), description, failingUrl); + } + + @Override + public void didNavigateAnyFrame(String url, String baseUrl, boolean isReload) { + AwContentsClient.this.doUpdateVisitedHistory(url, isReload); + } + + } + + private class AwContentViewClient extends ContentViewClient { + + @Override + public void onScaleChanged(float oldScale, float newScale) { + AwContentsClient.this.onScaleChangedScaled((float)(oldScale * mDIPScale), + (float)(newScale * mDIPScale)); + } + + @Override + public void onStartContentIntent(Context context, String contentUrl) { + // Callback when detecting a click on a content link. + AwContentsClient.this.shouldOverrideUrlLoading(contentUrl); + } + + @Override + public void onTabCrash() { + // This is not possible so long as the webview is run single process! + throw new RuntimeException("Renderer crash reported."); + } + + @Override + public void onUpdateTitle(String title) { + AwContentsClient.this.onReceivedTitle(title); + } + + @Override + public boolean shouldOverrideKeyEvent(KeyEvent event) { + return AwContentsClient.this.shouldOverrideKeyEvent(event); + } + + } + + final void installWebContentsObserver(ContentViewCore contentViewCore) { + if (mWebContentsObserver != null) { + mWebContentsObserver.detachFromWebContents(); + } + mWebContentsObserver = new AwWebContentsObserver(contentViewCore); + } + + final void setDIPScale(double dipScale) { + mDIPScale = dipScale; + } + + final AwContentsClientCallbackHelper getCallbackHelper() { + return mCallbackHelper; + } + + final ContentViewClient getContentViewClient() { + return mContentViewClient; + } + + //-------------------------------------------------------------------------------------------- + // WebView specific methods that map directly to WebViewClient / WebChromeClient + //-------------------------------------------------------------------------------------------- + + public abstract void getVisitedHistory(ValueCallback callback); + + public abstract void doUpdateVisitedHistory(String url, boolean isReload); + + public abstract void onProgressChanged(int progress); + + public abstract InterceptedRequestData shouldInterceptRequest(String url); + + public abstract boolean shouldOverrideKeyEvent(KeyEvent event); + + public abstract boolean shouldOverrideUrlLoading(String url); + + public abstract void onLoadResource(String url); + + public abstract void onUnhandledKeyEvent(KeyEvent event); + + public abstract boolean onConsoleMessage(ConsoleMessage consoleMessage); + + public abstract void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, + String host, String realm); + + public abstract void onReceivedSslError(ValueCallback callback, SslError error); + + public abstract void onReceivedLoginRequest(String realm, String account, String args); + + public abstract void onFormResubmission(Message dontResend, Message resend); + + public abstract void onDownloadStart(String url, String userAgent, String contentDisposition, + String mimeType, long contentLength); + + public abstract void onGeolocationPermissionsShowPrompt(String origin, + GeolocationPermissions.Callback callback); + + public abstract void onGeolocationPermissionsHidePrompt(); + + public abstract void onScaleChangedScaled(float oldScale, float newScale); + + protected abstract void handleJsAlert(String url, String message, JsResultReceiver receiver); + + protected abstract void handleJsBeforeUnload(String url, String message, + JsResultReceiver receiver); + + protected abstract void handleJsConfirm(String url, String message, JsResultReceiver receiver); + + protected abstract void handleJsPrompt(String url, String message, String defaultValue, + JsPromptResultReceiver receiver); + + protected abstract boolean onCreateWindow(boolean isDialog, boolean isUserGesture); + + protected abstract void onCloseWindow(); + + public abstract void onReceivedTouchIconUrl(String url, boolean precomposed); + + public abstract void onReceivedIcon(Bitmap bitmap); + + public abstract void onReceivedTitle(String title); + + protected abstract void onRequestFocus(); + + protected abstract View getVideoLoadingProgressView(); + + public abstract void onPageStarted(String url); + + public abstract void onPageFinished(String url); + + public abstract void onReceivedError(int errorCode, String description, String failingUrl); + + // TODO (michaelbai): Remove this method once the same method remove from + // WebViewContentsClientAdapter. + public void onShowCustomView(View view, + int requestedOrientation, WebChromeClient.CustomViewCallback callback) { + } + + // TODO (michaelbai): This method should be abstract, having empty body here + // makes the merge to the Android easy. + public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) { + onShowCustomView(view, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, callback); + } + + public abstract void onHideCustomView(); + + public abstract Bitmap getDefaultVideoPoster(); + + //-------------------------------------------------------------------------------------------- + // Other WebView-specific methods + //-------------------------------------------------------------------------------------------- + // + public abstract void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, + boolean isDoneCounting); + + /** + * Called whenever there is a new content picture available. + * @param picture New picture. + */ + public abstract void onNewPicture(Picture picture); + +} diff --git a/src/org/chromium/android_webview/AwContentsClientBridge.java b/src/org/chromium/android_webview/AwContentsClientBridge.java new file mode 100644 index 0000000..7172cfb --- /dev/null +++ b/src/org/chromium/android_webview/AwContentsClientBridge.java @@ -0,0 +1,113 @@ +// 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.android_webview; + +import android.net.http.SslCertificate; +import android.net.http.SslError; +import android.webkit.ValueCallback; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.base.ThreadUtils; + +/** + * This class handles the JNI communication logic for the the AwContentsClient class. + * Both the Java and the native peers of AwContentsClientBridge are owned by the + * corresponding AwContents instances. This class and its native peer are connected + * via weak references. The native AwContentsClientBridge sets up and clear these weak + * references. + */ +@JNINamespace("android_webview") +public class AwContentsClientBridge { + + private AwContentsClient mClient; + // The native peer of this object. + private int mNativeContentsClientBridge; + + public AwContentsClientBridge(AwContentsClient client) { + assert client != null; + mClient = client; + } + + // Used by the native peer to set/reset a weak ref to the native peer. + @CalledByNative + private void setNativeContentsClientBridge(int nativeContentsClientBridge) { + mNativeContentsClientBridge = nativeContentsClientBridge; + } + + // If returns false, the request is immediately canceled, and any call to proceedSslError + // has no effect. If returns true, the request should be canceled or proceeded using + // proceedSslError(). + // Unlike the webview classic, we do not keep keep a database of certificates that + // are allowed by the user, because this functionality is already handled via + // ssl_policy in native layers. + @CalledByNative + private boolean allowCertificateError(int certError, byte[] derBytes, final String url, + final int id) { + final SslCertificate cert = SslUtil.getCertificateFromDerBytes(derBytes); + if (cert == null) { + // if the certificate or the client is null, cancel the request + return false; + } + final SslError sslError = SslUtil.sslErrorFromNetErrorCode(certError, cert, url); + ValueCallback callback = new ValueCallback() { + @Override + public void onReceiveValue(Boolean value) { + proceedSslError(value.booleanValue(), id); + } + }; + mClient.onReceivedSslError(callback, sslError); + return true; + } + + private void proceedSslError(boolean proceed, int id) { + if (mNativeContentsClientBridge == 0) return; + nativeProceedSslError(mNativeContentsClientBridge, proceed, id); + } + + @CalledByNative + private void handleJsAlert(String url, String message, int id) { + JsResultHandler handler = new JsResultHandler(this, id); + mClient.handleJsAlert(url, message, handler); + } + + @CalledByNative + private void handleJsConfirm(String url, String message, int id) { + JsResultHandler handler = new JsResultHandler(this, id); + mClient.handleJsConfirm(url, message, handler); + } + + @CalledByNative + private void handleJsPrompt(String url, String message, String defaultValue, int id) { + JsResultHandler handler = new JsResultHandler(this, id); + mClient.handleJsPrompt(url, message, defaultValue, handler); + } + + @CalledByNative + private void handleJsBeforeUnload(String url, String message, int id) { + JsResultHandler handler = new JsResultHandler(this, id); + mClient.handleJsBeforeUnload(url, message, handler); + } + + void confirmJsResult(int id, String prompt) { + if (mNativeContentsClientBridge == 0) return; + nativeConfirmJsResult(mNativeContentsClientBridge, id, prompt); + } + + void cancelJsResult(int id) { + if (mNativeContentsClientBridge == 0) return; + nativeCancelJsResult(mNativeContentsClientBridge, id); + } + + //-------------------------------------------------------------------------------------------- + // Native methods + //-------------------------------------------------------------------------------------------- + private native void nativeProceedSslError(int nativeAwContentsClientBridge, boolean proceed, + int id); + + private native void nativeConfirmJsResult(int nativeAwContentsClientBridge, int id, + String prompt); + private native void nativeCancelJsResult(int nativeAwContentsClientBridge, int id); +} \ No newline at end of file diff --git a/src/org/chromium/android_webview/AwContentsClientCallbackHelper.java b/src/org/chromium/android_webview/AwContentsClientCallbackHelper.java new file mode 100644 index 0000000..8dbd571 --- /dev/null +++ b/src/org/chromium/android_webview/AwContentsClientCallbackHelper.java @@ -0,0 +1,142 @@ +// 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.android_webview; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import org.chromium.content.browser.ContentViewCore; + +/** + * This class is responsible for calling certain client callbacks on the UI thread. + * + * Most callbacks do no go through here, but get forwarded to AwContentsClient directly. The + * messages processed here may originate from the IO or UI thread. + */ +class AwContentsClientCallbackHelper { + + // TODO(boliu): Consider removing DownloadInfo and LoginRequestInfo by using native + // MessageLoop to post directly to AwContents. + + private static class DownloadInfo { + final String mUrl; + final String mUserAgent; + final String mContentDisposition; + final String mMimeType; + final long mContentLength; + + DownloadInfo(String url, + String userAgent, + String contentDisposition, + String mimeType, + long contentLength) { + mUrl = url; + mUserAgent = userAgent; + mContentDisposition = contentDisposition; + mMimeType = mimeType; + mContentLength = contentLength; + } + } + + private static class LoginRequestInfo { + final String mRealm; + final String mAccount; + final String mArgs; + + LoginRequestInfo(String realm, String account, String args) { + mRealm = realm; + mAccount = account; + mArgs = args; + } + } + + private static class OnReceivedErrorInfo { + final int mErrorCode; + final String mDescription; + final String mFailingUrl; + + OnReceivedErrorInfo(int errorCode, String description, String failingUrl) { + mErrorCode = errorCode; + mDescription = description; + mFailingUrl = failingUrl; + } + } + + private final static int MSG_ON_LOAD_RESOURCE = 1; + private final static int MSG_ON_PAGE_STARTED = 2; + 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 AwContentsClient mContentsClient; + + private final Handler mHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case MSG_ON_LOAD_RESOURCE: { + final String url = (String) msg.obj; + mContentsClient.onLoadResource(url); + break; + } + case MSG_ON_PAGE_STARTED: { + final String url = (String) msg.obj; + mContentsClient.onPageStarted(url); + break; + } + case MSG_ON_DOWNLOAD_START: { + DownloadInfo info = (DownloadInfo) msg.obj; + mContentsClient.onDownloadStart(info.mUrl, info.mUserAgent, + info.mContentDisposition, info.mMimeType, info.mContentLength); + break; + } + case MSG_ON_RECEIVED_LOGIN_REQUEST: { + LoginRequestInfo info = (LoginRequestInfo) msg.obj; + mContentsClient.onReceivedLoginRequest(info.mRealm, info.mAccount, info.mArgs); + break; + } + case MSG_ON_RECEIVED_ERROR: { + OnReceivedErrorInfo info = (OnReceivedErrorInfo) msg.obj; + mContentsClient.onReceivedError(info.mErrorCode, info.mDescription, + info.mFailingUrl); + break; + } + default: + throw new IllegalStateException( + "AwContentsClientCallbackHelper: unhandled message " + msg.what); + } + } + }; + + public AwContentsClientCallbackHelper(AwContentsClient contentsClient) { + mContentsClient = contentsClient; + } + + public void postOnLoadResource(String url) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_LOAD_RESOURCE, url)); + } + + public void postOnPageStarted(String url) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_PAGE_STARTED, url)); + } + + public void postOnDownloadStart(String url, String userAgent, String contentDisposition, + String mimeType, long contentLength) { + DownloadInfo info = new DownloadInfo(url, userAgent, contentDisposition, mimeType, + contentLength); + mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_DOWNLOAD_START, info)); + } + + public void postOnReceivedLoginRequest(String realm, String account, String args) { + LoginRequestInfo info = new LoginRequestInfo(realm, account, args); + mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_RECEIVED_LOGIN_REQUEST, info)); + } + + public void postOnReceivedError(int errorCode, String description, String failingUrl) { + OnReceivedErrorInfo info = new OnReceivedErrorInfo(errorCode, description, failingUrl); + mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_RECEIVED_ERROR, info)); + } +} diff --git a/src/org/chromium/android_webview/AwContentsIoThreadClient.java b/src/org/chromium/android_webview/AwContentsIoThreadClient.java new file mode 100644 index 0000000..3d3dd61 --- /dev/null +++ b/src/org/chromium/android_webview/AwContentsIoThreadClient.java @@ -0,0 +1,42 @@ +// 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 org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; + +/** + * Delegate for handling callbacks. All methods are called on the IO thread. + * + * You should create a separate instance for every WebContents that requires the + * provided functionality. + */ +@JNINamespace("android_webview") +public interface AwContentsIoThreadClient { + @CalledByNative + public int getCacheMode(); + + @CalledByNative + public InterceptedRequestData shouldInterceptRequest(String url, boolean isMainFrame); + + @CalledByNative + public boolean shouldBlockContentUrls(); + + @CalledByNative + public boolean shouldBlockFileUrls(); + + @CalledByNative + public boolean shouldBlockNetworkLoads(); + + @CalledByNative + public void onDownloadStart(String url, + String userAgent, + String contentDisposition, + String mimeType, + long contentLength); + + @CalledByNative + public void newLoginRequest(String realm, String account, String args); +} diff --git a/src/org/chromium/android_webview/AwCookieManager.java b/src/org/chromium/android_webview/AwCookieManager.java new file mode 100644 index 0000000..53db5d8 --- /dev/null +++ b/src/org/chromium/android_webview/AwCookieManager.java @@ -0,0 +1,128 @@ +// 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.net.ParseException; +import android.util.Log; + +import org.chromium.base.JNINamespace; +import org.chromium.base.ThreadUtils; + +import java.util.concurrent.Callable; + +/** + * AwCookieManager manages cookies according to RFC2109 spec. + * + * Methods in this class are thread safe. + */ +@JNINamespace("android_webview") +public final class AwCookieManager { + /** + * Control whether cookie is enabled or disabled + * @param accept TRUE if accept cookie + */ + public void setAcceptCookie(boolean accept) { + nativeSetAcceptCookie(accept); + } + + /** + * Return whether cookie is enabled + * @return TRUE if accept cookie + */ + public boolean acceptCookie() { + return nativeAcceptCookie(); + } + + /** + * Set cookie for a given url. The old cookie with same host/path/name will + * be removed. The new cookie will be added if it is not expired or it does + * not have expiration which implies it is session cookie. + * @param url The url which cookie is set for + * @param value The value for set-cookie: in http response header + */ + public void setCookie(final String url, final String value) { + nativeSetCookie(url, value); + } + + /** + * Get cookie(s) for a given url so that it can be set to "cookie:" in http + * request header. + * @param url The url needs cookie + * @return The cookies in the format of NAME=VALUE [; NAME=VALUE] + */ + public String getCookie(final String url) { + String cookie = nativeGetCookie(url.toString()); + // Return null if the string is empty to match legacy behavior + return cookie == null || cookie.trim().isEmpty() ? null : cookie; + } + + /** + * Remove all session cookies, which are cookies without expiration date + */ + public void removeSessionCookie() { + nativeRemoveSessionCookie(); + } + + /** + * Remove all cookies + */ + public void removeAllCookie() { + nativeRemoveAllCookie(); + } + + /** + * Return true if there are stored cookies. + */ + public boolean hasCookies() { + return nativeHasCookies(); + } + + /** + * Remove all expired cookies + */ + public void removeExpiredCookie() { + nativeRemoveExpiredCookie(); + } + + public void flushCookieStore() { + nativeFlushCookieStore(); + } + + /** + * Whether cookies are accepted for file scheme URLs. + */ + public boolean allowFileSchemeCookies() { + return nativeAllowFileSchemeCookies(); + } + + /** + * Sets whether cookies are accepted for file scheme URLs. + * + * Use of cookies with file scheme URLs is potentially insecure. Do not use this feature unless + * you can be sure that no unintentional sharing of cookie data can take place. + *

+ * Note that calls to this method will have no effect if made after a WebView or CookieManager + * instance has been created. + */ + public void setAcceptFileSchemeCookies(boolean accept) { + nativeSetAcceptFileSchemeCookies(accept); + } + + private native void nativeSetAcceptCookie(boolean accept); + private native boolean nativeAcceptCookie(); + + private native void nativeSetCookie(String url, String value); + private native String nativeGetCookie(String url); + + private native void nativeRemoveSessionCookie(); + private native void nativeRemoveAllCookie(); + private native void nativeRemoveExpiredCookie(); + private native void nativeFlushCookieStore(); + + private native boolean nativeHasCookies(); + + private native boolean nativeAllowFileSchemeCookies(); + private native void nativeSetAcceptFileSchemeCookies(boolean accept); +} diff --git a/src/org/chromium/android_webview/AwFormDatabase.java b/src/org/chromium/android_webview/AwFormDatabase.java new file mode 100644 index 0000000..9687a1c --- /dev/null +++ b/src/org/chromium/android_webview/AwFormDatabase.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.android_webview; + +import org.chromium.base.JNINamespace; + +/** + * Exposes a subset of Chromium form database to Webview database for managing autocomplete + * functionality. + */ +@JNINamespace("android_webview") +public class AwFormDatabase { + + public static boolean hasFormData() { + return nativeHasFormData(); + } + + public static void clearFormData() { + nativeClearFormData(); + } + + //-------------------------------------------------------------------------------------------- + // Native methods + //-------------------------------------------------------------------------------------------- + private static native boolean nativeHasFormData(); + + private static native void nativeClearFormData(); +} diff --git a/src/org/chromium/android_webview/AwGeolocationPermissions.java b/src/org/chromium/android_webview/AwGeolocationPermissions.java new file mode 100644 index 0000000..b091b3b --- /dev/null +++ b/src/org/chromium/android_webview/AwGeolocationPermissions.java @@ -0,0 +1,135 @@ +// 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.SharedPreferences; +import android.webkit.ValueCallback; + +import org.chromium.base.ThreadUtils; +import org.chromium.net.GURLUtils; + +import java.util.HashSet; +import java.util.Set; + +/** + * This class is used to manage permissions for the WebView's Geolocation JavaScript API. + * + * Callbacks are posted on the UI thread. + */ +public final class AwGeolocationPermissions { + + private static final String PREF_PREFIX = + AwGeolocationPermissions.class.getCanonicalName() + "%"; + private final SharedPreferences mSharedPreferences; + + public AwGeolocationPermissions(SharedPreferences sharedPreferences) { + mSharedPreferences = sharedPreferences; + } + + /** + * Set one origin to be allowed. + */ + public void allow(String origin) { + String key = getOriginKey(origin); + if (key != null) { + mSharedPreferences.edit().putBoolean(key, true).apply(); + } + } + + /** + * Set one origin to be denied. + */ + public void deny(String origin) { + String key = getOriginKey(origin); + if (key != null) { + mSharedPreferences.edit().putBoolean(key, false).apply(); + } + } + + /** + * Clear the stored permission for a particular origin. + */ + public void clear(String origin) { + String key = getOriginKey(origin); + if (key != null) { + mSharedPreferences.edit().remove(key).apply(); + } + } + + /** + * Clear stored permissions for all origins. + */ + public void clearAll() { + SharedPreferences.Editor editor = null; + for (String name : mSharedPreferences.getAll().keySet()) { + if (name.startsWith(PREF_PREFIX)) { + if (editor == null) { + editor = mSharedPreferences.edit(); + } + editor.remove(name); + } + } + if (editor != null) { + editor.apply(); + } + } + + /** + * Synchronous method to get if an origin is set to be allowed. + */ + public boolean isOriginAllowed(String origin) { + return mSharedPreferences.getBoolean(getOriginKey(origin), false); + } + + /** + * Returns true if the origin is either set to allowed or denied. + */ + public boolean hasOrigin(String origin) { + return mSharedPreferences.contains(getOriginKey(origin)); + } + + /** + * Asynchronous method to get if an origin set to be allowed. + */ + public void getAllowed(String origin, final ValueCallback callback) { + final boolean finalAllowed = isOriginAllowed(origin); + ThreadUtils.postOnUiThread(new Runnable() { + @Override + public void run() { + callback.onReceiveValue(finalAllowed); + } + }); + } + + /** + * Async method to get the domains currently allowed or denied. + */ + public void getOrigins(final ValueCallback> callback) { + final Set origins = new HashSet(); + for (String name : mSharedPreferences.getAll().keySet()) { + if (name.startsWith(PREF_PREFIX)) { + origins.add(name.substring(PREF_PREFIX.length())); + } + } + ThreadUtils.postOnUiThread(new Runnable() { + @Override + public void run() { + callback.onReceiveValue(origins); + } + }); + } + + /** + * Get the domain of an URL using the GURL library. + */ + private String getOriginKey(String url) { + String origin = GURLUtils.getOrigin(url); + if (origin.isEmpty()) { + return null; + } + + return PREF_PREFIX + origin; + } +} diff --git a/src/org/chromium/android_webview/AwHttpAuthHandler.java b/src/org/chromium/android_webview/AwHttpAuthHandler.java new file mode 100644 index 0000000..5e5a9f1 --- /dev/null +++ b/src/org/chromium/android_webview/AwHttpAuthHandler.java @@ -0,0 +1,52 @@ +// 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 org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; + +@JNINamespace("android_webview") +public class AwHttpAuthHandler { + + private int mNativeAwHttpAuthHandler; + private final boolean mFirstAttempt; + + public void proceed(String username, String password) { + if (mNativeAwHttpAuthHandler != 0) { + nativeProceed(mNativeAwHttpAuthHandler, username, password); + mNativeAwHttpAuthHandler = 0; + } + } + + public void cancel() { + if (mNativeAwHttpAuthHandler != 0) { + nativeCancel(mNativeAwHttpAuthHandler); + mNativeAwHttpAuthHandler = 0; + } + } + + public boolean isFirstAttempt() { + return mFirstAttempt; + } + + @CalledByNative + public static AwHttpAuthHandler create(int nativeAwAuthHandler, boolean firstAttempt) { + return new AwHttpAuthHandler(nativeAwAuthHandler, firstAttempt); + } + + private AwHttpAuthHandler(int nativeAwHttpAuthHandler, boolean firstAttempt) { + mNativeAwHttpAuthHandler = nativeAwHttpAuthHandler; + mFirstAttempt = firstAttempt; + } + + @CalledByNative + void handlerDestroyed() { + mNativeAwHttpAuthHandler = 0; + } + + private native void nativeProceed(int nativeAwHttpAuthHandler, + String username, String password); + private native void nativeCancel(int nativeAwHttpAuthHandler); +} diff --git a/src/org/chromium/android_webview/AwLayoutSizer.java b/src/org/chromium/android_webview/AwLayoutSizer.java new file mode 100644 index 0000000..7a48bfe --- /dev/null +++ b/src/org/chromium/android_webview/AwLayoutSizer.java @@ -0,0 +1,178 @@ +// 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.util.Pair; +import android.view.View.MeasureSpec; +import android.view.View; + +import org.chromium.content.browser.ContentViewCore; + +/** + * Helper methods used to manage the layout of the View that contains AwContents. + */ +public class AwLayoutSizer { + // These are used to prevent a re-layout if the content size changes within a dimension that is + // fixed by the view system. + private boolean mWidthMeasurementIsFixed; + private boolean mHeightMeasurementIsFixed; + + // Size of the rendered content, as reported by native. + private int mContentHeightCss; + private int mContentWidthCss; + + // Page scale factor. This is set to zero initially so that we don't attempt to do a layout if + // we get the content size change notification first and a page scale change second. + private double mPageScaleFactor = 0.0; + + // Whether to postpone layout requests. + private boolean mFreezeLayoutRequests; + // Did we try to request a layout since the last time mPostponeLayoutRequests was set to true. + private boolean mFrozenLayoutRequestPending; + + private double mDIPScale; + + // Callback object for interacting with the View. + private Delegate mDelegate; + + public interface Delegate { + void requestLayout(); + void setMeasuredDimension(int measuredWidth, int measuredHeight); + } + + /** + * Default constructor. Note: both setDelegate and setDIPScale must be called before the class + * is ready for use. + */ + public AwLayoutSizer() { + } + + public void setDelegate(Delegate delegate) { + mDelegate = delegate; + } + + public void setDIPScale(double dipScale) { + mDIPScale = dipScale; + } + + /** + * This is used to register the AwLayoutSizer to preferred content size change notifications in + * the AwWebContentsDelegate. + */ + public AwWebContentsDelegateAdapter.PreferredSizeChangedListener + getPreferredSizeChangedListener() { + return new AwWebContentsDelegateAdapter.PreferredSizeChangedListener() { + @Override + public void updatePreferredSize(int widthCss, int heightCss) { + onContentSizeChanged(widthCss, heightCss); + } + }; + } + + /** + * Postpone requesting layouts till unfreezeLayoutRequests is called. + */ + public void freezeLayoutRequests() { + mFreezeLayoutRequests = true; + mFrozenLayoutRequestPending = false; + } + + /** + * Stop postponing layout requests and request layout if such a request would have been made + * had the freezeLayoutRequests method not been called before. + */ + public void unfreezeLayoutRequests() { + mFreezeLayoutRequests = false; + if (mFrozenLayoutRequestPending) { + mFrozenLayoutRequestPending = false; + mDelegate.requestLayout(); + } + } + + /** + * Update the contents size. + * This should be called whenever the content size changes (due to DOM manipulation or page + * load, for example). + * The width and height should be in CSS pixels. + */ + public void onContentSizeChanged(int widthCss, int heightCss) { + doUpdate(widthCss, heightCss, mPageScaleFactor); + } + + /** + * Update the contents page scale. + * This should be called whenever the content page scale factor changes (due to pinch zoom, for + * example). + */ + public void onPageScaleChanged(double pageScaleFactor) { + doUpdate(mContentWidthCss, mContentHeightCss, pageScaleFactor); + } + + private void doUpdate(int widthCss, int heightCss, double pageScaleFactor) { + // We want to request layout only if the size or scale change, however if any of the + // measurements are 'fixed', then changing the underlying size won't have any effect, so we + // ignore changes to dimensions that are 'fixed'. + boolean anyMeasurementNotFixed = !mWidthMeasurementIsFixed || !mHeightMeasurementIsFixed; + boolean layoutNeeded = (mContentWidthCss != widthCss && !mWidthMeasurementIsFixed) || + (mContentHeightCss != heightCss && !mHeightMeasurementIsFixed) || + (mPageScaleFactor != pageScaleFactor && anyMeasurementNotFixed); + + mContentWidthCss = widthCss; + mContentHeightCss = heightCss; + mPageScaleFactor = pageScaleFactor; + + if (layoutNeeded) { + if (mFreezeLayoutRequests) { + mFrozenLayoutRequestPending = true; + } else { + mDelegate.requestLayout(); + } + } + } + + /** + * Calculate the size of the view. + * This is designed to be used to implement the android.view.View#onMeasure() method. + */ + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + + int measuredHeight = heightSize; + int measuredWidth = widthSize; + + int contentHeightPix = (int) (mContentHeightCss * mPageScaleFactor * mDIPScale); + int contentWidthPix = (int) (mContentWidthCss * mPageScaleFactor * mDIPScale); + + // Always use the given size unless unspecified. This matches WebViewClassic behavior. + mWidthMeasurementIsFixed = (widthMode != MeasureSpec.UNSPECIFIED); + // Freeze the height if an exact size is given by the parent or if the content size has + // exceeded the maximum size specified by the parent. + // TODO(mkosiba): Actually we'd like the reduction in content size to cause the WebView to + // shrink back again but only as a result of a page load. + mHeightMeasurementIsFixed = (heightMode == MeasureSpec.EXACTLY) || + (heightMode == MeasureSpec.AT_MOST && contentHeightPix > heightSize); + + if (!mHeightMeasurementIsFixed) { + measuredHeight = contentHeightPix; + } + + if (!mWidthMeasurementIsFixed) { + measuredWidth = contentWidthPix; + } + + if (measuredHeight < contentHeightPix) { + measuredHeight |= View.MEASURED_STATE_TOO_SMALL; + } + + if (measuredWidth < contentWidthPix) { + measuredWidth |= View.MEASURED_STATE_TOO_SMALL; + } + + mDelegate.setMeasuredDimension(measuredWidth, measuredHeight); + } +} diff --git a/src/org/chromium/android_webview/AwQuotaManagerBridge.java b/src/org/chromium/android_webview/AwQuotaManagerBridge.java new file mode 100644 index 0000000..6e94134 --- /dev/null +++ b/src/org/chromium/android_webview/AwQuotaManagerBridge.java @@ -0,0 +1,162 @@ +// 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.android_webview; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.base.ThreadUtils; + +import android.webkit.ValueCallback; + +import java.util.Map; +import java.util.HashMap; + +/** + * Bridge between android.webview.WebStorage and native QuotaManager. This object is owned by Java + * AwBrowserContext and the native side is owned by the native AwBrowserContext. + * + * TODO(boliu): Actually make this true after Java AwBrowserContext is added. + */ +@JNINamespace("android_webview") +public class AwQuotaManagerBridge { + // TODO(boliu): This should be obtained from Java AwBrowserContext that owns this. + private static native int nativeGetDefaultNativeAwQuotaManagerBridge(); + + // TODO(boliu): This should be owned by Java AwBrowserContext, not a singleton. + private static AwQuotaManagerBridge sInstance; + public static AwQuotaManagerBridge getInstance() { + ThreadUtils.assertOnUiThread(); + if (sInstance == null) { + sInstance = new AwQuotaManagerBridge(nativeGetDefaultNativeAwQuotaManagerBridge()); + } + return sInstance; + } + + /** + * This class represent the callback value of android.webview.WebStorage.getOrigins. The values + * are optimized for JNI convenience and need to be converted. + */ + public static class Origins { + // Origin, usage, and quota data in parallel arrays of same length. + public final String[] mOrigins; + public final long[] mUsages; + public final long[] mQuotas; + + Origins(String[] origins, long[] usages, long[] quotas) { + mOrigins = origins; + mUsages = usages; + mQuotas = quotas; + } + } + + // This is not owning. The native object is owned by the native AwBrowserContext. + private int mNativeAwQuotaManagerBridgeImpl; + + // The Java callbacks are saved here. An incrementing callback id is generated for each saved + // callback and is passed to the native side to identify callback. + private int mNextId; + private Map> mPendingGetOriginCallbacks; + private Map> mPendingGetQuotaForOriginCallbacks; + private Map> mPendingGetUsageForOriginCallbacks; + + private AwQuotaManagerBridge(int nativeAwQuotaManagerBridgeImpl) { + mNativeAwQuotaManagerBridgeImpl = nativeAwQuotaManagerBridgeImpl; + mPendingGetOriginCallbacks = + new HashMap>(); + mPendingGetQuotaForOriginCallbacks = new HashMap>(); + mPendingGetUsageForOriginCallbacks = new HashMap>(); + nativeInit(mNativeAwQuotaManagerBridgeImpl); + } + + private int getNextId() { + ThreadUtils.assertOnUiThread(); + return ++mNextId; + } + + /* + * There are five HTML5 offline storage APIs. + * 1) Web Storage (ie the localStorage and sessionStorage variables) + * 2) Web SQL database + * 3) Application cache + * 4) Indexed Database + * 5) Filesystem API + */ + + /** + * Implements WebStorage.deleteAllData(). Clear the storage of all five offline APIs. + * + * TODO(boliu): Actually clear Web Storage. + */ + public void deleteAllData() { + nativeDeleteAllData(mNativeAwQuotaManagerBridgeImpl); + } + + /** + * Implements WebStorage.deleteOrigin(). Clear the storage of APIs 2-5 for the given origin. + */ + public void deleteOrigin(String origin) { + nativeDeleteOrigin(mNativeAwQuotaManagerBridgeImpl, origin); + } + + /** + * Implements WebStorage.getOrigins. Get the per origin usage and quota of APIs 2-5 in + * aggregate. + */ + public void getOrigins(ValueCallback callback) { + int callbackId = getNextId(); + assert !mPendingGetOriginCallbacks.containsKey(callbackId); + mPendingGetOriginCallbacks.put(callbackId, callback); + nativeGetOrigins(mNativeAwQuotaManagerBridgeImpl, callbackId); + } + + /** + * Implements WebStorage.getQuotaForOrigin. Get the quota of APIs 2-5 in aggregate for given + * origin. + */ + public void getQuotaForOrigin(String origin, ValueCallback callback) { + int callbackId = getNextId(); + assert !mPendingGetQuotaForOriginCallbacks.containsKey(callbackId); + mPendingGetQuotaForOriginCallbacks.put(callbackId, callback); + nativeGetUsageAndQuotaForOrigin(mNativeAwQuotaManagerBridgeImpl, origin, callbackId, true); + } + + /** + * Implements WebStorage.getUsageForOrigin. Get the usage of APIs 2-5 in aggregate for given + * origin. + */ + public void getUsageForOrigin(String origin, ValueCallback callback) { + int callbackId = getNextId(); + assert !mPendingGetUsageForOriginCallbacks.containsKey(callbackId); + mPendingGetUsageForOriginCallbacks.put(callbackId, callback); + nativeGetUsageAndQuotaForOrigin(mNativeAwQuotaManagerBridgeImpl, origin, callbackId, false); + } + + @CalledByNative + private void onGetOriginsCallback(int callbackId, String[] origin, long[] usages, + long[] quotas) { + assert mPendingGetOriginCallbacks.containsKey(callbackId); + mPendingGetOriginCallbacks.remove(callbackId).onReceiveValue( + new Origins(origin, usages, quotas)); + } + + @CalledByNative + private void onGetUsageAndQuotaForOriginCallback( + int callbackId, boolean isQuota, long usage, long quota) { + if (isQuota) { + assert mPendingGetQuotaForOriginCallbacks.containsKey(callbackId); + mPendingGetQuotaForOriginCallbacks.remove(callbackId).onReceiveValue(quota); + } else { + assert mPendingGetUsageForOriginCallbacks.containsKey(callbackId); + mPendingGetUsageForOriginCallbacks.remove(callbackId).onReceiveValue(usage); + } + } + + private native void nativeInit(int nativeAwQuotaManagerBridgeImpl); + private native void nativeDeleteAllData(int nativeAwQuotaManagerBridgeImpl); + private native void nativeDeleteOrigin(int nativeAwQuotaManagerBridgeImpl, String origin); + private native void nativeGetOrigins(int nativeAwQuotaManagerBridgeImpl, int callbackId); + private native void nativeGetUsageAndQuotaForOrigin(int nativeAwQuotaManagerBridgeImpl, + String origin, int callbackId, boolean isQuota); +} diff --git a/src/org/chromium/android_webview/AwResource.java b/src/org/chromium/android_webview/AwResource.java new file mode 100644 index 0000000..e98be0b --- /dev/null +++ b/src/org/chromium/android_webview/AwResource.java @@ -0,0 +1,118 @@ +// 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 org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; + +import android.content.res.Resources; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.ref.SoftReference; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Scanner; + +@JNINamespace("android_webview::AwResource") +public class AwResource { + // The following resource ID's must be initialized by the embedder. + + // Raw resource ID for an HTML page to be displayed in the case of + // a specific load error. + public static int RAW_LOAD_ERROR; + + // Raw resource ID for an HTML page to be displayed in the case of + // a generic load error. (It's called NO_DOMAIN for legacy reasons). + public static int RAW_NO_DOMAIN; + + // String resource ID for the default text encoding to use. + public static int STRING_DEFAULT_TEXT_ENCODING; + + // The embedder should inject a Resources object that will be used + // to resolve Resource IDs into the actual resources. + private static Resources sResources; + + // Loading some resources is expensive, so cache the results. + private static Map > sResourceCache; + + private static final int TYPE_STRING = 0; + private static final int TYPE_RAW = 1; + + public static void setResources(Resources resources) { + sResources = resources; + sResourceCache = new HashMap >(); + } + + @CalledByNative + public static String getDefaultTextEncoding() { + return getResource(STRING_DEFAULT_TEXT_ENCODING, TYPE_STRING); + } + + @CalledByNative + public static String getNoDomainPageContent() { + return getResource(RAW_NO_DOMAIN, TYPE_RAW); + } + + @CalledByNative + public static String getLoadErrorPageContent() { + return getResource(RAW_LOAD_ERROR, TYPE_RAW); + } + + private static String getResource(int resid, int type) { + assert resid != 0; + assert sResources != null; + assert sResourceCache != null; + + String result = sResourceCache.get(resid) == null ? + null : sResourceCache.get(resid).get(); + if (result == null) { + switch (type) { + case TYPE_STRING: + result = sResources.getString(resid); + break; + case TYPE_RAW: + result = getRawFileResourceContent(resid); + break; + default: + throw new IllegalArgumentException("Unknown resource type"); + } + + sResourceCache.put(resid, new SoftReference(result)); + } + return result; + } + + private static String getRawFileResourceContent(int resid) { + assert resid != 0; + assert sResources != null; + + InputStreamReader isr = null; + String result = null; + + try { + isr = new InputStreamReader( + sResources.openRawResource(resid)); + // \A tells the scanner to use the beginning of the input + // as the delimiter, hence causes it to read the entire text. + result = new Scanner(isr).useDelimiter("\\A").next(); + } catch (Resources.NotFoundException e) { + return ""; + } catch (NoSuchElementException e) { + return ""; + } + finally { + try { + if (isr != null) { + isr.close(); + } + } catch(IOException e) { + } + } + return result; + } +} diff --git a/src/org/chromium/android_webview/AwSettings.java b/src/org/chromium/android_webview/AwSettings.java new file mode 100644 index 0000000..a578116 --- /dev/null +++ b/src/org/chromium/android_webview/AwSettings.java @@ -0,0 +1,1371 @@ +// 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.PackageManager; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.webkit.WebSettings.PluginState; +import android.webkit.WebSettings; +import android.webkit.WebView; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.base.ThreadUtils; +import org.chromium.content.browser.ContentViewCore; + +/** + * Stores Android WebView specific settings that does not need to be synced to WebKit. + * Use {@link org.chromium.content.browser.ContentSettings} for WebKit settings. + * + * Methods in this class can be called from any thread, including threads created by + * the client of WebView. + */ +@JNINamespace("android_webview") +public class AwSettings { + // This enum corresponds to WebSettings.LayoutAlgorithm. We use our own to be + // able to extend it. + public enum LayoutAlgorithm { + NORMAL, + SINGLE_COLUMN, + NARROW_COLUMNS, + TEXT_AUTOSIZING, + } + + private static final String TAG = "AwSettings"; + + // This class must be created on the UI thread. Afterwards, it can be + // used from any thread. Internally, the class uses a message queue + // to call native code on the UI thread only. + + // 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"; + private String mFixedFontFamily = "monospace"; + private String mSansSerifFontFamily = "sans-serif"; + private String mSerifFontFamily = "serif"; + private String mCursiveFontFamily = "cursive"; + private String mFantasyFontFamily = "fantasy"; + // TODO(mnaganov): Should be obtained from Android. Problem: it is hidden. + private String mDefaultTextEncoding = "Latin-1"; + private String mUserAgent; + private int mMinimumFontSize = 8; + private int mMinimumLogicalFontSize = 8; + private int mDefaultFontSize = 16; + private int mDefaultFixedFontSize = 13; + private boolean mLoadsImagesAutomatically = true; + private boolean mImagesEnabled = true; + private boolean mJavaScriptEnabled = false; + private boolean mAllowUniversalAccessFromFileURLs = false; + private boolean mAllowFileAccessFromFileURLs = false; + private boolean mJavaScriptCanOpenWindowsAutomatically = false; + private boolean mSupportMultipleWindows = false; + private PluginState mPluginState = PluginState.OFF; + private boolean mAppCacheEnabled = false; + private boolean mDomStorageEnabled = false; + private boolean mDatabaseEnabled = false; + private boolean mUseWideViewport = false; + private boolean mLoadWithOverviewMode = false; + private boolean mMediaPlaybackRequiresUserGesture = true; + private String mDefaultVideoPosterURL; + private float mInitialPageScalePercent = 0; + + private final boolean mSupportDeprecatedTargetDensityDPI = true; + + // Not accessed by the native side. + private boolean mBlockNetworkLoads; // Default depends on permission of embedding APK. + private boolean mAllowContentUrlAccess = true; + private boolean mAllowFileUrlAccess = true; + private int mCacheMode = WebSettings.LOAD_DEFAULT; + private boolean mShouldFocusFirstNode = true; + private boolean mGeolocationEnabled = true; + private boolean mAutoCompleteEnabled = true; + private boolean mSupportZoom = true; + private boolean mBuiltInZoomControls = false; + private boolean mDisplayZoomControls = true; + static class LazyDefaultUserAgent{ + // Lazy Holder pattern + private static final String sInstance = nativeGetDefaultUserAgent(); + } + + // Protects access to settings global fields. + private static final Object sGlobalContentSettingsLock = new Object(); + // For compatibility with the legacy WebView, we can only enable AppCache when the path is + // provided. However, we don't use the path, so we just check if we have received it from the + // client. + private static boolean sAppCachePathIsSet = false; + + // The native side of this object. + 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. + private final EventHandler mEventHandler; + + private static final int MINIMUM_FONT_SIZE = 1; + private static final int MAXIMUM_FONT_SIZE = 72; + + // Class to handle messages to be processed on the UI thread. + private class EventHandler { + // Message id for updating Webkit preferences + private static final int UPDATE_WEBKIT_PREFERENCES = 0; + // Actual UI thread handler + private Handler mHandler; + + EventHandler() { + mHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case UPDATE_WEBKIT_PREFERENCES: + synchronized (mAwSettingsLock) { + updateWebkitPreferencesOnUiThreadLocked(); + mIsUpdateWebkitPrefsMessagePending = false; + mAwSettingsLock.notifyAll(); + } + break; + } + } + }; + } + + private void updateWebkitPreferencesLocked() { + assert Thread.holdsLock(mAwSettingsLock); + if (mNativeAwSettings == 0) return; + if (Looper.myLooper() == mHandler.getLooper()) { + updateWebkitPreferencesOnUiThreadLocked(); + } else { + // We're being called on a background thread, so post a message. + if (mIsUpdateWebkitPrefsMessagePending) { + return; + } + mIsUpdateWebkitPrefsMessagePending = true; + mHandler.sendMessage(Message.obtain(null, UPDATE_WEBKIT_PREFERENCES)); + // We must block until the settings have been sync'd to native to + // ensure that they have taken effect. + try { + while (mIsUpdateWebkitPrefsMessagePending) { + mAwSettingsLock.wait(); + } + } catch (InterruptedException e) {} + } + } + } + + public AwSettings(Context context, + int nativeWebContents, + ContentViewCore contentViewCore, + boolean isAccessFromFileURLsGrantedByDefault) { + ThreadUtils.assertOnUiThread(); + mContext = context; + mBlockNetworkLoads = mContext.checkPermission( + android.Manifest.permission.INTERNET, + Process.myPid(), + Process.myUid()) != PackageManager.PERMISSION_GRANTED; + mContentViewCore = contentViewCore; + mContentViewCore.updateMultiTouchZoomSupport(supportsMultiTouchZoomLocked()); + + if (isAccessFromFileURLsGrantedByDefault) { + mAllowUniversalAccessFromFileURLs = true; + mAllowFileAccessFromFileURLs = true; + } + + mEventHandler = new EventHandler(); + mUserAgent = LazyDefaultUserAgent.sInstance; + + synchronized (mAwSettingsLock) { + mNativeAwSettings = nativeInit(nativeWebContents); + } + assert mNativeAwSettings != 0; + } + + public void destroy() { + nativeDestroy(mNativeAwSettings); + mNativeAwSettings = 0; + } + + public void setDIPScale(double dipScale) { + synchronized (mAwSettingsLock) { + mDIPScale = dipScale; + } + } + + @CalledByNative + private double getDIPScaleLocked() { + return mDIPScale; + } + + public void setWebContents(int nativeWebContents) { + synchronized (mAwSettingsLock) { + nativeSetWebContentsLocked(mNativeAwSettings, nativeWebContents); + } + } + + /** + * See {@link android.webkit.WebSettings#setBlockNetworkLoads}. + */ + public void setBlockNetworkLoads(boolean flag) { + synchronized (mAwSettingsLock) { + if (!flag && mContext.checkPermission( + android.Manifest.permission.INTERNET, + Process.myPid(), + Process.myUid()) != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Permission denied - " + + "application missing INTERNET permission"); + } + mBlockNetworkLoads = flag; + } + } + + /** + * See {@link android.webkit.WebSettings#getBlockNetworkLoads}. + */ + public boolean getBlockNetworkLoads() { + synchronized (mAwSettingsLock) { + return mBlockNetworkLoads; + } + } + + /** + * See {@link android.webkit.WebSettings#setAllowFileAccess}. + */ + public void setAllowFileAccess(boolean allow) { + synchronized (mAwSettingsLock) { + if (mAllowFileUrlAccess != allow) { + mAllowFileUrlAccess = allow; + } + } + } + + /** + * See {@link android.webkit.WebSettings#getAllowFileAccess}. + */ + public boolean getAllowFileAccess() { + synchronized (mAwSettingsLock) { + return mAllowFileUrlAccess; + } + } + + /** + * See {@link android.webkit.WebSettings#setAllowContentAccess}. + */ + public void setAllowContentAccess(boolean allow) { + synchronized (mAwSettingsLock) { + if (mAllowContentUrlAccess != allow) { + mAllowContentUrlAccess = allow; + } + } + } + + /** + * See {@link android.webkit.WebSettings#getAllowContentAccess}. + */ + public boolean getAllowContentAccess() { + synchronized (mAwSettingsLock) { + return mAllowContentUrlAccess; + } + } + + /** + * See {@link android.webkit.WebSettings#setCacheMode}. + */ + public void setCacheMode(int mode) { + synchronized (mAwSettingsLock) { + if (mCacheMode != mode) { + mCacheMode = mode; + } + } + } + + /** + * See {@link android.webkit.WebSettings#getCacheMode}. + */ + public int getCacheMode() { + synchronized (mAwSettingsLock) { + return mCacheMode; + } + } + + /** + * See {@link android.webkit.WebSettings#setNeedInitialFocus}. + */ + public void setShouldFocusFirstNode(boolean flag) { + synchronized (mAwSettingsLock) { + mShouldFocusFirstNode = flag; + } + } + + /** + * See {@link android.webkit.WebView#setInitialScale}. + */ + public void setInitialPageScale(final float scaleInPercent) { + synchronized (mAwSettingsLock) { + if (mInitialPageScalePercent != scaleInPercent) { + mInitialPageScalePercent = scaleInPercent; + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + if (mNativeAwSettings != 0) { + nativeUpdateInitialPageScaleLocked(mNativeAwSettings); + } + } + }); + } + } + } + + @CalledByNative + private float getInitialPageScalePercentLocked() { + return mInitialPageScalePercent; + } + + /** + * See {@link android.webkit.WebSettings#setNeedInitialFocus}. + */ + public boolean shouldFocusFirstNode() { + synchronized(mAwSettingsLock) { + return mShouldFocusFirstNode; + } + } + + /** + * See {@link android.webkit.WebSettings#setGeolocationEnabled}. + */ + public void setGeolocationEnabled(boolean flag) { + synchronized (mAwSettingsLock) { + if (mGeolocationEnabled != flag) { + mGeolocationEnabled = flag; + } + } + } + + /** + * @return Returns if geolocation is currently enabled. + */ + boolean getGeolocationEnabled() { + synchronized (mAwSettingsLock) { + return mGeolocationEnabled; + } + } + + /** + * See {@link android.webkit.WebSettings#setSaveFormData}. + */ + public void setSaveFormData(final boolean enable) { + synchronized (mAwSettingsLock) { + if (mAutoCompleteEnabled != enable) { + mAutoCompleteEnabled = enable; + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + if (mNativeAwSettings != 0) { + nativeUpdateFormDataPreferencesLocked(mNativeAwSettings); + } + } + }); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getSaveFormData}. + */ + public boolean getSaveFormData() { + synchronized (mAwSettingsLock) { + return getSaveFormDataLocked(); + } + } + + @CalledByNative + private boolean getSaveFormDataLocked() { + return mAutoCompleteEnabled; + } + + /** + * @returns the default User-Agent used by each ContentViewCore instance, i.e. unless + * overridden by {@link #setUserAgentString()} + */ + public static String getDefaultUserAgent() { + return LazyDefaultUserAgent.sInstance; + } + + /** + * See {@link android.webkit.WebSettings#setUserAgentString}. + */ + public void setUserAgentString(String ua) { + synchronized (mAwSettingsLock) { + final String oldUserAgent = mUserAgent; + if (ua == null || ua.length() == 0) { + mUserAgent = LazyDefaultUserAgent.sInstance; + } else { + mUserAgent = ua; + } + if (!oldUserAgent.equals(mUserAgent)) { + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + if (mNativeAwSettings != 0) { + nativeUpdateUserAgentLocked(mNativeAwSettings); + } + } + }); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getUserAgentString}. + */ + public String getUserAgentString() { + synchronized (mAwSettingsLock) { + return mUserAgent; + } + } + + @CalledByNative + private String getUserAgentLocked() { + return mUserAgent; + } + + /** + * See {@link android.webkit.WebSettings#setLoadWithOverviewMode}. + */ + public void setLoadWithOverviewMode(boolean overview) { + synchronized (mAwSettingsLock) { + if (mLoadWithOverviewMode != overview) { + mLoadWithOverviewMode = overview; + mEventHandler.updateWebkitPreferencesLocked(); + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + if (mNativeAwSettings != 0) { + nativeResetScrollAndScaleState(mNativeAwSettings); + } + } + }); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getLoadWithOverviewMode}. + */ + public boolean getLoadWithOverviewMode() { + synchronized (mAwSettingsLock) { + return mLoadWithOverviewMode; + } + } + + @CalledByNative + private boolean getLoadWithOverviewModeLocked() { + return mLoadWithOverviewMode; + } + + /** + * See {@link android.webkit.WebSettings#setTextZoom}. + */ + public void setTextZoom(final int textZoom) { + synchronized (mAwSettingsLock) { + if (mTextSizePercent != textZoom) { + mTextSizePercent = textZoom; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getTextZoom}. + */ + public int getTextZoom() { + synchronized (mAwSettingsLock) { + return mTextSizePercent; + } + } + + @CalledByNative + private int getTextSizePercentLocked() { + return mTextSizePercent; + } + + /** + * See {@link android.webkit.WebSettings#setStandardFontFamily}. + */ + public void setStandardFontFamily(String font) { + synchronized (mAwSettingsLock) { + if (font != null && !mStandardFontFamily.equals(font)) { + mStandardFontFamily = font; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getStandardFontFamily}. + */ + public String getStandardFontFamily() { + synchronized (mAwSettingsLock) { + return mStandardFontFamily; + } + } + + @CalledByNative + private String getStandardFontFamilyLocked() { + return mStandardFontFamily; + } + + /** + * See {@link android.webkit.WebSettings#setFixedFontFamily}. + */ + public void setFixedFontFamily(String font) { + synchronized (mAwSettingsLock) { + if (font != null && !mFixedFontFamily.equals(font)) { + mFixedFontFamily = font; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getFixedFontFamily}. + */ + public String getFixedFontFamily() { + synchronized (mAwSettingsLock) { + return mFixedFontFamily; + } + } + + @CalledByNative + private String getFixedFontFamilyLocked() { + return mFixedFontFamily; + } + + /** + * See {@link android.webkit.WebSettings#setSansSerifFontFamily}. + */ + public void setSansSerifFontFamily(String font) { + synchronized (mAwSettingsLock) { + if (font != null && !mSansSerifFontFamily.equals(font)) { + mSansSerifFontFamily = font; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getSansSerifFontFamily}. + */ + public String getSansSerifFontFamily() { + synchronized (mAwSettingsLock) { + return mSansSerifFontFamily; + } + } + + @CalledByNative + private String getSansSerifFontFamilyLocked() { + return mSansSerifFontFamily; + } + + /** + * See {@link android.webkit.WebSettings#setSerifFontFamily}. + */ + public void setSerifFontFamily(String font) { + synchronized (mAwSettingsLock) { + if (font != null && !mSerifFontFamily.equals(font)) { + mSerifFontFamily = font; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getSerifFontFamily}. + */ + public String getSerifFontFamily() { + synchronized (mAwSettingsLock) { + return mSerifFontFamily; + } + } + + @CalledByNative + private String getSerifFontFamilyLocked() { + return mSerifFontFamily; + } + + /** + * See {@link android.webkit.WebSettings#setCursiveFontFamily}. + */ + public void setCursiveFontFamily(String font) { + synchronized (mAwSettingsLock) { + if (font != null && !mCursiveFontFamily.equals(font)) { + mCursiveFontFamily = font; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getCursiveFontFamily}. + */ + public String getCursiveFontFamily() { + synchronized (mAwSettingsLock) { + return mCursiveFontFamily; + } + } + + @CalledByNative + private String getCursiveFontFamilyLocked() { + return mCursiveFontFamily; + } + + /** + * See {@link android.webkit.WebSettings#setFantasyFontFamily}. + */ + public void setFantasyFontFamily(String font) { + synchronized (mAwSettingsLock) { + if (font != null && !mFantasyFontFamily.equals(font)) { + mFantasyFontFamily = font; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getFantasyFontFamily}. + */ + public String getFantasyFontFamily() { + synchronized (mAwSettingsLock) { + return mFantasyFontFamily; + } + } + + @CalledByNative + private String getFantasyFontFamilyLocked() { + return mFantasyFontFamily; + } + + /** + * See {@link android.webkit.WebSettings#setMinimumFontSize}. + */ + public void setMinimumFontSize(int size) { + synchronized (mAwSettingsLock) { + size = clipFontSize(size); + if (mMinimumFontSize != size) { + mMinimumFontSize = size; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getMinimumFontSize}. + */ + public int getMinimumFontSize() { + synchronized (mAwSettingsLock) { + return mMinimumFontSize; + } + } + + @CalledByNative + private int getMinimumFontSizeLocked() { + return mMinimumFontSize; + } + + /** + * See {@link android.webkit.WebSettings#setMinimumLogicalFontSize}. + */ + public void setMinimumLogicalFontSize(int size) { + synchronized (mAwSettingsLock) { + size = clipFontSize(size); + if (mMinimumLogicalFontSize != size) { + mMinimumLogicalFontSize = size; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getMinimumLogicalFontSize}. + */ + public int getMinimumLogicalFontSize() { + synchronized (mAwSettingsLock) { + return mMinimumLogicalFontSize; + } + } + + @CalledByNative + private int getMinimumLogicalFontSizeLocked() { + return mMinimumLogicalFontSize; + } + + /** + * See {@link android.webkit.WebSettings#setDefaultFontSize}. + */ + public void setDefaultFontSize(int size) { + synchronized (mAwSettingsLock) { + size = clipFontSize(size); + if (mDefaultFontSize != size) { + mDefaultFontSize = size; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getDefaultFontSize}. + */ + public int getDefaultFontSize() { + synchronized (mAwSettingsLock) { + return mDefaultFontSize; + } + } + + @CalledByNative + private int getDefaultFontSizeLocked() { + return mDefaultFontSize; + } + + /** + * See {@link android.webkit.WebSettings#setDefaultFixedFontSize}. + */ + public void setDefaultFixedFontSize(int size) { + synchronized (mAwSettingsLock) { + size = clipFontSize(size); + if (mDefaultFixedFontSize != size) { + mDefaultFixedFontSize = size; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getDefaultFixedFontSize}. + */ + public int getDefaultFixedFontSize() { + synchronized (mAwSettingsLock) { + return mDefaultFixedFontSize; + } + } + + @CalledByNative + private int getDefaultFixedFontSizeLocked() { + return mDefaultFixedFontSize; + } + + /** + * See {@link android.webkit.WebSettings#setJavaScriptEnabled}. + */ + public void setJavaScriptEnabled(boolean flag) { + synchronized (mAwSettingsLock) { + if (mJavaScriptEnabled != flag) { + mJavaScriptEnabled = flag; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#setAllowUniversalAccessFromFileURLs}. + */ + public void setAllowUniversalAccessFromFileURLs(boolean flag) { + synchronized (mAwSettingsLock) { + if (mAllowUniversalAccessFromFileURLs != flag) { + mAllowUniversalAccessFromFileURLs = flag; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#setAllowFileAccessFromFileURLs}. + */ + public void setAllowFileAccessFromFileURLs(boolean flag) { + synchronized (mAwSettingsLock) { + if (mAllowFileAccessFromFileURLs != flag) { + mAllowFileAccessFromFileURLs = flag; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#setLoadsImagesAutomatically}. + */ + public void setLoadsImagesAutomatically(boolean flag) { + synchronized (mAwSettingsLock) { + if (mLoadsImagesAutomatically != flag) { + mLoadsImagesAutomatically = flag; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getLoadsImagesAutomatically}. + */ + public boolean getLoadsImagesAutomatically() { + synchronized (mAwSettingsLock) { + return mLoadsImagesAutomatically; + } + } + + @CalledByNative + private boolean getLoadsImagesAutomaticallyLocked() { + return mLoadsImagesAutomatically; + } + + /** + * See {@link android.webkit.WebSettings#setImagesEnabled}. + */ + public void setImagesEnabled(boolean flag) { + synchronized (mAwSettingsLock) { + if (mImagesEnabled != flag) { + mImagesEnabled = flag; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getImagesEnabled}. + */ + public boolean getImagesEnabled() { + synchronized (mAwSettingsLock) { + return mImagesEnabled; + } + } + + @CalledByNative + private boolean getImagesEnabledLocked() { + return mImagesEnabled; + } + + /** + * See {@link android.webkit.WebSettings#getJavaScriptEnabled}. + */ + public boolean getJavaScriptEnabled() { + synchronized (mAwSettingsLock) { + return mJavaScriptEnabled; + } + } + + @CalledByNative + private boolean getJavaScriptEnabledLocked() { + return mJavaScriptEnabled; + } + + /** + * See {@link android.webkit.WebSettings#getAllowUniversalAccessFromFileURLs}. + */ + public boolean getAllowUniversalAccessFromFileURLs() { + synchronized (mAwSettingsLock) { + return mAllowUniversalAccessFromFileURLs; + } + } + + @CalledByNative + private boolean getAllowUniversalAccessFromFileURLsLocked() { + return mAllowUniversalAccessFromFileURLs; + } + + /** + * See {@link android.webkit.WebSettings#getAllowFileAccessFromFileURLs}. + */ + public boolean getAllowFileAccessFromFileURLs() { + synchronized (mAwSettingsLock) { + return mAllowFileAccessFromFileURLs; + } + } + + @CalledByNative + private boolean getAllowFileAccessFromFileURLsLocked() { + return mAllowFileAccessFromFileURLs; + } + + /** + * See {@link android.webkit.WebSettings#setPluginsEnabled}. + */ + @Deprecated + public void setPluginsEnabled(boolean flag) { + setPluginState(flag ? PluginState.ON : PluginState.OFF); + } + + /** + * See {@link android.webkit.WebSettings#setPluginState}. + */ + public void setPluginState(PluginState state) { + synchronized (mAwSettingsLock) { + if (mPluginState != state) { + mPluginState = state; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getPluginsEnabled}. + */ + @Deprecated + public boolean getPluginsEnabled() { + synchronized (mAwSettingsLock) { + return mPluginState == PluginState.ON; + } + } + + /** + * Return true if plugins are disabled. + * @return True if plugins are disabled. + * @hide + */ + @CalledByNative + private boolean getPluginsDisabledLocked() { + return mPluginState == PluginState.OFF; + } + + /** + * See {@link android.webkit.WebSettings#getPluginState}. + */ + public PluginState getPluginState() { + synchronized (mAwSettingsLock) { + return mPluginState; + } + } + + + /** + * See {@link android.webkit.WebSettings#setJavaScriptCanOpenWindowsAutomatically}. + */ + public void setJavaScriptCanOpenWindowsAutomatically(boolean flag) { + synchronized (mAwSettingsLock) { + if (mJavaScriptCanOpenWindowsAutomatically != flag) { + mJavaScriptCanOpenWindowsAutomatically = flag; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getJavaScriptCanOpenWindowsAutomatically}. + */ + public boolean getJavaScriptCanOpenWindowsAutomatically() { + synchronized (mAwSettingsLock) { + return mJavaScriptCanOpenWindowsAutomatically; + } + } + + @CalledByNative + private boolean getJavaScriptCanOpenWindowsAutomaticallyLocked() { + return mJavaScriptCanOpenWindowsAutomatically; + } + + /** + * See {@link android.webkit.WebSettings#setLayoutAlgorithm}. + */ + public void setLayoutAlgorithm(LayoutAlgorithm l) { + synchronized (mAwSettingsLock) { + if (mLayoutAlgorithm != l) { + mLayoutAlgorithm = l; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getLayoutAlgorithm}. + */ + public LayoutAlgorithm getLayoutAlgorithm() { + synchronized (mAwSettingsLock) { + return mLayoutAlgorithm; + } + } + + /** + * Gets whether Text Auto-sizing layout algorithm is enabled. + * + * @return true if Text Auto-sizing layout algorithm is enabled + * @hide + */ + @CalledByNative + private boolean getTextAutosizingEnabledLocked() { + return mLayoutAlgorithm == LayoutAlgorithm.TEXT_AUTOSIZING; + } + + /** + * See {@link android.webkit.WebSettings#setSupportMultipleWindows}. + */ + public void setSupportMultipleWindows(boolean support) { + synchronized (mAwSettingsLock) { + if (mSupportMultipleWindows != support) { + mSupportMultipleWindows = support; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#supportMultipleWindows}. + */ + public boolean supportMultipleWindows() { + synchronized (mAwSettingsLock) { + return mSupportMultipleWindows; + } + } + + @CalledByNative + private boolean getSupportMultipleWindowsLocked() { + return mSupportMultipleWindows; + } + + @CalledByNative + private boolean getSupportDeprecatedTargetDensityDPILocked() { + return mSupportDeprecatedTargetDensityDPI; + } + + /** + * See {@link android.webkit.WebSettings#setUseWideViewPort}. + */ + public void setUseWideViewPort(boolean use) { + synchronized (mAwSettingsLock) { + if (mUseWideViewport != use) { + mUseWideViewport = use; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getUseWideViewPort}. + */ + public boolean getUseWideViewPort() { + synchronized (mAwSettingsLock) { + return mUseWideViewport; + } + } + + @CalledByNative + private boolean getUseWideViewportLocked() { + return mUseWideViewport; + } + + /** + * See {@link android.webkit.WebSettings#setAppCacheEnabled}. + */ + public void setAppCacheEnabled(boolean flag) { + synchronized (mAwSettingsLock) { + if (mAppCacheEnabled != flag) { + mAppCacheEnabled = flag; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#setAppCachePath}. + */ + public void setAppCachePath(String path) { + boolean needToSync = false; + synchronized (sGlobalContentSettingsLock) { + // AppCachePath can only be set once. + if (!sAppCachePathIsSet && path != null && !path.isEmpty()) { + sAppCachePathIsSet = true; + needToSync = true; + } + } + // The obvious problem here is that other WebViews will not be updated, + // until they execute synchronization from Java to the native side. + // But this is the same behaviour as it was in the legacy WebView. + if (needToSync) { + synchronized (mAwSettingsLock) { + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * Gets whether Application Cache is enabled. + * + * @return true if Application Cache is enabled + * @hide + */ + @CalledByNative + private boolean getAppCacheEnabledLocked() { + if (!mAppCacheEnabled) { + return false; + } + synchronized (sGlobalContentSettingsLock) { + return sAppCachePathIsSet; + } + } + + /** + * See {@link android.webkit.WebSettings#setDomStorageEnabled}. + */ + public void setDomStorageEnabled(boolean flag) { + synchronized (mAwSettingsLock) { + if (mDomStorageEnabled != flag) { + mDomStorageEnabled = flag; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getDomStorageEnabled}. + */ + public boolean getDomStorageEnabled() { + synchronized (mAwSettingsLock) { + return mDomStorageEnabled; + } + } + + @CalledByNative + private boolean getDomStorageEnabledLocked() { + return mDomStorageEnabled; + } + + /** + * See {@link android.webkit.WebSettings#setDatabaseEnabled}. + */ + public void setDatabaseEnabled(boolean flag) { + synchronized (mAwSettingsLock) { + if (mDatabaseEnabled != flag) { + mDatabaseEnabled = flag; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getDatabaseEnabled}. + */ + public boolean getDatabaseEnabled() { + synchronized (mAwSettingsLock) { + return mDatabaseEnabled; + } + } + + @CalledByNative + private boolean getDatabaseEnabledLocked() { + return mDatabaseEnabled; + } + + /** + * See {@link android.webkit.WebSettings#setDefaultTextEncodingName}. + */ + public void setDefaultTextEncodingName(String encoding) { + synchronized (mAwSettingsLock) { + if (encoding != null && !mDefaultTextEncoding.equals(encoding)) { + mDefaultTextEncoding = encoding; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getDefaultTextEncodingName}. + */ + public String getDefaultTextEncodingName() { + synchronized (mAwSettingsLock) { + return mDefaultTextEncoding; + } + } + + @CalledByNative + private String getDefaultTextEncodingLocked() { + return mDefaultTextEncoding; + } + + /** + * See {@link android.webkit.WebSettings#setMediaPlaybackRequiresUserGesture}. + */ + public void setMediaPlaybackRequiresUserGesture(boolean require) { + synchronized (mAwSettingsLock) { + if (mMediaPlaybackRequiresUserGesture != require) { + mMediaPlaybackRequiresUserGesture = require; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getMediaPlaybackRequiresUserGesture}. + */ + public boolean getMediaPlaybackRequiresUserGesture() { + synchronized (mAwSettingsLock) { + return mMediaPlaybackRequiresUserGesture; + } + } + + @CalledByNative + private boolean getMediaPlaybackRequiresUserGestureLocked() { + return mMediaPlaybackRequiresUserGesture; + } + + /** + * See {@link android.webkit.WebSettings#setDefaultVideoPosterURL}. + */ + public void setDefaultVideoPosterURL(String url) { + synchronized (mAwSettingsLock) { + if (mDefaultVideoPosterURL != null && !mDefaultVideoPosterURL.equals(url) || + mDefaultVideoPosterURL == null && url != null) { + mDefaultVideoPosterURL = url; + mEventHandler.updateWebkitPreferencesLocked(); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getDefaultVideoPosterURL}. + */ + public String getDefaultVideoPosterURL() { + synchronized (mAwSettingsLock) { + return mDefaultVideoPosterURL; + } + } + + @CalledByNative + private String getDefaultVideoPosterURLLocked() { + return mDefaultVideoPosterURL; + } + + private void updateMultiTouchZoomSupport(final boolean supportsMultiTouchZoom) { + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + mContentViewCore.updateMultiTouchZoomSupport(supportsMultiTouchZoom); + } + }); + } + + /** + * See {@link android.webkit.WebSettings#setSupportZoom}. + */ + public void setSupportZoom(boolean support) { + synchronized (mAwSettingsLock) { + if (mSupportZoom != support) { + mSupportZoom = support; + updateMultiTouchZoomSupport(supportsMultiTouchZoomLocked()); + } + } + } + + /** + * See {@link android.webkit.WebSettings#supportZoom}. + */ + public boolean supportZoom() { + synchronized (mAwSettingsLock) { + return mSupportZoom; + } + } + + /** + * See {@link android.webkit.WebSettings#setBuiltInZoomControls}. + */ + public void setBuiltInZoomControls(boolean enabled) { + synchronized (mAwSettingsLock) { + if (mBuiltInZoomControls != enabled) { + mBuiltInZoomControls = enabled; + updateMultiTouchZoomSupport(supportsMultiTouchZoomLocked()); + } + } + } + + /** + * See {@link android.webkit.WebSettings#getBuiltInZoomControls}. + */ + public boolean getBuiltInZoomControls() { + synchronized (mAwSettingsLock) { + return mBuiltInZoomControls; + } + } + + /** + * See {@link android.webkit.WebSettings#setDisplayZoomControls}. + */ + public void setDisplayZoomControls(boolean enabled) { + synchronized (mAwSettingsLock) { + mDisplayZoomControls = enabled; + } + } + + /** + * See {@link android.webkit.WebSettings#getDisplayZoomControls}. + */ + public boolean getDisplayZoomControls() { + synchronized (mAwSettingsLock) { + return mDisplayZoomControls; + } + } + + private boolean supportsMultiTouchZoomLocked() { + return mSupportZoom && mBuiltInZoomControls; + } + + boolean supportsMultiTouchZoom() { + synchronized (mAwSettingsLock) { + return supportsMultiTouchZoomLocked(); + } + } + + boolean shouldDisplayZoomControls() { + synchronized (mAwSettingsLock) { + return supportsMultiTouchZoomLocked() && mDisplayZoomControls; + } + } + + private int clipFontSize(int size) { + if (size < MINIMUM_FONT_SIZE) { + return MINIMUM_FONT_SIZE; + } else if (size > MAXIMUM_FONT_SIZE) { + return MAXIMUM_FONT_SIZE; + } + return size; + } + + @CalledByNative + private void updateEverything() { + synchronized (mAwSettingsLock) { + nativeUpdateEverythingLocked(mNativeAwSettings); + } + } + + private void updateWebkitPreferencesOnUiThreadLocked() { + if (mNativeAwSettings != 0) { + ThreadUtils.assertOnUiThread(); + nativeUpdateWebkitPreferencesLocked(mNativeAwSettings); + } + } + + private native int nativeInit(int webContentsPtr); + + private native void nativeDestroy(int nativeAwSettings); + + 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); + + private native void nativeUpdateUserAgentLocked(int nativeAwSettings); + + private native void nativeUpdateWebkitPreferencesLocked(int nativeAwSettings); + + private static native String nativeGetDefaultUserAgent(); + + private native void nativeUpdateFormDataPreferencesLocked(int nativeAwSettings); +} diff --git a/src/org/chromium/android_webview/AwWebContentsDelegate.java b/src/org/chromium/android_webview/AwWebContentsDelegate.java new file mode 100644 index 0000000..23f48eb --- /dev/null +++ b/src/org/chromium/android_webview/AwWebContentsDelegate.java @@ -0,0 +1,36 @@ +// 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 org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.components.web_contents_delegate_android.WebContentsDelegateAndroid; + +/** + * WebView-specific WebContentsDelegate. + * This file is the Java version of the native class of the same name. + * It should contain abstract WebContentsDelegate methods to be implemented by the embedder. + * These methods belong to WebView but are not shared with the Chromium Android port. + */ +@JNINamespace("android_webview") +public abstract class AwWebContentsDelegate extends WebContentsDelegateAndroid { + @CalledByNative + public abstract boolean addNewContents(boolean isDialog, boolean isUserGesture); + + @CalledByNative + public abstract void closeContents(); + + @CalledByNative + public abstract void activateContents(); + + /** + * Report a change in the preferred size. + * @param width preferred width in CSS pixels. + * @param height scroll height of the document element in CSS pixels. + */ + @CalledByNative + public void updatePreferredSize(int widthCss, int heightCss) { + } +} diff --git a/src/org/chromium/android_webview/AwWebContentsDelegateAdapter.java b/src/org/chromium/android_webview/AwWebContentsDelegateAdapter.java new file mode 100644 index 0000000..0b888d3 --- /dev/null +++ b/src/org/chromium/android_webview/AwWebContentsDelegateAdapter.java @@ -0,0 +1,152 @@ +// 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.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.view.KeyEvent; +import android.webkit.ConsoleMessage; + +import org.chromium.content.browser.ContentViewCore; + +/** + * Adapts the AwWebContentsDelegate interface to the AwContentsClient interface. + * This class also serves a secondary function of routing certain callbacks from the content layer + * to specific listener interfaces. + */ +class AwWebContentsDelegateAdapter extends AwWebContentsDelegate { + private static final String TAG = "AwWebContentsDelegateAdapter"; + + /** + * Listener definition for a callback to be invoked when the preferred size of the page + * contents changes. + */ + public interface PreferredSizeChangedListener { + /** + * Called when the preferred size of the page contents changes. + * @see AwWebContentsDelegate#updatePreferredSize + */ + void updatePreferredSize(int width, int height); + } + + final AwContentsClient mContentsClient; + final PreferredSizeChangedListener mPreferredSizeChangedListener; + + public AwWebContentsDelegateAdapter(AwContentsClient contentsClient, + PreferredSizeChangedListener preferredSizeChangedListener) { + mContentsClient = contentsClient; + mPreferredSizeChangedListener = preferredSizeChangedListener; + } + + @Override + public void onLoadProgressChanged(int progress) { + mContentsClient.onProgressChanged(progress); + } + + @Override + public void handleKeyboardEvent(KeyEvent event) { + mContentsClient.onUnhandledKeyEvent(event); + } + + @Override + public boolean addMessageToConsole(int level, String message, int lineNumber, + String sourceId) { + ConsoleMessage.MessageLevel messageLevel = ConsoleMessage.MessageLevel.DEBUG; + switch(level) { + case LOG_LEVEL_TIP: + messageLevel = ConsoleMessage.MessageLevel.TIP; + break; + case LOG_LEVEL_LOG: + messageLevel = ConsoleMessage.MessageLevel.LOG; + break; + case LOG_LEVEL_WARNING: + messageLevel = ConsoleMessage.MessageLevel.WARNING; + break; + case LOG_LEVEL_ERROR: + messageLevel = ConsoleMessage.MessageLevel.ERROR; + break; + default: + Log.w(TAG, "Unknown message level, defaulting to DEBUG"); + break; + } + + return mContentsClient.onConsoleMessage( + new ConsoleMessage(message, sourceId, lineNumber, messageLevel)); + } + + @Override + public void onUpdateUrl(String url) { + // TODO: implement + } + + @Override + public void openNewTab(String url, boolean incognito) { + // TODO: implement + } + + @Override + public boolean addNewContents(int nativeSourceWebContents, int nativeWebContents, + int disposition, Rect initialPosition, boolean userGesture) { + // TODO: implement + return false; + } + + @Override + public void closeContents() { + mContentsClient.onCloseWindow(); + } + + @Override + public void showRepostFormWarningDialog(final ContentViewCore contentViewCore) { + // TODO(mkosiba) We should be using something akin to the JsResultReceiver as the + // callback parameter (instead of ContentViewCore) and implement a way of converting + // that to a pair of messages. + final int MSG_CONTINUE_PENDING_RELOAD = 1; + final int MSG_CANCEL_PENDING_RELOAD = 2; + + // TODO(sgurun) Remember the URL to cancel the reload behavior + // if it is different than the most recent NavigationController entry. + final Handler handler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case MSG_CONTINUE_PENDING_RELOAD: { + contentViewCore.continuePendingReload(); + break; + } + case MSG_CANCEL_PENDING_RELOAD: { + contentViewCore.cancelPendingReload(); + break; + } + default: + throw new IllegalStateException( + "WebContentsDelegateAdapter: unhandled message " + msg.what); + } + } + }; + + Message resend = handler.obtainMessage(MSG_CONTINUE_PENDING_RELOAD); + Message dontResend = handler.obtainMessage(MSG_CANCEL_PENDING_RELOAD); + mContentsClient.onFormResubmission(dontResend, resend); + } + + @Override + public boolean addNewContents(boolean isDialog, boolean isUserGesture) { + return mContentsClient.onCreateWindow(isDialog, isUserGesture); + } + + @Override + public void activateContents() { + mContentsClient.onRequestFocus(); + } + + @Override + public void updatePreferredSize(int width, int height) { + mPreferredSizeChangedListener.updatePreferredSize(width, height); + } +} diff --git a/src/org/chromium/android_webview/AwZoomControls.java b/src/org/chromium/android_webview/AwZoomControls.java new file mode 100644 index 0000000..0bcae18 --- /dev/null +++ b/src/org/chromium/android_webview/AwZoomControls.java @@ -0,0 +1,102 @@ +// 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.android_webview; + +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ZoomButtonsController; +import org.chromium.content.browser.ContentViewCore.ZoomControlsDelegate; + +class AwZoomControls implements ZoomControlsDelegate { + + private AwContents mAwContents; + // It is advised to use getZoomController() where possible. + private ZoomButtonsController mZoomButtonsController; + + AwZoomControls(AwContents awContents) { + mAwContents = awContents; + } + + @Override + public void invokeZoomPicker() { + ZoomButtonsController zoomController = getZoomController(); + if (zoomController != null) { + zoomController.setVisible(true); + } + } + + @Override + public void dismissZoomPicker() { + ZoomButtonsController zoomController = getZoomController(); + if (zoomController != null) { + zoomController.setVisible(false); + } + } + + @Override + public void updateZoomControls() { + ZoomButtonsController zoomController = getZoomController(); + if (zoomController == null) { + return; + } + boolean canZoomIn = mAwContents.canZoomIn(); + boolean canZoomOut = mAwContents.canZoomOut(); + if (!canZoomIn && !canZoomOut) { + // Hide the zoom in and out buttons if the page cannot zoom + zoomController.getZoomControls().setVisibility(View.GONE); + } else { + // Set each one individually, as a page may be able to zoom in or out + zoomController.setZoomInEnabled(canZoomIn); + zoomController.setZoomOutEnabled(canZoomOut); + } + } + + // This method is used in tests. It doesn't modify the state of zoom controls. + View getZoomControlsViewForTest() { + return mZoomButtonsController != null ? mZoomButtonsController.getZoomControls() : null; + } + + private ZoomButtonsController getZoomController() { + if (mZoomButtonsController == null && + mAwContents.getSettings().shouldDisplayZoomControls()) { + mZoomButtonsController = new ZoomButtonsController( + mAwContents.getContentViewCore().getContainerView()); + mZoomButtonsController.setOnZoomListener(new ZoomListener()); + // ZoomButtonsController positions the buttons at the bottom, but in + // the middle. Change their layout parameters so they appear on the + // right. + View controls = mZoomButtonsController.getZoomControls(); + ViewGroup.LayoutParams params = controls.getLayoutParams(); + if (params instanceof FrameLayout.LayoutParams) { + ((FrameLayout.LayoutParams) params).gravity = Gravity.RIGHT; + } + } + return mZoomButtonsController; + } + + private class ZoomListener implements ZoomButtonsController.OnZoomListener { + @Override + public void onVisibilityChanged(boolean visible) { + if (visible) { + // Bring back the hidden zoom controls. + mZoomButtonsController.getZoomControls().setVisibility(View.VISIBLE); + updateZoomControls(); + } + } + + @Override + public void onZoom(boolean zoomIn) { + if (zoomIn) { + mAwContents.zoomIn(); + } else { + mAwContents.zoomOut(); + } + // ContentView will call updateZoomControls after its current page scale + // is got updated from the native code. + } + } +} diff --git a/src/org/chromium/android_webview/DefaultVideoPosterRequestHandler.java b/src/org/chromium/android_webview/DefaultVideoPosterRequestHandler.java new file mode 100644 index 0000000..22d57ca --- /dev/null +++ b/src/org/chromium/android_webview/DefaultVideoPosterRequestHandler.java @@ -0,0 +1,110 @@ +// 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.graphics.Bitmap; +import android.os.AsyncTask; +import android.util.Log; + +import org.chromium.base.ThreadUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.Random; + +/** + * This class takes advantage of shouldInterceptRequest(), returns the bitmap from + * WebChromeClient.getDefaultVidoePoster() when the mDefaultVideoPosterURL is requested. + * + * The shouldInterceptRequest is used to get the default video poster, if the url is + * the mDefaultVideoPosterURL. + */ +public class DefaultVideoPosterRequestHandler { + private static InputStream getInputStream(final AwContentsClient contentClient) + throws IOException { + final PipedInputStream inputStream = new PipedInputStream(); + final PipedOutputStream outputStream = new PipedOutputStream(inputStream); + + // Send the request to UI thread to callback to the client, and if it provides a + // valid bitmap bounce on to the worker thread pool to compress it into the piped + // input/output stream. + ThreadUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + final Bitmap defaultVideoPoster = contentClient.getDefaultVideoPoster(); + if (defaultVideoPoster == null) { + closeOutputStream(outputStream); + return; + } + AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() { + @Override + public void run() { + try { + defaultVideoPoster.compress(Bitmap.CompressFormat.PNG, 100, + outputStream); + outputStream.flush(); + } catch (IOException e) { + Log.e(TAG, null, e); + } finally { + closeOutputStream(outputStream); + } + } + }); + } + }); + return inputStream; + } + + private static void closeOutputStream(OutputStream outputStream) { + try { + outputStream.close(); + } catch (IOException e) { + Log.e(TAG, null, e); + } + } + + private static final String TAG = "DefaultVideoPosterRequestHandler"; + private String mDefaultVideoPosterURL; + private AwContentsClient mContentClient; + + public DefaultVideoPosterRequestHandler(AwContentsClient contentClient) { + mDefaultVideoPosterURL = GenerateDefaulVideoPosterURL(); + mContentClient = contentClient; + } + + /** + * Used to get the image if the url is mDefaultVideoPosterURL. + * + * @param url the url requested + * @return InterceptedRequestData which caller can get the image if the url is + * the default video poster URL, otherwise null is returned. + */ + public InterceptedRequestData shouldInterceptRequest(final String url) { + if (!mDefaultVideoPosterURL.equals(url)) return null; + + try { + return new InterceptedRequestData("image/png", null, getInputStream(mContentClient)); + } catch (IOException e) { + Log.e(TAG, null, e); + return null; + } + } + + public String getDefaultVideoPosterURL() { + return mDefaultVideoPosterURL; + } + + /** + * @return a unique URL which has little chance to be used by application. + */ + private static String GenerateDefaulVideoPosterURL() { + Random randomGenerator = new Random(); + String path = String.valueOf(randomGenerator.nextLong()); + return "android-webview:default_video_poster/" + path; + } +} diff --git a/src/org/chromium/android_webview/ErrorCodeConversionHelper.java b/src/org/chromium/android_webview/ErrorCodeConversionHelper.java new file mode 100644 index 0000000..9b4d377 --- /dev/null +++ b/src/org/chromium/android_webview/ErrorCodeConversionHelper.java @@ -0,0 +1,143 @@ +// 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 org.chromium.net.NetError; + +/** + * This is a helper class to map native error code about loading a page to Android specific ones. + */ +public abstract class ErrorCodeConversionHelper { + // Success + public static final int ERROR_OK = 0; + // Generic error + public static final int ERROR_UNKNOWN = -1; + // Server or proxy hostname lookup failed + public static final int ERROR_HOST_LOOKUP = -2; + // Unsupported authentication scheme (not basic or digest) + public static final int ERROR_UNSUPPORTED_AUTH_SCHEME = -3; + // User authentication failed on server + public static final int ERROR_AUTHENTICATION = -4; + // User authentication failed on proxy + public static final int ERROR_PROXY_AUTHENTICATION = -5; + // Failed to connect to the server + public static final int ERROR_CONNECT = -6; + // Failed to read or write to the server + public static final int ERROR_IO = -7; + // Connection timed out + public static final int ERROR_TIMEOUT = -8; + // Too many redirects + public static final int ERROR_REDIRECT_LOOP = -9; + // Unsupported URI scheme + public static final int ERROR_UNSUPPORTED_SCHEME = -10; + // Failed to perform SSL handshake + public static final int ERROR_FAILED_SSL_HANDSHAKE = -11; + // Malformed URL + public static final int ERROR_BAD_URL = -12; + // Generic file error + public static final int ERROR_FILE = -13; + // File not found + public static final int ERROR_FILE_NOT_FOUND = -14; + // Too many requests during this load + public static final int ERROR_TOO_MANY_REQUESTS = -15; + + static int convertErrorCode(int netError) { + // Note: many NetError.Error constants don't have an obvious mapping. + // These will be handled by the default case, ERROR_UNKNOWN. + switch (netError) { + case NetError.ERR_UNSUPPORTED_AUTH_SCHEME: + return ERROR_UNSUPPORTED_AUTH_SCHEME; + + case NetError.ERR_INVALID_AUTH_CREDENTIALS: + case NetError.ERR_MISSING_AUTH_CREDENTIALS: + case NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT: + return ERROR_AUTHENTICATION; + + case NetError.ERR_TOO_MANY_REDIRECTS: + return ERROR_REDIRECT_LOOP; + + case NetError.ERR_UPLOAD_FILE_CHANGED: + return ERROR_FILE_NOT_FOUND; + + case NetError.ERR_INVALID_URL: + return ERROR_BAD_URL; + + case NetError.ERR_DISALLOWED_URL_SCHEME: + case NetError.ERR_UNKNOWN_URL_SCHEME: + return ERROR_UNSUPPORTED_SCHEME; + + case NetError.ERR_IO_PENDING: + case NetError.ERR_NETWORK_IO_SUSPENDED: + return ERROR_IO; + + case NetError.ERR_CONNECTION_TIMED_OUT: + case NetError.ERR_TIMED_OUT: + return ERROR_TIMEOUT; + + case NetError.ERR_FILE_TOO_BIG: + return ERROR_FILE; + + case NetError.ERR_HOST_RESOLVER_QUEUE_TOO_LARGE: + case NetError.ERR_INSUFFICIENT_RESOURCES: + case NetError.ERR_OUT_OF_MEMORY: + return ERROR_TOO_MANY_REQUESTS; + + case NetError.ERR_CONNECTION_CLOSED: + case NetError.ERR_CONNECTION_RESET: + case NetError.ERR_CONNECTION_REFUSED: + case NetError.ERR_CONNECTION_ABORTED: + case NetError.ERR_CONNECTION_FAILED: + case NetError.ERR_SOCKET_NOT_CONNECTED: + return ERROR_CONNECT; + + case NetError.ERR_INTERNET_DISCONNECTED: + case NetError.ERR_ADDRESS_INVALID: + case NetError.ERR_ADDRESS_UNREACHABLE: + case NetError.ERR_NAME_NOT_RESOLVED: + case NetError.ERR_NAME_RESOLUTION_FAILED: + return ERROR_HOST_LOOKUP; + + case NetError.ERR_SSL_PROTOCOL_ERROR: + case NetError.ERR_SSL_CLIENT_AUTH_CERT_NEEDED: + case NetError.ERR_TUNNEL_CONNECTION_FAILED: + case NetError.ERR_NO_SSL_VERSIONS_ENABLED: + case NetError.ERR_SSL_VERSION_OR_CIPHER_MISMATCH: + case NetError.ERR_SSL_RENEGOTIATION_REQUESTED: + case NetError.ERR_CERT_ERROR_IN_SSL_RENEGOTIATION: + case NetError.ERR_BAD_SSL_CLIENT_AUTH_CERT: + case NetError.ERR_SSL_NO_RENEGOTIATION: + case NetError.ERR_SSL_DECOMPRESSION_FAILURE_ALERT: + case NetError.ERR_SSL_BAD_RECORD_MAC_ALERT: + case NetError.ERR_SSL_UNSAFE_NEGOTIATION: + case NetError.ERR_SSL_WEAK_SERVER_EPHEMERAL_DH_KEY: + case NetError.ERR_SSL_CLIENT_AUTH_PRIVATE_KEY_ACCESS_DENIED: + case NetError.ERR_SSL_CLIENT_AUTH_CERT_NO_PRIVATE_KEY: + return ERROR_FAILED_SSL_HANDSHAKE; + + case NetError.ERR_PROXY_AUTH_UNSUPPORTED: + case NetError.ERR_PROXY_AUTH_REQUESTED: + case NetError.ERR_PROXY_CONNECTION_FAILED: + case NetError.ERR_UNEXPECTED_PROXY_AUTH: + return ERROR_PROXY_AUTHENTICATION; + + // The certificate errors are handled by onReceivedSslError + // and don't need to be reported here. + case NetError.ERR_CERT_COMMON_NAME_INVALID: + case NetError.ERR_CERT_DATE_INVALID: + case NetError.ERR_CERT_AUTHORITY_INVALID: + case NetError.ERR_CERT_CONTAINS_ERRORS: + case NetError.ERR_CERT_NO_REVOCATION_MECHANISM: + case NetError.ERR_CERT_UNABLE_TO_CHECK_REVOCATION: + case NetError.ERR_CERT_REVOKED: + case NetError.ERR_CERT_INVALID: + case NetError.ERR_CERT_WEAK_SIGNATURE_ALGORITHM: + case NetError.ERR_CERT_NON_UNIQUE_NAME: + return ERROR_OK; + + default: + return ERROR_UNKNOWN; + } + } +} diff --git a/src/org/chromium/android_webview/HttpAuthDatabase.java b/src/org/chromium/android_webview/HttpAuthDatabase.java new file mode 100644 index 0000000..0286a67 --- /dev/null +++ b/src/org/chromium/android_webview/HttpAuthDatabase.java @@ -0,0 +1,264 @@ +// 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.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.util.Log; + +/** + * This database is used to support WebView's setHttpAuthUsernamePassword and + * getHttpAuthUsernamePassword methods, and WebViewDatabase's clearHttpAuthUsernamePassword and + * hasHttpAuthUsernamePassword methods. + * + * While this class is intended to be used as a singleton, this property is not enforced in this + * layer, primarily for ease of testing. To line up with the classic implementation and behavior, + * there is no specific handling and reporting when SQL errors occur. + * + * Note on thread-safety: As per the classic implementation, most API functions have thread safety + * provided by the underlying SQLiteDatabase instance. The exception is database opening: this + * is handled in the dedicated background thread, which also provides a performance gain + * if triggered early on (e.g. as a side effect of CookieSyncManager.createInstance() call), + * sufficiently in advance of the first blocking usage of the API. + */ +public class HttpAuthDatabase { + + private static final String DATABASE_FILE = "http_auth.db"; + + private static final String LOGTAG = HttpAuthDatabase.class.getName(); + + private static final int DATABASE_VERSION = 1; + + private static HttpAuthDatabase sInstance = null; + + private SQLiteDatabase mDatabase = null; + + private static final String ID_COL = "_id"; + + private static final String[] ID_PROJECTION = new String[] { + ID_COL + }; + + // column id strings for "httpauth" table + private static final String HTTPAUTH_TABLE_NAME = "httpauth"; + private static final String HTTPAUTH_HOST_COL = "host"; + private static final String HTTPAUTH_REALM_COL = "realm"; + private static final String HTTPAUTH_USERNAME_COL = "username"; + private static final String HTTPAUTH_PASSWORD_COL = "password"; + + /** + * Initially false until the background thread completes. + */ + private boolean mInitialized = false; + + /** + * Create an instance of HttpAuthDatabase for the named file, and kick-off background + * initialization of that database. + * + * @param context the Context to use for opening the database + * @param databaseFile Name of the file to be initialized. + */ + public HttpAuthDatabase(final Context context, final String databaseFile) { + new Thread() { + @Override + public void run() { + initOnBackgroundThread(context, databaseFile); + } + }.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. + * + * @param context the Context to use for opening the database + * @param databaseFile Name of the file to be initialized. + */ + private synchronized void initOnBackgroundThread(Context context, String databaseFile) { + if (mInitialized) { + return; + } + + initDatabase(context, databaseFile); + + // Thread done, notify. + mInitialized = true; + notifyAll(); + } + + /** + * Opens the database, and upgrades it if necessary. + * + * @param context the Context to use for opening the database + * @param databaseFile Name of the file to be initialized. + */ + private void initDatabase(Context context, String databaseFile) { + try { + mDatabase = context.openOrCreateDatabase(databaseFile, 0, null); + } catch (SQLiteException e) { + // try again by deleting the old db and create a new one + if (context.deleteDatabase(databaseFile)) { + mDatabase = context.openOrCreateDatabase(databaseFile, 0, null); + } + } + + if (mDatabase == null) { + // Not much we can do to recover at this point + Log.e(LOGTAG, "Unable to open or create " + databaseFile); + return; + } + + if (mDatabase.getVersion() != DATABASE_VERSION) { + mDatabase.beginTransactionNonExclusive(); + try { + createTable(); + mDatabase.setTransactionSuccessful(); + } finally { + mDatabase.endTransaction(); + } + } + } + + private void createTable() { + mDatabase.execSQL("CREATE TABLE " + HTTPAUTH_TABLE_NAME + + " (" + ID_COL + " INTEGER PRIMARY KEY, " + + HTTPAUTH_HOST_COL + " TEXT, " + HTTPAUTH_REALM_COL + + " TEXT, " + HTTPAUTH_USERNAME_COL + " TEXT, " + + HTTPAUTH_PASSWORD_COL + " TEXT," + " UNIQUE (" + + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL + + ") ON CONFLICT REPLACE);"); + + mDatabase.setVersion(DATABASE_VERSION); + } + + /** + * Waits for the background initialization thread to complete and check the database creation + * status. + * + * @return true if the database was initialized, false otherwise + */ + private boolean waitForInit() { + synchronized (this) { + while (!mInitialized) { + try { + wait(); + } catch (InterruptedException e) { + Log.e(LOGTAG, "Caught exception while checking initialization", e); + } + } + } + return mDatabase != null; + } + + /** + * Sets the HTTP authentication password. Tuple (HTTPAUTH_HOST_COL, HTTPAUTH_REALM_COL, + * HTTPAUTH_USERNAME_COL) is unique. + * + * @param host the host for the password + * @param realm the realm for the password + * @param username the username for the password. + * @param password the password + */ + public void setHttpAuthUsernamePassword(String host, String realm, String username, + String password) { + if (host == null || realm == null || !waitForInit()) { + return; + } + + final ContentValues c = new ContentValues(); + c.put(HTTPAUTH_HOST_COL, host); + c.put(HTTPAUTH_REALM_COL, realm); + c.put(HTTPAUTH_USERNAME_COL, username); + c.put(HTTPAUTH_PASSWORD_COL, password); + mDatabase.insert(HTTPAUTH_TABLE_NAME, HTTPAUTH_HOST_COL, c); + } + + /** + * Retrieves the HTTP authentication username and password for a given host and realm pair. If + * there are multiple username/password combinations for a host/realm, only the first one will + * be returned. + * + * @param host the host the password applies to + * @param realm the realm the password applies to + * @return a String[] if found where String[0] is username (which can be null) and + * String[1] is password. Null is returned if it can't find anything. + */ + public String[] getHttpAuthUsernamePassword(String host, String realm) { + if (host == null || realm == null || !waitForInit()){ + return null; + } + + final String[] columns = new String[] { + HTTPAUTH_USERNAME_COL, HTTPAUTH_PASSWORD_COL + }; + final String selection = "(" + HTTPAUTH_HOST_COL + " == ?) AND " + + "(" + HTTPAUTH_REALM_COL + " == ?)"; + + String[] ret = null; + Cursor cursor = null; + try { + cursor = mDatabase.query(HTTPAUTH_TABLE_NAME, columns, selection, + new String[] { host, realm }, null, null, null); + if (cursor.moveToFirst()) { + ret = new String[] { + cursor.getString(cursor.getColumnIndex(HTTPAUTH_USERNAME_COL)), + cursor.getString(cursor.getColumnIndex(HTTPAUTH_PASSWORD_COL)), + }; + } + } catch (IllegalStateException e) { + Log.e(LOGTAG, "getHttpAuthUsernamePassword", e); + } finally { + if (cursor != null) cursor.close(); + } + return ret; + } + + /** + * Determines if there are any HTTP authentication passwords saved. + * + * @return true if there are passwords saved + */ + public boolean hasHttpAuthUsernamePassword() { + if (!waitForInit()) { + return false; + } + + Cursor cursor = null; + boolean ret = false; + try { + cursor = mDatabase.query(HTTPAUTH_TABLE_NAME, ID_PROJECTION, null, null, null, null, + null); + ret = cursor.moveToFirst(); + } catch (IllegalStateException e) { + Log.e(LOGTAG, "hasEntries", e); + } finally { + if (cursor != null) cursor.close(); + } + return ret; + } + + /** + * Clears the HTTP authentication password database. + */ + public void clearHttpAuthUsernamePassword() { + if (!waitForInit()) { + return; + } + mDatabase.delete(HTTPAUTH_TABLE_NAME, null, null); + } +} diff --git a/src/org/chromium/android_webview/InterceptedRequestData.java b/src/org/chromium/android_webview/InterceptedRequestData.java new file mode 100644 index 0000000..f6f3d9d --- /dev/null +++ b/src/org/chromium/android_webview/InterceptedRequestData.java @@ -0,0 +1,41 @@ +// 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 org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; + +import java.io.InputStream; + +/** + * The response information that is to be returned for a particular resource fetch. + */ +@JNINamespace("android_webview") +public class InterceptedRequestData { + private String mMimeType; + private String mCharset; + private InputStream mData; + + public InterceptedRequestData(String mimeType, String encoding, InputStream data) { + mMimeType = mimeType; + mCharset = encoding; + mData = data; + } + + @CalledByNative + public String getMimeType() { + return mMimeType; + } + + @CalledByNative + public String getCharset() { + return mCharset; + } + + @CalledByNative + public InputStream getData() { + return mData; + } +} diff --git a/src/org/chromium/android_webview/JavaBrowserViewRendererHelper.java b/src/org/chromium/android_webview/JavaBrowserViewRendererHelper.java new file mode 100644 index 0000000..d4e79c8 --- /dev/null +++ b/src/org/chromium/android_webview/JavaBrowserViewRendererHelper.java @@ -0,0 +1,56 @@ +// 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.android_webview; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Picture; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; + +/** + * Provides auxiliary methods related to Picture objects and native SkPictures. + */ +@JNINamespace("android_webview") +public class JavaBrowserViewRendererHelper { + + /** + * Provides a Bitmap object with a given width and height used for auxiliary rasterization. + */ + @CalledByNative + private static Bitmap createBitmap(int width, int height) { + return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + } + + /** + * Draws a provided bitmap into a canvas. + * 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); + } + + /** + * Creates a new Picture that records drawing a provided bitmap. + * Will return an empty Picture if the Bitmap is null. + */ + @CalledByNative + private static Picture recordBitmapIntoPicture(Bitmap bitmap) { + Picture picture = new Picture(); + if (bitmap != null) { + Canvas recordingCanvas = picture.beginRecording(bitmap.getWidth(), bitmap.getHeight()); + drawBitmapIntoCanvas(bitmap, recordingCanvas); + picture.endRecording(); + } + return picture; + } + + // Should never be instantiated. + private JavaBrowserViewRendererHelper() { + } +} diff --git a/src/org/chromium/android_webview/JsPromptResultReceiver.java b/src/org/chromium/android_webview/JsPromptResultReceiver.java new file mode 100644 index 0000000..232140e --- /dev/null +++ b/src/org/chromium/android_webview/JsPromptResultReceiver.java @@ -0,0 +1,17 @@ +// 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; + +/** + * This interface is used when the AwContentsClient offers a JavaScript + * modal prompt dialog to enable the client to handle the dialog in their own way. + * AwContentsClient will offer an object that implements this interface to the + * client and when the client has handled the dialog, it must either callback with + * confirm() or cancel() to allow processing to continue. + */ +public interface JsPromptResultReceiver { + public void confirm(String result); + public void cancel(); +} diff --git a/src/org/chromium/android_webview/JsResultHandler.java b/src/org/chromium/android_webview/JsResultHandler.java new file mode 100644 index 0000000..30d4898 --- /dev/null +++ b/src/org/chromium/android_webview/JsResultHandler.java @@ -0,0 +1,46 @@ +// 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 org.chromium.base.ThreadUtils; + +class JsResultHandler implements JsResultReceiver, JsPromptResultReceiver { + private AwContentsClientBridge mBridge; + private final int mId; + + JsResultHandler(AwContentsClientBridge bridge, int id) { + mBridge = bridge; + mId = id; + } + + @Override + public void confirm() { + confirm(null); + } + + @Override + public void confirm(final String promptResult) { + ThreadUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + if (mBridge != null) + mBridge.confirmJsResult(mId, promptResult); + mBridge = null; + } + }); + } + + @Override + public void cancel() { + ThreadUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + if (mBridge != null) + mBridge.cancelJsResult(mId); + mBridge = null; + } + }); + } +} diff --git a/src/org/chromium/android_webview/JsResultReceiver.java b/src/org/chromium/android_webview/JsResultReceiver.java new file mode 100644 index 0000000..30df245 --- /dev/null +++ b/src/org/chromium/android_webview/JsResultReceiver.java @@ -0,0 +1,18 @@ +// 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; + +/** + * This interface is used when the AwContentsClient offers a JavaScript + * modal dialog (alert, beforeunload or confirm) to enable the client to + * handle the dialog in their own way. AwContentsClient will offer an object + * that implements this interface to the client and when the client has handled + * the dialog, it must either callback with confirm() or cancel() to allow + * processing to continue. + */ +public interface JsResultReceiver { + public void confirm(); + public void cancel(); +} diff --git a/src/org/chromium/android_webview/SslUtil.java b/src/org/chromium/android_webview/SslUtil.java new file mode 100644 index 0000000..41b4c18 --- /dev/null +++ b/src/org/chromium/android_webview/SslUtil.java @@ -0,0 +1,62 @@ +// 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.android_webview; + +import android.net.http.SslCertificate; +import android.net.http.SslError; +import android.util.Log; + +import org.chromium.net.NetError; +import org.chromium.net.X509Util; + +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +public class SslUtil { + private static final String TAG = SslUtil.class.getSimpleName(); + + /** + * Creates an SslError object from a chromium net error code. + */ + public static SslError sslErrorFromNetErrorCode(int error, SslCertificate cert, String url) { + assert (error >= NetError.ERR_CERT_END && error <= NetError.ERR_CERT_COMMON_NAME_INVALID); + switch(error) { + case NetError.ERR_CERT_COMMON_NAME_INVALID: + return new SslError(SslError.SSL_IDMISMATCH, cert, url); + case NetError.ERR_CERT_DATE_INVALID: + return new SslError(SslError.SSL_DATE_INVALID, cert, url); + case NetError.ERR_CERT_AUTHORITY_INVALID: + return new SslError(SslError.SSL_UNTRUSTED, cert, url); + default: + break; + } + // Map all other codes to SSL_INVALID. + return new SslError(SslError.SSL_INVALID, cert, url); + } + + public static SslCertificate getCertificateFromDerBytes(byte[] derBytes) { + if (derBytes == null) { + return null; + } + + try { + X509Certificate x509Certificate = + X509Util.createCertificateFromBytes(derBytes); + return new SslCertificate(x509Certificate); + } catch (CertificateException e) { + // A SSL related exception must have occured. This shouldn't happen. + Log.w(TAG, "Could not read certificate: " + e); + } catch (KeyStoreException e) { + // A SSL related exception must have occured. This shouldn't happen. + Log.w(TAG, "Could not read certificate: " + e); + } catch (NoSuchAlgorithmException e) { + // A SSL related exception must have occured. This shouldn't happen. + Log.w(TAG, "Could not read certificate: " + e); + } + return null; + } +} \ No newline at end of file diff --git a/src/org/chromium/base/AccessedByNative.java b/src/org/chromium/base/AccessedByNative.java new file mode 100644 index 0000000..0a73258 --- /dev/null +++ b/src/org/chromium/base/AccessedByNative.java @@ -0,0 +1,20 @@ +// 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.base; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @AccessedByNative is used to ensure proguard will keep this field, since it's + * only accessed by native. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.CLASS) +public @interface AccessedByNative { + public String value() default ""; +} diff --git a/src/org/chromium/base/ActivityState.java b/src/org/chromium/base/ActivityState.java new file mode 100644 index 0000000..c3c8980 --- /dev/null +++ b/src/org/chromium/base/ActivityState.java @@ -0,0 +1,9 @@ +package org.chromium.base; +interface ActivityState { +public final int CREATED = 1; +public final int STARTED = 2; +public final int RESUMED = 3; +public final int PAUSED = 4; +public final int STOPPED = 5; +public final int DESTROYED = 6; +} diff --git a/src/org/chromium/base/ActivityState.template b/src/org/chromium/base/ActivityState.template new file mode 100644 index 0000000..adf990a --- /dev/null +++ b/src/org/chromium/base/ActivityState.template @@ -0,0 +1,14 @@ +// 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.base; + +// A simple auto-generated interface used to list the various +// states of an activity as used by both org.chromium.base.ActivityStatus +// and base/android/activity_status.h +interface ActivityState { +#define DEFINE_ACTIVITY_STATE(x,y) public final int x = y; +#include "base/android/activity_state_list.h" +#undef DEFINE_ACTIVITY_STATE +} diff --git a/src/org/chromium/base/ActivityStatus.java b/src/org/chromium/base/ActivityStatus.java new file mode 100644 index 0000000..4747234 --- /dev/null +++ b/src/org/chromium/base/ActivityStatus.java @@ -0,0 +1,136 @@ +// 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.base; + +import android.app.Activity; +import android.os.Handler; +import android.os.Looper; + +/** + * Provides information about the current activity's status, and a way + * to register / unregister listeners for state changes. + */ +@JNINamespace("base::android") +public class ActivityStatus { + + // Constants matching activity states reported to StateListener.onStateChange + // As an implementation detail, these are now defined in the auto-generated + // ActivityState interface, to be shared with C++. + public static final int CREATED = ActivityState.CREATED; + public static final int STARTED = ActivityState.STARTED; + public static final int RESUMED = ActivityState.RESUMED; + public static final int PAUSED = ActivityState.PAUSED; + public static final int STOPPED = ActivityState.STOPPED; + public static final int DESTROYED = ActivityState.DESTROYED; + + // Current main activity, or null if none. + private static Activity sActivity; + + // Current main activity's state. This can be set even if sActivity is null, to simplify unit + // testing. + private static int sActivityState; + + private static final ObserverList sStateListeners = + new ObserverList(); + + /** + * Interface to be implemented by listeners. + */ + public interface StateListener { + /** + * Called when the activity's state changes. + * @param newState New activity state. + */ + public void onActivityStateChange(int newState); + } + + private ActivityStatus() {} + + /** + * Must be called by the main activity when it changes state. + * @param activity Current activity. + * @param newState New state value. + */ + public static void onStateChange(Activity activity, int newState) { + if (sActivity != activity) { + // ActivityStatus is notified with the CREATED event very late during the main activity + // creation to avoid making startup performance worse than it is by notifying observers + // that could do some expensive work. This can lead to non-CREATED events being fired + // before the CREATED event which is problematic. + // TODO(pliard): fix http://crbug.com/176837. + sActivity = activity; + } + sActivityState = newState; + for (StateListener listener : sStateListeners) { + listener.onActivityStateChange(newState); + } + if (newState == DESTROYED) { + sActivity = null; + } + } + + /** + * Indicates that the parent activity is currently paused. + */ + public static boolean isPaused() { + return sActivityState == PAUSED; + } + + /** + * Returns the current main application activity. + */ + public static Activity getActivity() { + return sActivity; + } + + /** + * Returns the current main application activity's state. + */ + public static int getState() { + return sActivityState; + } + + /** + * Registers the given listener to receive activity state changes. + * @param listener Listener to receive state changes. + */ + public static void registerStateListener(StateListener listener) { + sStateListeners.addObserver(listener); + } + + /** + * Unregisters the given listener from receiving activity state changes. + * @param listener Listener that doesn't want to receive state changes. + */ + public static void unregisterStateListener(StateListener listener) { + sStateListeners.removeObserver(listener); + } + + /** + * Registers the single thread-safe native activity status listener. + * This handles the case where the caller is not on the main thread. + * Note that this is used by a leaky singleton object from the native + * side, hence lifecycle management is greatly simplified. + */ + @CalledByNative + private static void registerThreadSafeNativeStateListener() { + ThreadUtils.runOnUiThread(new Runnable () { + @Override + public void run() { + // Register a new listener that calls nativeOnActivityStateChange. + sStateListeners.addObserver(new StateListener() { + @Override + public void onActivityStateChange(int newState) { + nativeOnActivityStateChange(newState); + } + }); + } + }); + } + + // Called to notify the native side of state changes. + // IMPORTANT: This is always called on the main thread! + private static native void nativeOnActivityStateChange(int newState); +} diff --git a/src/org/chromium/base/BuildInfo.java b/src/org/chromium/base/BuildInfo.java new file mode 100644 index 0000000..2314051 --- /dev/null +++ b/src/org/chromium/base/BuildInfo.java @@ -0,0 +1,114 @@ +// 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.base; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; +import android.util.Log; + +import org.chromium.base.CalledByNative; + +/** + * BuildInfo is a utility class providing easy access to {@link PackageInfo} + * information. This is primarly of use for accessesing package information + * from native code. + */ +public class BuildInfo { + private static final String TAG = "BuildInfo"; + private static final int MAX_FINGERPRINT_LENGTH = 128; + + /** + * BuildInfo is a static utility class and therefore shouldn't be + * instantiated. + */ + private BuildInfo() { + } + + @CalledByNative + public static String getDevice() { + return Build.DEVICE; + } + + @CalledByNative + public static String getBrand() { + return Build.BRAND; + } + + @CalledByNative + public static String getAndroidBuildId() { + return Build.ID; + } + + /** + * @return The build fingerprint for the current Android install. The value is truncated to a + * 128 characters as this is used for crash and UMA reporting, which should avoid huge + * strings. + */ + @CalledByNative + public static String getAndroidBuildFingerprint() { + return Build.FINGERPRINT.substring( + 0, Math.min(Build.FINGERPRINT.length(), MAX_FINGERPRINT_LENGTH)); + } + + @CalledByNative + public static String getDeviceModel() { + return Build.MODEL; + } + + @CalledByNative + public static String getPackageVersionCode(Context context) { + String msg = "versionCode not available."; + try { + PackageManager pm = context.getPackageManager(); + PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0); + msg = "" + pi.versionCode; + } catch (NameNotFoundException e) { + Log.d(TAG, msg); + } + return msg; + + } + + @CalledByNative + public static String getPackageVersionName(Context context) { + String msg = "versionName not available"; + try { + PackageManager pm = context.getPackageManager(); + PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0); + msg = pi.versionName; + } catch (NameNotFoundException e) { + Log.d(TAG, msg); + } + return msg; + } + + @CalledByNative + public static String getPackageLabel(Context context) { + try { + PackageManager packageManager = context.getPackageManager(); + ApplicationInfo appInfo = packageManager.getApplicationInfo(context.getPackageName(), + PackageManager.GET_META_DATA); + CharSequence label = packageManager.getApplicationLabel(appInfo); + return label != null ? label.toString() : ""; + } catch (NameNotFoundException e) { + return ""; + } + } + + @CalledByNative + public static String getPackageName(Context context) { + String packageName = context != null ? context.getPackageName() : null; + return packageName != null ? packageName : ""; + } + + @CalledByNative + public static int getSdkInt() { + return Build.VERSION.SDK_INT; + } +} diff --git a/src/org/chromium/base/CalledByNative.java b/src/org/chromium/base/CalledByNative.java new file mode 100644 index 0000000..db01b0d --- /dev/null +++ b/src/org/chromium/base/CalledByNative.java @@ -0,0 +1,23 @@ +// 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.base; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @CalledByNative is used by the JNI generator to create the necessary JNI + * bindings and expose this method to native code. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.CLASS) +public @interface CalledByNative { + /* + * If present, tells which inner class the method belongs to. + */ + public String value() default ""; +} diff --git a/src/org/chromium/base/CalledByNativeUnchecked.java b/src/org/chromium/base/CalledByNativeUnchecked.java new file mode 100644 index 0000000..38bb0c0 --- /dev/null +++ b/src/org/chromium/base/CalledByNativeUnchecked.java @@ -0,0 +1,27 @@ +// 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.base; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @CalledByNativeUnchecked is used to generate JNI bindings that do not check for exceptions. + * It only makes sense to use this annotation on methods that declare a throws... spec. + * However, note that the exception received native side maybe an 'unchecked' (RuntimeExpception) + * such as NullPointerException, so the native code should differentiate these cases. + * Usage of this should be very rare; where possible handle exceptions in the Java side and use a + * return value to indicate success / failure. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.CLASS) +public @interface CalledByNativeUnchecked { + /* + * If present, tells which inner class the method belongs to. + */ + public String value() default ""; +} diff --git a/src/org/chromium/base/ChromiumActivity.java b/src/org/chromium/base/ChromiumActivity.java new file mode 100644 index 0000000..65f5ce9 --- /dev/null +++ b/src/org/chromium/base/ChromiumActivity.java @@ -0,0 +1,49 @@ +// 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.base; + +import android.app.Activity; +import android.os.Bundle; + +// All Chromium main activities should extend this class. This allows various sub-systems to +// properly react to activity state changes. +public class ChromiumActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstance) { + super.onCreate(savedInstance); + ActivityStatus.onStateChange(this, ActivityStatus.CREATED); + } + + @Override + protected void onStart() { + super.onStart(); + ActivityStatus.onStateChange(this, ActivityStatus.STARTED); + } + + @Override + protected void onResume() { + super.onResume(); + ActivityStatus.onStateChange(this, ActivityStatus.RESUMED); + } + + @Override + protected void onPause() { + ActivityStatus.onStateChange(this, ActivityStatus.PAUSED); + super.onPause(); + } + + @Override + protected void onStop() { + ActivityStatus.onStateChange(this, ActivityStatus.STOPPED); + super.onStop(); + } + + @Override + protected void onDestroy() { + ActivityStatus.onStateChange(this, ActivityStatus.DESTROYED); + super.onDestroy(); + } +} diff --git a/src/org/chromium/base/ContextTypes.java b/src/org/chromium/base/ContextTypes.java new file mode 100644 index 0000000..35ecd8f --- /dev/null +++ b/src/org/chromium/base/ContextTypes.java @@ -0,0 +1,96 @@ +// 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.base; + +import android.content.Context; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.Map; + +/** + * Maintains the {@link Context}-to-"context type" mapping. The context type + * {@code MODE_APP} is chosen for the application context associated with + * the activity running in application mode, while {@code MODE_NORMAL} for main + * Chromium activity. + * + *

Used as singleton instance. + */ +public class ContextTypes { + + // Available context types. + public static final int CONTEXT_TYPE_NORMAL = 1; + public static final int CONTEXT_TYPE_WEBAPP = 2; + + private final Map mContextMap; + + private ContextTypes() { + mContextMap = new ConcurrentHashMap(); + } + + private static class ContextTypesHolder { + private static final ContextTypes INSTANCE = new ContextTypes(); + } + + public static ContextTypes getInstance() { + return ContextTypesHolder.INSTANCE; + } + + /** + * Adds the mapping for the given {@link Context}. + * + * @param context {@link Context} in interest + * @param type the type associated with the context + * @throws IllegalArgumentException if type is not a valid one. + */ + public void put(Context context, int type) throws IllegalArgumentException { + if (type != CONTEXT_TYPE_NORMAL && type != CONTEXT_TYPE_WEBAPP) { + throw new IllegalArgumentException("Wrong context type"); + } + mContextMap.put(context, type); + } + + /** + * Removes the mapping for the given context. + * + * @param context {@link Context} in interest + */ + public void remove(Context context) { + mContextMap.remove(context); + } + + /** + * Returns type of the given context. + * + * @param context {@link Context} in interest + * @return type associated with the context. Returns {@code MODE_NORMAL} by + * default if the mapping for the queried context is not present. + */ + public int getType(Context context) { + Integer contextType = mContextMap.get(context); + return contextType == null ? CONTEXT_TYPE_NORMAL : contextType; + } + + /** + * Returns whether activity is running in web app mode. + * + * @param appContext {@link Context} in interest + * @return {@code true} when activity is running in web app mode. + */ + @CalledByNative + public static boolean isRunningInWebapp(Context appContext) { + return ContextTypes.getInstance().getType(appContext) + == CONTEXT_TYPE_WEBAPP; + } + + /** + * Checks if the mapping exists for the given context. + * + * @param context {@link Context} in interest + * @return {@code true} if the mapping exists; otherwise {@code false} + */ + public boolean contains(Context context) { + return mContextMap.containsKey(context); + } +} diff --git a/src/org/chromium/base/CpuFeatures.java b/src/org/chromium/base/CpuFeatures.java new file mode 100644 index 0000000..f298fb1 --- /dev/null +++ b/src/org/chromium/base/CpuFeatures.java @@ -0,0 +1,40 @@ +// 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.base; + +// The only purpose of this class is to allow sending CPU properties +// from the browser process to sandboxed renderer processes. This is +// needed because sandboxed processes cannot, on ARM, query the kernel +// about the CPU's properties by parsing /proc, so this operation must +// be performed in the browser process, and the result passed to +// renderer ones. +// +// For more context, see http://crbug.com/164154 +// +// Technically, this is a wrapper around the native NDK cpufeatures +// library. The exact CPU features bits are never used in Java so +// there is no point in duplicating their definitions here. +// +@JNINamespace("base::android") +public abstract class CpuFeatures { + /** + * Return the number of CPU Cores on the device. + */ + public static int getCount() { + return nativeGetCoreCount(); + } + + /** + * Return the CPU feature mask. + * This is a 64-bit integer that corresponds to the CPU's features. + * The value comes directly from android_getCpuFeatures(). + */ + public static long getMask() { + return nativeGetCpuFeatures(); + } + + private static native int nativeGetCoreCount(); + private static native long nativeGetCpuFeatures(); +} diff --git a/src/org/chromium/base/ImportantFileWriterAndroid.java b/src/org/chromium/base/ImportantFileWriterAndroid.java new file mode 100644 index 0000000..1c7c018 --- /dev/null +++ b/src/org/chromium/base/ImportantFileWriterAndroid.java @@ -0,0 +1,29 @@ +// 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.base; + +/** + * This class provides an interface to the native class for writing + * important data files without risking data loss. + */ +@JNINamespace("base::android") +public class ImportantFileWriterAndroid { + + /** + * Write a binary file atomically. + * + * This either writes all the data or leaves the file unchanged. + * + * @param fileName The complete path of the file to be written + * @param data The data to be written to the file + * @return true if the data was written to the file, false if not. + */ + public static boolean writeFileAtomically(String fileName, byte[] data) { + return nativeWriteFileAtomically(fileName, data); + } + + private static native boolean nativeWriteFileAtomically( + String fileName, byte[] data); +} diff --git a/src/org/chromium/base/JNINamespace.java b/src/org/chromium/base/JNINamespace.java new file mode 100644 index 0000000..cfffc91 --- /dev/null +++ b/src/org/chromium/base/JNINamespace.java @@ -0,0 +1,20 @@ +// 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.base; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @JNINamespace is used by the JNI generator to create the necessary JNI + * bindings and expose this method to native code using the specified namespace. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface JNINamespace { + public String value(); +} diff --git a/src/org/chromium/base/NativeClassQualifiedName.java b/src/org/chromium/base/NativeClassQualifiedName.java new file mode 100644 index 0000000..309169b --- /dev/null +++ b/src/org/chromium/base/NativeClassQualifiedName.java @@ -0,0 +1,25 @@ +// 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.base; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @NativeClassQualifiedName is used by the JNI generator to create the necessary JNI + * bindings to call into the specified native class name. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface NativeClassQualifiedName { + /* + * Tells which native class the method is going to be bound to. + * The first parameter of the annotated method must be an int nativePtr pointing to + * an instance of this class. + */ + public String value(); +} diff --git a/src/org/chromium/base/ObserverList.java b/src/org/chromium/base/ObserverList.java new file mode 100644 index 0000000..13a81c5 --- /dev/null +++ b/src/org/chromium/base/ObserverList.java @@ -0,0 +1,177 @@ +// 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 java.lang.Iterable; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import javax.annotation.concurrent.NotThreadSafe; + +/** + * A container for a list of observers. + *

+ * This container can be modified during iteration without invalidating the iterator. + * So, it safely handles the case of an observer removing itself or other observers from the list + * while observers are being notified. + *

+ * The implementation (and the interface) is heavily influenced by the C++ ObserverList. + * Notable differences: + * - The iterator implements NOTIFY_EXISTING_ONLY. + * - The FOR_EACH_OBSERVER closure is left to the clients to implement in terms of iterator(). + *

+ * This class is not threadsafe. Observers MUST be added, removed and will be notified on the same + * thread this is created. + */ +@NotThreadSafe +public class ObserverList implements Iterable { + public final List mObservers = new ArrayList(); + private int mIterationDepth = 0; + + public ObserverList() {} + + /** + * Add an observer to the list. + *

+ * An observer should not be added to the same list more than once. If an iteration is already + * in progress, this observer will be not be visible during that iteration. + */ + public void addObserver(E obs) { + // Avoid adding null elements to the list as they may be removed on a compaction. + if (obs == null || mObservers.contains(obs)) { + assert false; + return; + } + + // Structurally modifying the underlying list here. This means we + // cannot use the underlying list's iterator to iterate over the list. + mObservers.add(obs); + } + + /** + * Remove an observer from the list if it is in the list. + */ + public void removeObserver(E obs) { + int index = mObservers.indexOf(obs); + + if (index == -1) + return; + + if (mIterationDepth == 0) { + // No one is iterating over the list. + mObservers.remove(obs); + } else { + mObservers.set(index, null); + } + } + + public boolean hasObserver(E obs) { + return mObservers.contains(obs); + } + + public void clear() { + if (mIterationDepth == 0) { + mObservers.clear(); + return; + } + + int size = mObservers.size(); + for (int i = 0; i < size; i++) + mObservers.set(i, null); + } + + @Override + public Iterator iterator() { + return new ObserverListIterator(); + } + + /** + * Compact the underlying list be removing null elements. + *

+ * Should only be called when mIterationDepth is zero. + */ + private void compact() { + assert mIterationDepth == 0; + // Safe to use the underlying list's iterator, as we know that no-one else + // is iterating over the list. + Iterator it = mObservers.iterator(); + while (it.hasNext()) { + E el = it.next(); + if (el == null) + it.remove(); + } + } + + private void incrementIterationDepth() { + mIterationDepth++; + } + + private void decrementIterationDepthAndCompactIfNeeded() { + mIterationDepth--; + assert mIterationDepth >= 0; + if (mIterationDepth == 0) + compact(); + } + + private int getSize() { + return mObservers.size(); + } + + private E getObserverAt(int index) { + return mObservers.get(index); + } + + private class ObserverListIterator implements Iterator { + private final int mListEndMarker; + private int mIndex = 0; + private boolean mIsExhausted = false; + + private ObserverListIterator() { + ObserverList.this.incrementIterationDepth(); + mListEndMarker = ObserverList.this.getSize(); + } + + @Override + public boolean hasNext() { + int lookupIndex = mIndex; + while (lookupIndex < mListEndMarker && + ObserverList.this.getObserverAt(lookupIndex) == null) + lookupIndex++; + if (lookupIndex < mListEndMarker) + return true; + + // We have reached the end of the list, allow for compaction. + compactListIfNeeded(); + return false; + } + + @Override + public E next() { + // Advance if the current element is null. + while (mIndex < mListEndMarker && ObserverList.this.getObserverAt(mIndex) == null) + mIndex++; + if (mIndex < mListEndMarker) + return ObserverList.this.getObserverAt(mIndex++); + + // We have reached the end of the list, allow for compaction. + compactListIfNeeded(); + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private void compactListIfNeeded() { + if (!mIsExhausted) { + mIsExhausted = true; + ObserverList.this.decrementIterationDepthAndCompactIfNeeded(); + } + } + } +} diff --git a/src/org/chromium/base/PathService.java b/src/org/chromium/base/PathService.java new file mode 100644 index 0000000..dfda736 --- /dev/null +++ b/src/org/chromium/base/PathService.java @@ -0,0 +1,24 @@ +// 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.base; + +/** + * This class provides java side access to the native PathService. + */ +@JNINamespace("base::android") +public abstract class PathService { + + // Must match the value of DIR_MODULE in base/base_paths.h! + public static final int DIR_MODULE = 3; + + // Prevent instantiation. + private PathService() {} + + public static void override(int what, String path) { + nativeOverride(what, path); + } + + private static native void nativeOverride(int what, String path); +} diff --git a/src/org/chromium/base/PathUtils.java b/src/org/chromium/base/PathUtils.java new file mode 100644 index 0000000..aee5c05 --- /dev/null +++ b/src/org/chromium/base/PathUtils.java @@ -0,0 +1,108 @@ +// 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.base; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.os.Environment; + +import java.io.File; + +/** + * This class provides the path related methods for the native library. + */ +public abstract class PathUtils { + + private static String sDataDirectorySuffix; + private static String sWebappDirectorySuffix; + private static String sWebappCacheDirectory; + + // Prevent instantiation. + private PathUtils() {} + + /** + * Sets the suffix that should be used for the directory where private data is to be stored + * by the application. + * @param suffix The private data directory suffix. + * @see Context#getDir(String, int) + */ + public static void setPrivateDataDirectorySuffix(String suffix) { + sDataDirectorySuffix = suffix; + } + + /** + * Sets the directory info used for chrome process running in application mode. + * + * @param webappSuffix The suffix of the directory used for storing webapp-specific profile + * @param cacheDir Cache directory name for web apps. + */ + public static void setWebappDirectoryInfo(String webappSuffix, String cacheDir) { + sWebappDirectorySuffix = webappSuffix; + sWebappCacheDirectory = cacheDir; + } + + /** + * @return the private directory that is used to store application data. + */ + @CalledByNative + public static String getDataDirectory(Context appContext) { + if (sDataDirectorySuffix == null) { + throw new IllegalStateException( + "setDataDirectorySuffix must be called before getDataDirectory"); + } + return appContext.getDir(sDataDirectorySuffix, Context.MODE_PRIVATE).getPath(); + } + + /** + * @return the cache directory. + */ + @SuppressWarnings("unused") + @CalledByNative + public static String getCacheDirectory(Context appContext) { + if (ContextTypes.getInstance().getType(appContext) == ContextTypes.CONTEXT_TYPE_NORMAL) { + return appContext.getCacheDir().getPath(); + } + if (sWebappDirectorySuffix == null || sWebappCacheDirectory == null) { + throw new IllegalStateException( + "setWebappDirectoryInfo must be called before getCacheDirectory"); + } + return new File(appContext.getDir(sWebappDirectorySuffix, appContext.MODE_PRIVATE), + sWebappCacheDirectory).getPath(); + } + + /** + * @return the public downloads directory. + */ + @SuppressWarnings("unused") + @CalledByNative + private static String getDownloadsDirectory(Context appContext) { + return Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS).getPath(); + } + + /** + * @return the path to native libraries. + */ + @SuppressWarnings("unused") + @CalledByNative + private static String getNativeLibraryDirectory(Context appContext) { + ApplicationInfo ai = appContext.getApplicationInfo(); + if ((ai.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 || + (ai.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { + return ai.nativeLibraryDir; + } + + return "/system/lib/"; + } + + /** + * @return the external storage directory. + */ + @SuppressWarnings("unused") + @CalledByNative + public static String getExternalStorageDirectory() { + return Environment.getExternalStorageDirectory().getAbsolutePath(); + } +} diff --git a/src/org/chromium/base/PowerMonitor.java b/src/org/chromium/base/PowerMonitor.java new file mode 100644 index 0000000..b7a691e --- /dev/null +++ b/src/org/chromium/base/PowerMonitor.java @@ -0,0 +1,92 @@ +// 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.base; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; +import android.os.Handler; +import android.os.Looper; + + +/** + * Integrates native PowerMonitor with the java side. + */ +@JNINamespace("base::android") +public class PowerMonitor implements ActivityStatus.StateListener { + private static final long SUSPEND_DELAY_MS = 1 * 60 * 1000; // 1 minute. + private static class LazyHolder { + private static final PowerMonitor INSTANCE = new PowerMonitor(); + } + private static PowerMonitor sInstance; + + private boolean mIsBatteryPower; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + // Asynchronous task used to fire the "paused" event to the native side 1 minute after the main + // activity transitioned to the "paused" state. This event is not sent immediately because it + // would be too aggressive. An Android activity can be in the "paused" state quite often. This + // can happen when a dialog window shows up for instance. + private static final Runnable sSuspendTask = new Runnable() { + @Override + public void run() { + nativeOnMainActivitySuspended(); + } + }; + + public static void createForTests(Context context) { + // Applications will create this once the JNI side has been fully wired up both sides. For + // tests, we just need native -> java, that is, we don't need to notify java -> native on + // creation. + sInstance = LazyHolder.INSTANCE; + } + + public static void create(Context context) { + if (sInstance == null) { + sInstance = LazyHolder.INSTANCE; + ActivityStatus.registerStateListener(sInstance); + IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + Intent batteryStatusIntent = context.registerReceiver(null, ifilter); + onBatteryChargingChanged(batteryStatusIntent); + } + } + + private PowerMonitor() { + } + + public static void onBatteryChargingChanged(Intent intent) { + if (sInstance == null) { + // We may be called by the framework intent-filter before being fully initialized. This + // is not a problem, since our constructor will check for the state later on. + return; + } + int chargePlug = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); + // If we're not plugged, assume we're running on battery power. + sInstance.mIsBatteryPower = chargePlug != BatteryManager.BATTERY_PLUGGED_USB && + chargePlug != BatteryManager.BATTERY_PLUGGED_AC; + nativeOnBatteryChargingChanged(); + } + + @Override + public void onActivityStateChange(int newState) { + if (newState == ActivityStatus.RESUMED) { + // Remove the callback from the message loop in case it hasn't been executed yet. + mHandler.removeCallbacks(sSuspendTask); + nativeOnMainActivityResumed(); + } else if (newState == ActivityStatus.PAUSED) { + mHandler.postDelayed(sSuspendTask, SUSPEND_DELAY_MS); + } + } + + @CalledByNative + private static boolean isBatteryPower() { + return sInstance.mIsBatteryPower; + } + + private static native void nativeOnBatteryChargingChanged(); + private static native void nativeOnMainActivitySuspended(); + private static native void nativeOnMainActivityResumed(); +} diff --git a/src/org/chromium/base/PowerStatusReceiver.java b/src/org/chromium/base/PowerStatusReceiver.java new file mode 100644 index 0000000..f36c146 --- /dev/null +++ b/src/org/chromium/base/PowerStatusReceiver.java @@ -0,0 +1,23 @@ +// 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.base; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + + +/** + * A BroadcastReceiver that listens to changes in power status and notifies + * PowerMonitor. + * It's instantiated by the framework via the application intent-filter + * declared in its manifest. + */ +public class PowerStatusReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + PowerMonitor.onBatteryChargingChanged(intent); + } +} diff --git a/src/org/chromium/base/SystemMessageHandler.java b/src/org/chromium/base/SystemMessageHandler.java new file mode 100644 index 0000000..f7bb19f --- /dev/null +++ b/src/org/chromium/base/SystemMessageHandler.java @@ -0,0 +1,93 @@ +// 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.base; + +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; + +import java.util.concurrent.atomic.AtomicBoolean; + +class SystemMessageHandler extends Handler { + + private static final int TIMER_MESSAGE = 1; + private static final int DELAYED_TIMER_MESSAGE = 2; + + // 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; + } + } + } + + @CalledByNative + private void setTimer() { + if (!mTimerFired.getAndSet(false)) { + // mTimerFired was already false. + return; + } + if (startupComplete()) { + sendEmptyMessage(TIMER_MESSAGE); + } else { + sendMessageAtFrontOfQueue(obtainMessage(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); + } + } + + @SuppressWarnings("unused") + @CalledByNative + private void removeTimer() { + removeMessages(TIMER_MESSAGE); + removeMessages(DELAYED_TIMER_MESSAGE); + } + + @CalledByNative + private static SystemMessageHandler create(int messagePumpDelegateNative) { + return new SystemMessageHandler(messagePumpDelegateNative); + } + + private native boolean nativeDoRunLoopOnce(int messagePumpDelegateNative); +} diff --git a/src/org/chromium/base/ThreadUtils.java b/src/org/chromium/base/ThreadUtils.java new file mode 100644 index 0000000..cdf73c3 --- /dev/null +++ b/src/org/chromium/base/ThreadUtils.java @@ -0,0 +1,161 @@ +// 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.base; + +import android.os.Handler; +import android.os.Looper; +import android.os.Process; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +/** + * Helper methods to deal with threading related tasks. + */ +public class ThreadUtils { + + /** + * Run the supplied Runnable on the main thread. The method will block until + * the Runnable completes. + * + * @param r The Runnable to run. + */ + public static void runOnUiThreadBlocking(final Runnable r) { + if (runningOnUiThread()) { + r.run(); + } else { + FutureTask task = new FutureTask(r, null); + postOnUiThread(task); + try { + task.get(); + } catch (Exception e) { + throw new RuntimeException("Exception occured while waiting for runnable", e); + } + } + } + + /** + * 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 + */ + public static T runOnUiThreadBlockingNoException(Callable c) { + try { + return runOnUiThreadBlocking(c); + } catch (ExecutionException e) { + throw new RuntimeException("Error occured waiting for callable", e); + } + } + + /** + * 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 + * @throws ExecutionException c's exception + */ + public static T runOnUiThreadBlocking(Callable c) throws ExecutionException { + FutureTask task = new FutureTask(c); + runOnUiThread(task); + try { + return task.get(); + } catch (InterruptedException e) { + throw new RuntimeException("Interrupted waiting for callable", e); + } + } + + /** + * 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) + */ + public static FutureTask runOnUiThread(FutureTask task) { + if (runningOnUiThread()) { + task.run(); + } else { + postOnUiThread(task); + } + return task; + } + + /** + * 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 + */ + public static FutureTask runOnUiThread(Callable c) { + return runOnUiThread(new FutureTask(c)); + } + + /** + * 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 + */ + public static void runOnUiThread(Runnable r) { + if (runningOnUiThread()) { + r.run(); + } else { + LazyHolder.sUiThreadHandler.post(r); + } + } + + /** + * 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) + */ + public static FutureTask postOnUiThread(FutureTask task) { + LazyHolder.sUiThreadHandler.post(task); + return task; + } + + /** + * 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 + */ + public static void postOnUiThread(Runnable r) { + LazyHolder.sUiThreadHandler.post(r); + } + + /** + * Asserts that the current thread is running on the main thread. + */ + public static void assertOnUiThread() { + assert runningOnUiThread(); + } + + /** + * @return true iff the current thread is the main (UI) thread. + */ + public static boolean runningOnUiThread() { + return Looper.getMainLooper() == Looper.myLooper(); + } + + /** + * Set thread priority to audio. + */ + @CalledByNative + public static void setThreadPriorityAudio(int tid) { + Process.setThreadPriority(tid, Process.THREAD_PRIORITY_AUDIO); + } + + private static class LazyHolder { + private static Handler sUiThreadHandler = new Handler(Looper.getMainLooper()); + } +} diff --git a/src/org/chromium/base/WeakContext.java b/src/org/chromium/base/WeakContext.java new file mode 100644 index 0000000..d660cc9 --- /dev/null +++ b/src/org/chromium/base/WeakContext.java @@ -0,0 +1,45 @@ +// 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.base; + +import android.content.Context; + +import java.lang.ref.WeakReference; +import java.util.concurrent.Callable; + +// Holds a WeakReference to Context to allow it to be GC'd. +// Also provides utility functions to getSystemService from the UI or any +// other thread (may return null, if the Context has been nullified). +public class WeakContext { + private static WeakReference sWeakContext; + + public static void initializeWeakContext(final Context context) { + sWeakContext = new WeakReference(context); + } + + public static Context getContext() { + return sWeakContext.get(); + } + + // Returns a system service. May be called from any thread. + // If necessary, it will send a message to the main thread to acquire the + // service, and block waiting for it to complete. + // May return null if context is no longer available. + public static Object getSystemService(final String name) { + final Context context = sWeakContext.get(); + if (context == null) { + return null; + } + if (ThreadUtils.runningOnUiThread()) { + return context.getSystemService(name); + } + return ThreadUtils.runOnUiThreadBlockingNoException(new Callable() { + @Override + public Object call() { + return context.getSystemService(name); + } + }); + } +} diff --git a/src/org/chromium/chrome/browser/sync/ModelTypeSelection.java b/src/org/chromium/chrome/browser/sync/ModelTypeSelection.java new file mode 100644 index 0000000..e0166d1 --- /dev/null +++ b/src/org/chromium/chrome/browser/sync/ModelTypeSelection.java @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..463522e --- /dev/null +++ b/src/org/chromium/chrome/browser/ui/toolbar/ToolbarModelSecurityLevel.java @@ -0,0 +1,10 @@ +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/components/navigation_interception/InterceptNavigationDelegate.java b/src/org/chromium/components/navigation_interception/InterceptNavigationDelegate.java new file mode 100644 index 0000000..f214467 --- /dev/null +++ b/src/org/chromium/components/navigation_interception/InterceptNavigationDelegate.java @@ -0,0 +1,21 @@ +// 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.components.navigation_interception; + +import org.chromium.base.CalledByNative; + +public interface InterceptNavigationDelegate { + + /** + * This method is called for every top-level navigation within the associated WebContents. + * The method allows the embedder to ignore navigations. This is used on Android to 'convert' + * certain navigations to Intents to 3rd party applications. + * + * @param navigationParams parameters describing the navigation. + * @return true if the navigation should be ignored. + */ + @CalledByNative + boolean shouldIgnoreNavigation(NavigationParams navigationParams); +} diff --git a/src/org/chromium/components/navigation_interception/NavigationParams.java b/src/org/chromium/components/navigation_interception/NavigationParams.java new file mode 100644 index 0000000..cdfd883 --- /dev/null +++ b/src/org/chromium/components/navigation_interception/NavigationParams.java @@ -0,0 +1,36 @@ +// 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.components.navigation_interception; + +import org.chromium.base.CalledByNative; + +public class NavigationParams { + // Target url of the navigation. + public final String url; + // True if the the navigation method is "POST". + public final boolean isPost; + // True if the navigation was initiated by the user. + public final boolean hasUserGesture; + // Page transition type (e.g. link / typed). + public final int pageTransitionType; + // Is the navigation a redirect (in which case url is the "target" address). + public final boolean isRedirect; + + public NavigationParams(String url, boolean isPost, boolean hasUserGesture, + int pageTransitionType, boolean isRedirect) { + this.url = url; + this.isPost = isPost; + this.hasUserGesture = hasUserGesture; + this.pageTransitionType = pageTransitionType; + this.isRedirect = isRedirect; + } + + @CalledByNative + public static NavigationParams create(String url, boolean isPost, boolean hasUserGesture, + int pageTransitionType, boolean isRedirect) { + return new NavigationParams(url, isPost, hasUserGesture, pageTransitionType, + isRedirect); + } +} diff --git a/src/org/chromium/components/web_contents_delegate_android/ColorChooserAndroid.java b/src/org/chromium/components/web_contents_delegate_android/ColorChooserAndroid.java new file mode 100644 index 0000000..cdab32b --- /dev/null +++ b/src/org/chromium/components/web_contents_delegate_android/ColorChooserAndroid.java @@ -0,0 +1,61 @@ +// 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.components.web_contents_delegate_android; + +import android.content.Context; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.content.browser.ContentViewCore; +import org.chromium.ui.ColorPickerDialog; + +/** + * ColorChooserAndroid communicates with the java ColorPickerDialog and the + * native color_chooser_android.cc + */ +@JNINamespace("components") +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() { + + @Override + public void colorChanged(int color) { + mDialog.dismiss(); + nativeOnColorChosen(mNativeColorChooserAndroid, color); + } + }; + + mNativeColorChooserAndroid = nativeColorChooserAndroid; + mDialog = new ColorPickerDialog(context, listener, initialColor); + } + + private void openColorChooser() { + mDialog.show(); + } + + @CalledByNative + public void closeColorChooser() { + mDialog.dismiss(); + } + + @CalledByNative + public static ColorChooserAndroid createColorChooserAndroid( + int nativeColorChooserAndroid, + ContentViewCore contentViewCore, + int initialColor) { + ColorChooserAndroid chooser = new ColorChooserAndroid(nativeColorChooserAndroid, + contentViewCore.getContext(), initialColor); + chooser.openColorChooser(); + return chooser; + } + + // Implemented in color_chooser_android.cc + private native void nativeOnColorChosen(int nativeColorChooserAndroid, int 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 new file mode 100644 index 0000000..9769985 --- /dev/null +++ b/src/org/chromium/components/web_contents_delegate_android/WebContentsDelegateAndroid.java @@ -0,0 +1,135 @@ +// 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.components.web_contents_delegate_android; + +import android.graphics.Rect; +import android.view.KeyEvent; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.content.browser.ContentViewCore; + +/** + * Java peer of the native class of the same name. + */ +@JNINamespace("components") +public class WebContentsDelegateAndroid { + + // Equivalent of WebCore::WebConsoleMessage::LevelTip. + public static final int LOG_LEVEL_TIP = 0; + // Equivalent of WebCore::WebConsoleMessage::LevelLog. + public static final int LOG_LEVEL_LOG = 1; + // Equivalent of WebCore::WebConsoleMessage::LevelWarning. + public static final int LOG_LEVEL_WARNING = 2; + // Equivalent of WebCore::WebConsoleMessage::LevelError. + public static final int LOG_LEVEL_ERROR = 3; + // 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; + + public int getMostRecentProgress() { + return mMostRecentProgress; + } + + @CalledByNative + public void openNewTab(String url, boolean incognito) { + } + + @CalledByNative + public boolean addNewContents(int nativeSourceWebContents, int nativeWebContents, + int disposition, Rect initialPosition, boolean userGesture) { + return false; + } + + @CalledByNative + public void closeContents() { + } + + @CalledByNative + public void onLoadStarted() { + } + + @CalledByNative + public void onLoadStopped() { + } + + @CalledByNative + public void onTitleUpdated() { + } + + @SuppressWarnings("unused") + @CalledByNative + private final void notifyLoadProgressChanged(double progress) { + mMostRecentProgress = (int) (100.0 * progress); + onLoadProgressChanged(mMostRecentProgress); + } + + /** + * @param progress The load progress [0, 100] for the current web contents. + */ + public void onLoadProgressChanged(int progress) { + } + + /** + * Signaled when the renderer has been deemed to be unresponsive. + */ + @CalledByNative + public void rendererUnresponsive() { + } + + /** + * Signaled when the render has been deemed to be responsive. + */ + @CalledByNative + public void rendererResponsive() { + } + + @CalledByNative + public void onUpdateUrl(String url) { + } + + @CalledByNative + public boolean takeFocus(boolean reverse) { + return false; + } + + @CalledByNative + public void handleKeyboardEvent(KeyEvent event) { + // TODO(bulach): we probably want to re-inject the KeyEvent back into + // the system. Investigate if this is at all possible. + } + + /** + * Report a JavaScript console message. + * + * @param level message level. One of WebContentsDelegateAndroid.LOG_LEVEL*. + * @param message the error message. + * @param lineNumber the line number int the source file at which the error is reported. + * @param sourceId the name of the source file that caused the error. + * @return true if the client will handle logging the message. + */ + @CalledByNative + public boolean addMessageToConsole(int level, String message, int lineNumber, + String sourceId) { + return false; + } + + /** + * Report a form resubmission. The overwriter of this function should eventually call + * either of ContentViewCore.ContinuePendingReload or ContentViewCore.CancelPendingReload. + */ + @CalledByNative + public void showRepostFormWarningDialog(ContentViewCore contentViewCore) { + } + + @CalledByNative + public void toggleFullscreenModeForTab(boolean enterFullscreen) { + } + + @CalledByNative + public boolean isFullscreenForTabOrPending() { + return false; + } +} diff --git a/src/org/chromium/content/R.java b/src/org/chromium/content/R.java new file mode 100644 index 0000000..daa8a77 --- /dev/null +++ b/src/org/chromium/content/R.java @@ -0,0 +1,57 @@ +// 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; + +/** + * Provide Android internal resources to Chrome's content layer. This allows + * classes that access resources via org.chromium.content.R to function properly + * in webview. In a normal Chrome build, content resources live in a res folder + * in the content layer and the org.chromium.content.R class is generated at + * build time based on these resources. In webview, resources live in the + * Android framework and can't be accessed directly from the content layer. + * Instead, we copy resources needed by content into the Android framework and + * use this R class to map resources IDs from org.chromium.content.R to + * com.android.internal.R. + */ +public final class R { + public static final class dimen { + public static int link_preview_overlay_radius; + } + public static final class drawable { + public static int ic_menu_search_holo_light; + public static int ic_menu_share_holo_light; + public static int ondemand_overlay; + } + public static final class id { + public static int date_picker; + public static int month; + 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 final class string { + public static int accessibility_content_view; + public static int accessibility_date_picker_month; + public static int accessibility_date_picker_year; + public static int accessibility_datetime_picker_date; + public static int accessibility_datetime_picker_time; + public static int actionbar_share; + public static int actionbar_web_search; + public static int date_picker_dialog_clear; + public static int date_picker_dialog_set; + public static int date_picker_dialog_title; + public static int date_time_picker_dialog_title; + public static int media_player_error_button; + public static int media_player_error_text_invalid_progressive_playback; + public static int media_player_error_text_unknown; + public static int media_player_error_title; + public static int media_player_loading_video; + public static int month_picker_dialog_title; + } +} diff --git a/src/org/chromium/content/app/ChildProcessService.java b/src/org/chromium/content/app/ChildProcessService.java new file mode 100644 index 0000000..3d85310 --- /dev/null +++ b/src/org/chromium/content/app/ChildProcessService.java @@ -0,0 +1,275 @@ +// 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.app; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.SurfaceTexture; +import android.os.Bundle; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.os.RemoteException; +import android.util.Log; +import android.view.Surface; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.content.browser.ChildProcessConnection; +import org.chromium.content.common.IChildProcessCallback; +import org.chromium.content.common.IChildProcessService; +import org.chromium.content.browser.ChildProcessLauncher; +import org.chromium.content.common.ProcessInitException; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicReference; + +/** + * This is the base class for child services; the [Non]SandboxedProcessService0, 1.. etc + * subclasses provide the concrete service entry points, to enable the browser to connect + * to more than one distinct process (i.e. one process per service number, up to limit of N). + * The embedding application must declare these service instances in the application section + * of its AndroidManifest.xml, for example with N entries of the form:- + * + * for X in 0...N-1 (where N is {@link ChildProcessLauncher#MAX_REGISTERED_SERVICES}) + */ +@JNINamespace("content") +public class ChildProcessService extends Service { + private static final String MAIN_THREAD_NAME = "ChildProcessMain"; + private static final String TAG = "ChildProcessService"; + private IChildProcessCallback mCallback; + + // This is the native "Main" thread for the renderer / utility process. + private Thread mMainThread; + // Parameters received via IPC, only accessed while holding the mMainThread monitor. + private String[] mCommandLineParams; + private int mCpuCount; + private long mCpuFeatures; + // Pairs IDs and file descriptors that should be registered natively. + private ArrayList mFileIds; + private ArrayList mFileFds; + + private static AtomicReference sContext = new AtomicReference(null); + private boolean mLibraryInitialized = false; + + // Binder object used by clients for this service. + private final IChildProcessService.Stub mBinder = new IChildProcessService.Stub() { + // NOTE: Implement any IChildProcessService methods here. + @Override + public int setupConnection(Bundle args, IChildProcessCallback callback) { + mCallback = callback; + synchronized (mMainThread) { + // Allow the command line to be set via bind() intent or setupConnection, but + // the FD can only be transferred here. + if (mCommandLineParams == null) { + mCommandLineParams = args.getStringArray( + ChildProcessConnection.EXTRA_COMMAND_LINE); + } + // We must have received the command line by now + assert mCommandLineParams != null; + mCpuCount = args.getInt(ChildProcessConnection.EXTRA_CPU_COUNT); + mCpuFeatures = args.getLong(ChildProcessConnection.EXTRA_CPU_FEATURES); + assert mCpuCount > 0; + mFileIds = new ArrayList(); + mFileFds = new ArrayList(); + for (int i = 0;; i++) { + String fdName = ChildProcessConnection.EXTRA_FILES_PREFIX + i + + ChildProcessConnection.EXTRA_FILES_FD_SUFFIX; + ParcelFileDescriptor parcel = args.getParcelable(fdName); + if (parcel == null) { + // End of the file list. + break; + } + mFileFds.add(parcel); + String idName = ChildProcessConnection.EXTRA_FILES_PREFIX + i + + ChildProcessConnection.EXTRA_FILES_ID_SUFFIX; + mFileIds.add(args.getInt(idName)); + } + mMainThread.notifyAll(); + } + return Process.myPid(); + } + }; + + /* package */ static Context getContext() { + return sContext.get(); + } + + @Override + public void onCreate() { + Log.i(TAG, "Creating new ChildProcessService pid=" + Process.myPid()); + if (sContext.get() != null) { + Log.e(TAG, "ChildProcessService created again in process!"); + } + sContext.set(this); + super.onCreate(); + + mMainThread = new Thread(new Runnable() { + @Override + public void run() { + try { + try { + LibraryLoader.loadNow(); + } catch (ProcessInitException e) { + Log.e(TAG, "Failed to load native library, exiting child process", e); + return; + } + synchronized (mMainThread) { + while (mCommandLineParams == null) { + mMainThread.wait(); + } + } + LibraryLoader.initialize(mCommandLineParams); + synchronized (mMainThread) { + mLibraryInitialized = true; + mMainThread.notifyAll(); + while (mFileIds == null) { + mMainThread.wait(); + } + } + assert mFileIds.size() == mFileFds.size(); + int[] fileIds = new int[mFileIds.size()]; + int[] fileFds = new int[mFileFds.size()]; + for (int i = 0; i < mFileIds.size(); ++i) { + fileIds[i] = mFileIds.get(i); + fileFds[i] = mFileFds.get(i).detachFd(); + } + ContentMain.initApplicationContext(sContext.get().getApplicationContext()); + nativeInitChildProcess(sContext.get().getApplicationContext(), + ChildProcessService.this, fileIds, fileFds, + mCpuCount, mCpuFeatures); + ContentMain.start(); + nativeExitChildProcess(); + } catch (InterruptedException e) { + Log.w(TAG, MAIN_THREAD_NAME + " startup failed: " + e); + } catch (ProcessInitException e) { + Log.w(TAG, MAIN_THREAD_NAME + " startup failed: " + e); + } + } + }, MAIN_THREAD_NAME); + mMainThread.start(); + } + + @Override + public void onDestroy() { + Log.i(TAG, "Destroying ChildProcessService pid=" + Process.myPid()); + super.onDestroy(); + if (mCommandLineParams == null) { + // This process was destroyed before it even started. Nothing more to do. + return; + } + synchronized (mMainThread) { + try { + while (!mLibraryInitialized) { + // Avoid a potential race in calling through to native code before the library + // has loaded. + mMainThread.wait(); + } + } catch (InterruptedException e) { + } + } + // Try to shutdown the MainThread gracefully, but it might not + // have chance to exit normally. + nativeShutdownMainThread(); + } + + @Override + public IBinder onBind(Intent intent) { + // We call stopSelf() to request that this service be stopped as soon as the client + // unbinds. Otherwise the system may keep it around and available for a reconnect. The + // child processes do not currently support reconnect; they must be initialized from + // scratch every time. + stopSelf(); + + synchronized (mMainThread) { + mCommandLineParams = intent.getStringArrayExtra( + ChildProcessConnection.EXTRA_COMMAND_LINE); + mMainThread.notifyAll(); + } + + return mBinder; + } + + /** + * Called from native code to share a surface texture with another child process. + * Through using the callback object the browser is used as a proxy to route the + * call to the correct process. + * + * @param pid Process handle of the child process to share the SurfaceTexture with. + * @param surfaceObject The Surface or SurfaceTexture to share with the other child process. + * @param primaryID Used to route the call to the correct client instance. + * @param secondaryID Used to route the call to the correct client instance. + */ + @SuppressWarnings("unused") + @CalledByNative + private void establishSurfaceTexturePeer( + int pid, Object surfaceObject, int primaryID, int secondaryID) { + if (mCallback == null) { + Log.e(TAG, "No callback interface has been provided."); + return; + } + + Surface surface = null; + boolean needRelease = false; + if (surfaceObject instanceof Surface) { + surface = (Surface)surfaceObject; + } else if (surfaceObject instanceof SurfaceTexture) { + surface = new Surface((SurfaceTexture)surfaceObject); + needRelease = true; + } else { + Log.e(TAG, "Not a valid surfaceObject: " + surfaceObject); + return; + } + try { + mCallback.establishSurfacePeer(pid, surface, primaryID, secondaryID); + } catch (RemoteException e) { + Log.e(TAG, "Unable to call establishSurfaceTexturePeer: " + e); + return; + } finally { + if (needRelease) { + surface.release(); + } + } + } + + @SuppressWarnings("unused") + @CalledByNative + private Surface getViewSurface(int surfaceId) { + if (mCallback == null) { + Log.e(TAG, "No callback interface has been provided."); + return null; + } + + try { + return mCallback.getViewSurface(surfaceId); + } catch (RemoteException e) { + Log.e(TAG, "Unable to call establishSurfaceTexturePeer: " + e); + return null; + } + } + + /** + * The main entry point for a child process. This should be called from a new thread since + * it will not return until the child process exits. See child_process_service.{h,cc} + * + * @param applicationContext The Application Context of the current process. + * @param service The current ChildProcessService object. + * @param fileIds A list of file IDs that should be registered for access by the renderer. + * @param fileFds A list of file descriptors that should be registered for access by the + * renderer. + */ + private static native void nativeInitChildProcess(Context applicationContext, + ChildProcessService service, int[] extraFileIds, int[] extraFileFds, + int cpuCount, long cpuFeatures); + + /** + * Force the child process to exit. + */ + private static native void nativeExitChildProcess(); + + private native void nativeShutdownMainThread(); +} diff --git a/src/org/chromium/content/app/ContentMain.java b/src/org/chromium/content/app/ContentMain.java new file mode 100644 index 0000000..09ba783 --- /dev/null +++ b/src/org/chromium/content/app/ContentMain.java @@ -0,0 +1,39 @@ +// 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.app; + +import android.content.Context; + +import org.chromium.base.JNINamespace; + +/** + * This class is used to initialize all types of process. It corresponds to + * content/public/app/content_main.h which is not used in Android as it has + * the different initialization process. + * + * TODO (michaelbai): Refactorying the BrowserProcessMain.java and the + * ChildProcessService.java to start ContentMain, and run the process + * specific initialization code in ContentMainRunner::Initialize. + * + **/ +@JNINamespace("content") +public class ContentMain { + /** + * Initialize application context in native side. + **/ + public static void initApplicationContext(Context context) { + nativeInitApplicationContext(context); + } + + /** + * Start the ContentMainRunner in native side. + **/ + public static int start() { + return nativeStart(); + } + + private static native void nativeInitApplicationContext(Context context); + private static native int nativeStart(); +}; diff --git a/src/org/chromium/content/app/LibraryLoader.java b/src/org/chromium/content/app/LibraryLoader.java new file mode 100644 index 0000000..38cd2df --- /dev/null +++ b/src/org/chromium/content/app/LibraryLoader.java @@ -0,0 +1,143 @@ +// 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.app; + +import android.text.TextUtils; +import android.util.Log; + +import org.chromium.base.JNINamespace; +import org.chromium.content.common.CommandLine; +import org.chromium.content.common.ProcessInitException; +import org.chromium.content.common.ResultCodes; +import org.chromium.content.common.TraceEvent; + +/** + * This class provides functionality to load and register the native libraries. + * Callers are allowed to separate loading the libraries from initializing them. + * This may be an advantage for Android Webview, where the libraries can be loaded + * by the zygote process, but then needs per process initialization after the + * application processes are forked from the zygote process. + * + * The libraries may be loaded and initialized from any thread. Synchronization + * primitives are used to ensure that overlapping requests from different + * threads are handled sequentially. + * + * See also content/app/android/library_loader_hooks.cc, which contains + * the native counterpart to this class. + */ +@JNINamespace("content") +public class LibraryLoader { + private static final String TAG = "LibraryLoader"; + + // Guards all access to the libraries + private static final Object sLock = new Object(); + + // One-way switch becomes true when the libraries are loaded. + private static boolean sLoaded = false; + + // One-way switch becomes true when the libraries are initialized ( + // by calling nativeLibraryLoaded, which forwards to LibraryLoaded(...) in + // library_loader_hooks.cc). + private static boolean sInitialized = false; + + // TODO(cjhopman): Remove this once it's unused. + /** + * Doesn't do anything. + */ + @Deprecated + public static void setLibraryToLoad(String library) { + } + + /** + * This method blocks until the library is fully loaded and initialized. + */ + public static void ensureInitialized() throws ProcessInitException { + synchronized (sLock) { + if (sInitialized) { + // Already initialized, nothing to do. + return; + } + loadAlreadyLocked(); + initializeAlreadyLocked(CommandLine.getJavaSwitchesOrNull()); + } + } + + + /** + * Loads the library and blocks until the load completes. The caller is responsible + * for subsequently calling ensureInitialized(). + * May be called on any thread, but should only be called once. Note the thread + * this is called on will be the thread that runs the native code's static initializers. + * See the comment in doInBackground() for more considerations on this. + * + * @throws ProcessInitException if the native library failed to load. + */ + public static void loadNow() throws ProcessInitException { + synchronized (sLock) { + loadAlreadyLocked(); + } + } + + + /** + * initializes the library here and now: must be called on the thread that the + * native will call its "main" thread. The library must have previously been + * loaded with loadNow. + * @param initCommandLine The command line arguments that native command line will + * be initialized with. + */ + static void initialize(String[] initCommandLine) throws ProcessInitException { + synchronized (sLock) { + initializeAlreadyLocked(initCommandLine); + } + } + + + // Invoke System.loadLibrary(...), triggering JNI_OnLoad in native code + private static void loadAlreadyLocked() throws ProcessInitException { + try { + if (!sLoaded) { + assert !sInitialized; + for (String sLibrary : NativeLibraries.libraries) { + Log.i(TAG, "loading: " + sLibrary); + System.loadLibrary(sLibrary); + Log.i(TAG, "loaded: " + sLibrary); + } + sLoaded = true; + } + } catch (UnsatisfiedLinkError e) { + throw new ProcessInitException(ResultCodes.RESULT_CODE_NATIVE_LIBRARY_LOAD_FAILED, e); + } + } + + + // Invoke content::LibraryLoaded in library_loader_hooks.cc + private static void initializeAlreadyLocked(String[] initCommandLine) + throws ProcessInitException { + if (sInitialized) { + return; + } + int resultCode = nativeLibraryLoaded(initCommandLine); + if (resultCode != 0) { + Log.e(TAG, "error calling nativeLibraryLoaded"); + throw new ProcessInitException(resultCode); + } + // From this point on, native code is ready to use and checkIsReady() + // shouldn't complain from now on (and in fact, it's used by the + // following calls). + sInitialized = true; + CommandLine.enableNativeProxy(); + TraceEvent.setEnabledToMatchNative(); + } + + // This is the only method that is registered during System.loadLibrary. We then call it + // to register everything else. This process is called "initialization". + // This method will be mapped (by generated code) to the LibraryLoaded + // definition in content/app/android/library_loader_hooks.cc. + // + // Return 0 on success, otherwise return the error code from + // content/public/common/result_codes.h. + private static native int nativeLibraryLoaded(String[] initCommandLine); +} diff --git a/src/org/chromium/content/app/NativeLibraries.java b/src/org/chromium/content/app/NativeLibraries.java new file mode 100644 index 0000000..d35a7dd --- /dev/null +++ b/src/org/chromium/content/app/NativeLibraries.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; + +public class NativeLibraries { + // This is the list of native libraries to load. In the normal chromium build, this would be + // automatically generated. + // TODO(torne, cjhopman): Use a generated file for this. + static String[] libraries = { "webviewchromium" }; +} diff --git a/src/org/chromium/content/app/PrivilegedProcessService.java b/src/org/chromium/content/app/PrivilegedProcessService.java new file mode 100644 index 0000000..49ce280 --- /dev/null +++ b/src/org/chromium/content/app/PrivilegedProcessService.java @@ -0,0 +1,13 @@ +// 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.app; + +// Privleged (unsandboxed) Services inherit from this class. We enforce the +// privileged/sandboxed distinction by type-checking objects against this parent +// class. + +public class PrivilegedProcessService extends ChildProcessService { + +} diff --git a/src/org/chromium/content/app/PrivilegedProcessService0.java b/src/org/chromium/content/app/PrivilegedProcessService0.java new file mode 100644 index 0000000..0520538 --- /dev/null +++ b/src/org/chromium/content/app/PrivilegedProcessService0.java @@ -0,0 +1,12 @@ +// 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.app; + +// This is needed to register multiple PrivilegedProcess services so that we can have +// more than one privileged process. + +public class PrivilegedProcessService0 extends PrivilegedProcessService { + +} diff --git a/src/org/chromium/content/app/PrivilegedProcessService1.java b/src/org/chromium/content/app/PrivilegedProcessService1.java new file mode 100644 index 0000000..47c16e6 --- /dev/null +++ b/src/org/chromium/content/app/PrivilegedProcessService1.java @@ -0,0 +1,12 @@ +// 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.app; + +// This is needed to register multiple SandboxedProcess services so that we can have +// more than one privileged process. + +public class PrivilegedProcessService1 extends PrivilegedProcessService { + +} diff --git a/src/org/chromium/content/app/PrivilegedProcessService2.java b/src/org/chromium/content/app/PrivilegedProcessService2.java new file mode 100644 index 0000000..16cdd6d --- /dev/null +++ b/src/org/chromium/content/app/PrivilegedProcessService2.java @@ -0,0 +1,12 @@ +// 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.app; + +// This is needed to register multiple SandboxedProcess services so that we can have +// more than one sandboxed process. + +public class PrivilegedProcessService2 extends PrivilegedProcessService { + +} diff --git a/src/org/chromium/content/app/SandboxedProcessService.java b/src/org/chromium/content/app/SandboxedProcessService.java new file mode 100644 index 0000000..7b38af8 --- /dev/null +++ b/src/org/chromium/content/app/SandboxedProcessService.java @@ -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.app; + +// Sandboxed Services inherit from this class. We enforce the privileged/sandboxed +// distinction by type-checking objects against this parent class. + +public class SandboxedProcessService extends ChildProcessService { + +} diff --git a/src/org/chromium/content/app/SandboxedProcessService0.java b/src/org/chromium/content/app/SandboxedProcessService0.java new file mode 100644 index 0000000..1fcd974 --- /dev/null +++ b/src/org/chromium/content/app/SandboxedProcessService0.java @@ -0,0 +1,12 @@ +// 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.app; + +// This is needed to register multiple SandboxedProcess services so that we can have +// more than one sandboxed process. + +public class SandboxedProcessService0 extends SandboxedProcessService { + +} diff --git a/src/org/chromium/content/app/SandboxedProcessService1.java b/src/org/chromium/content/app/SandboxedProcessService1.java new file mode 100644 index 0000000..24846a7 --- /dev/null +++ b/src/org/chromium/content/app/SandboxedProcessService1.java @@ -0,0 +1,12 @@ +// 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.app; + +// This is needed to register multiple SandboxedProcess services so that we can have +// more than one sandboxed process. + +public class SandboxedProcessService1 extends SandboxedProcessService { + +} diff --git a/src/org/chromium/content/app/SandboxedProcessService2.java b/src/org/chromium/content/app/SandboxedProcessService2.java new file mode 100644 index 0000000..f8d1802 --- /dev/null +++ b/src/org/chromium/content/app/SandboxedProcessService2.java @@ -0,0 +1,12 @@ +// 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.app; + +// This is needed to register multiple SandboxedProcess services so that we can have +// more than one sandboxed process. + +public class SandboxedProcessService2 extends SandboxedProcessService { + +} diff --git a/src/org/chromium/content/app/SandboxedProcessService3.java b/src/org/chromium/content/app/SandboxedProcessService3.java new file mode 100644 index 0000000..f5b8fa5 --- /dev/null +++ b/src/org/chromium/content/app/SandboxedProcessService3.java @@ -0,0 +1,12 @@ +// 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.app; + +// This is needed to register multiple SandboxedProcess services so that we can have +// more than one sandboxed process. + +public class SandboxedProcessService3 extends SandboxedProcessService { + +} diff --git a/src/org/chromium/content/app/SandboxedProcessService4.java b/src/org/chromium/content/app/SandboxedProcessService4.java new file mode 100644 index 0000000..bec8dea --- /dev/null +++ b/src/org/chromium/content/app/SandboxedProcessService4.java @@ -0,0 +1,12 @@ +// 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.app; + +// This is needed to register multiple SandboxedProcess services so that we can have +// more than one sandboxed process. + +public class SandboxedProcessService4 extends SandboxedProcessService { + +} diff --git a/src/org/chromium/content/app/SandboxedProcessService5.java b/src/org/chromium/content/app/SandboxedProcessService5.java new file mode 100644 index 0000000..9a852e1 --- /dev/null +++ b/src/org/chromium/content/app/SandboxedProcessService5.java @@ -0,0 +1,12 @@ +// 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.app; + +// This is needed to register multiple SandboxedProcess services so that we can have +// more than one sandboxed process. + +public class SandboxedProcessService5 extends SandboxedProcessService { + +} diff --git a/src/org/chromium/content/browser/ActivityContentVideoViewDelegate.java b/src/org/chromium/content/browser/ActivityContentVideoViewDelegate.java new file mode 100644 index 0000000..0af3393 --- /dev/null +++ b/src/org/chromium/content/browser/ActivityContentVideoViewDelegate.java @@ -0,0 +1,54 @@ +// 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.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; + +/** + * Uses an exisiting Activity to handle displaying video in full screen. + */ +public class ActivityContentVideoViewDelegate implements ContentVideoViewContextDelegate { + private Activity mActivity; + + public ActivityContentVideoViewDelegate(Activity activity) { + this.mActivity = activity; + } + + @Override + public void onShowCustomView(View view) { + mActivity.getWindow().setFlags( + WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + + mActivity.getWindow().addContentView(view, + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + Gravity.CENTER)); + } + + @Override + public void onDestroyContentVideoView() { + mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + + @Override + public Context getContext() { + return mActivity; + } + + @Override + public View getVideoLoadingProgressView() { + return null; + } +} diff --git a/src/org/chromium/content/browser/AndroidBrowserProcess.java b/src/org/chromium/content/browser/AndroidBrowserProcess.java new file mode 100644 index 0000000..d4dd6f0 --- /dev/null +++ b/src/org/chromium/content/browser/AndroidBrowserProcess.java @@ -0,0 +1,133 @@ +// 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.app.ActivityManager; +import android.content.Context; +import android.util.Log; + +import org.chromium.base.JNINamespace; +import org.chromium.content.app.ContentMain; +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 + // - 1: The regular New Tab Page + // - 1: The incognito New Tab Page + // - 1: A regular incognito tab + public static final int MAX_RENDERERS_LIMIT = + ChildProcessLauncher.MAX_REGISTERED_SANDBOXED_SERVICES - 3; + + /** + * Initialize the process as a ContentView host. This must be called from the main UI thread. + * This should be called by the ContentView constructor to prepare this process for ContentView + * use outside of the browser. In the case where ContentView is used in the browser then + * initBrowserProcess() should already have been called and this is a no-op. + * + * @param context Context used to obtain the application context. + * @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. + + * @return Whether the process actually needed to be initialized (false if already running). + */ + public static boolean init(Context context, int maxRendererProcesses) + throws ProcessInitException { + if (sInitialized) return false; + sInitialized = true; + + // 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. + 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, + nativeIsPluginEnabled() ? getPlugins(context) : null); + ContentMain.initApplicationContext(appContext); + int result = ContentMain.start(); + if (result > 0) throw new ProcessInitException(result); + return true; + } + + /** + * Initialization needed for tests. Mainly used by content browsertests. + */ + public static void initChromiumBrowserProcessForTests(Context context) { + ResourceExtractor resourceExtractor = ResourceExtractor.get(context); + resourceExtractor.startExtractingResources(); + resourceExtractor.waitForCompletion(); + + // Having a single renderer should be sufficient for tests. + // We can't have more than MAX_RENDERERS_LIMIT. + nativeSetCommandLineFlags(1 /* maxRenderers */, null); + } + + private static String getPlugins(final Context context) { + return PepperPluginManager.getPlugins(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. + private static native boolean nativeIsOfficialBuild(); + + private static native boolean nativeIsPluginEnabled(); +} diff --git a/src/org/chromium/content/browser/ChildProcessConnection.java b/src/org/chromium/content/browser/ChildProcessConnection.java new file mode 100644 index 0000000..cfd5f54 --- /dev/null +++ b/src/org/chromium/content/browser/ChildProcessConnection.java @@ -0,0 +1,373 @@ +// 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.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.chromium.base.CalledByNative; +import org.chromium.base.CpuFeatures; +import org.chromium.base.ThreadUtils; +import org.chromium.content.app.ChildProcessService; +import org.chromium.content.common.CommandLine; +import org.chromium.content.common.IChildProcessCallback; +import org.chromium.content.common.IChildProcessService; +import org.chromium.content.common.TraceEvent; + +public class ChildProcessConnection implements ServiceConnection { + interface DeathCallback { + void onChildProcessDied(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"; + // Note the FDs may only be passed in the connection bundle. + public static final String EXTRA_FILES_PREFIX = + "com.google.android.apps.chrome.extra.extraFile_"; + public static final String EXTRA_FILES_ID_SUFFIX = "_id"; + public static final String EXTRA_FILES_FD_SUFFIX = "_fd"; + + // Used to pass the CPU core count to child processes. + public static final String EXTRA_CPU_COUNT = + "com.google.android.apps.chrome.extra.cpu_count"; + // Used to pass the CPU features mask to child processes. + public static final String EXTRA_CPU_FEATURES = + "com.google.android.apps.chrome.extra.cpu_features"; + + private final Context mContext; + private final int mServiceNumber; + private final boolean mInSandbox; + private final ChildProcessConnection.DeathCallback mDeathCallback; + 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. + private final Object mUiThreadLock = new Object(); + private IChildProcessService mService = null; + private boolean mServiceConnectComplete = false; + private int mPID = 0; // Process ID of the corresponding child process. + private HighPriorityConnection mHighPriorityConnection = null; + private int mHighPriorityConnectionCount = 0; + + private static final String TAG = "ChildProcessConnection"; + + 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) { + mCommandLine = commandLine; + mFilesToBeMapped = filesToBeMapped; + mCallback = callback; + mOnConnectionCallback = onConnectionCallback; + } + } + + // This is only valid while the connection is being established. + private ConnectionParams mConnectionParams; + private boolean mIsBound; + + ChildProcessConnection(Context context, int number, boolean inSandbox, + ChildProcessConnection.DeathCallback deathCallback, + Class serviceClass) { + mContext = context; + mServiceNumber = number; + mInSandbox = inSandbox; + mDeathCallback = deathCallback; + mServiceClass = serviceClass; + } + + int getServiceNumber() { + return mServiceNumber; + } + + boolean isInSandbox() { + return mInSandbox; + } + + IChildProcessService getService() { + synchronized(mUiThreadLock) { + return mService; + } + } + + private Intent createServiceBindIntent() { + Intent intent = new Intent(); + intent.setClassName(mContext, mServiceClass.getName() + mServiceNumber); + intent.setPackage(mContext.getPackageName()); + return intent; + } + + /** + * 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). + * @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) { + 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) { + onBindFailed(); + } + TraceEvent.end(); + } + } + + /** Setup a connection previous bound via a call to bind(). + * + * This establishes the parameters that were not already supplied in bind. + * @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. + */ + void setupConnection( + String[] commandLine, + FileDescriptorInfo[] filesToBeMapped, + IChildProcessCallback callback, + Runnable onConnectionCallback) { + synchronized(mUiThreadLock) { + TraceEvent.begin(); + assert mConnectionParams == null; + mConnectionParams = new ConnectionParams(commandLine, filesToBeMapped, callback, + onConnectionCallback); + if (mServiceConnectComplete) { + doConnectionSetup(); + } + TraceEvent.end(); + } + } + + /** + * Unbind the IChildProcessService. It is safe to call this multiple times. + */ + void unbind() { + synchronized(mUiThreadLock) { + if (mIsBound) { + mContext.unbindService(this); + mIsBound = false; + } + if (mService != null) { + if (mHighPriorityConnection != null) { + unbindHighPriority(true); + } + mService = null; + mPID = 0; + } + mConnectionParams = null; + mServiceConnectComplete = false; + } + } + + // 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; + if (mConnectionParams != null) { + doConnectionSetup(); + } + } + + /** + * 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). + */ + 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) { + Bundle bundle = new Bundle(); + bundle.putStringArray(EXTRA_COMMAND_LINE, mConnectionParams.mCommandLine); + + FileDescriptorInfo[] fileInfos = mConnectionParams.mFilesToBeMapped; + ParcelFileDescriptor[] parcelFiles = new ParcelFileDescriptor[fileInfos.length]; + for (int i = 0; i < fileInfos.length; i++) { + if (fileInfos[i].mFd == -1) { + // If someone provided an invalid FD, they are doing something wrong. + Log.e(TAG, "Invalid FD (id=" + fileInfos[i].mId + ") for process connection, " + + "aborting connection."); + return; + } + String idName = EXTRA_FILES_PREFIX + i + EXTRA_FILES_ID_SUFFIX; + String fdName = EXTRA_FILES_PREFIX + i + EXTRA_FILES_FD_SUFFIX; + if (fileInfos[i].mAutoClose) { + // Adopt the FD, it will be closed when we close the ParcelFileDescriptor. + parcelFiles[i] = ParcelFileDescriptor.adoptFd(fileInfos[i].mFd); + } else { + try { + parcelFiles[i] = ParcelFileDescriptor.fromFd(fileInfos[i].mFd); + } catch(IOException e) { + Log.e(TAG, + "Invalid FD provided for process connection, aborting connection.", + e); + return; + } + + } + bundle.putParcelable(fdName, parcelFiles[i]); + bundle.putInt(idName, fileInfos[i].mId); + } + // Add the CPU properties now. + bundle.putInt(EXTRA_CPU_COUNT, CpuFeatures.getCount()); + bundle.putLong(EXTRA_CPU_FEATURES, CpuFeatures.getMask()); + + try { + mPID = mService.setupConnection(bundle, mConnectionParams.mCallback); + } catch (android.os.RemoteException re) { + Log.e(TAG, "Failed to setup connection.", re); + } + // We proactivley close the FDs rather than wait for GC & finalizer. + try { + for (ParcelFileDescriptor parcelFile : parcelFiles) { + if (parcelFile != null) parcelFile.close(); + } + } catch (IOException ioe) { + Log.w(TAG, "Failed to close FD.", ioe); + } + } + mConnectionParams = null; + if (onConnectionCallback != null) { + onConnectionCallback.run(); + } + 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(); + } + } + + /** + * Bind the service with a new high priority connection. This will make the service + * as important as the main process. + */ + void bindHighPriority() { + synchronized(mUiThreadLock) { + if (mService == null) { + Log.w(TAG, "The connection is not bound for " + mPID); + return; + } + if (mHighPriorityConnection == null) { + mHighPriorityConnection = new HighPriorityConnection(); + mHighPriorityConnection.bind(); + } + mHighPriorityConnectionCount++; + } + } + + /** + * Unbind the service as the high priority connection. + */ + void unbindHighPriority(boolean force) { + 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; + } + } + } + + private class HighPriorityConnection implements ServiceConnection { + + private boolean mHBound = false; + + void bind() { + final Intent intent = createServiceBindIntent(); + + mHBound = mContext.bindService(intent, this, + Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT); + } + + void unbind() { + if (mHBound) { + mContext.unbindService(this); + mHBound = false; + } + } + + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + } + + @Override + public void onServiceDisconnected(ComponentName className) { + } + } + + /** + * @return The connection PID, or 0 if not yet connected. + */ + public int getPid() { + synchronized(mUiThreadLock) { + return mPID; + } + } +} diff --git a/src/org/chromium/content/browser/ChildProcessLauncher.java b/src/org/chromium/content/browser/ChildProcessLauncher.java new file mode 100644 index 0000000..92669a6 --- /dev/null +++ b/src/org/chromium/content/browser/ChildProcessLauncher.java @@ -0,0 +1,394 @@ +// 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.util.Log; +import android.view.Surface; + +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.base.ThreadUtils; +import org.chromium.content.app.ChildProcessService; +import org.chromium.content.app.PrivilegedProcessService; +import org.chromium.content.app.SandboxedProcessService; +import org.chromium.content.common.IChildProcessCallback; +import org.chromium.content.common.IChildProcessService; + +/** + * This class provides the method to start/stop ChildProcess called by + * native. + */ +@JNINamespace("content") +public class ChildProcessLauncher { + private static String TAG = "ChildProcessLauncher"; + + private static final int CALLBACK_FOR_UNKNOWN_PROCESS = 0; + private static final int CALLBACK_FOR_GPU_PROCESS = 1; + private static final int CALLBACK_FOR_RENDERER_PROCESS = 2; + + private static final String SWITCH_PROCESS_TYPE = "type"; + private static final String SWITCH_PPAPI_BROKER_PROCESS = "ppapi-broker"; + private static final String SWITCH_RENDERER_PROCESS = "renderer"; + private static final String SWITCH_GPU_PROCESS = "gpu-process"; + + // 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. + // (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_PRIVILEGED_SERVICES = 3; + + private static class ChildConnectionAllocator { + 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. + private ArrayList mFreeConnectionIndices; + private final Object mConnectionLock = new Object(); + + private Class mChildClass; + private final boolean mInSandbox; + + public ChildConnectionAllocator(boolean inSandbox) { + int numChildServices = inSandbox ? + MAX_REGISTERED_SANDBOXED_SERVICES : MAX_REGISTERED_PRIVILEGED_SERVICES; + mChildProcessConnections = new ChildProcessConnection[numChildServices]; + mFreeConnectionIndices = new ArrayList(numChildServices); + for (int i = 0; i < numChildServices; i++) { + mFreeConnectionIndices.add(i); + } + setServiceClass(inSandbox ? + SandboxedProcessService.class : PrivilegedProcessService.class); + mInSandbox = inSandbox; + } + + public void setServiceClass(Class childClass) { + mChildClass = childClass; + } + + public ChildProcessConnection allocate( + Context context, ChildProcessConnection.DeathCallback deathCallback) { + synchronized(mConnectionLock) { + if (mFreeConnectionIndices.isEmpty()) { + Log.w(TAG, "Ran out of service." ); + return null; + } + int slot = mFreeConnectionIndices.remove(0); + assert mChildProcessConnections[slot] == null; + mChildProcessConnections[slot] = new ChildProcessConnection(context, slot, + mInSandbox, deathCallback, mChildClass); + return mChildProcessConnections[slot]; + } + } + + public void free(ChildProcessConnection connection) { + synchronized(mConnectionLock) { + int slot = connection.getServiceNumber(); + if (mChildProcessConnections[slot] != connection) { + int occupier = mChildProcessConnections[slot] == null ? + -1 : mChildProcessConnections[slot].getServiceNumber(); + Log.e(TAG, "Unable to find connection to free in slot: " + slot + + " already occupied by service: " + occupier); + assert false; + } else { + mChildProcessConnections[slot] = null; + assert !mFreeConnectionIndices.contains(slot); + mFreeConnectionIndices.add(slot); + } + } + } + } + + // 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 = + new ChildConnectionAllocator(false); + + private static boolean mConnectionAllocated = false; + + // Sets service class for sandboxed service and privileged service + public static void setChildProcessClass( + Class sandboxedServiceClass, + Class privilegedServiceClass) { + // We should guarantee this is called before allocating connection. + assert !mConnectionAllocated; + mSandboxedChildConnectionAllocator.setServiceClass(sandboxedServiceClass); + mPrivilegedChildConnectionAllocator.setServiceClass(privilegedServiceClass); + } + + private static ChildConnectionAllocator getConnectionAllocator(boolean inSandbox) { + return inSandbox ? + mSandboxedChildConnectionAllocator : mPrivilegedChildConnectionAllocator; + } + + private static ChildProcessConnection allocateConnection(Context context, + boolean inSandbox) { + ChildProcessConnection.DeathCallback deathCallback = + new ChildProcessConnection.DeathCallback() { + @Override + public void onChildProcessDied(int pid) { + stop(pid); + } + }; + mConnectionAllocated = true; + return getConnectionAllocator(inSandbox).allocate(context, deathCallback); + } + + private static ChildProcessConnection allocateBoundConnection(Context context, + String[] commandLine, boolean inSandbox) { + ChildProcessConnection connection = allocateConnection(context, inSandbox); + if (connection != null) { + connection.bind(commandLine); + } + return connection; + } + + private static void freeConnection(ChildProcessConnection connection) { + if (connection == null) { + return; + } + getConnectionAllocator(connection.isInSandbox()).free(connection); + return; + } + + // Represents an invalid process handle; same as base/process.h kNullProcessHandle. + private static final int NULL_PROCESS_HANDLE = 0; + + // Map from pid to ChildService connection. + private static Map mServiceMap = + new ConcurrentHashMap(); + + // A pre-allocated and pre-bound connection ready for connection setup, or null. + static ChildProcessConnection mSpareSandboxedConnection = null; + + /** + * Returns the child process service interface for the given pid. This may be called on + * any thread, but the caller must assume that the service can disconnect at any time. All + * service calls should catch and handle android.os.RemoteException. + * + * @param pid The pid (process handle) of the service obtained from {@link #start}. + * @return The IChildProcessService or null if the service no longer exists. + */ + public static IChildProcessService getChildService(int pid) { + ChildProcessConnection connection = mServiceMap.get(pid); + if (connection != null) { + return connection.getService(); + } + return null; + } + + /** + * 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) { + synchronized (ChildProcessLauncher.class) { + assert !ThreadUtils.runningOnUiThread(); + if (mSpareSandboxedConnection == null) { + mSpareSandboxedConnection = allocateBoundConnection(context, null, true); + } + } + } + + private static String getSwitchValue(final String[] commandLine, String switchKey) { + if (commandLine == null || switchKey == null) { + return null; + } + // This format should be matched with the one defined in command_line.h. + final String switchKeyPrefix = "--" + switchKey + "="; + for (String command : commandLine) { + if (command != null && command.startsWith(switchKeyPrefix)) { + return command.substring(switchKeyPrefix.length()); + } + } + return null; + } + + /** + * 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. + * @param file_ids The ID that should be used when mapping files in the created process. + * @param file_fds The file descriptors that should be mapped in the created process. + * @param file_auto_close Whether the file descriptors should be closed once they were passed to + * the created process. + * @param clientContext Arbitrary parameter used by the client to distinguish this connection. + */ + @CalledByNative + static void start( + Context context, + final String[] commandLine, + int[] fileIds, + int[] fileFds, + boolean[] fileAutoClose, + final int clientContext) { + assert fileIds.length == fileFds.length && fileFds.length == fileAutoClose.length; + FileDescriptorInfo[] filesToBeMapped = new FileDescriptorInfo[fileFds.length]; + for (int i = 0; i < fileFds.length; i++) { + filesToBeMapped[i] = + new FileDescriptorInfo(fileIds[i], fileFds[i], fileAutoClose[i]); + } + assert clientContext != 0; + + int callbackType = CALLBACK_FOR_UNKNOWN_PROCESS; + boolean inSandbox = true; + String processType = getSwitchValue(commandLine, SWITCH_PROCESS_TYPE); + if (SWITCH_RENDERER_PROCESS.equals(processType)) { + callbackType = CALLBACK_FOR_RENDERER_PROCESS; + } else if (SWITCH_GPU_PROCESS.equals(processType)) { + callbackType = CALLBACK_FOR_GPU_PROCESS; + } else if (SWITCH_PPAPI_BROKER_PROCESS.equals(processType)) { + inSandbox = false; + } + + ChildProcessConnection allocatedConnection = null; + synchronized (ChildProcessLauncher.class) { + if (inSandbox) { + allocatedConnection = mSpareSandboxedConnection; + mSpareSandboxedConnection = null; + } + } + if (allocatedConnection == null) { + allocatedConnection = allocateBoundConnection(context, commandLine, inSandbox); + if (allocatedConnection == null) { + // Notify the native code so it can free the heap allocated callback. + nativeOnChildProcessStarted(clientContext, 0); + return; + } + } + 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(); + Log.d(TAG, "on connect callback, pid=" + pid + " context=" + clientContext); + if (pid != NULL_PROCESS_HANDLE) { + mServiceMap.put(pid, connection); + } else { + freeConnection(connection); + } + nativeOnChildProcessStarted(clientContext, 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); + } + + /** + * Terminates a child process. This may be called from any thread. + * + * @param pid The pid (process handle) of the service connection obtained from {@link #start}. + */ + @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); + return; + } + connection.unbind(); + 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. + * + * @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); + return; + } + connection.bindHighPriority(); + } + + /** + * Unbind a high priority process which is bound by {@link #bindAsHighPriority}. + * + * @param pid The process handle of the service obtained from {@link #start}. + */ + 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); + return; + } + connection.unbindHighPriority(false); + } + + /** + * This implementation is used to receive callbacks from the remote service. + */ + 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. + */ + @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. + if (callbackType != CALLBACK_FOR_GPU_PROCESS) { + Log.e(TAG, "Illegal callback for non-GPU process."); + return; + } + + nativeEstablishSurfacePeer(pid, surface, primaryID, secondaryID); + } + + @Override + public Surface getViewSurface(int surfaceId) { + // Do not allow a malicious renderer to get to our view surface. + if (callbackType != CALLBACK_FOR_GPU_PROCESS) { + Log.e(TAG, "Illegal callback for non-GPU process."); + return null; + } + + return nativeGetViewSurface(surfaceId); + } + }; + }; + + 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); +} diff --git a/src/org/chromium/content/browser/ContentSettings.java b/src/org/chromium/content/browser/ContentSettings.java new file mode 100644 index 0000000..d3ca0c6 --- /dev/null +++ b/src/org/chromium/content/browser/ContentSettings.java @@ -0,0 +1,62 @@ +// 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 org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.base.ThreadUtils; + +/** + * Manages settings state for a ContentView. A ContentSettings instance is obtained + * from ContentViewCore.getContentSettings(). + */ +@JNINamespace("content") +public class ContentSettings { + + private static final String TAG = "ContentSettings"; + + // The native side of this object. Ownership is retained native-side by the WebContents + // instance that backs the associated ContentViewCore. + private int mNativeContentSettings = 0; + + private ContentViewCore mContentViewCore; + + /** + * Package constructor to prevent clients from creating a new settings + * instance. Must be called on the UI thread. + */ + ContentSettings(ContentViewCore contentViewCore, int nativeContentView) { + ThreadUtils.assertOnUiThread(); + mContentViewCore = contentViewCore; + mNativeContentSettings = nativeInit(nativeContentView); + assert mNativeContentSettings != 0; + } + + /** + * Notification from the native side that it is being destroyed. + * @param nativeContentSettings the native instance that is going away. + */ + @CalledByNative + private void onNativeContentSettingsDestroyed(int nativeContentSettings) { + assert mNativeContentSettings == nativeContentSettings; + mNativeContentSettings = 0; + } + + /** + * Return true if JavaScript is enabled. Must be called on the UI thread. + * + * @return True if JavaScript is enabled. + */ + public boolean getJavaScriptEnabled() { + ThreadUtils.assertOnUiThread(); + return mNativeContentSettings != 0 ? + nativeGetJavaScriptEnabled(mNativeContentSettings) : false; + } + + // Initialize the ContentSettings native side. + private native int nativeInit(int contentViewPtr); + + private native boolean nativeGetJavaScriptEnabled(int nativeContentSettings); +} diff --git a/src/org/chromium/content/browser/ContentVideoView.java b/src/org/chromium/content/browser/ContentVideoView.java new file mode 100644 index 0000000..9c0e369 --- /dev/null +++ b/src/org/chromium/content/browser/ContentVideoView.java @@ -0,0 +1,629 @@ +// 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.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.Color; +import android.os.Handler; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +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; + +import java.lang.ref.WeakReference; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.content.common.IChildProcessService; +import org.chromium.content.R; + +@JNINamespace("content") +public class ContentVideoView extends FrameLayout implements MediaPlayerControl, + SurfaceHolder.Callback, View.OnTouchListener, View.OnKeyListener { + + private static final String TAG = "ContentVideoView"; + + /* Do not change these values without updating their counterparts + * in include/media/mediaplayer.h! + */ + private static final int MEDIA_NOP = 0; // interface test message + private static final int MEDIA_PREPARED = 1; + private static final int MEDIA_PLAYBACK_COMPLETE = 2; + private static final int MEDIA_BUFFERING_UPDATE = 3; + private static final int MEDIA_SEEK_COMPLETE = 4; + private static final int MEDIA_SET_VIDEO_SIZE = 5; + 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. + */ + public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 2; + + // all possible internal states + private static final int STATE_ERROR = -1; + private static final int STATE_IDLE = 0; + private static final int STATE_PLAYING = 1; + 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 int mCurrentBufferPercentage; + private int mDuration; + private MediaController mMediaController = null; + private boolean mCanPause; + private boolean mCanSeekBack; + private boolean mCanSeekForward; + + // Native pointer to C++ ContentVideoView object. + private int mNativeContentVideoView = 0; + + // 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; + + // This view will contain the video. + private VideoSurfaceView mVideoSurfaceView; + + // Progress view when the video is loading. + private View mProgressView; + + private Surface mSurface = null; + + // 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 class VideoSurfaceView extends SurfaceView { + + public VideoSurfaceView(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mVideoWidth == 0 && mVideoHeight == 0) { + setMeasuredDimension(1, 1); + return; + } + int width = getDefaultSize(mVideoWidth, widthMeasureSpec); + int height = getDefaultSize(mVideoHeight, heightMeasureSpec); + if (mVideoWidth > 0 && mVideoHeight > 0) { + if ( mVideoWidth * height > width * mVideoHeight ) { + height = width * mVideoHeight / mVideoWidth; + } else if ( mVideoWidth * height < width * mVideoHeight ) { + width = height * mVideoWidth / mVideoHeight; + } + } + setMeasuredDimension(width, height); + } + } + + private static class ProgressView extends LinearLayout { + + private ProgressBar mProgressBar; + private TextView mTextView; + + public ProgressView(Context context) { + super(context); + setOrientation(LinearLayout.VERTICAL); + setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + mProgressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge); + mTextView = new TextView(context); + mTextView.setText(mVideoLoadingText); + addView(mProgressBar); + addView(mTextView); + } + } + + private static class FullScreenMediaController extends MediaController { + + View mVideoView; + + public FullScreenMediaController(Context context, View video) { + super(context); + mVideoView = video; + } + + @Override + public void show() { + super.show(); + if (mVideoView != null) { + mVideoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); + } + } + + @Override + public void hide() { + if (mVideoView != null) { + mVideoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); + } + super.hide(); + } + } + + private Runnable mExitFullscreenRunnable = new Runnable() { + @Override + public void run() { + destroyContentVideoView(); + } + }; + + public ContentVideoView(Context context) { + this(context, 0); + } + + private ContentVideoView(Context context, int nativeContentVideoView) { + super(context); + initResources(context); + + if (nativeContentVideoView == 0) return; + mNativeContentVideoView = nativeContentVideoView; + + mCurrentBufferPercentage = 0; + mVideoSurfaceView = new VideoSurfaceView(context); + } + + private static void initResources(Context context) { + if (mPlaybackErrorText != null) return; + mPlaybackErrorText = context.getString( + org.chromium.content.R.string.media_player_error_text_invalid_progressive_playback); + mUnknownErrorText = context.getString( + org.chromium.content.R.string.media_player_error_text_unknown); + mErrorButton = context.getString( + org.chromium.content.R.string.media_player_error_button); + mErrorTitle = context.getString( + org.chromium.content.R.string.media_player_error_title); + mVideoLoadingText = context.getString( + org.chromium.content.R.string.media_player_loading_video); + } + + 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(); + if (progressView != null) { + mProgressView = progressView; + } else { + mProgressView = new ProgressView(getContext()); + } + 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(); + } + + @CalledByNative + public void onMediaPlayerError(int errorType) { + Log.d(TAG, "OnMediaPlayerError: " + errorType); + if (mCurrentState == STATE_ERROR || mCurrentState == STATE_PLAYBACK_COMPLETED) { + return; + } + + mCurrentState = STATE_ERROR; + if (mMediaController != null) { + mMediaController.hide(); + } + + /* Pop up an error dialog so the user knows that + * something bad has happened. Only try and pop up the dialog + * if we're attached to a window. When we're going away and no + * longer have a window, don't bother showing the user an error. + * + * TODO(qinmin): We need to review whether this Dialog is OK with + * the rest of the browser UI elements. + */ + if (getWindowToken() != null) { + String message; + + if (errorType == MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) { + message = mPlaybackErrorText; + } else { + message = mUnknownErrorText; + } + + new AlertDialog.Builder(getContext()) + .setTitle(mErrorTitle) + .setMessage(message) + .setPositiveButton(mErrorButton, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + /* Inform that the video is over. + */ + onCompletion(); + } + }) + .setCancelable(false) + .show(); + } + } + + @CalledByNative + public void onVideoSizeChanged(int width, int height) { + mVideoWidth = width; + mVideoHeight = height; + if (mVideoWidth != 0 && mVideoHeight != 0) { + mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight); + } + } + + @CalledByNative + public void onBufferingUpdate(int percent) { + mCurrentBufferPercentage = percent; + } + + @CalledByNative + public void onPlaybackComplete() { + onCompletion(); + } + + @CalledByNative + public void updateMediaMetadata( + int videoWidth, + int videoHeight, + int duration, + boolean canPause, + boolean canSeekBack, + boolean canSeekForward) { + mProgressView.setVisibility(View.GONE); + mDuration = duration; + mCanPause = canPause; + mCanSeekBack = canSeekBack; + mCanSeekForward = canSeekForward; + mCurrentState = isPlaying() ? STATE_PLAYING : STATE_PAUSED; + if (mMediaController != null) { + mMediaController.setEnabled(true); + // If paused , should show the controller for ever. + if (isPlaying()) + mMediaController.show(); + else + mMediaController.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(); + } + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + mSurfaceHolder = holder; + openVideo(); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mSurfaceHolder = null; + if (mNativeContentVideoView != 0) { + nativeExitFullscreen(mNativeContentVideoView, true); + mNativeContentVideoView = 0; + post(mExitFullscreenRunnable); + } + removeMediaController(); + } + + public void setMediaController(MediaController controller) { + if (mMediaController != null) { + mMediaController.hide(); + } + mMediaController = controller; + attachMediaController(); + } + + private void attachMediaController() { + if (mMediaController != null) { + mMediaController.setMediaPlayer(this); + mMediaController.setAnchorView(mVideoSurfaceView); + mMediaController.setEnabled(false); + } + } + + @CalledByNative + public void openVideo() { + if (mSurfaceHolder != null) { + mCurrentState = STATE_IDLE; + setMediaController(new FullScreenMediaController(sDelegate.getContext(), this)); + if (mNativeContentVideoView != 0) { + nativeUpdateMediaMetadata(mNativeContentVideoView); + } + mCurrentBufferPercentage = 0; + if (mNativeContentVideoView != 0) { + nativeSetSurface(mNativeContentVideoView, + mSurfaceHolder.getSurface()); + } + } + } + + private void onCompletion() { + mCurrentState = STATE_PLAYBACK_COMPLETED; + if (mMediaController != null) { + mMediaController.hide(); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (isInPlaybackState() && mMediaController != null && + event.getAction() == MotionEvent.ACTION_DOWN) { + toggleMediaControlsVisiblity(); + } + return true; + } + + @Override + public boolean onTrackballEvent(MotionEvent ev) { + if (isInPlaybackState() && mMediaController != null) { + toggleMediaControlsVisiblity(); + } + return false; + } + + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK && + keyCode != KeyEvent.KEYCODE_VOLUME_UP && + keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && + keyCode != KeyEvent.KEYCODE_VOLUME_MUTE && + keyCode != KeyEvent.KEYCODE_CALL && + keyCode != KeyEvent.KEYCODE_MENU && + keyCode != KeyEvent.KEYCODE_SEARCH && + keyCode != KeyEvent.KEYCODE_ENDCALL; + if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) { + if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || + keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { + if (isPlaying()) { + pause(); + mMediaController.show(); + } else { + start(); + mMediaController.hide(); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { + if (!isPlaying()) { + start(); + mMediaController.hide(); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP + || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { + if (isPlaying()) { + pause(); + mMediaController.show(); + } + return true; + } else { + toggleMediaControlsVisiblity(); + } + } else if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { + if (mNativeContentVideoView != 0) { + nativeExitFullscreen(mNativeContentVideoView, false); + destroyNativeView(); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_MENU || keyCode == KeyEvent.KEYCODE_SEARCH) { + return true; + } + return super.onKeyDown(keyCode, event); + } + + private void toggleMediaControlsVisiblity() { + if (mMediaController.isShowing()) { + mMediaController.hide(); + } else { + mMediaController.show(); + } + } + + private boolean isInPlaybackState() { + return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE); + } + + public void start() { + if (isInPlaybackState()) { + if (mNativeContentVideoView != 0) { + nativePlay(mNativeContentVideoView); + } + mCurrentState = STATE_PLAYING; + } + } + + public void pause() { + if (isInPlaybackState()) { + if (isPlaying()) { + if (mNativeContentVideoView != 0) { + nativePause(mNativeContentVideoView); + } + mCurrentState = STATE_PAUSED; + } + } + } + + // cache duration as mDuration for faster access + public int getDuration() { + if (isInPlaybackState()) { + if (mDuration > 0) { + return mDuration; + } + if (mNativeContentVideoView != 0) { + mDuration = nativeGetDurationInMilliSeconds(mNativeContentVideoView); + } else { + mDuration = 0; + } + return mDuration; + } + mDuration = -1; + return mDuration; + } + + public int getCurrentPosition() { + if (isInPlaybackState() && mNativeContentVideoView != 0) { + return nativeGetCurrentPosition(mNativeContentVideoView); + } + return 0; + } + + public void seekTo(int msec) { + if (mNativeContentVideoView != 0) { + nativeSeekTo(mNativeContentVideoView, msec); + } + } + + public boolean isPlaying() { + return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView); + } + + public int getBufferPercentage() { + return mCurrentBufferPercentage; + } + public boolean canPause() { + return mCanPause; + } + + public boolean canSeekBackward() { + return mCanSeekBack; + } + + public boolean canSeekForward() { + return mCanSeekForward; + } + + public int getAudioSessionId() { + return 0; + } + + @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; + } + return null; + } + + public void removeMediaController() { + if (mMediaController != null) { + mMediaController.setEnabled(false); + mMediaController.hide(); + mMediaController = null; + } + } + + public void removeSurfaceView() { + removeView(mVideoSurfaceView); + removeView(mProgressView); + mVideoSurfaceView = null; + mProgressView = null; + } + + @CalledByNative + public static void destroyContentVideoView() { + sDelegate.onDestroyContentVideoView(); + if (sContentVideoView != null) { + sContentVideoView.removeMediaController(); + sContentVideoView.removeSurfaceView(); + sContentVideoView.setVisibility(View.GONE); + } + sContentVideoView = null; + } + + public static ContentVideoView getContentVideoView() { + return sContentVideoView; + } + + public static void registerContentVideoViewContextDelegate( + ContentVideoViewContextDelegate delegate) { + sDelegate = delegate; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + return true; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { + destroyContentVideoView(); + return true; + } + return super.onKeyDown(keyCode, event); + } + + private native void nativeExitFullscreen(int nativeContentVideoView, boolean relaseMediaPlayer); + private native int nativeGetCurrentPosition(int nativeContentVideoView); + private native int nativeGetDurationInMilliSeconds(int nativeContentVideoView); + private native void nativeUpdateMediaMetadata(int nativeContentVideoView); + private native int nativeGetVideoWidth(int nativeContentVideoView); + private native int nativeGetVideoHeight(int nativeContentVideoView); + private native boolean nativeIsPlaying(int nativeContentVideoView); + private native void nativePause(int nativeContentVideoView); + private native void nativePlay(int nativeContentVideoView); + private native void nativeSeekTo(int nativeContentVideoView, int msec); + private native void nativeSetSurface(int nativeContentVideoView, Surface surface); +} diff --git a/src/org/chromium/content/browser/ContentVideoViewContextDelegate.java b/src/org/chromium/content/browser/ContentVideoViewContextDelegate.java new file mode 100644 index 0000000..6eb21b6 --- /dev/null +++ b/src/org/chromium/content/browser/ContentVideoViewContextDelegate.java @@ -0,0 +1,20 @@ +// 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/ContentView.java b/src/org/chromium/content/browser/ContentView.java new file mode 100644 index 0000000..0e6e381 --- /dev/null +++ b/src/org/chromium/content/browser/ContentView.java @@ -0,0 +1,818 @@ +// 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.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Build; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.widget.FrameLayout; + +import com.google.common.annotations.VisibleForTesting; + +import org.chromium.content.common.TraceEvent; +import org.chromium.ui.WindowAndroid; + +/** + * The containing view for {@link ContentViewCore} that exists in the Android UI hierarchy and + * exposes the various {@link View} functionality to it. + * + * TODO(joth): Remove any methods overrides from this class that were added for WebView + * compatibility. + */ +public class ContentView extends FrameLayout + implements ContentViewCore.InternalAccessDelegate, PageInfo { + + private final ContentViewCore mContentViewCore; + + private float mCurrentTouchOffsetX; + private float mCurrentTouchOffsetY; + + /** + * Creates an instance of a ContentView. + * @param context The Context the view is running in, through which it can + * access the current theme, resources, etc. + * @param nativeWebContents A pointer to the native web contents. + * @param windowAndroid An instance of the WindowAndroid. + * @return A ContentView instance. + */ + public static ContentView newInstance(Context context, int nativeWebContents, + WindowAndroid windowAndroid) { + return newInstance(context, nativeWebContents, windowAndroid, null, + android.R.attr.webViewStyle); + } + + /** + * Creates an instance of a ContentView. + * @param context The Context the view is running in, through which it can + * access the current theme, resources, etc. + * @param nativeWebContents A pointer to the native web contents. + * @param windowAndroid An instance of the WindowAndroid. + * @param attrs The attributes of the XML tag that is inflating the view. + * @return A ContentView instance. + */ + public static ContentView newInstance(Context context, int nativeWebContents, + WindowAndroid windowAndroid, AttributeSet attrs) { + // TODO(klobag): use the WebViewStyle as the default style for now. It enables scrollbar. + // When ContentView is moved to framework, we can define its own style in the res. + return newInstance(context, nativeWebContents, windowAndroid, attrs, + android.R.attr.webViewStyle); + } + + /** + * Creates an instance of a ContentView. + * @param context The Context the view is running in, through which it can + * access the current theme, resources, etc. + * @param nativeWebContents A pointer to the native web contents. + * @param windowAndroid An instance of the WindowAndroid. + * @param attrs The attributes of the XML tag that is inflating the view. + * @param defStyle The default style to apply to this view. + * @return A ContentView instance. + */ + public static ContentView newInstance(Context context, int nativeWebContents, + WindowAndroid windowAndroid, AttributeSet attrs, int defStyle) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + return new ContentView(context, nativeWebContents, windowAndroid, attrs, defStyle); + } else { + return new JellyBeanContentView(context, nativeWebContents, windowAndroid, attrs, + defStyle); + } + } + + protected ContentView(Context context, int nativeWebContents, WindowAndroid windowAndroid, + AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mContentViewCore = new ContentViewCore(context); + mContentViewCore.initialize(this, this, nativeWebContents, windowAndroid, + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN ? + ContentViewCore.INPUT_EVENTS_DELIVERED_AT_VSYNC : + ContentViewCore.INPUT_EVENTS_DELIVERED_IMMEDIATELY); + } + + // PageInfo implementation. + + @Override + public String getUrl() { + return mContentViewCore.getUrl(); + } + + @Override + public String getTitle() { + return mContentViewCore.getTitle(); + } + + @Override + public boolean isReadyForSnapshot() { + return !isCrashed() && isReady(); + } + + @Override + public Bitmap getBitmap() { + return getBitmap(getWidth(), getHeight()); + } + + @Override + public Bitmap getBitmap(int width, int height) { + return mContentViewCore.getBitmap(width, height); + } + + @Override + public int getBackgroundColor() { + return mContentViewCore.getBackgroundColor(); + } + + @Override + public View getView() { + return this; + } + + /** + * @return The core component of the ContentView that handles JNI communication. Should only be + * used for passing to native. + */ + public ContentViewCore getContentViewCore() { + return mContentViewCore; + } + + /** + * @return The cache of scales and positions used to convert coordinates from/to CSS. + */ + public RenderCoordinates getRenderCoordinates() { + return mContentViewCore.getRenderCoordinates(); + } + + /** + * Returns true if the given Activity has hardware acceleration enabled + * in its manifest, or in its foreground window. + * + * TODO(husky): Remove when ContentViewCore.initialize() is refactored (see TODO there) + * TODO(dtrainor) This is still used by other classes. Make sure to pull some version of this + * out before removing it. + */ + public static boolean hasHardwareAcceleration(Activity activity) { + return ContentViewCore.hasHardwareAcceleration(activity); + } + + /** + * Destroy the internal state of the WebView. This method may only be called + * after the WebView has been removed from the view system. No other methods + * may be called on this WebView after this method has been called. + */ + public void destroy() { + mContentViewCore.destroy(); + } + + /** + * Returns true initially, false after destroy() has been called. + * It is illegal to call any other public method after destroy(). + */ + public boolean isAlive() { + return mContentViewCore.isAlive(); + } + + /** + * For internal use. Throws IllegalStateException if mNativeContentView is 0. + * Use this to ensure we get a useful Java stack trace, rather than a native + * crash dump, from use-after-destroy bugs in Java code. + */ + void checkIsAlive() throws IllegalStateException { + mContentViewCore.checkIsAlive(); + } + + public void setContentViewClient(ContentViewClient client) { + mContentViewCore.setContentViewClient(client); + } + + @VisibleForTesting + public ContentViewClient getContentViewClient() { + return mContentViewCore.getContentViewClient(); + } + + /** + * Load url without fixing up the url string. Consumers of ContentView are responsible for + * ensuring the URL passed in is properly formatted (i.e. the scheme has been added if left + * off during user input). + * + * @param params Parameters for this load. + */ + public void loadUrl(LoadUrlParams params) { + mContentViewCore.loadUrl(params); + } + + /** + * Stops loading the current web contents. + */ + public void stopLoading() { + mContentViewCore.stopLoading(); + } + + /** + * @return Whether the current WebContents has a previous navigation entry. + */ + public boolean canGoBack() { + return mContentViewCore.canGoBack(); + } + + /** + * @return Whether the current WebContents has a navigation entry after the current one. + */ + public boolean canGoForward() { + return mContentViewCore.canGoForward(); + } + + /** + * @param offset The offset into the navigation history. + * @return Whether we can move in history by given offset + */ + public boolean canGoToOffset(int offset) { + return mContentViewCore.canGoToOffset(offset); + } + + /** + * Navigates to the specified offset from the "current entry". Does nothing if the offset is out + * of bounds. + * @param offset The offset into the navigation history. + */ + public void goToOffset(int offset) { + mContentViewCore.goToOffset(offset); + } + + /** + * Goes to the navigation entry before the current one. + */ + public void goBack() { + mContentViewCore.goBack(); + } + + /** + * Goes to the navigation entry following the current one. + */ + public void goForward() { + mContentViewCore.goForward(); + } + + /** + * Reload the current page. + */ + public void reload() { + mContentViewCore.reload(); + } + + /** + * Clears the WebView's page history in both the backwards and forwards + * directions. + */ + public void clearHistory() { + mContentViewCore.clearHistory(); + } + + String getSelectedText() { + return mContentViewCore.getSelectedText(); + } + + /** + * Start profiling the update speed. You must call {@link #stopFpsProfiling} + * to stop profiling. + */ + @VisibleForTesting + public void startFpsProfiling() { + // TODO(nileshagrawal): Implement this. + } + + /** + * Stop profiling the update speed. + */ + @VisibleForTesting + public float stopFpsProfiling() { + // TODO(nileshagrawal): Implement this. + return 0.0f; + } + + /** + * Fling the ContentView from the current position. + * @param x Fling touch starting position + * @param y Fling touch starting position + * @param velocityX Initial velocity of the fling (X) measured in pixels per second. + * @param velocityY Initial velocity of the fling (Y) measured in pixels per second. + */ + @VisibleForTesting + public void fling(long timeMs, int x, int y, int velocityX, int velocityY) { + mContentViewCore.getContentViewGestureHandler().fling(timeMs, x, y, velocityX, velocityY); + } + + void endFling(long timeMs) { + mContentViewCore.getContentViewGestureHandler().endFling(timeMs); + } + + /** + * Start pinch zoom. You must call {@link #pinchEnd} to stop. + */ + @VisibleForTesting + public void pinchBegin(long timeMs, int x, int y) { + mContentViewCore.getContentViewGestureHandler().pinchBegin(timeMs, x, y); + } + + /** + * Stop pinch zoom. + */ + @VisibleForTesting + public void pinchEnd(long timeMs) { + mContentViewCore.getContentViewGestureHandler().pinchEnd(timeMs); + } + + void setIgnoreSingleTap(boolean value) { + mContentViewCore.getContentViewGestureHandler().setIgnoreSingleTap(value); + } + + /** + * Modify the ContentView magnification level. The effect of calling this + * method is exactly as after "pinch zoom". + * + * @param timeMs The event time in milliseconds. + * @param delta The ratio of the new magnification level over the current + * magnification level. + * @param anchorX The magnification anchor (X) in the current view + * coordinate. + * @param anchorY The magnification anchor (Y) in the current view + * coordinate. + */ + @VisibleForTesting + public void pinchBy(long timeMs, int anchorX, int anchorY, float delta) { + mContentViewCore.getContentViewGestureHandler().pinchBy(timeMs, anchorX, anchorY, delta); + } + + /** + * Injects the passed JavaScript code in the current page and evaluates it. + * + * @throws IllegalStateException If the ContentView has been destroyed. + */ + public void evaluateJavaScript(String script) throws IllegalStateException { + mContentViewCore.evaluateJavaScript(script, null); + } + + /** + * This method should be called when the containing activity is paused. + **/ + public void onActivityPause() { + mContentViewCore.onActivityPause(); + } + + /** + * This method should be called when the containing activity is resumed. + **/ + public void onActivityResume() { + mContentViewCore.onActivityResume(); + } + + /** + * To be called when the ContentView is shown. + **/ + public void onShow() { + mContentViewCore.onShow(); + } + + /** + * To be called when the ContentView is hidden. + **/ + public void onHide() { + mContentViewCore.onHide(); + } + + /** + * Return the ContentSettings object used to retrieve the settings for this + * ContentView. + * @return A ContentSettings object that can be used to retrieve this ContentView's + * settings. + */ + public ContentSettings getContentSettings() { + return mContentViewCore.getContentSettings(); + } + + /** + * Hides the select action bar. + */ + public void hideSelectActionBar() { + mContentViewCore.hideSelectActionBar(); + } + + // FrameLayout overrides. + + // Needed by ContentViewCore.InternalAccessDelegate + @Override + 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(); + super.onSizeChanged(w, h, ow, oh); + mContentViewCore.onSizeChanged(w, h, ow, oh); + TraceEvent.end(); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return mContentViewCore.onCreateInputConnection(outAttrs); + } + + @Override + public boolean onCheckIsTextEditor() { + return mContentViewCore.onCheckIsTextEditor(); + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + TraceEvent.begin(); + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + mContentViewCore.onFocusChanged(gainFocus); + TraceEvent.end(); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return mContentViewCore.onKeyUp(keyCode, event); + } + + @Override + public boolean dispatchKeyEventPreIme(KeyEvent event) { + return mContentViewCore.dispatchKeyEventPreIme(event); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (isFocused()) { + return mContentViewCore.dispatchKeyEvent(event); + } else { + return super.dispatchKeyEvent(event); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + MotionEvent offset = createOffsetMotionEvent(event); + boolean consumed = mContentViewCore.onTouchEvent(offset); + offset.recycle(); + return consumed; + } + + /** + * Mouse move events are sent on hover enter, hover move and hover exit. + * They are sent on hover exit because sometimes it acts as both a hover + * move and hover exit. + */ + @Override + public boolean onHoverEvent(MotionEvent event) { + return mContentViewCore.onHoverEvent(event); + } + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + return mContentViewCore.onGenericMotionEvent(event); + } + + /** + * 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. + * @param dx The X offset in pixels to shift touch events. + * @param dy The Y offset in pixels to shift touch events. + */ + public void setCurrentMotionEventOffsets(float dx, float dy) { + mCurrentTouchOffsetX = dx; + mCurrentTouchOffsetY = dy; + } + + private MotionEvent createOffsetMotionEvent(MotionEvent src) { + MotionEvent dst = MotionEvent.obtain(src); + dst.offsetLocation(mCurrentTouchOffsetX, mCurrentTouchOffsetY); + return dst; + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + mContentViewCore.onConfigurationChanged(newConfig); + } + + /** + * Currently the ContentView scrolling happens in the native side. In + * the Java view system, it is always pinned at (0, 0). scrollBy() and scrollTo() + * are overridden, so that View's mScrollX and mScrollY will be unchanged at + * (0, 0). This is critical for drawing ContentView correctly. + */ + @Override + public void scrollBy(int x, int y) { + mContentViewCore.scrollBy(x, y); + } + + @Override + public void scrollTo(int x, int y) { + mContentViewCore.scrollTo(x, y); + } + + @Override + protected int computeHorizontalScrollExtent() { + // TODO (dtrainor): Need to expose scroll events properly to public. Either make getScroll* + // work or expose computeHorizontalScrollOffset()/computeVerticalScrollOffset as public. + return mContentViewCore.computeHorizontalScrollExtent(); + } + + @Override + protected int computeHorizontalScrollOffset() { + return mContentViewCore.computeHorizontalScrollOffset(); + } + + @Override + protected int computeHorizontalScrollRange() { + return mContentViewCore.computeHorizontalScrollRange(); + } + + @Override + protected int computeVerticalScrollExtent() { + return mContentViewCore.computeVerticalScrollExtent(); + } + + @Override + protected int computeVerticalScrollOffset() { + return mContentViewCore.computeVerticalScrollOffset(); + } + + @Override + protected int computeVerticalScrollRange() { + return mContentViewCore.computeVerticalScrollRange(); + } + + // End FrameLayout overrides. + + @Override + public boolean awakenScrollBars(int startDelay, boolean invalidate) { + return mContentViewCore.awakenScrollBars(startDelay, invalidate); + } + + @Override + public boolean awakenScrollBars() { + return super.awakenScrollBars(); + } + + public int getSingleTapX() { + return mContentViewCore.getContentViewGestureHandler().getSingleTapX(); + } + + public int getSingleTapY() { + return mContentViewCore.getContentViewGestureHandler().getSingleTapY(); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + mContentViewCore.onInitializeAccessibilityNodeInfo(info); + } + + /** + * Fills in scrolling values for AccessibilityEvents. + * @param event Event being fired. + */ + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + mContentViewCore.onInitializeAccessibilityEvent(event); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mContentViewCore.onAttachedToWindow(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mContentViewCore.onDetachedFromWindow(); + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + mContentViewCore.onVisibilityChanged(changedView, visibility); + } + + /** + * Register the delegate to be used when content can not be handled by + * the rendering engine, and should be downloaded instead. This will replace + * the current delegate. + * @param delegate An implementation of ContentViewDownloadDelegate. + */ + public void setDownloadDelegate(ContentViewDownloadDelegate delegate) { + mContentViewCore.setDownloadDelegate(delegate); + } + + // Called by DownloadController. + ContentViewDownloadDelegate getDownloadDelegate() { + return mContentViewCore.getDownloadDelegate(); + } + + public boolean getUseDesktopUserAgent() { + return mContentViewCore.getUseDesktopUserAgent(); + } + + /** + * Set whether or not we're using a desktop user agent for the currently loaded page. + * @param override If true, use a desktop user agent. Use a mobile one otherwise. + * @param reloadOnChange Reload the page if the UA has changed. + */ + public void setUseDesktopUserAgent(boolean override, boolean reloadOnChange) { + mContentViewCore.setUseDesktopUserAgent(override, reloadOnChange); + } + + /** + * @return Whether the native ContentView has crashed. + */ + public boolean isCrashed() { + return mContentViewCore.isCrashed(); + } + + /** + * @return Whether a reload happens when this ContentView is activated. + */ + public boolean needsReload() { + return mContentViewCore.needsReload(); + } + + /** + * Checks whether the WebView can be zoomed in. + * + * @return True if the WebView can be zoomed in. + */ + // This method uses the term 'zoom' for legacy reasons, but relates + // to what chrome calls the 'page scale factor'. + public boolean canZoomIn() { + return mContentViewCore.canZoomIn(); + } + + /** + * Checks whether the WebView can be zoomed out. + * + * @return True if the WebView can be zoomed out. + */ + // This method uses the term 'zoom' for legacy reasons, but relates + // to what chrome calls the 'page scale factor'. + public boolean canZoomOut() { + return mContentViewCore.canZoomOut(); + } + + /** + * Zooms in the WebView by 25% (or less if that would result in zooming in + * more than possible). + * + * @return True if there was a zoom change, false otherwise. + */ + // This method uses the term 'zoom' for legacy reasons, but relates + // to what chrome calls the 'page scale factor'. + public boolean zoomIn() { + return mContentViewCore.zoomIn(); + } + + /** + * Zooms out the WebView by 20% (or less if that would result in zooming out + * more than possible). + * + * @return True if there was a zoom change, false otherwise. + */ + // This method uses the term 'zoom' for legacy reasons, but relates + // to what chrome calls the 'page scale factor'. + public boolean zoomOut() { + return mContentViewCore.zoomOut(); + } + + /** + * Resets the zoom factor of the WebView. + * + * @return True if there was a zoom change, false otherwise. + */ + // This method uses the term 'zoom' for legacy reasons, but relates + // to what chrome calls the 'page scale factor'. + public boolean zoomReset() { + return mContentViewCore.zoomReset(); + } + + /** + * Return the current scale of the WebView + * @return The current scale. + */ + public float getScale() { + return mContentViewCore.getScale(); + } + + /** + * If the view is ready to draw contents to the screen. In hardware mode, + * the initialization of the surface texture may not occur until after the + * view has been added to the layout. This method will return {@code true} + * once the texture is actually ready. + */ + public boolean isReady() { + return mContentViewCore.isReady(); + } + + /** + * Returns whether or not accessibility injection is being used. + */ + public boolean isInjectingAccessibilityScript() { + return mContentViewCore.isInjectingAccessibilityScript(); + } + + /** + * Enable or disable accessibility features. + */ + public void setAccessibilityState(boolean state) { + mContentViewCore.setAccessibilityState(state); + } + + /** + * Stop any TTS notifications that are currently going on. + */ + public void stopCurrentAccessibilityNotifications() { + mContentViewCore.stopCurrentAccessibilityNotifications(); + } + + /** + * Inform WebKit that Fullscreen mode has been exited by the user. + */ + public void exitFullscreen() { + mContentViewCore.exitFullscreen(); + } + + /** + * Return content scroll y. + * + * @return The vertical scroll position in pixels. + */ + public int getContentScrollY() { + return mContentViewCore.computeVerticalScrollOffset(); + } + + /** + * Return content height. + * + * @return The height of the content in pixels. + */ + public int getContentHeight() { + return mContentViewCore.computeVerticalScrollRange(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // Start Implementation of ContentViewCore.InternalAccessDelegate // + /////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public boolean super_onKeyUp(int keyCode, KeyEvent event) { + return super.onKeyUp(keyCode, event); + } + + @Override + public boolean super_dispatchKeyEventPreIme(KeyEvent event) { + return super.dispatchKeyEventPreIme(event); + } + + @Override + public boolean super_dispatchKeyEvent(KeyEvent event) { + return super.dispatchKeyEvent(event); + } + + @Override + public boolean super_onGenericMotionEvent(MotionEvent event) { + return super.onGenericMotionEvent(event); + } + + @Override + public void super_onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + } + + @Override + public boolean super_awakenScrollBars(int startDelay, boolean invalidate) { + return super.awakenScrollBars(startDelay, invalidate); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // End Implementation of ContentViewCore.InternalAccessDelegate // + /////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/src/org/chromium/content/browser/ContentViewClient.java b/src/org/chromium/content/browser/ContentViewClient.java new file mode 100644 index 0000000..a9ff266 --- /dev/null +++ b/src/org/chromium/content/browser/ContentViewClient.java @@ -0,0 +1,172 @@ +// 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.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.ActionMode; +import android.view.KeyEvent; + +import org.chromium.content.browser.SelectActionModeCallback.ActionHandler; + +import java.net.URISyntaxException; + +/** + * Main callback class used by ContentView. + * + * This contains the superset of callbacks required to implement the browser UI and the callbacks + * required to implement the WebView API. + * The memory and reference ownership of this class is unusual - see the .cc file and ContentView + * for more details. + * + * TODO(mkosiba): Rid this guy of default implementations. This class is used by both WebView and + * the browser and we don't want a the browser-specific default implementation to accidentally leak + * over to WebView. + */ +public class ContentViewClient { + // Tag used for logging. + private static final String TAG = "ContentViewClient"; + + public void onUpdateTitle(String title) { + } + + /** + * Called whenever the background color of the page changes as notified by WebKit. + * @param color The new ARGB color of the page background. + */ + public void onBackgroundColorChanged(int color) { + } + + /** + * Lets client listen on the scaling changes on delayed, throttled + * and best-effort basis. Used for WebView.onScaleChanged. + */ + public void onScaleChanged(float oldScale, float newScale) { + } + + /** + * Notifies the client that the position of the top controls has changed. + * @param topControlsOffsetYPix The Y offset of the top controls in physical pixels. + * @param contentOffsetYPix The Y offset of the content in physical pixels. + * @param overdrawBottomHeightPix The overdraw height. + */ + public void onOffsetsForFullscreenChanged( + float topControlsOffsetYPix, float contentOffsetYPix, float overdrawBottomHeightPix) { + } + + public void onTabCrash() { + } + + public boolean shouldOverrideKeyEvent(KeyEvent event) { + int keyCode = event.getKeyCode(); + // We need to send almost every key to WebKit. However: + // 1. We don't want to block the device on the renderer for + // some keys like menu, home, call. + // 2. There are no WebKit equivalents for some of these keys + // (see app/keyboard_codes_win.h) + // Note that these are not the same set as KeyEvent.isSystemKey: + // for instance, AKEYCODE_MEDIA_* will be dispatched to webkit. + if (keyCode == KeyEvent.KEYCODE_MENU || + keyCode == KeyEvent.KEYCODE_HOME || + keyCode == KeyEvent.KEYCODE_BACK || + keyCode == KeyEvent.KEYCODE_CALL || + keyCode == KeyEvent.KEYCODE_ENDCALL || + keyCode == KeyEvent.KEYCODE_POWER || + keyCode == KeyEvent.KEYCODE_HEADSETHOOK || + keyCode == KeyEvent.KEYCODE_CAMERA || + keyCode == KeyEvent.KEYCODE_FOCUS || + keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || + keyCode == KeyEvent.KEYCODE_VOLUME_MUTE || + keyCode == KeyEvent.KEYCODE_VOLUME_UP) { + return true; + } + + // We also have to intercept some shortcuts before we send them to the ContentView. + if (event.isCtrlPressed() && ( + keyCode == KeyEvent.KEYCODE_TAB || + keyCode == KeyEvent.KEYCODE_W || + keyCode == KeyEvent.KEYCODE_F4)) { + return true; + } + + return false; + } + + // Called when an ImeEvent is sent to the page. Can be used to know when some text is entered + // in a page. + public void onImeEvent() { + } + + /** + * Notified when a change to the IME was requested. + * + * @param requestShow Whether the IME was requested to be shown (may already be showing + * though). + */ + public void onImeStateChangeRequested(boolean requestShow) { + } + + // TODO (dtrainor): Should expose getScrollX/Y from ContentView or make + // computeHorizontalScrollOffset()/computeVerticalScrollOffset() public. + /** + * Gives the UI the chance to override each scroll event. + * @param dx The amount scrolled in the X direction (in physical pixels). + * @param dy The amount scrolled in the Y direction (in physical pixels). + * @param scrollX The current X scroll offset (in physical pixels). + * @param scrollY The current Y scroll offset (in physical pixels). + * @return Whether or not the UI consumed and handled this event. + */ + public boolean shouldOverrideScroll(float dx, float dy, float scrollX, float scrollY) { + return false; + } + + /** + * Returns an ActionMode.Callback for in-page selection. + */ + public ActionMode.Callback getSelectActionModeCallback( + Context context, ActionHandler actionHandler, boolean incognito) { + return new SelectActionModeCallback(context, actionHandler, incognito); + } + + /** + * Called when the contextual ActionBar is shown. + */ + public void onContextualActionBarShown() { + } + + /** + * Called when the contextual ActionBar is hidden. + */ + public void onContextualActionBarHidden() { + } + + /** + * Called when a new content intent is requested to be started. + */ + public void onStartContentIntent(Context context, String intentUrl) { + Intent intent; + // Perform generic parsing of the URI to turn it into an Intent. + try { + intent = Intent.parseUri(intentUrl, Intent.URI_INTENT_SCHEME); + } catch (URISyntaxException ex) { + Log.w(TAG, "Bad URI " + intentUrl + ": " + ex.getMessage()); + return; + } + + try { + context.startActivity(intent); + } catch (ActivityNotFoundException ex) { + Log.w(TAG, "No application can handle " + intentUrl); + } + } + + public void onExternalVideoSurfaceRequested(int playerId) { + } + + public void onGeometryChanged(int playerId, float x, float y, float width, float height) { + } +} diff --git a/src/org/chromium/content/browser/ContentViewCore.java b/src/org/chromium/content/browser/ContentViewCore.java new file mode 100644 index 0000000..7bbf3ed --- /dev/null +++ b/src/org/chromium/content/browser/ContentViewCore.java @@ -0,0 +1,2993 @@ +// 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.app.Activity; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.text.Editable; +import android.util.Log; +import android.util.Pair; +import android.view.ActionMode; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.widget.FrameLayout; + +import com.google.common.annotations.VisibleForTesting; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.base.WeakContext; +import org.chromium.content.R; +import org.chromium.content.browser.ContentViewGestureHandler.MotionEventDelegate; +import org.chromium.content.browser.accessibility.AccessibilityInjector; +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.ImeAdapter.AdapterInputConnectionFactory; +import org.chromium.content.browser.input.InsertionHandleController; +import org.chromium.content.browser.input.SelectPopupDialog; +import org.chromium.content.browser.input.SelectionHandleController; +import org.chromium.content.common.TraceEvent; +import org.chromium.ui.ViewAndroid; +import org.chromium.ui.ViewAndroidDelegate; +import org.chromium.ui.WindowAndroid; +import org.chromium.ui.gfx.DeviceDisplayInfo; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +/** + * Provides a Java-side 'wrapper' around a WebContent (native) instance. + * Contains all the major functionality necessary to manage the lifecycle of a ContentView without + * being tied to the view system. + */ +@JNINamespace("content") +public class ContentViewCore implements MotionEventDelegate, NavigationClient { + /** + * Indicates that input events are batched together and delivered just before vsync. + */ + public static final int INPUT_EVENTS_DELIVERED_AT_VSYNC = 1; + + /** + * Opposite of INPUT_EVENTS_DELIVERED_AT_VSYNC. + */ + public static final int INPUT_EVENTS_DELIVERED_IMMEDIATELY = 0; + + private static final String TAG = ContentViewCore.class.getName(); + + // Used to avoid enabling zooming in / out if resulting zooming will + // produce little visible difference. + private static final float ZOOM_CONTROLS_EPSILON = 0.007f; + + // Used to represent gestures for long press and long tap. + private static final int IS_LONG_PRESS = 1; + private static final int IS_LONG_TAP = 2; + + // Length of the delay (in ms) before fading in handles after the last page movement. + private static final int TEXT_HANDLE_FADE_IN_DELAY = 300; + + // If the embedder adds a JavaScript interface object that contains an indirect reference to + // the ContentViewCore, then storing a strong ref to the interface object on the native + // side would prevent garbage collection of the ContentViewCore (as that strong ref would + // create a new GC root). + // For that reason, we store only a weak reference to the interface object on the + // native side. However we still need a strong reference on the Java side to + // prevent garbage collection if the embedder doesn't maintain their own ref to the + // interface object - the Java side ref won't create a new GC root. + // This map stores those refernces. We put into the map on addJavaScriptInterface() + // and remove from it in removeJavaScriptInterface(). + private final Map mJavaScriptInterfaces = new HashMap(); + + // Additionally, we keep track of all Java bound JS objects that are in use on the + // current page to ensure that they are not garbage collected until the page is + // navigated. This includes interface objects that have been removed + // via the removeJavaScriptInterface API and transient objects returned from methods + // on the interface object. Note we use HashSet rather than Set as the native side + // expects HashSet (no bindings for interfaces). + private final HashSet mRetainedJavaScriptObjects = new HashSet(); + + /** + * Interface that consumers of {@link ContentViewCore} must implement to allow the proper + * dispatching of view methods through the containing view. + * + *

+ * All methods with the "super_" prefix should be routed to the parent of the + * implementing container view. + */ + @SuppressWarnings("javadoc") + public interface InternalAccessDelegate { + /** + * @see View#drawChild(Canvas, View, long) + */ + boolean drawChild(Canvas canvas, View child, long drawingTime); + + /** + * @see View#onKeyUp(keyCode, KeyEvent) + */ + boolean super_onKeyUp(int keyCode, KeyEvent event); + + /** + * @see View#dispatchKeyEventPreIme(KeyEvent) + */ + boolean super_dispatchKeyEventPreIme(KeyEvent event); + + /** + * @see View#dispatchKeyEvent(KeyEvent) + */ + boolean super_dispatchKeyEvent(KeyEvent event); + + /** + * @see View#onGenericMotionEvent(MotionEvent) + */ + boolean super_onGenericMotionEvent(MotionEvent event); + + /** + * @see View#onConfigurationChanged(Configuration) + */ + 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() + */ + boolean awakenScrollBars(); + + /** + * @see View#awakenScrollBars(int, boolean) + */ + boolean super_awakenScrollBars(int startDelay, boolean invalidate); + } + + /** + * An interface that allows the embedder to be notified when the pinch gesture starts and + * stops. + */ + public interface PinchGestureStateListener { + /** + * Called when the pinch gesture starts. + */ + void onPinchGestureStart(); + /** + * Called when the pinch gesture ends. + */ + void onPinchGestureEnd(); + } + + /** + * An interface for controlling visibility and state of embedder-provided zoom controls. + */ + 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(); + } + + private VSyncManager.Provider mVSyncProvider; + private VSyncManager.Listener mVSyncListener; + private int mVSyncSubscriberCount; + private boolean mVSyncListenerRegistered; + + // To avoid IPC delay we use input events to directly trigger a vsync signal in the renderer. + // When we do this, we also need to avoid sending the real vsync signal for the current + // frame to avoid double-ticking. This flag is used to inhibit the next vsync notification. + private boolean mDidSignalVSyncUsingInputEvent; + + public VSyncManager.Listener getVSyncListener(VSyncManager.Provider vsyncProvider) { + if (mVSyncProvider != null && mVSyncListenerRegistered) { + mVSyncProvider.unregisterVSyncListener(mVSyncListener); + mVSyncListenerRegistered = false; + } + + mVSyncProvider = vsyncProvider; + mVSyncListener = new VSyncManager.Listener() { + @Override + public void updateVSync(long tickTimeMicros, long intervalMicros) { + if (mNativeContentViewCore != 0) { + nativeUpdateVSyncParameters(mNativeContentViewCore, tickTimeMicros, + intervalMicros); + } + } + + @Override + public void onVSync(long frameTimeMicros) { + animateIfNecessary(frameTimeMicros); + + if (mDidSignalVSyncUsingInputEvent) { + TraceEvent.instant("ContentViewCore::onVSync ignored"); + mDidSignalVSyncUsingInputEvent = false; + return; + } + if (mNativeContentViewCore != 0) { + nativeOnVSync(mNativeContentViewCore, frameTimeMicros); + } + } + }; + + if (mVSyncSubscriberCount > 0) { + // setVSyncNotificationEnabled(true) is called before getVSyncListener. + vsyncProvider.registerVSyncListener(mVSyncListener); + mVSyncListenerRegistered = true; + } + + return mVSyncListener; + } + + @CalledByNative + void setVSyncNotificationEnabled(boolean enabled) { + if (!isVSyncNotificationEnabled() && enabled) { + mDidSignalVSyncUsingInputEvent = false; + } + if (mVSyncProvider != null) { + if (!mVSyncListenerRegistered && enabled) { + mVSyncProvider.registerVSyncListener(mVSyncListener); + mVSyncListenerRegistered = true; + } else if (mVSyncSubscriberCount == 1 && !enabled) { + assert mVSyncListenerRegistered; + mVSyncProvider.unregisterVSyncListener(mVSyncListener); + mVSyncListenerRegistered = false; + } + } + mVSyncSubscriberCount += enabled ? 1 : -1; + assert mVSyncSubscriberCount >= 0; + } + + @CalledByNative + private void resetVSyncNotification() { + while (isVSyncNotificationEnabled()) setVSyncNotificationEnabled(false); + mVSyncSubscriberCount = 0; + mVSyncListenerRegistered = false; + mNeedAnimate = false; + } + + private boolean isVSyncNotificationEnabled() { + return mVSyncProvider != null && mVSyncListenerRegistered; + } + + @CalledByNative + private void setNeedsAnimate() { + if (!mNeedAnimate) { + mNeedAnimate = true; + setVSyncNotificationEnabled(true); + } + } + + private final Context mContext; + private ViewGroup mContainerView; + private InternalAccessDelegate mContainerViewInternals; + private WebContentsObserverAndroid mWebContentsObserver; + + private ContentViewClient mContentViewClient; + + private ContentSettings mContentSettings; + + // Native pointer to C++ ContentViewCoreImpl object which will be set by nativeInit(). + private int mNativeContentViewCore = 0; + + private boolean mAttachedToWindow = false; + + private ContentViewGestureHandler mContentViewGestureHandler; + private PinchGestureStateListener mPinchGestureStateListener; + private ZoomManager mZoomManager; + private ZoomControlsDelegate mZoomControlsDelegate; + + private PopupZoomer mPopupZoomer; + + private Runnable mFakeMouseMoveRunnable = null; + + // Only valid when focused on a text / password field. + private ImeAdapter mImeAdapter; + private ImeAdapter.AdapterInputConnectionFactory mAdapterInputConnectionFactory; + private AdapterInputConnection mInputConnection; + + private SelectionHandleController mSelectionHandleController; + private InsertionHandleController mInsertionHandleController; + + private Runnable mDeferredHandleFadeInRunnable; + + // Size of the viewport in physical pixels as set from onSizeChanged or setInitialViewportSize. + private int mViewportWidthPix; + private int mViewportHeightPix; + private int mPhysicalBackingWidthPix; + private int mPhysicalBackingHeightPix; + private int mOverdrawBottomHeightPix; + private int mViewportSizeOffsetWidthPix; + private int mViewportSizeOffsetHeightPix; + + // Cached copy of all positions and scales as reported by the renderer. + private final RenderCoordinates mRenderCoordinates; + + private final RenderCoordinates.NormalizedPoint mStartHandlePoint; + private final RenderCoordinates.NormalizedPoint mEndHandlePoint; + private final RenderCoordinates.NormalizedPoint mInsertionHandlePoint; + + // Tracks whether a selection is currently active. When applied to selected text, indicates + // whether the last selected text is still highlighted. + private boolean mHasSelection; + private String mLastSelectedText; + private boolean mSelectionEditable; + private ActionMode mActionMode; + private boolean mUnselectAllOnActionModeDismiss; + + // Delegate that will handle GET downloads, and be notified of completion of POST downloads. + private ContentViewDownloadDelegate mDownloadDelegate; + + // The AccessibilityInjector that handles loading Accessibility scripts into the web page. + private AccessibilityInjector mAccessibilityInjector; + + // Temporary notification to tell onSizeChanged to focus a form element, + // because the OSK was just brought up. + private boolean mUnfocusOnNextSizeChanged = false; + private final Rect mFocusPreOSKViewportRect = new Rect(); + + private boolean mNeedUpdateOrientationChanged; + + // Used to keep track of whether we should try to undo the last zoom-to-textfield operation. + private boolean mScrolledAndZoomedFocusedEditableNode = false; + + // Whether we use hardware-accelerated drawing. + private boolean mHardwareAccelerated = false; + + // Whether we received a new frame since consumePendingRendererFrame() was last called. + private boolean mPendingRendererFrame = false; + + // Whether we should animate at the next vsync tick. + private boolean mNeedAnimate = false; + + private ViewAndroid mViewAndroid; + + /** + * Constructs a new ContentViewCore. Embedders must call initialize() after constructing + * a ContentViewCore and before using it. + * + * @param context The context used to create this. + */ + public ContentViewCore(Context context) { + mContext = context; + + WeakContext.initializeWeakContext(context); + HeapStatsLogger.init(mContext.getApplicationContext()); + mAdapterInputConnectionFactory = new AdapterInputConnectionFactory(); + + mRenderCoordinates = new RenderCoordinates(); + mRenderCoordinates.setDeviceScaleFactor( + getContext().getResources().getDisplayMetrics().density); + mStartHandlePoint = mRenderCoordinates.createNormalizedPoint(); + mEndHandlePoint = mRenderCoordinates.createNormalizedPoint(); + mInsertionHandlePoint = mRenderCoordinates.createNormalizedPoint(); + } + + /** + * @return The context used for creating this ContentViewCore. + */ + public Context getContext() { + return mContext; + } + + /** + * @return The ViewGroup that all view actions of this ContentViewCore should interact with. + */ + 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. + * @param offsetXPix The X amount in pixels to shrink the viewport by. + * @param offsetYPix The Y amount in pixels to shrink the viewport by. + */ + public void setViewportSizeOffset(int offsetXPix, int offsetYPix) { + if (offsetXPix != mViewportSizeOffsetWidthPix || + offsetYPix != mViewportSizeOffsetHeightPix) { + mViewportSizeOffsetWidthPix = offsetXPix; + mViewportSizeOffsetHeightPix = offsetYPix; + if (mNativeContentViewCore != 0) nativeWasResized(mNativeContentViewCore); + } + } + + /** + * Returns a delegate that can be used to add and remove views from the ContainerView. + * + * NOTE: Use with care, as not all ContentViewCore users setup their ContainerView in the same + * way. In particular, the Android WebView has limitations on what implementation details can + * be provided via a child view, as they are visible in the API and could introduce + * compatibility breaks with existing applications. If in doubt, contact the + * android_webview/OWNERS + * + * @return A ViewAndroidDelegate that can be used to add and remove views. + */ + @VisibleForTesting + public ViewAndroidDelegate getViewAndroidDelegate() { + return new ViewAndroidDelegate() { + @Override + public View acquireAnchorView() { + View anchorView = new View(getContext()); + mContainerView.addView(anchorView); + return anchorView; + } + + @Override + 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( + scaledWidth, Math.round(height * scale)); + lp.leftMargin = scaledX; + lp.topMargin = (int) mRenderCoordinates.getContentOffsetYPix() + + Math.round(y * scale); + view.setLayoutParams(lp); + } + + @Override + public void releaseAnchorView(View anchorView) { + mContainerView.removeView(anchorView); + } + }; + } + + @VisibleForTesting + public ImeAdapter getImeAdapterForTest() { + return mImeAdapter; + } + + @VisibleForTesting + public void setAdapterInputConnectionFactory(AdapterInputConnectionFactory factory) { + mAdapterInputConnectionFactory = factory; + } + + @VisibleForTesting + public AdapterInputConnection getInputConnectionForTest() { + return mInputConnection; + } + + private ImeAdapter createImeAdapter(Context context) { + return new ImeAdapter(context, getSelectionHandleController(), + getInsertionHandleController(), + new ImeAdapter.ViewEmbedder() { + @Override + public void onImeEvent(boolean isFinish) { + getContentViewClient().onImeEvent(); + if (!isFinish) { + undoScrollFocusedEditableNodeIntoViewIfNeeded(false); + } + } + + @Override + public void onSetFieldValue() { + scrollFocusedEditableNodeIntoView(); + } + + @Override + public void onDismissInput() { + getContentViewClient().onImeStateChangeRequested(false); + } + + @Override + public View getAttachedView() { + return mContainerView; + } + + @Override + public ResultReceiver getNewShowKeyboardReceiver() { + return new ResultReceiver(new Handler()) { + @Override + public void onReceiveResult(int resultCode, Bundle resultData) { + getContentViewClient().onImeStateChangeRequested( + resultCode == InputMethodManager.RESULT_SHOWN || + resultCode == InputMethodManager.RESULT_UNCHANGED_SHOWN); + if (resultCode == InputMethodManager.RESULT_SHOWN) { + // If OSK is newly shown, delay the form focus until + // the onSizeChanged (in order to adjust relative to the + // new size). + getContainerView().getWindowVisibleDisplayFrame( + mFocusPreOSKViewportRect); + } else if (resultCode == + InputMethodManager.RESULT_UNCHANGED_SHOWN) { + // If the OSK was already there, focus the form immediately. + scrollFocusedEditableNodeIntoView(); + } else { + undoScrollFocusedEditableNodeIntoViewIfNeeded(false); + } + } + }; + } + } + ); + } + + /** + * Returns true if the given Activity has hardware acceleration enabled + * in its manifest, or in its foreground window. + * + * TODO(husky): Remove when initialize() is refactored (see TODO there) + * TODO(dtrainor) This is still used by other classes. Make sure to pull some version of this + * out before removing it. + */ + public static boolean hasHardwareAcceleration(Activity activity) { + // Has HW acceleration been enabled manually in the current window? + Window window = activity.getWindow(); + if (window != null) { + if ((window.getAttributes().flags + & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) != 0) { + return true; + } + } + + // Has HW acceleration been enabled in the manifest? + try { + ActivityInfo info = activity.getPackageManager().getActivityInfo( + activity.getComponentName(), 0); + if ((info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0) { + return true; + } + } catch (PackageManager.NameNotFoundException e) { + Log.e("Chrome", "getActivityInfo(self) should not fail"); + } + + return false; + } + + /** + * Returns true if the given Context is a HW-accelerated Activity. + * + * TODO(husky): Remove when initialize() is refactored (see TODO there) + */ + private static boolean hasHardwareAcceleration(Context context) { + if (context instanceof Activity) { + return hasHardwareAcceleration((Activity) context); + } + return false; + } + + /** + * + * @param containerView The view that will act as a container for all views created by this. + * @param internalDispatcher Handles dispatching all hidden or super methods to the + * containerView. + * @param nativeWebContents A pointer to the native web contents. + * @param windowAndroid An instance of the WindowAndroid. + */ + // Perform important post-construction set up of the ContentViewCore. + // We do not require the containing view in the constructor to allow embedders to create a + // ContentViewCore without having fully created its containing view. The containing view + // is a vital component of the ContentViewCore, so embedders must exercise caution in what + // they do with the ContentViewCore before calling initialize(). + // We supply the nativeWebContents pointer here rather than in the constructor to allow us + // to set the private browsing mode at a later point for the WebView implementation. + // Note that the caller remains the owner of the nativeWebContents and is responsible for + // deleting it after destroying the ContentViewCore. + public void initialize(ViewGroup containerView, InternalAccessDelegate internalDispatcher, + int nativeWebContents, WindowAndroid windowAndroid, + int inputEventDeliveryMode) { + // Check whether to use hardware acceleration. This is a bit hacky, and + // only works if the Context is actually an Activity (as it is in the + // Chrome application). + // + // What we're doing here is checking whether the app has *requested* + // hardware acceleration by setting the appropriate flags. This does not + // necessarily mean we're going to *get* hardware acceleration -- that's + // up to the Android framework. + // + // TODO(husky): Once the native code has been updated so that the + // HW acceleration flag can be set dynamically (Grace is doing this), + // move this check into onAttachedToWindow(), where we can test for + // HW support directly. + mHardwareAccelerated = hasHardwareAcceleration(mContext); + + mContainerView = containerView; + + int windowNativePointer = windowAndroid != null ? windowAndroid.getNativePointer() : 0; + + int viewAndroidNativePointer = 0; + if (windowNativePointer != 0) { + mViewAndroid = new ViewAndroid(windowAndroid, getViewAndroidDelegate()); + viewAndroidNativePointer = mViewAndroid.getNativePointer(); + } + + mNativeContentViewCore = nativeInit(mHardwareAccelerated, + nativeWebContents, viewAndroidNativePointer, windowNativePointer); + mContentSettings = new ContentSettings(this, mNativeContentViewCore); + initializeContainerView(internalDispatcher, inputEventDeliveryMode); + + mAccessibilityInjector = AccessibilityInjector.newInstance(this); + mAccessibilityInjector.addOrRemoveAccessibilityApisIfNecessary(); + + String contentDescription = "Web View"; + if (R.string.accessibility_content_view == 0) { + Log.w(TAG, "Setting contentDescription to 'Web View' as no value was specified."); + } else { + contentDescription = mContext.getResources().getString( + R.string.accessibility_content_view); + } + mContainerView.setContentDescription(contentDescription); + mWebContentsObserver = new WebContentsObserverAndroid(this) { + @Override + public void didStartLoading(String url) { + hidePopupDialog(); + resetGestureDetectors(); + } + }; + } + + @CalledByNative + void onNativeContentViewCoreDestroyed(int nativeContentViewCore) { + assert nativeContentViewCore == mNativeContentViewCore; + mNativeContentViewCore = 0; + } + + /** + * Initializes the View that will contain all Views created by the ContentViewCore. + * + * @param internalDispatcher Handles dispatching all hidden or super methods to the + * containerView. + */ + private void initializeContainerView(InternalAccessDelegate internalDispatcher, + int inputEventDeliveryMode) { + TraceEvent.begin(); + mContainerViewInternals = internalDispatcher; + + mContainerView.setWillNotDraw(false); + mContainerView.setFocusable(true); + mContainerView.setFocusableInTouchMode(true); + mContainerView.setClickable(true); + + if (mContainerView.getScrollBarStyle() == View.SCROLLBARS_INSIDE_OVERLAY) { + mContainerView.setHorizontalScrollBarEnabled(false); + mContainerView.setVerticalScrollBarEnabled(false); + } + + mZoomManager = new ZoomManager(mContext, this); + mContentViewGestureHandler = new ContentViewGestureHandler(mContext, this, mZoomManager, + inputEventDeliveryMode); + mZoomControlsDelegate = new ZoomControlsDelegate() { + @Override + public void invokeZoomPicker() {} + @Override + public void dismissZoomPicker() {} + @Override + public void updateZoomControls() {} + }; + + mRenderCoordinates.reset(); + + initPopupZoomer(mContext); + mImeAdapter = createImeAdapter(mContext); + TraceEvent.end(); + } + + private void initPopupZoomer(Context context){ + mPopupZoomer = new PopupZoomer(context); + mPopupZoomer.setOnVisibilityChangedListener(new PopupZoomer.OnVisibilityChangedListener() { + @Override + public void onPopupZoomerShown(final PopupZoomer zoomer) { + mContainerView.post(new Runnable() { + @Override + public void run() { + if (mContainerView.indexOfChild(zoomer) == -1) { + mContainerView.addView(zoomer); + } else { + assert false : "PopupZoomer should never be shown without being hidden"; + } + } + }); + } + + @Override + public void onPopupZoomerHidden(final PopupZoomer zoomer) { + mContainerView.post(new Runnable() { + @Override + public void run() { + if (mContainerView.indexOfChild(zoomer) != -1) { + mContainerView.removeView(zoomer); + mContainerView.invalidate(); + } else { + assert false : "PopupZoomer should never be hidden without being shown"; + } + } + }); + } + }); + // TODO(yongsheng): LONG_TAP is not enabled in PopupZoomer. So need to dispatch a LONG_TAP + // gesture if a user completes a tap on PopupZoomer UI after a LONG_PRESS gesture. + PopupZoomer.OnTapListener listener = new PopupZoomer.OnTapListener() { + @Override + public boolean onSingleTap(View v, MotionEvent e) { + mContainerView.requestFocus(); + if (mNativeContentViewCore != 0) { + nativeSingleTap(mNativeContentViewCore, e.getEventTime(), + e.getX(), e.getY(), true); + } + return true; + } + + @Override + public boolean onLongPress(View v, MotionEvent e) { + if (mNativeContentViewCore != 0) { + nativeLongPress(mNativeContentViewCore, e.getEventTime(), + e.getX(), e.getY(), true); + } + return true; + } + }; + mPopupZoomer.setOnTapListener(listener); + } + + /** + * Destroy the internal state of the ContentView. This method may only be + * called after the ContentView has been removed from the view system. No + * other methods may be called on this ContentView after this method has + * been called. + */ + public void destroy() { + if (mNativeContentViewCore != 0) { + nativeOnJavaContentViewCoreDestroyed(mNativeContentViewCore); + } + resetVSyncNotification(); + mVSyncProvider = null; + if (mViewAndroid != null) mViewAndroid.destroy(); + mNativeContentViewCore = 0; + mContentSettings = null; + mJavaScriptInterfaces.clear(); + mRetainedJavaScriptObjects.clear(); + } + + /** + * Returns true initially, false after destroy() has been called. + * It is illegal to call any other public method after destroy(). + */ + public boolean isAlive() { + return mNativeContentViewCore != 0; + } + + /** + * This is only useful for passing over JNI to native code that requires ContentViewCore*. + * @return native ContentViewCore pointer. + */ + @CalledByNative + public int getNativeContentViewCore() { + return mNativeContentViewCore; + } + + /** + * For internal use. Throws IllegalStateException if mNativeContentView is 0. + * Use this to ensure we get a useful Java stack trace, rather than a native + * crash dump, from use-after-destroy bugs in Java code. + */ + void checkIsAlive() throws IllegalStateException { + if (!isAlive()) { + throw new IllegalStateException("ContentView used after destroy() was called"); + } + } + + public void setContentViewClient(ContentViewClient client) { + if (client == null) { + throw new IllegalArgumentException("The client can't be null."); + } + mContentViewClient = client; + } + + ContentViewClient getContentViewClient() { + if (mContentViewClient == null) { + // We use the Null Object pattern to avoid having to perform a null check in this class. + // We create it lazily because most of the time a client will be set almost immediately + // after ContentView is created. + mContentViewClient = new ContentViewClient(); + // We don't set the native ContentViewClient pointer here on purpose. The native + // implementation doesn't mind a null delegate and using one is better than passing a + // Null Object, since we cut down on the number of JNI calls. + } + return mContentViewClient; + } + + public int getBackgroundColor() { + if (mNativeContentViewCore != 0) { + return nativeGetBackgroundColor(mNativeContentViewCore); + } + 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); + } + + /** + * Load url without fixing up the url string. Consumers of ContentView are responsible for + * ensuring the URL passed in is properly formatted (i.e. the scheme has been added if left + * off during user input). + * + * @param params Parameters for this load. + */ + public void loadUrl(LoadUrlParams params) { + if (mNativeContentViewCore == 0) return; + + nativeLoadUrl(mNativeContentViewCore, + params.mUrl, + params.mLoadUrlType, + params.mTransitionType, + params.mUaOverrideOption, + params.getExtraHeadersString(), + params.mPostData, + params.mBaseUrlForDataUrl, + params.mVirtualUrlForDataUrl, + params.mCanLoadLocalResources); + } + + /** + * Stops loading the current web contents. + */ + public void stopLoading() { + if (mNativeContentViewCore != 0) nativeStopLoading(mNativeContentViewCore); + } + + /** + * Get the URL of the current page. + * + * @return The URL of the current page. + */ + public String getUrl() { + if (mNativeContentViewCore != 0) return nativeGetURL(mNativeContentViewCore); + return null; + } + + /** + * Get the title of the current page. + * + * @return The title of the current page. + */ + public String getTitle() { + if (mNativeContentViewCore != 0) return nativeGetTitle(mNativeContentViewCore); + return null; + } + + /** + * Shows an interstitial page driven by the passed in delegate. + * + * @param url The URL being blocked by the interstitial. + * @param delegate The delegate handling the interstitial. + */ + @VisibleForTesting + public void showInterstitialPage( + String url, InterstitialPageDelegateAndroid delegate) { + if (mNativeContentViewCore == 0) return; + nativeShowInterstitialPage(mNativeContentViewCore, url, delegate.getNative()); + } + + /** + * @return Whether the page is currently showing an interstitial, such as a bad HTTPS page. + */ + public boolean isShowingInterstitialPage() { + return mNativeContentViewCore == 0 ? + false : nativeIsShowingInterstitialPage(mNativeContentViewCore); + } + + /** + * Mark any new frames that have arrived since this function was last called as non-pending. + * + * @return Whether there was a pending frame from the renderer. + */ + public boolean consumePendingRendererFrame() { + boolean hadPendingFrame = mPendingRendererFrame; + mPendingRendererFrame = false; + return hadPendingFrame; + } + + /** + * @return Viewport width in physical pixels as set from onSizeChanged or + * setInitialViewportSize. + */ + @CalledByNative + public int getViewportWidthPix() { return mViewportWidthPix; } + + /** + * @return Viewport height in physical pixels as set from onSizeChanged or + * setInitialViewportSize. + */ + @CalledByNative + public int getViewportHeightPix() { return mViewportHeightPix; } + + /** + * @return Width of underlying physical surface. + */ + @CalledByNative + public int getPhysicalBackingWidthPix() { return mPhysicalBackingWidthPix; } + + /** + * @return Height of underlying physical surface. + */ + @CalledByNative + public int getPhysicalBackingHeightPix() { return mPhysicalBackingHeightPix; } + + /** + * @return Amount the output surface extends past the bottom of the window viewport. + */ + @CalledByNative + public int getOverdrawBottomHeightPix() { return mOverdrawBottomHeightPix; } + + /** + * @return The amount to shrink the viewport relative to {@link #getViewportWidthPix()}. + */ + @CalledByNative + public int getViewportSizeOffsetWidthPix() { return mViewportSizeOffsetWidthPix; } + + /** + * @return The amount to shrink the viewport relative to {@link #getViewportHeightPix()}. + */ + @CalledByNative + public int getViewportSizeOffsetHeightPix() { return mViewportSizeOffsetHeightPix; } + + /** + * @see android.webkit.WebView#getContentHeight() + */ + public float getContentHeightCss() { + return mRenderCoordinates.getContentHeightCss(); + } + + /** + * @see android.webkit.WebView#getContentWidth() + */ + public float getContentWidthCss() { + return mRenderCoordinates.getContentWidthCss(); + } + + public Bitmap getBitmap() { + return getBitmap(getViewportWidthPix(), getViewportHeightPix()); + } + + public Bitmap getBitmap(int width, int height) { + if (width == 0 || height == 0 + || getViewportWidthPix() == 0 || getViewportHeightPix() == 0) { + return null; + } + + Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + + if (mNativeContentViewCore != 0 && + nativePopulateBitmapFromCompositor(mNativeContentViewCore, b)) { + // If we successfully grabbed a bitmap, check if we have to draw the Android overlay + // components as well. + if (mContainerView.getChildCount() > 0) { + Canvas c = new Canvas(b); + c.scale(width / (float) getViewportWidthPix(), + height / (float) getViewportHeightPix()); + mContainerView.draw(c); + } + return b; + } + + return null; + } + + /** + * Generates a bitmap of the content that is performance optimized based on capture time. + * + *

+ * To have a consistent capture time across devices, we will scale down the captured bitmap + * where necessary to reduce the time to generate the bitmap. + * + * @param width The width of the content to be captured. + * @param height The height of the content to be captured. + * @return A pair of the generated bitmap, and the scale that needs to be applied to return the + * bitmap to it's original size (i.e. if the bitmap is scaled down 50%, this + * will be 2). + */ + public Pair getScaledPerformanceOptimizedBitmap(int width, int height) { + float scale = 1f; + // On tablets, always scale down to MDPI for performance reasons. + if (DeviceUtils.isTablet(getContext())) { + scale = getContext().getResources().getDisplayMetrics().density; + } + return Pair.create( + getBitmap((int) (width / scale), (int) (height / scale)), + scale); + } + + /** + * @return Whether the current WebContents has a previous navigation entry. + */ + public boolean canGoBack() { + return mNativeContentViewCore != 0 && nativeCanGoBack(mNativeContentViewCore); + } + + /** + * @return Whether the current WebContents has a navigation entry after the current one. + */ + public boolean canGoForward() { + return mNativeContentViewCore != 0 && nativeCanGoForward(mNativeContentViewCore); + } + + /** + * @param offset The offset into the navigation history. + * @return Whether we can move in history by given offset + */ + public boolean canGoToOffset(int offset) { + return mNativeContentViewCore != 0 && nativeCanGoToOffset(mNativeContentViewCore, offset); + } + + /** + * Navigates to the specified offset from the "current entry". Does nothing if the offset is out + * of bounds. + * @param offset The offset into the navigation history. + */ + public void goToOffset(int offset) { + if (mNativeContentViewCore != 0) nativeGoToOffset(mNativeContentViewCore, offset); + } + + @Override + public void goToNavigationIndex(int index) { + if (mNativeContentViewCore != 0) nativeGoToNavigationIndex(mNativeContentViewCore, index); + } + + /** + * Goes to the navigation entry before the current one. + */ + public void goBack() { + if (mNativeContentViewCore != 0) nativeGoBack(mNativeContentViewCore); + } + + /** + * Goes to the navigation entry following the current one. + */ + public void goForward() { + if (mNativeContentViewCore != 0) nativeGoForward(mNativeContentViewCore); + } + + /** + * Reload the current page. + */ + public void reload() { + mAccessibilityInjector.addOrRemoveAccessibilityApisIfNecessary(); + if (mNativeContentViewCore != 0) nativeReload(mNativeContentViewCore); + } + + /** + * Cancel the pending reload. + */ + public void cancelPendingReload() { + if (mNativeContentViewCore != 0) nativeCancelPendingReload(mNativeContentViewCore); + } + + /** + * Continue the pending reload. + */ + public void continuePendingReload() { + if (mNativeContentViewCore != 0) nativeContinuePendingReload(mNativeContentViewCore); + } + + /** + * Clears the ContentViewCore's page history in both the backwards and + * forwards directions. + */ + public void clearHistory() { + if (mNativeContentViewCore != 0) nativeClearHistory(mNativeContentViewCore); + } + + String getSelectedText() { + return mHasSelection ? mLastSelectedText : ""; + } + + // End FrameLayout overrides. + + /** + * @see {@link android.webkit.WebView#flingScroll(int, int)} + */ + public void flingScroll(int vx, int vy) { + // Notes: + // (1) Use large negative values for the x/y parameters so we don't accidentally scroll a + // nested frame. + // (2) vx and vy are inverted to match WebView behavior. + mContentViewGestureHandler.fling( + System.currentTimeMillis(), -Integer.MAX_VALUE, -Integer.MIN_VALUE, -vx, -vy); + } + + /** + * @see View#onTouchEvent(MotionEvent) + */ + public boolean onTouchEvent(MotionEvent event) { + undoScrollFocusedEditableNodeIntoViewIfNeeded(false); + return mContentViewGestureHandler.onTouchEvent(event); + } + + /** + * @return ContentViewGestureHandler for all MotionEvent and gesture related calls. + */ + ContentViewGestureHandler getContentViewGestureHandler() { + return mContentViewGestureHandler; + } + + @Override + public boolean sendTouchEvent(long timeMs, int action, TouchPoint[] pts) { + if (mNativeContentViewCore != 0) { + return nativeSendTouchEvent(mNativeContentViewCore, timeMs, action, pts); + } + return false; + } + + @SuppressWarnings("unused") + @CalledByNative + private void hasTouchEventHandlers(boolean hasTouchHandlers) { + mContentViewGestureHandler.hasTouchEventHandlers(hasTouchHandlers); + } + + @SuppressWarnings("unused") + @CalledByNative + private void confirmTouchEvent(int ackResult) { + mContentViewGestureHandler.confirmTouchEvent(ackResult); + } + + @Override + public boolean sendGesture(int type, long timeMs, int x, int y, boolean lastInputEventForVSync, + Bundle b) { + if (mNativeContentViewCore == 0) return false; + updateTextHandlesForGesture(type); + updatePinchGestureStateListener(type); + if (lastInputEventForVSync && isVSyncNotificationEnabled()) { + assert type == ContentViewGestureHandler.GESTURE_SCROLL_BY || + type == ContentViewGestureHandler.GESTURE_PINCH_BY; + mDidSignalVSyncUsingInputEvent = true; + } + switch (type) { + case ContentViewGestureHandler.GESTURE_SHOW_PRESSED_STATE: + nativeShowPressState(mNativeContentViewCore, timeMs, x, y); + return true; + case ContentViewGestureHandler.GESTURE_SHOW_PRESS_CANCEL: + nativeShowPressCancel(mNativeContentViewCore, timeMs, x, y); + return true; + case ContentViewGestureHandler.GESTURE_DOUBLE_TAP: + nativeDoubleTap(mNativeContentViewCore, timeMs, x, y); + return true; + case ContentViewGestureHandler.GESTURE_SINGLE_TAP_UP: + nativeSingleTap(mNativeContentViewCore, timeMs, x, y, false); + return true; + case ContentViewGestureHandler.GESTURE_SINGLE_TAP_CONFIRMED: + handleTapOrPress(timeMs, x, y, 0, + b.getBoolean(ContentViewGestureHandler.SHOW_PRESS, false)); + return true; + case ContentViewGestureHandler.GESTURE_SINGLE_TAP_UNCONFIRMED: + nativeSingleTapUnconfirmed(mNativeContentViewCore, timeMs, x, y); + return true; + case ContentViewGestureHandler.GESTURE_LONG_PRESS: + handleTapOrPress(timeMs, x, y, IS_LONG_PRESS, false); + return true; + case ContentViewGestureHandler.GESTURE_LONG_TAP: + handleTapOrPress(timeMs, x, y, IS_LONG_TAP, false); + return true; + case ContentViewGestureHandler.GESTURE_SCROLL_START: + nativeScrollBegin(mNativeContentViewCore, timeMs, x, y); + return true; + case ContentViewGestureHandler.GESTURE_SCROLL_BY: { + int dx = b.getInt(ContentViewGestureHandler.DISTANCE_X); + int dy = b.getInt(ContentViewGestureHandler.DISTANCE_Y); + nativeScrollBy(mNativeContentViewCore, timeMs, x, y, dx, dy, + lastInputEventForVSync); + return true; + } + case ContentViewGestureHandler.GESTURE_SCROLL_END: + nativeScrollEnd(mNativeContentViewCore, timeMs); + return true; + case ContentViewGestureHandler.GESTURE_FLING_START: + nativeFlingStart(mNativeContentViewCore, timeMs, x, y, + b.getInt(ContentViewGestureHandler.VELOCITY_X, 0), + b.getInt(ContentViewGestureHandler.VELOCITY_Y, 0)); + return true; + case ContentViewGestureHandler.GESTURE_FLING_CANCEL: + nativeFlingCancel(mNativeContentViewCore, timeMs); + return true; + case ContentViewGestureHandler.GESTURE_PINCH_BEGIN: + nativePinchBegin(mNativeContentViewCore, timeMs, x, y); + return true; + case ContentViewGestureHandler.GESTURE_PINCH_BY: + nativePinchBy(mNativeContentViewCore, timeMs, x, y, + b.getFloat(ContentViewGestureHandler.DELTA, 0), + lastInputEventForVSync); + return true; + case ContentViewGestureHandler.GESTURE_PINCH_END: + nativePinchEnd(mNativeContentViewCore, timeMs); + return true; + default: + return false; + } + } + + public void setPinchGestureStateListener(PinchGestureStateListener pinchGestureStateListener) { + mPinchGestureStateListener = pinchGestureStateListener; + } + + void updatePinchGestureStateListener(int gestureType) { + if (mPinchGestureStateListener == null) return; + + switch (gestureType) { + case ContentViewGestureHandler.GESTURE_PINCH_BEGIN: + mPinchGestureStateListener.onPinchGestureStart(); + break; + case ContentViewGestureHandler.GESTURE_PINCH_END: + mPinchGestureStateListener.onPinchGestureEnd(); + break; + default: + break; + } + } + + public interface JavaScriptCallback { + void handleJavaScriptResult(String jsonResult); + } + + /** + * Injects the passed Javascript code in the current page and evaluates it. + * If a result is required, pass in a callback. + * Used in automation tests. + * + * @param script The Javascript to execute. + * @param callback The callback to be fired off when a result is ready. The script's + * result will be json encoded and passed as the parameter, and the call + * will be made on the main thread. + * If no result is required, pass null. + * @throws IllegalStateException If the ContentView has been destroyed. + */ + public void evaluateJavaScript( + String script, JavaScriptCallback callback) throws IllegalStateException { + checkIsAlive(); + nativeEvaluateJavaScript(mNativeContentViewCore, script, callback); + } + + /** + * This method should be called when the containing activity is paused. + */ + public void onActivityPause() { + TraceEvent.begin(); + hidePopupDialog(); + nativeOnHide(mNativeContentViewCore); + setAccessibilityState(false); + TraceEvent.end(); + } + + /** + * This method should be called when the containing activity is resumed. + */ + public void onActivityResume() { + nativeOnShow(mNativeContentViewCore); + setAccessibilityState(true); + } + + /** + * To be called when the ContentView is shown. + */ + public void onShow() { + nativeOnShow(mNativeContentViewCore); + setAccessibilityState(true); + } + + /** + * To be called when the ContentView is hidden. + */ + public void onHide() { + hidePopupDialog(); + setAccessibilityState(false); + nativeOnHide(mNativeContentViewCore); + } + + /** + * Return the ContentSettings object used to retrieve the settings for this + * ContentViewCore. For modifications, ChromeNativePreferences is to be used. + * @return A ContentSettings object that can be used to retrieve this + * ContentViewCore's settings. + */ + public ContentSettings getContentSettings() { + return mContentSettings; + } + + @Override + public boolean didUIStealScroll(float x, float y) { + return getContentViewClient().shouldOverrideScroll( + x, y, computeHorizontalScrollOffset(), computeVerticalScrollOffset()); + } + + @Override + public boolean hasFixedPageScale() { + return mRenderCoordinates.hasFixedPageScale(); + } + + private void hidePopupDialog() { + SelectPopupDialog.hide(this); + hideHandles(); + hideSelectActionBar(); + } + + void hideSelectActionBar() { + if (mActionMode != null) { + mActionMode.finish(); + } + } + + private void resetGestureDetectors() { + mContentViewGestureHandler.resetGestureHandlers(); + } + + /** + * @see View#onAttachedToWindow() + */ + @SuppressWarnings("javadoc") + public void onAttachedToWindow() { + mAttachedToWindow = true; + if (mNativeContentViewCore != 0) { + int pid = nativeGetCurrentRenderProcessId(mNativeContentViewCore); + if (pid > 0) { + ChildProcessLauncher.bindAsHighPriority(pid); + } + } + setAccessibilityState(true); + } + + /** + * @see View#onDetachedFromWindow() + */ + @SuppressWarnings("javadoc") + public void onDetachedFromWindow() { + mAttachedToWindow = false; + if (mNativeContentViewCore != 0) { + int pid = nativeGetCurrentRenderProcessId(mNativeContentViewCore); + if (pid > 0) { + ChildProcessLauncher.unbindAsHighPriority(pid); + } + } + setAccessibilityState(false); + hidePopupDialog(); + mZoomControlsDelegate.dismissZoomPicker(); + } + + /** + * @see View#onVisibilityChanged(android.view.View, int) + */ + public void onVisibilityChanged(View changedView, int visibility) { + if (visibility != View.VISIBLE) { + mZoomControlsDelegate.dismissZoomPicker(); + } + } + + /** + * @see View#onCreateInputConnection(EditorInfo) + */ + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + if (!mImeAdapter.hasTextInputType()) { + // Although onCheckIsTextEditor will return false in this case, the EditorInfo + // is still used by the InputMethodService. Need to make sure the IME doesn't + // enter fullscreen mode. + outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; + } + mInputConnection = + mAdapterInputConnectionFactory.get(mContainerView, mImeAdapter, outAttrs); + return mInputConnection; + } + + public Editable getEditableForTest() { + return mInputConnection.getEditable(); + } + + /** + * @see View#onCheckIsTextEditor() + */ + public boolean onCheckIsTextEditor() { + return mImeAdapter.hasTextInputType(); + } + + /** + * @see View#onConfigurationChanged(Configuration) + */ + @SuppressWarnings("javadoc") + public void onConfigurationChanged(Configuration newConfig) { + TraceEvent.begin(); + + if (newConfig.keyboard != Configuration.KEYBOARD_NOKEYS) { + mImeAdapter.attach(nativeGetNativeImeAdapter(mNativeContentViewCore), + ImeAdapter.getTextInputTypeNone(), + AdapterInputConnection.INVALID_SELECTION, + AdapterInputConnection.INVALID_SELECTION); + InputMethodManager manager = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + manager.restartInput(mContainerView); + } + mContainerViewInternals.super_onConfigurationChanged(newConfig); + mNeedUpdateOrientationChanged = true; + TraceEvent.end(); + } + + /** + * @see View#onSizeChanged(int, int, int, int) + */ + @SuppressWarnings("javadoc") + public void onSizeChanged(int wPix, int hPix, int owPix, int ohPix) { + if (getViewportWidthPix() == wPix && getViewportHeightPix() == hPix) return; + + mViewportWidthPix = wPix; + mViewportHeightPix = hPix; + if (mNativeContentViewCore != 0) { + nativeWasResized(mNativeContentViewCore); + } + + updateAfterSizeChanged(); + } + + /** + * Called when the underlying surface the compositor draws to changes size. + * This may be larger than the viewport size. + */ + public void onPhysicalBackingSizeChanged(int wPix, int hPix) { + if (mPhysicalBackingWidthPix == wPix && mPhysicalBackingHeightPix == hPix) return; + + mPhysicalBackingWidthPix = wPix; + mPhysicalBackingHeightPix = hPix; + + if (mNativeContentViewCore != 0) { + nativeWasResized(mNativeContentViewCore); + } + } + + /** + * Called when the amount the surface is overdrawing off the bottom has changed. + * @param overdrawHeightPix The overdraw height. + */ + public void onOverdrawBottomHeightChanged(int overdrawHeightPix) { + if (mOverdrawBottomHeightPix == overdrawHeightPix) return; + + mOverdrawBottomHeightPix = overdrawHeightPix; + + if (mNativeContentViewCore != 0) { + nativeWasResized(mNativeContentViewCore); + } + } + + private void updateAfterSizeChanged() { + mPopupZoomer.hide(false); + + // Execute a delayed form focus operation because the OSK was brought + // up earlier. + if (!mFocusPreOSKViewportRect.isEmpty()) { + Rect rect = new Rect(); + getContainerView().getWindowVisibleDisplayFrame(rect); + if (!rect.equals(mFocusPreOSKViewportRect)) { + scrollFocusedEditableNodeIntoView(); + mFocusPreOSKViewportRect.setEmpty(); + } + } else if (mUnfocusOnNextSizeChanged) { + undoScrollFocusedEditableNodeIntoViewIfNeeded(true); + mUnfocusOnNextSizeChanged = false; + } + + if (mNeedUpdateOrientationChanged) { + sendOrientationChangeEvent(); + mNeedUpdateOrientationChanged = false; + } + } + + private void scrollFocusedEditableNodeIntoView() { + if (mNativeContentViewCore != 0) { + Runnable scrollTask = new Runnable() { + @Override + public void run() { + if (mNativeContentViewCore != 0) { + nativeScrollFocusedEditableNodeIntoView(mNativeContentViewCore); + } + } + }; + + scrollTask.run(); + + // The native side keeps track of whether the zoom and scroll actually occurred. It is + // more efficient to do it this way and sometimes fire an unnecessary message rather + // than synchronize with the renderer and always have an additional message. + mScrolledAndZoomedFocusedEditableNode = true; + } + } + + private void undoScrollFocusedEditableNodeIntoViewIfNeeded(boolean backButtonPressed) { + // The only call to this function that matters is the first call after the + // scrollFocusedEditableNodeIntoView function call. + // If the first call to this function is a result of a back button press we want to undo the + // preceding scroll. If the call is a result of some other action we don't want to perform + // an undo. + // All subsequent calls are ignored since only the scroll function sets + // mScrolledAndZoomedFocusedEditableNode to true. + if (mScrolledAndZoomedFocusedEditableNode && backButtonPressed && + mNativeContentViewCore != 0) { + Runnable scrollTask = new Runnable() { + @Override + public void run() { + if (mNativeContentViewCore != 0) { + nativeUndoScrollFocusedEditableNodeIntoView(mNativeContentViewCore); + } + } + }; + + scrollTask.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); + } + + /** + * @see View#onKeyUp(int, KeyEvent) + */ + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (mPopupZoomer.isShowing() && keyCode == KeyEvent.KEYCODE_BACK) { + mPopupZoomer.hide(true); + return true; + } + return mContainerViewInternals.super_onKeyUp(keyCode, event); + } + + /** + * @see View#dispatchKeyEventPreIme(KeyEvent) + */ + public boolean dispatchKeyEventPreIme(KeyEvent event) { + try { + TraceEvent.begin(); + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && mImeAdapter.isActive()) { + mUnfocusOnNextSizeChanged = true; + } else { + undoScrollFocusedEditableNodeIntoViewIfNeeded(false); + } + return mContainerViewInternals.super_dispatchKeyEventPreIme(event); + } finally { + TraceEvent.end(); + } + } + + /** + * @see View#dispatchKeyEvent(KeyEvent) + */ + public boolean dispatchKeyEvent(KeyEvent event) { + if (getContentViewClient().shouldOverrideKeyEvent(event)) { + return mContainerViewInternals.super_dispatchKeyEvent(event); + } + + if (mImeAdapter.dispatchKeyEvent(event)) return true; + + return mContainerViewInternals.super_dispatchKeyEvent(event); + } + + /** + * @see View#onHoverEvent(MotionEvent) + * Mouse move events are sent on hover enter, hover move and hover exit. + * They are sent on hover exit because sometimes it acts as both a hover + * move and hover exit. + */ + public boolean onHoverEvent(MotionEvent event) { + TraceEvent.begin("onHoverEvent"); + mContainerView.removeCallbacks(mFakeMouseMoveRunnable); + if (mNativeContentViewCore != 0) { + nativeSendMouseMoveEvent(mNativeContentViewCore, event.getEventTime(), + event.getX(), event.getY()); + } + TraceEvent.end("onHoverEvent"); + return true; + } + + /** + * @see View#onGenericMotionEvent(MotionEvent) + */ + public boolean onGenericMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEvent.ACTION_SCROLL: + nativeSendMouseWheelEvent(mNativeContentViewCore, event.getEventTime(), + event.getX(), event.getY(), + event.getAxisValue(MotionEvent.AXIS_VSCROLL)); + + mContainerView.removeCallbacks(mFakeMouseMoveRunnable); + // Send a delayed onMouseMove event so that we end + // up hovering over the right position after the scroll. + final MotionEvent eventFakeMouseMove = MotionEvent.obtain(event); + mFakeMouseMoveRunnable = new Runnable() { + @Override + public void run() { + onHoverEvent(eventFakeMouseMove); + } + }; + mContainerView.postDelayed(mFakeMouseMoveRunnable, 250); + return true; + } + } + return mContainerViewInternals.super_onGenericMotionEvent(event); + } + + /** + * @see View#scrollBy(int, int) + * Currently the ContentView scrolling happens in the native side. In + * the Java view system, it is always pinned at (0, 0). scrollBy() and scrollTo() + * are overridden, so that View's mScrollX and mScrollY will be unchanged at + * (0, 0). This is critical for drawing ContentView correctly. + */ + public void scrollBy(int xPix, int yPix) { + if (mNativeContentViewCore != 0) { + nativeScrollBy(mNativeContentViewCore, + System.currentTimeMillis(), 0, 0, xPix, yPix, false); + } + } + + /** + * @see View#scrollTo(int, int) + */ + public void scrollTo(int xPix, int yPix) { + if (mNativeContentViewCore == 0) return; + final float xCurrentPix = mRenderCoordinates.getScrollXPix(); + final float yCurrentPix = mRenderCoordinates.getScrollYPix(); + final float dxPix = xPix - xCurrentPix; + final float dyPix = yPix - yCurrentPix; + if (dxPix != 0 || dyPix != 0) { + long time = System.currentTimeMillis(); + nativeScrollBegin(mNativeContentViewCore, time, xCurrentPix, yCurrentPix); + nativeScrollBy(mNativeContentViewCore, + time, xCurrentPix, yCurrentPix, dxPix, dyPix, false); + nativeScrollEnd(mNativeContentViewCore, time); + } + } + + // NOTE: this can go away once ContentView.getScrollX() reports correct values. + // see: b/6029133 + public int getNativeScrollXForTest() { + return mRenderCoordinates.getScrollXPixInt(); + } + + // NOTE: this can go away once ContentView.getScrollY() reports correct values. + // see: b/6029133 + public int getNativeScrollYForTest() { + return mRenderCoordinates.getScrollYPixInt(); + } + + /** + * @see View#computeHorizontalScrollExtent() + */ + @SuppressWarnings("javadoc") + public int computeHorizontalScrollExtent() { + return mRenderCoordinates.getLastFrameViewportWidthPixInt(); + } + + /** + * @see View#computeHorizontalScrollOffset() + */ + @SuppressWarnings("javadoc") + public int computeHorizontalScrollOffset() { + return mRenderCoordinates.getScrollXPixInt(); + } + + /** + * @see View#computeHorizontalScrollRange() + */ + @SuppressWarnings("javadoc") + public int computeHorizontalScrollRange() { + return mRenderCoordinates.getContentWidthPixInt(); + } + + /** + * @see View#computeVerticalScrollExtent() + */ + @SuppressWarnings("javadoc") + public int computeVerticalScrollExtent() { + return mRenderCoordinates.getLastFrameViewportHeightPixInt(); + } + + /** + * @see View#computeVerticalScrollOffset() + */ + @SuppressWarnings("javadoc") + public int computeVerticalScrollOffset() { + return mRenderCoordinates.getScrollYPixInt(); + } + + /** + * @see View#computeVerticalScrollRange() + */ + @SuppressWarnings("javadoc") + public int computeVerticalScrollRange() { + return mRenderCoordinates.getContentHeightPixInt(); + } + + // End FrameLayout overrides. + + /** + * @see View#awakenScrollBars(int, boolean) + */ + @SuppressWarnings("javadoc") + public boolean awakenScrollBars(int startDelay, boolean invalidate) { + // For the default implementation of ContentView which draws the scrollBars on the native + // side, calling this function may get us into a bad state where we keep drawing the + // scrollBars, so disable it by always returning false. + if (mContainerView.getScrollBarStyle() == View.SCROLLBARS_INSIDE_OVERLAY) { + return false; + } else { + return mContainerViewInternals.super_awakenScrollBars(startDelay, invalidate); + } + } + + @SuppressWarnings("unused") + @CalledByNative + private void onTabCrash() { + getContentViewClient().onTabCrash(); + } + + private void handleTapOrPress( + long timeMs, float xPix, float yPix, int isLongPressOrTap, boolean showPress) { + if (!mContainerView.isFocused()) mContainerView.requestFocus(); + + if (!mPopupZoomer.isShowing()) mPopupZoomer.setLastTouch(xPix, yPix); + + if (isLongPressOrTap == IS_LONG_PRESS) { + getInsertionHandleController().allowAutomaticShowing(); + getSelectionHandleController().allowAutomaticShowing(); + if (mNativeContentViewCore != 0) { + nativeLongPress(mNativeContentViewCore, timeMs, xPix, yPix, false); + } + } else if (isLongPressOrTap == IS_LONG_TAP) { + getInsertionHandleController().allowAutomaticShowing(); + getSelectionHandleController().allowAutomaticShowing(); + if (mNativeContentViewCore != 0) { + nativeLongTap(mNativeContentViewCore, timeMs, xPix, yPix, false); + } + } else { + if (!showPress && mNativeContentViewCore != 0) { + nativeShowPressState(mNativeContentViewCore, timeMs, xPix, yPix); + } + if (mSelectionEditable) getInsertionHandleController().allowAutomaticShowing(); + if (mNativeContentViewCore != 0) { + nativeSingleTap(mNativeContentViewCore, timeMs, xPix, yPix, false); + } + } + } + + public void setZoomControlsDelegate(ZoomControlsDelegate zoomControlsDelegate) { + mZoomControlsDelegate = zoomControlsDelegate; + } + + public void updateMultiTouchZoomSupport(boolean supportsMultiTouchZoom) { + mZoomManager.updateMultiTouchSupport(supportsMultiTouchZoom); + } + + public void selectPopupMenuItems(int[] indices) { + if (mNativeContentViewCore != 0) { + nativeSelectPopupMenuItems(mNativeContentViewCore, indices); + } + } + + /** + * Get the screen orientation from the OS and push it to WebKit. + * + * TODO(husky): Add a hook for mock orientations. + * + * TODO(husky): Currently each new tab starts with an orientation of 0 until you actually + * rotate the device. This is wrong if you actually started in landscape mode. To fix this, we + * need to push the correct orientation, but only after WebKit's Frame object has been fully + * initialized. Need to find a good time to do that. onPageFinished() would probably work but + * it isn't implemented yet. + */ + private void sendOrientationChangeEvent() { + if (mNativeContentViewCore == 0) return; + + WindowManager windowManager = + (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); + switch (windowManager.getDefaultDisplay().getRotation()) { + case Surface.ROTATION_90: + nativeSendOrientationChangeEvent(mNativeContentViewCore, 90); + break; + case Surface.ROTATION_180: + nativeSendOrientationChangeEvent(mNativeContentViewCore, 180); + break; + case Surface.ROTATION_270: + nativeSendOrientationChangeEvent(mNativeContentViewCore, -90); + break; + case Surface.ROTATION_0: + nativeSendOrientationChangeEvent(mNativeContentViewCore, 0); + break; + default: + Log.w(TAG, "Unknown rotation!"); + break; + } + } + + /** + * Register the delegate to be used when content can not be handled by + * the rendering engine, and should be downloaded instead. This will replace + * the current delegate, if any. + * @param delegate An implementation of ContentViewDownloadDelegate. + */ + public void setDownloadDelegate(ContentViewDownloadDelegate delegate) { + mDownloadDelegate = delegate; + } + + // Called by DownloadController. + ContentViewDownloadDelegate getDownloadDelegate() { + return mDownloadDelegate; + } + + private SelectionHandleController getSelectionHandleController() { + if (mSelectionHandleController == null) { + mSelectionHandleController = new SelectionHandleController(getContainerView()) { + @Override + public void selectBetweenCoordinates(int x1, int y1, int x2, int y2) { + if (mNativeContentViewCore != 0 && !(x1 == x2 && y1 == y2)) { + nativeSelectBetweenCoordinates(mNativeContentViewCore, + x1, y1 - mRenderCoordinates.getContentOffsetYPix(), + x2, y2 - mRenderCoordinates.getContentOffsetYPix()); + } + } + + @Override + public void showHandles(int startDir, int endDir) { + super.showHandles(startDir, endDir); + showSelectActionBar(); + } + + }; + + mSelectionHandleController.hideAndDisallowAutomaticShowing(); + } + + return mSelectionHandleController; + } + + private InsertionHandleController getInsertionHandleController() { + if (mInsertionHandleController == null) { + mInsertionHandleController = new InsertionHandleController(getContainerView()) { + private static final int AVERAGE_LINE_HEIGHT = 14; + + @Override + public void setCursorPosition(int x, int y) { + if (mNativeContentViewCore != 0) { + nativeMoveCaret(mNativeContentViewCore, + x, y - mRenderCoordinates.getContentOffsetYPix()); + } + } + + @Override + public void paste() { + mImeAdapter.paste(); + hideHandles(); + } + + @Override + public int getLineHeight() { + return (int) Math.ceil( + mRenderCoordinates.fromLocalCssToPix(AVERAGE_LINE_HEIGHT)); + } + + @Override + public void showHandle() { + super.showHandle(); + } + }; + + mInsertionHandleController.hideAndDisallowAutomaticShowing(); + } + + return mInsertionHandleController; + } + + public InsertionHandleController getInsertionHandleControllerForTest() { + return mInsertionHandleController; + } + + private void updateHandleScreenPositions() { + if (isSelectionHandleShowing()) { + mSelectionHandleController.setStartHandlePosition( + mStartHandlePoint.getXPix(), mStartHandlePoint.getYPix()); + mSelectionHandleController.setEndHandlePosition( + mEndHandlePoint.getXPix(), mEndHandlePoint.getYPix()); + } + + if (isInsertionHandleShowing()) { + mInsertionHandleController.setHandlePosition( + mInsertionHandlePoint.getXPix(), mInsertionHandlePoint.getYPix()); + } + } + + private void hideHandles() { + if (mSelectionHandleController != null) { + mSelectionHandleController.hideAndDisallowAutomaticShowing(); + } + if (mInsertionHandleController != null) { + mInsertionHandleController.hideAndDisallowAutomaticShowing(); + } + } + + private void showSelectActionBar() { + if (mActionMode != null) { + mActionMode.invalidate(); + return; + } + + // Start a new action mode with a SelectActionModeCallback. + SelectActionModeCallback.ActionHandler actionHandler = + new SelectActionModeCallback.ActionHandler() { + @Override + public boolean selectAll() { + return mImeAdapter.selectAll(); + } + + @Override + public boolean cut() { + return mImeAdapter.cut(); + } + + @Override + public boolean copy() { + return mImeAdapter.copy(); + } + + @Override + public boolean paste() { + return mImeAdapter.paste(); + } + + @Override + public boolean isSelectionEditable() { + return mSelectionEditable; + } + + @Override + public String getSelectedText() { + return ContentViewCore.this.getSelectedText(); + } + + @Override + public void onDestroyActionMode() { + mActionMode = null; + if (mUnselectAllOnActionModeDismiss) mImeAdapter.unselect(); + getContentViewClient().onContextualActionBarHidden(); + } + }; + mActionMode = null; + // On ICS, startActionMode throws an NPE when getParent() is null. + if (mContainerView.getParent() != null) { + mActionMode = mContainerView.startActionMode( + getContentViewClient().getSelectActionModeCallback(getContext(), actionHandler, + nativeIsIncognito(mNativeContentViewCore))); + } + mUnselectAllOnActionModeDismiss = true; + if (mActionMode == null) { + // There is no ActionMode, so remove the selection. + mImeAdapter.unselect(); + } else { + getContentViewClient().onContextualActionBarShown(); + } + } + + public boolean getUseDesktopUserAgent() { + if (mNativeContentViewCore != 0) { + return nativeGetUseDesktopUserAgent(mNativeContentViewCore); + } + return false; + } + + /** + * Set whether or not we're using a desktop user agent for the currently loaded page. + * @param override If true, use a desktop user agent. Use a mobile one otherwise. + * @param reloadOnChange Reload the page if the UA has changed. + */ + public void setUseDesktopUserAgent(boolean override, boolean reloadOnChange) { + if (mNativeContentViewCore != 0) { + nativeSetUseDesktopUserAgent(mNativeContentViewCore, override, reloadOnChange); + } + } + + public void clearSslPreferences() { + nativeClearSslPreferences(mNativeContentViewCore); + } + + /** + * @return Whether the native ContentView has crashed. + */ + public boolean isCrashed() { + if (mNativeContentViewCore == 0) return false; + return nativeCrashed(mNativeContentViewCore); + } + + private boolean isSelectionHandleShowing() { + return mSelectionHandleController != null && mSelectionHandleController.isShowing(); + } + + private boolean isInsertionHandleShowing() { + return mInsertionHandleController != null && mInsertionHandleController.isShowing(); + } + + private void updateTextHandlesForGesture(int type) { + switch(type) { + case ContentViewGestureHandler.GESTURE_DOUBLE_TAP: + case ContentViewGestureHandler.GESTURE_SCROLL_START: + case ContentViewGestureHandler.GESTURE_FLING_START: + case ContentViewGestureHandler.GESTURE_PINCH_BEGIN: + temporarilyHideTextHandles(); + break; + + default: + break; + } + } + + // Makes the insertion/selection handles invisible. They will fade back in shortly after the + // last call to scheduleTextHandleFadeIn (or temporarilyHideTextHandles). + private void temporarilyHideTextHandles() { + if (isSelectionHandleShowing()) { + mSelectionHandleController.setHandleVisibility(HandleView.INVISIBLE); + } + if (isInsertionHandleShowing()) { + mInsertionHandleController.setHandleVisibility(HandleView.INVISIBLE); + } + scheduleTextHandleFadeIn(); + } + + // Cancels any pending fade in and schedules a new one. + private void scheduleTextHandleFadeIn() { + if (!isInsertionHandleShowing() && !isSelectionHandleShowing()) return; + + if (mDeferredHandleFadeInRunnable == null) { + mDeferredHandleFadeInRunnable = new Runnable() { + @Override + public void run() { + if (mContentViewGestureHandler.isNativeScrolling() || + mContentViewGestureHandler.isNativePinching()) { + // Delay fade in until no longer scrolling or pinching. + scheduleTextHandleFadeIn(); + } else { + if (isSelectionHandleShowing()) { + mSelectionHandleController.beginHandleFadeIn(); + } + if (isInsertionHandleShowing()) { + mInsertionHandleController.beginHandleFadeIn(); + } + } + } + }; + } + + mContainerView.removeCallbacks(mDeferredHandleFadeInRunnable); + mContainerView.postDelayed(mDeferredHandleFadeInRunnable, TEXT_HANDLE_FADE_IN_DELAY); + } + + /** + * Shows the IME if the focused widget could accept text input. + */ + public void showImeIfNeeded() { + if (mNativeContentViewCore != 0) nativeShowImeIfNeeded(mNativeContentViewCore); + } + + @SuppressWarnings("unused") + @CalledByNative + private void updateFrameInfo( + float scrollOffsetX, float scrollOffsetY, + float pageScaleFactor, float minPageScaleFactor, float maxPageScaleFactor, + float contentWidth, float contentHeight, + float viewportWidth, float viewportHeight, + 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). + contentWidth = Math.max(contentWidth, + mRenderCoordinates.fromPixToLocalCss(mViewportWidthPix)); + contentHeight = Math.max(contentHeight, + mRenderCoordinates.fromPixToLocalCss(mViewportHeightPix)); + + final float contentOffsetYPix = mRenderCoordinates.fromDipToPix(contentOffsetYCss); + + final boolean contentSizeChanged = + contentWidth != mRenderCoordinates.getContentWidthCss() + || contentHeight != mRenderCoordinates.getContentHeightCss(); + final boolean scaleLimitsChanged = + minPageScaleFactor != mRenderCoordinates.getMinPageScaleFactor() + || maxPageScaleFactor != mRenderCoordinates.getMaxPageScaleFactor(); + final boolean pageScaleChanged = + pageScaleFactor != mRenderCoordinates.getPageScaleFactor(); + final boolean scrollChanged = + pageScaleChanged + || scrollOffsetX != mRenderCoordinates.getScrollX() + || scrollOffsetY != mRenderCoordinates.getScrollY(); + final boolean contentOffsetChanged = + contentOffsetYPix != mRenderCoordinates.getContentOffsetYPix(); + + final boolean needHidePopupZoomer = contentSizeChanged || scrollChanged; + final boolean needUpdateZoomControls = scaleLimitsChanged || scrollChanged; + final boolean needTemporarilyHideHandles = scrollChanged; + + 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 + // update mNativeScrollX/Y here for consistency. + getContentViewClient().onScaleChanged( + mRenderCoordinates.getPageScaleFactor(), pageScaleFactor); + } + + mRenderCoordinates.updateFrameInfo( + scrollOffsetX, scrollOffsetY, + contentWidth, contentHeight, + viewportWidth, viewportHeight, + pageScaleFactor, minPageScaleFactor, maxPageScaleFactor, + contentOffsetYPix); + + if (needTemporarilyHideHandles) temporarilyHideTextHandles(); + if (needUpdateZoomControls) mZoomControlsDelegate.updateZoomControls(); + if (contentOffsetChanged) updateHandleScreenPositions(); + + // Update offsets for fullscreen. + final float deviceScale = mRenderCoordinates.getDeviceScaleFactor(); + final float controlsOffsetPix = controlsOffsetYCss * deviceScale; + final float overdrawBottomHeightPix = overdrawBottomHeightCss * deviceScale; + getContentViewClient().onOffsetsForFullscreenChanged( + controlsOffsetPix, contentOffsetYPix, overdrawBottomHeightPix); + + mPendingRendererFrame = true; + } + + @SuppressWarnings("unused") + @CalledByNative + private void updateImeAdapter(int nativeImeAdapterAndroid, int textInputType, + String text, int selectionStart, int selectionEnd, + int compositionStart, int compositionEnd, boolean showImeIfNeeded) { + TraceEvent.begin(); + mSelectionEditable = (textInputType != ImeAdapter.getTextInputTypeNone()); + + if (mActionMode != null) mActionMode.invalidate(); + + mImeAdapter.attachAndShowIfNeeded(nativeImeAdapterAndroid, textInputType, + selectionStart, selectionEnd, showImeIfNeeded); + + if (mInputConnection != null) { + mInputConnection.setEditableText(text, selectionStart, selectionEnd, + compositionStart, compositionEnd); + } + TraceEvent.end(); + } + + @SuppressWarnings("unused") + @CalledByNative + private void processImeBatchStateAck(boolean isBegin) { + if (mInputConnection == null) return; + mInputConnection.setIgnoreTextInputStateUpdates(isBegin); + } + + @SuppressWarnings("unused") + @CalledByNative + private void setTitle(String title) { + getContentViewClient().onUpdateTitle(title); + } + + /** + * Called (from native) when the