diff --git a/.classpath b/.classpath deleted file mode 100644 index bd9f213fe..000000000 --- a/.classpath +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 34c103e92..000000000 --- a/.gitattributes +++ /dev/null @@ -1,4 +0,0 @@ -*.sh eol=lf -*.java text -*.xml text -*.ai binary diff --git a/.gitignore b/.gitignore index 743185de0..e3ae87a64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ -.idea/ -bin/ -gen/ -out/ *.apk +*.ap_ *.iml -proguard/ -proguard_logs/ +*.dex +*.class +.idea/ +.gradle/ +build/ +local.properties +app/lint.xml +app/proguard-project.txt \ No newline at end of file diff --git a/.project b/.project deleted file mode 100644 index 81dfad7b8..000000000 --- a/.project +++ /dev/null @@ -1,40 +0,0 @@ - - - XposedInstaller - - - - - - 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 - - - - src/NOTICE.txt - 1 - PROJECT_LOC/NOTICE.txt - - - diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 73673c639..000000000 --- a/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,291 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 -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=0 -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=0 -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=0 -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=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=4 -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=9999 -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=tab -org.eclipse.jdt.core.formatter.tabulation.size=4 -org.eclipse.jdt.core.formatter.use_on_off_tags=false -org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=true -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 deleted file mode 100644 index dff111304..000000000 --- a/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null @@ -1,3 +0,0 @@ -eclipse.preferences.version=1 -formatter_profile=_Xposed -formatter_settings_version=12 diff --git a/AndroidManifest.xml b/AndroidManifest.xml deleted file mode 100644 index dce62bd70..000000000 --- a/AndroidManifest.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/NOTICE.txt b/NOTICE.txt deleted file mode 100644 index 97a552032..000000000 --- a/NOTICE.txt +++ /dev/null @@ -1,53 +0,0 @@ -LICENSE FOR THE MAIN PRODUCT -============================ - -Copyright 2013 rovo89, Tungstwenty - -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. - - - -INCLUDED LIBRARIES -================== - -This product includes software based on work by: -Copyright (c) 2005-2008, The Android Open Source Project -Licensed under the Apache License, Version 2.0 -See the Xposed project for details. - - -This product includes slightly modified code of the "Apache Commons Lang" -library. See lib/apache-commons-lang in the XposedBridge project for details. -Here is a copy of the NOTICE.txt from that directory: -------------------------------------------------------------------------- -Apache Commons Lang -Copyright 2001-2011 The Apache Software Foundation - -This product includes software developed by -The Apache Software Foundation (http://www.apache.org/). - -This product includes software from the Spring Framework, -under the Apache License 2.0 (see: StringUtils.containsWhitespace()) -------------------------------------------------------------------------- - - -This product includes the StickyListHeaders library: -Copyright 2012-2013 Emil Sjlander -Licensed under the Apache License, Version 2.0 -https://github.com/emilsjolander/StickyListHeaders - - -This product includes the libsuperuser library: -Copyright 2012-2013 Jorrit "Chainfire" Jongma -Licensed under the Apache License, Version 2.0 -https://github.com/Chainfire/libsuperuser diff --git a/XposedInstaller.iml b/XposedInstaller.iml new file mode 100644 index 000000000..d83ae9e9a --- /dev/null +++ b/XposedInstaller.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 000000000..d3d07e757 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,37 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 27 + + defaultConfig { + applicationId "de.robv.android.xposed.installer" + minSdkVersion 15 + targetSdkVersion 25 + versionCode 43 + versionName "3.1.5" + archivesBaseName = "XposedInstaller_${versionName}".replace(' ', '_') + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + pseudoLocalesEnabled true + } + } +} + +dependencies { + implementation 'com.android.support:cardview-v7:27.0.2' + implementation 'com.android.support:design:27.0.2' + implementation 'com.android.support:customtabs:27.0.2' + implementation 'com.android.support:support-v13:27.0.2' + implementation 'com.afollestad.material-dialogs:commons:0.9.0.2' + implementation 'se.emilsjolander:stickylistheaders:2.7.0' + implementation 'eu.chainfire:libsuperuser:1.0.0.201608240809' + implementation 'com.squareup.picasso:picasso:2.5.2' + implementation 'de.psdev.licensesdialog:licensesdialog:1.8.1' + compileOnly fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/lib/AndroidHiddenAPI.jar b/app/libs/AndroidHiddenAPI.jar similarity index 100% rename from lib/AndroidHiddenAPI.jar rename to app/libs/AndroidHiddenAPI.jar diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 000000000..b8e8d4ccc --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 000000000..238242aa3 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,31 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in D:\Android\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# 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 *; +#} + +-dontobfuscate + +# Required when -dontobfuscate is set, see http://stackoverflow.com/a/7587680 +-optimizations !code/allocation/variable + +# Make sure that getActiveXposedVersion() is actually called +# (instead of always using it's seemingly static value -1) +-optimizations !method/propagation/returnvalue,!method/inlining/* + +# See https://code.google.com/p/android/issues/detail?id=58508 +-keep class android.support.v7.widget.SearchView { *; } + +-dontwarn com.squareup.okhttp.** diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..bd09fd1de --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/arm/busybox-xposed b/app/src/main/assets/arm/busybox-xposed new file mode 100644 index 000000000..e24b68e87 Binary files /dev/null and b/app/src/main/assets/arm/busybox-xposed differ diff --git a/app/src/main/assets/x86/busybox-xposed b/app/src/main/assets/x86/busybox-xposed new file mode 100644 index 000000000..7825113fd Binary files /dev/null and b/app/src/main/assets/x86/busybox-xposed differ diff --git a/app/src/main/java/de/robv/android/xposed/installer/AboutActivity.java b/app/src/main/java/de/robv/android/xposed/installer/AboutActivity.java new file mode 100644 index 000000000..df572c227 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/AboutActivity.java @@ -0,0 +1,131 @@ +package de.robv.android.xposed.installer; + +import android.app.Fragment; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.afollestad.materialdialogs.MaterialDialog; + +import de.psdev.licensesdialog.LicensesDialog; +import de.psdev.licensesdialog.licenses.ApacheSoftwareLicense20; +import de.psdev.licensesdialog.licenses.MITLicense; +import de.psdev.licensesdialog.licenses.SILOpenFontLicense11; +import de.psdev.licensesdialog.model.Notice; +import de.psdev.licensesdialog.model.Notices; +import de.robv.android.xposed.installer.util.NavUtil; +import de.robv.android.xposed.installer.util.ThemeUtil; + +public class AboutActivity extends XposedBaseActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeUtil.setTheme(this); + setContentView(R.layout.activity_container); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setTitle(R.string.nav_item_about); + ab.setDisplayHomeAsUpEnabled(true); + } + + setFloating(toolbar, R.string.nav_item_about); + + if (savedInstanceState == null) { + getFragmentManager().beginTransaction().add(R.id.container, new AboutFragment()).commit(); + } + } + + public static class AboutFragment extends Fragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.tab_about, container, false); + + View developersView = v.findViewById(R.id.developersView); + View licensesView = v.findViewById(R.id.licensesView); + View translatorsView = v.findViewById(R.id.translatorsView); + View sourceCodeView = v.findViewById(R.id.sourceCodeView); + + String packageName = getActivity().getPackageName(); + String translator = getResources().getString(R.string.translator); + + try { + String version = getActivity().getPackageManager().getPackageInfo(packageName, 0).versionName; + ((TextView) v.findViewById(R.id.app_version)).setText(version); + } catch (NameNotFoundException ignored) { + } + + licensesView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + createLicenseDialog(); + } + }); + + developersView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + MaterialDialog dialog = new MaterialDialog.Builder(getActivity()) + .title(R.string.about_developers_label) + .content(R.string.about_developers) + .positiveText(android.R.string.ok) + .show(); + + ((TextView) dialog.findViewById(R.id.md_content)).setMovementMethod(LinkMovementMethod.getInstance()); + } + }); + + sourceCodeView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + NavUtil.startURL(getActivity(), getString(R.string.about_source)); + } + }); + + if (translator.isEmpty()) { + translatorsView.setVisibility(View.GONE); + } + + return v; + } + + private void createLicenseDialog() { + Notices notices = new Notices(); + notices.addNotice(new Notice("material-dialogs", "https://github.com/afollestad/material-dialogs", "Copyright (c) 2014-2016 Aidan Michael Follestad", new MITLicense())); + notices.addNotice(new Notice("StickyListHeaders", "https://github.com/emilsjolander/StickyListHeaders", "Emil Sjölander", new ApacheSoftwareLicense20())); + notices.addNotice(new Notice("PreferenceFragment-Compat", "https://github.com/Machinarius/PreferenceFragment-Compat", "machinarius", new ApacheSoftwareLicense20())); + notices.addNotice(new Notice("libsuperuser", "https://github.com/Chainfire/libsuperuser", "Copyright (C) 2012-2015 Jorrit \"Chainfire\" Jongma", new ApacheSoftwareLicense20())); + notices.addNotice(new Notice("picasso", "https://github.com/square/picasso", "Copyright 2013 Square, Inc.", new ApacheSoftwareLicense20())); + notices.addNotice(new Notice("materialdesignicons", "http://materialdesignicons.com", "Copyright (c) 2014, Austin Andrews", new SILOpenFontLicense11())); + + new LicensesDialog.Builder(getActivity()) + .setNotices(notices) + .setIncludeOwnLicense(true) + .build() + .show(); + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsActivity.java b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsActivity.java new file mode 100644 index 000000000..9ca43e7ff --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsActivity.java @@ -0,0 +1,302 @@ +package de.robv.android.xposed.installer; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.design.widget.Snackbar; +import android.support.design.widget.TabLayout; +import android.support.v13.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.text.TextUtils; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.TextView; + +import java.util.List; + +import de.robv.android.xposed.installer.repo.Module; +import de.robv.android.xposed.installer.util.Loader; +import de.robv.android.xposed.installer.util.ModuleUtil; +import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; +import de.robv.android.xposed.installer.util.ModuleUtil.ModuleListener; +import de.robv.android.xposed.installer.util.RepoLoader; +import de.robv.android.xposed.installer.util.ThemeUtil; + +public class DownloadDetailsActivity extends XposedBaseActivity implements Loader.Listener, ModuleListener { + + public static final int DOWNLOAD_DESCRIPTION = 0; + public static final int DOWNLOAD_VERSIONS = 1; + public static final int DOWNLOAD_SETTINGS = 2; + private static RepoLoader sRepoLoader = RepoLoader.getInstance(); + private static ModuleUtil sModuleUtil = ModuleUtil.getInstance(); + private ViewPager mPager; + private String mPackageName; + private Module mModule; + private InstalledModule mInstalledModule; + private MenuItem mItemBookmark; + + @Override + public void onCreate(Bundle savedInstanceState) { + ThemeUtil.setTheme(this); + + mPackageName = getModulePackageName(); + mModule = sRepoLoader.getModule(mPackageName); + + mInstalledModule = ModuleUtil.getInstance().getModule(mPackageName); + + super.onCreate(savedInstanceState); + sRepoLoader.addListener(this); + sModuleUtil.addListener(this); + + if (mModule != null) { + setContentView(R.layout.activity_download_details); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + + ActionBar ab = getSupportActionBar(); + + if (ab != null) { + ab.setTitle(R.string.nav_item_download); + ab.setDisplayHomeAsUpEnabled(true); + } + + setFloating(toolbar, 0); + + setupTabs(); + + Boolean directDownload = getIntent().getBooleanExtra("direct_download", false); + // Updates available => start on the versions page + if (mInstalledModule != null && mInstalledModule.isUpdate(sRepoLoader.getLatestVersion(mModule)) || directDownload) + mPager.setCurrentItem(DOWNLOAD_VERSIONS); + + if (Build.VERSION.SDK_INT >= 21) + findViewById(R.id.fake_elevation).setVisibility(View.GONE); + + } else { + setContentView(R.layout.activity_download_details_not_found); + + TextView txtMessage = (TextView) findViewById(android.R.id.message); + txtMessage.setText(getResources().getString(R.string.download_details_not_found, mPackageName)); + + findViewById(R.id.reload).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + v.setEnabled(false); + sRepoLoader.triggerReload(true); + } + }); + } + } + + private void setupTabs() { + mPager = (ViewPager) findViewById(R.id.download_pager); + mPager.setAdapter(new SwipeFragmentPagerAdapter(getFragmentManager())); + TabLayout mTabLayout = (TabLayout) findViewById(R.id.sliding_tabs); + mTabLayout.setupWithViewPager(mPager); + } + + private String getModulePackageName() { + Uri uri = getIntent().getData(); + if (uri == null) + return null; + + String scheme = uri.getScheme(); + if (TextUtils.isEmpty(scheme)) { + return null; + } else if (scheme.equals("package")) { + return uri.getSchemeSpecificPart(); + } else if (scheme.equals("http")) { + List segments = uri.getPathSegments(); + if (segments.size() > 1) + return segments.get(1); + } + return null; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + sRepoLoader.removeListener(this); + sModuleUtil.removeListener(this); + } + + public Module getModule() { + return mModule; + } + + public InstalledModule getInstalledModule() { + return mInstalledModule; + } + + public void gotoPage(int page) { + mPager.setCurrentItem(page); + } + + private void reload() { + runOnUiThread(new Runnable() { + @Override + public void run() { + recreate(); + } + }); + } + + @Override + public void onReloadDone(RepoLoader loader) { + reload(); + } + + @Override + public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { + reload(); + } + + @Override + public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module) { + if (packageName.equals(mPackageName)) + reload(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_download_details, menu); + + // TODO maybe enable again after checking the implementation + menu.findItem(R.id.menu_bookmark).setVisible(false); + menu.findItem(R.id.menu_share).setVisible(false); + + //mItemBookmark = menu.findItem(R.id.menu_bookmark); + //setupBookmark(false); + return true; + } + + private void setupBookmark(boolean clicked) { + SharedPreferences myPref = getSharedPreferences("bookmarks", MODE_PRIVATE); + + boolean saved = myPref.getBoolean(mModule.packageName, false); + boolean newValue; + + if (clicked) { + newValue = !saved; + myPref.edit().putBoolean(mModule.packageName, newValue).apply(); + + int msg = newValue ? R.string.bookmark_added : R.string.bookmark_removed; + + Snackbar.make(findViewById(android.R.id.content), msg, Snackbar.LENGTH_SHORT).show(); + } + + saved = myPref.getBoolean(mModule.packageName, false); + + if (saved) { + mItemBookmark.setTitle(R.string.remove_bookmark); + mItemBookmark.setIcon(R.drawable.ic_bookmark); + } else { + mItemBookmark.setTitle(R.string.add_bookmark); + mItemBookmark.setIcon(R.drawable.ic_bookmark_outline); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_bookmark: + setupBookmark(true); + break; + case R.id.menu_refresh: + RepoLoader.getInstance().triggerReload(true); + return true; + case R.id.menu_share: + String text = mModule.name + " - "; + + if (isPackageInstalled(mPackageName, this)) { + String s = getPackageManager().getInstallerPackageName(mPackageName); + boolean playStore; + + try { + playStore = s.equals(ModulesFragment.PLAY_STORE_PACKAGE); + } catch (NullPointerException e) { + playStore = false; + } + + if (playStore) { + text += String.format(ModulesFragment.PLAY_STORE_LINK, mPackageName); + } else { + text += String.format(ModulesFragment.XPOSED_REPO_LINK, mPackageName); + } + } else { + text += String.format(ModulesFragment.XPOSED_REPO_LINK, + mPackageName); + } + + Intent sharingIntent = new Intent(Intent.ACTION_SEND); + sharingIntent.setType("text/plain"); + sharingIntent.putExtra(Intent.EXTRA_TEXT, text); + startActivity(Intent.createChooser(sharingIntent, getString(R.string.share))); + return true; + } + return super.onOptionsItemSelected(item); + } + + private boolean isPackageInstalled(String packagename, Context context) { + PackageManager pm = context.getPackageManager(); + try { + pm.getPackageInfo(packagename, PackageManager.GET_ACTIVITIES); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + class SwipeFragmentPagerAdapter extends FragmentPagerAdapter { + final int PAGE_COUNT = 3; + private String tabTitles[] = new String[]{getString(R.string.download_details_page_description), getString(R.string.download_details_page_versions), getString(R.string.download_details_page_settings),}; + + public SwipeFragmentPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public int getCount() { + return PAGE_COUNT; + } + + @Override + public Fragment getItem(int position) { + switch (position) { + case DOWNLOAD_DESCRIPTION: + return new DownloadDetailsFragment(); + case DOWNLOAD_VERSIONS: + return new DownloadDetailsVersionsFragment(); + case DOWNLOAD_SETTINGS: + return new DownloadDetailsSettingsFragment(); + default: + return null; + } + } + + @Override + public CharSequence getPageTitle(int position) { + // Generate title based on item position + return tabTitles[position]; + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsFragment.java b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsFragment.java new file mode 100644 index 000000000..8fbbfbbd5 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsFragment.java @@ -0,0 +1,84 @@ +package de.robv.android.xposed.installer; + +import android.app.Activity; +import android.app.Fragment; +import android.net.Uri; +import android.os.Bundle; +import android.text.method.LinkMovementMethod; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import de.robv.android.xposed.installer.repo.Module; +import de.robv.android.xposed.installer.repo.RepoParser; +import de.robv.android.xposed.installer.util.NavUtil; +import de.robv.android.xposed.installer.util.chrome.LinkTransformationMethod; + +public class DownloadDetailsFragment extends Fragment { + private DownloadDetailsActivity mActivity; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mActivity = (DownloadDetailsActivity) activity; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final Module module = mActivity.getModule(); + if (module == null) + return null; + + final View view = inflater.inflate(R.layout.download_details, container, false); + + TextView title = (TextView) view.findViewById(R.id.download_title); + title.setText(module.name); + + TextView author = (TextView) view.findViewById(R.id.download_author); + if (module.author != null && !module.author.isEmpty()) + author.setText(getString(R.string.download_author, module.author)); + else + author.setText(R.string.download_unknown_author); + + TextView description = (TextView) view + .findViewById(R.id.download_description); + if (module.description != null) { + if (module.descriptionIsHtml) { + description.setText(RepoParser.parseSimpleHtml(getActivity(), module.description, description)); + description.setTransformationMethod(new LinkTransformationMethod(getActivity())); + description.setMovementMethod(LinkMovementMethod.getInstance()); + } else { + description.setText(module.description); + } + } else { + description.setVisibility(View.GONE); + } + + ViewGroup moreInfoContainer = (ViewGroup) view.findViewById(R.id.download_moreinfo_container); + for (Pair moreInfoEntry : module.moreInfo) { + View moreInfoView = inflater.inflate(R.layout.download_moreinfo, moreInfoContainer, false); + TextView txtTitle = (TextView) moreInfoView.findViewById(android.R.id.title); + TextView txtValue = (TextView) moreInfoView.findViewById(android.R.id.message); + + txtTitle.setText(moreInfoEntry.first + ":"); + txtValue.setText(moreInfoEntry.second); + + final Uri link = NavUtil.parseURL(moreInfoEntry.second); + if (link != null) { + txtValue.setTextColor(txtValue.getLinkTextColors()); + moreInfoView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + NavUtil.startURL(getActivity(), link); + } + }); + } + + moreInfoContainer.addView(moreInfoView); + } + + return view; + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsSettingsFragment.java b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsSettingsFragment.java new file mode 100644 index 000000000..c5857da0b --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsSettingsFragment.java @@ -0,0 +1,65 @@ +package de.robv.android.xposed.installer; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; + +import java.util.Map; + +import de.robv.android.xposed.installer.repo.Module; +import de.robv.android.xposed.installer.util.PrefixedSharedPreferences; +import de.robv.android.xposed.installer.util.RepoLoader; + +public class DownloadDetailsSettingsFragment extends PreferenceFragment { + private DownloadDetailsActivity mActivity; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mActivity = (DownloadDetailsActivity) activity; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Module module = mActivity.getModule(); + if (module == null) + return; + + final String packageName = module.packageName; + + PreferenceManager prefManager = getPreferenceManager(); + prefManager.setSharedPreferencesName("module_settings"); + PrefixedSharedPreferences.injectToPreferenceManager(prefManager, module.packageName); + addPreferencesFromResource(R.xml.module_prefs); + + SharedPreferences prefs = getActivity().getSharedPreferences("module_settings", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + if (prefs.getBoolean("no_global", true)) { + for (Map.Entry k : prefs.getAll().entrySet()) { + if (prefs.getString(k.getKey(), "").equals("global")) { + editor.putString(k.getKey(), "").apply(); + } + } + + editor.putBoolean("no_global", false).apply(); + } + + findPreference("release_type").setOnPreferenceChangeListener( + new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, + Object newValue) { + RepoLoader.getInstance().setReleaseTypeLocal(packageName, (String) newValue); + return true; + } + }); + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsVersionsFragment.java b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsVersionsFragment.java new file mode 100644 index 000000000..60bd2784c --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsVersionsFragment.java @@ -0,0 +1,248 @@ +package de.robv.android.xposed.installer; + +import android.app.Activity; +import android.app.ListFragment; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.text.method.LinkMovementMethod; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.File; +import java.text.DateFormat; +import java.util.Date; + +import de.robv.android.xposed.installer.repo.Module; +import de.robv.android.xposed.installer.repo.ModuleVersion; +import de.robv.android.xposed.installer.repo.ReleaseType; +import de.robv.android.xposed.installer.repo.RepoParser; +import de.robv.android.xposed.installer.util.DownloadsUtil; +import de.robv.android.xposed.installer.util.HashUtil; +import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; +import de.robv.android.xposed.installer.util.RepoLoader; +import de.robv.android.xposed.installer.util.ThemeUtil; +import de.robv.android.xposed.installer.util.chrome.LinkTransformationMethod; +import de.robv.android.xposed.installer.widget.DownloadView; + +public class DownloadDetailsVersionsFragment extends ListFragment { + private static VersionsAdapter sAdapter; + private DownloadDetailsActivity mActivity; + private Module module; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mActivity = (DownloadDetailsActivity) activity; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + module = mActivity.getModule(); + if (module == null) + return; + + if (module.versions.isEmpty()) { + setEmptyText(getString(R.string.download_no_versions)); + setListShown(true); + } else { + RepoLoader repoLoader = RepoLoader.getInstance(); + if (!repoLoader.isVersionShown(module.versions.get(0))) { + TextView txtHeader = new TextView(getActivity()); + txtHeader.setText(R.string.download_test_version_not_shown); + txtHeader.setTextColor(getResources().getColor(R.color.warning)); + txtHeader.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mActivity.gotoPage(DownloadDetailsActivity.DOWNLOAD_SETTINGS); + } + }); + getListView().addHeaderView(txtHeader); + } + + sAdapter = new VersionsAdapter(mActivity, mActivity.getInstalledModule()); + for (ModuleVersion version : module.versions) { + if (repoLoader.isVersionShown(version)) + sAdapter.add(version); + } + setListAdapter(sAdapter); + } + + DisplayMetrics metrics = getResources().getDisplayMetrics(); + int sixDp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6, metrics); + int eightDp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, metrics); + getListView().setDivider(null); + getListView().setDividerHeight(sixDp); + getListView().setPadding(eightDp, eightDp, eightDp, eightDp); + getListView().setClipToPadding(false); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + setListAdapter(null); + } + + static class ViewHolder { + TextView txtStatus; + TextView txtVersion; + TextView txtRelType; + TextView txtUploadDate; + DownloadView downloadView; + TextView txtChangesTitle; + TextView txtChanges; + } + + public static class DownloadModuleCallback implements DownloadsUtil.DownloadFinishedCallback { + private final ModuleVersion moduleVersion; + + public DownloadModuleCallback(ModuleVersion moduleVersion) { + this.moduleVersion = moduleVersion; + } + + @Override + public void onDownloadFinished(Context context, + DownloadsUtil.DownloadInfo info) { + File localFile = new File(info.localFilename); + if (!localFile.isFile()) + return; + + if (moduleVersion.md5sum != null && !moduleVersion.md5sum.isEmpty()) { + try { + String actualMd5Sum = HashUtil.md5(localFile); + if (!moduleVersion.md5sum.equals(actualMd5Sum)) { + Toast.makeText(context, context.getString(R.string.download_md5sum_incorrect, actualMd5Sum, moduleVersion.md5sum), Toast.LENGTH_LONG).show(); + DownloadsUtil.removeById(context, info.id); + return; + } + } catch (Exception e) { + Toast.makeText(context, context.getString(R.string.download_could_not_read_file, e.getMessage()), Toast.LENGTH_LONG).show(); + DownloadsUtil.removeById(context, info.id); + return; + } + } + + PackageManager pm = context.getPackageManager(); + PackageInfo packageInfo = pm.getPackageArchiveInfo(info.localFilename, 0); + + if (packageInfo == null) { + Toast.makeText(context, R.string.download_no_valid_apk, Toast.LENGTH_LONG).show(); + DownloadsUtil.removeById(context, info.id); + return; + } + + if (!packageInfo.packageName + .equals(moduleVersion.module.packageName)) { + Toast.makeText(context, context.getString(R.string.download_incorrect_package_name, packageInfo.packageName, moduleVersion.module.packageName), Toast.LENGTH_LONG).show(); + DownloadsUtil.removeById(context, info.id); + return; + } + + XposedApp.installApk(context, info); + } + } + + private class VersionsAdapter extends ArrayAdapter { + private final DateFormat mDateFormatter = DateFormat + .getDateInstance(DateFormat.SHORT); + private final int mColorRelTypeStable; + private final int mColorRelTypeOthers; + private final int mColorInstalled; + private final int mColorUpdateAvailable; + private final String mTextInstalled; + private final String mTextUpdateAvailable; + private final int mInstalledVersionCode; + + public VersionsAdapter(Context context, InstalledModule installed) { + super(context, R.layout.list_item_version); + mColorRelTypeStable = ThemeUtil.getThemeColor(context, android.R.attr.textColorTertiary); + mColorRelTypeOthers = getResources().getColor(R.color.warning); + mColorInstalled = ThemeUtil.getThemeColor(context, R.attr.download_status_installed); + mColorUpdateAvailable = getResources().getColor(R.color.download_status_update_available); + mTextInstalled = getString(R.string.download_section_installed) + ":"; + mTextUpdateAvailable = getString(R.string.download_section_update_available) + ":"; + mInstalledVersionCode = (installed != null) ? installed.versionCode : -1; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = inflater.inflate(R.layout.list_item_version, null, true); + ViewHolder viewHolder = new ViewHolder(); + viewHolder.txtStatus = (TextView) view.findViewById(R.id.txtStatus); + viewHolder.txtVersion = (TextView) view.findViewById(R.id.txtVersion); + viewHolder.txtRelType = (TextView) view.findViewById(R.id.txtRelType); + viewHolder.txtUploadDate = (TextView) view.findViewById(R.id.txtUploadDate); + viewHolder.downloadView = (DownloadView) view.findViewById(R.id.downloadView); + viewHolder.txtChangesTitle = (TextView) view.findViewById(R.id.txtChangesTitle); + viewHolder.txtChanges = (TextView) view.findViewById(R.id.txtChanges); + viewHolder.downloadView.fragment = DownloadDetailsVersionsFragment.this; + view.setTag(viewHolder); + } + + ViewHolder holder = (ViewHolder) view.getTag(); + ModuleVersion item = getItem(position); + + holder.txtVersion.setText(item.name); + holder.txtRelType.setText(item.relType.getTitleId()); + holder.txtRelType.setTextColor(item.relType == ReleaseType.STABLE + ? mColorRelTypeStable : mColorRelTypeOthers); + + if (item.uploaded > 0) { + holder.txtUploadDate.setText( + mDateFormatter.format(new Date(item.uploaded))); + holder.txtUploadDate.setVisibility(View.VISIBLE); + } else { + holder.txtUploadDate.setVisibility(View.GONE); + } + + if (item.code <= 0 || mInstalledVersionCode <= 0 + || item.code < mInstalledVersionCode) { + holder.txtStatus.setVisibility(View.GONE); + } else if (item.code == mInstalledVersionCode) { + holder.txtStatus.setText(mTextInstalled); + holder.txtStatus.setTextColor(mColorInstalled); + holder.txtStatus.setVisibility(View.VISIBLE); + } else { // item.code > mInstalledVersionCode + holder.txtStatus.setText(mTextUpdateAvailable); + holder.txtStatus.setTextColor(mColorUpdateAvailable); + holder.txtStatus.setVisibility(View.VISIBLE); + } + + holder.downloadView.setUrl(item.downloadLink); + holder.downloadView.setTitle(mActivity.getModule().name); + holder.downloadView.setDownloadFinishedCallback(new DownloadModuleCallback(item)); + + if (item.changelog != null && !item.changelog.isEmpty()) { + holder.txtChangesTitle.setVisibility(View.VISIBLE); + holder.txtChanges.setVisibility(View.VISIBLE); + + if (item.changelogIsHtml) { + holder.txtChanges.setText(RepoParser.parseSimpleHtml(getActivity(), item.changelog, holder.txtChanges)); + holder.txtChanges.setTransformationMethod(new LinkTransformationMethod(getActivity())); + holder.txtChanges.setMovementMethod(LinkMovementMethod.getInstance()); + } else { + holder.txtChanges.setText(item.changelog); + holder.txtChanges.setMovementMethod(null); + } + + } else { + holder.txtChangesTitle.setVisibility(View.GONE); + holder.txtChanges.setVisibility(View.GONE); + } + + return view; + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/DownloadFragment.java b/app/src/main/java/de/robv/android/xposed/installer/DownloadFragment.java new file mode 100644 index 000000000..8f2382737 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/DownloadFragment.java @@ -0,0 +1,353 @@ +package de.robv.android.xposed.installer; + +import android.app.Fragment; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.SearchView; +import android.text.TextUtils; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.CursorAdapter; +import android.widget.FilterQueryProvider; +import android.widget.TextView; + +import com.afollestad.materialdialogs.MaterialDialog; + +import java.text.DateFormat; +import java.util.Date; + +import de.robv.android.xposed.installer.repo.RepoDb; +import de.robv.android.xposed.installer.repo.RepoDbDefinitions.OverviewColumnsIndexes; +import de.robv.android.xposed.installer.util.Loader; +import de.robv.android.xposed.installer.util.ModuleUtil; +import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; +import de.robv.android.xposed.installer.util.ModuleUtil.ModuleListener; +import de.robv.android.xposed.installer.util.RepoLoader; +import de.robv.android.xposed.installer.util.ThemeUtil; +import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + +public class DownloadFragment extends Fragment implements Loader.Listener, ModuleListener { + private SharedPreferences mPref; + private DownloadsAdapter mAdapter; + private String mFilterText; + private RepoLoader mRepoLoader; + private ModuleUtil mModuleUtil; + private int mSortingOrder; + private SearchView mSearchView; + private StickyListHeadersListView mListView; + private View mRefreshHint; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mPref = XposedApp.getPreferences(); + mRepoLoader = RepoLoader.getInstance(); + mModuleUtil = ModuleUtil.getInstance(); + mAdapter = new DownloadsAdapter(getActivity()); + mAdapter.setFilterQueryProvider(new FilterQueryProvider() { + @Override + public Cursor runQuery(CharSequence constraint) { + return RepoDb.queryModuleOverview(mSortingOrder, constraint); + } + }); + mSortingOrder = mPref.getInt("download_sorting_order", + RepoDb.SORT_STATUS); + + setHasOptionsMenu(true); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (mAdapter != null && mListView != null) { + mListView.setAdapter(mAdapter); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.tab_downloader, container, false); + + mRefreshHint = v.findViewById(R.id.refresh_hint); + final SwipeRefreshLayout refreshLayout = (SwipeRefreshLayout) v.findViewById(R.id.swiperefreshlayout); + refreshLayout.setColorSchemeColors(getResources().getColor(R.color.colorPrimary)); + mRepoLoader.addListener(this); + mRepoLoader.setSwipeRefreshLayout(refreshLayout); + mModuleUtil.addListener(this); + + mListView = (StickyListHeadersListView) v.findViewById(R.id.listModules); + if (Build.VERSION.SDK_INT >= 26) { + mListView.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS); + } + mListView.setAdapter(mAdapter); + mListView.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + if (view.getChildAt(0) != null) { + refreshLayout.setEnabled(view.getFirstVisiblePosition() == 0 && view.getChildAt(0).getTop() == 0); + } + } + }); + reloadItems(); + + mListView.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Cursor cursor = (Cursor) mAdapter.getItem(position); + String packageName = cursor.getString(OverviewColumnsIndexes.PKGNAME); + + Intent detailsIntent = new Intent(getActivity(), DownloadDetailsActivity.class); + detailsIntent.setData(Uri.fromParts("package", packageName, null)); + startActivity(detailsIntent); + } + }); + mListView.setOnKeyListener(new View.OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + // Expand the search view when the SEARCH key is triggered + if (keyCode == KeyEvent.KEYCODE_SEARCH && event.getAction() == KeyEvent.ACTION_UP && (event.getFlags() & KeyEvent.FLAG_CANCELED) == 0) { + if (mSearchView != null) + mSearchView.setIconified(false); + return true; + } + return false; + } + }); + + setHasOptionsMenu(true); + + return v; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mRepoLoader.removeListener(this); + mRepoLoader.setSwipeRefreshLayout(null); + mModuleUtil.removeListener(this); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_download, menu); + + // Setup search button + final MenuItem searchItem = menu.findItem(R.id.menu_search); + mSearchView = (SearchView) searchItem.getActionView(); + mSearchView.setIconifiedByDefault(true); + mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + setFilter(query); + mSearchView.clearFocus(); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + setFilter(newText); + return true; + } + }); + MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() { + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + setFilter(null); + return true; // Return true to collapse action view + } + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return true; // Return true to expand action view + } + }); + } + + private void setFilter(String filterText) { + mFilterText = filterText; + reloadItems(); + mRefreshHint.setVisibility(TextUtils.isEmpty(filterText) ? View.VISIBLE : View.GONE); + } + + private void reloadItems() { + mAdapter.getFilter().filter(mFilterText); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_sort: + new MaterialDialog.Builder(getActivity()) + .title(R.string.download_sorting_title) + .items(R.array.download_sort_order) + .itemsCallbackSingleChoice(mSortingOrder, + new MaterialDialog.ListCallbackSingleChoice() { + @Override + public boolean onSelection(MaterialDialog materialDialog, View view, int i, CharSequence charSequence) { + mSortingOrder = i; + mPref.edit().putInt("download_sorting_order", mSortingOrder).apply(); + reloadItems(); + materialDialog.dismiss(); + return true; + } + }) + .show(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onReloadDone(final RepoLoader loader) { + reloadItems(); + } + + @Override + public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module) { + reloadItems(); + } + + @Override + public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { + reloadItems(); + } + + private class DownloadsAdapter extends CursorAdapter implements StickyListHeadersAdapter { + private final Context mContext; + private final DateFormat mDateFormatter = DateFormat.getDateInstance(DateFormat.SHORT); + private final LayoutInflater mInflater; + private String[] sectionHeadersStatus; + private String[] sectionHeadersDate; + + public DownloadsAdapter(Context context) { + super(context, null, 0); + mContext = context; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + Resources res = context.getResources(); + sectionHeadersStatus = new String[]{ + res.getString(R.string.download_section_framework), + res.getString(R.string.download_section_update_available), + res.getString(R.string.download_section_installed), + res.getString(R.string.download_section_not_installed),}; + sectionHeadersDate = new String[]{ + res.getString(R.string.download_section_24h), + res.getString(R.string.download_section_7d), + res.getString(R.string.download_section_30d), + res.getString(R.string.download_section_older)}; + } + + @Override + public View getHeaderView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = mInflater.inflate(R.layout.list_sticky_header_download, parent, false); + } + + long section = getHeaderId(position); + + TextView tv = (TextView) convertView.findViewById(android.R.id.title); + tv.setText(mSortingOrder == RepoDb.SORT_STATUS + ? sectionHeadersStatus[(int) section] + : sectionHeadersDate[(int) section]); + return convertView; + } + + @Override + public long getHeaderId(int position) { + Cursor cursor = (Cursor) getItem(position); + long created = cursor.getLong(OverviewColumnsIndexes.CREATED); + long updated = cursor.getLong(OverviewColumnsIndexes.UPDATED); + boolean isFramework = cursor.getInt(OverviewColumnsIndexes.IS_FRAMEWORK) > 0; + boolean isInstalled = cursor.getInt(OverviewColumnsIndexes.IS_INSTALLED) > 0; + boolean hasUpdate = cursor.getInt(OverviewColumnsIndexes.HAS_UPDATE) > 0; + + if (mSortingOrder != RepoDb.SORT_STATUS) { + long timestamp = (mSortingOrder == RepoDb.SORT_UPDATED) ? updated : created; + long age = System.currentTimeMillis() - timestamp; + final long mSecsPerDay = 24 * 60 * 60 * 1000L; + if (age < mSecsPerDay) + return 0; + if (age < 7 * mSecsPerDay) + return 1; + if (age < 30 * mSecsPerDay) + return 2; + return 3; + } else { + if (isFramework) + return 0; + + if (hasUpdate) + return 1; + else if (isInstalled) + return 2; + else + return 3; + } + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.list_item_download, parent, false); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + String title = cursor.getString(OverviewColumnsIndexes.TITLE); + String summary = cursor.getString(OverviewColumnsIndexes.SUMMARY); + String installedVersion = cursor.getString(OverviewColumnsIndexes.INSTALLED_VERSION); + String latestVersion = cursor.getString(OverviewColumnsIndexes.LATEST_VERSION); + long created = cursor.getLong(OverviewColumnsIndexes.CREATED); + long updated = cursor.getLong(OverviewColumnsIndexes.UPDATED); + boolean isInstalled = cursor.getInt(OverviewColumnsIndexes.IS_INSTALLED) > 0; + boolean hasUpdate = cursor.getInt(OverviewColumnsIndexes.HAS_UPDATE) > 0; + + TextView txtTitle = (TextView) view.findViewById(android.R.id.text1); + txtTitle.setText(title); + + TextView txtSummary = (TextView) view.findViewById(android.R.id.text2); + txtSummary.setText(summary); + + TextView txtStatus = (TextView) view.findViewById(R.id.downloadStatus); + if (hasUpdate) { + txtStatus.setText(mContext.getString( + R.string.download_status_update_available, + installedVersion, latestVersion)); + txtStatus.setTextColor(getResources().getColor(R.color.download_status_update_available)); + txtStatus.setVisibility(View.VISIBLE); + } else if (isInstalled) { + txtStatus.setText(mContext.getString( + R.string.download_status_installed, installedVersion)); + txtStatus.setTextColor(ThemeUtil.getThemeColor(mContext, R.attr.download_status_installed)); + txtStatus.setVisibility(View.VISIBLE); + } else { + txtStatus.setVisibility(View.GONE); + } + + String creationDate = mDateFormatter.format(new Date(created)); + String updateDate = mDateFormatter.format(new Date(updated)); + ((TextView) view.findViewById(R.id.timestamps)).setText(getString(R.string.download_timestamps, creationDate, updateDate)); + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/LogsFragment.java b/app/src/main/java/de/robv/android/xposed/installer/LogsFragment.java new file mode 100644 index 000000000..9897a0bfa --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/LogsFragment.java @@ -0,0 +1,326 @@ +package de.robv.android.xposed.installer; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Fragment; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.v13.app.FragmentCompat; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.FileProvider; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.HorizontalScrollView; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; + +import com.afollestad.materialdialogs.MaterialDialog; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.util.Calendar; + +import static de.robv.android.xposed.installer.XposedApp.WRITE_EXTERNAL_PERMISSION; + +public class LogsFragment extends Fragment { + + private File mFileErrorLog = new File(XposedApp.BASE_DIR + "log/error.log"); + private File mFileErrorLogOld = new File( + XposedApp.BASE_DIR + "log/error.log.old"); + private TextView mTxtLog; + private ScrollView mSVLog; + private HorizontalScrollView mHSVLog; + private MenuItem mClickedMenuItem = null; + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.tab_logs, container, false); + mTxtLog = (TextView) v.findViewById(R.id.txtLog); + mTxtLog.setTextIsSelectable(true); + mSVLog = (ScrollView) v.findViewById(R.id.svLog); + mHSVLog = (HorizontalScrollView) v.findViewById(R.id.hsvLog); + reloadErrorLog(); +/* + View scrollTop = v.findViewById(R.id.scroll_top); + View scrollDown = v.findViewById(R.id.scroll_down); + + scrollTop.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + scrollTop(); + } + }); + scrollDown.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + scrollDown(); + } + }); +*/ + return v; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_logs, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + mClickedMenuItem = item; + switch (item.getItemId()) { + case R.id.menu_scroll_top: + scrollTop(); + break; + case R.id.menu_scroll_down: + scrollDown(); + break; + case R.id.menu_refresh: + reloadErrorLog(); + return true; + case R.id.menu_send: + try { + send(); + } catch (NullPointerException ignored) { + } + return true; + case R.id.menu_save: + save(); + return true; + case R.id.menu_clear: + clear(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void scrollTop() { + mSVLog.post(new Runnable() { + @Override + public void run() { + mSVLog.scrollTo(0, 0); + } + }); + mHSVLog.post(new Runnable() { + @Override + public void run() { + mHSVLog.scrollTo(0, 0); + } + }); + } + + private void scrollDown() { + mSVLog.post(new Runnable() { + @Override + public void run() { + mSVLog.scrollTo(0, mTxtLog.getHeight()); + } + }); + mHSVLog.post(new Runnable() { + @Override + public void run() { + mHSVLog.scrollTo(0, 0); + } + }); + } + + private void reloadErrorLog() { + new LogsReader().execute(mFileErrorLog); + mSVLog.post(new Runnable() { + @Override + public void run() { + mSVLog.scrollTo(0, mTxtLog.getHeight()); + } + }); + mHSVLog.post(new Runnable() { + @Override + public void run() { + mHSVLog.scrollTo(0, 0); + } + }); + } + + private void clear() { + try { + new FileOutputStream(mFileErrorLog).close(); + mFileErrorLogOld.delete(); + mTxtLog.setText(R.string.log_is_empty); + Toast.makeText(getActivity(), R.string.logs_cleared, + Toast.LENGTH_SHORT).show(); + reloadErrorLog(); + } catch (IOException e) { + Toast.makeText(getActivity(), getResources().getString(R.string.logs_clear_failed) + "n" + e.getMessage(), Toast.LENGTH_LONG).show(); + } + } + + private void send() { + Uri uri = FileProvider.getUriForFile(getActivity(), "de.robv.android.xposed.installer.fileprovider", mFileErrorLog); + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_STREAM, uri); + sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + sendIntent.setType("application/html"); + startActivity(Intent.createChooser(sendIntent, getResources().getString(R.string.menuSend))); + } + + @Override + public void onRequestPermissionsResult(int requestCode, + @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, + grantResults); + if (requestCode == WRITE_EXTERNAL_PERMISSION) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (mClickedMenuItem != null) { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + onOptionsItemSelected(mClickedMenuItem); + } + }, 500); + } + } else { + Toast.makeText(getActivity(), R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); + } + } + } + + @SuppressLint("DefaultLocale") + private File save() { + if (ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + FragmentCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_PERMISSION); + return null; + } + + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + Toast.makeText(getActivity(), R.string.sdcard_not_writable, Toast.LENGTH_LONG).show(); + return null; + } + + Calendar now = Calendar.getInstance(); + String filename = String.format( + "xposed_%s_%04d%02d%02d_%02d%02d%02d.log", "error", + now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1, + now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY), + now.get(Calendar.MINUTE), now.get(Calendar.SECOND)); + + File dir = getActivity().getExternalFilesDir(null); + + if (!dir.exists()) dir.mkdir(); + + File targetFile = new File(dir, filename); + + try { + FileInputStream in = new FileInputStream(mFileErrorLog); + FileOutputStream out = new FileOutputStream(targetFile); + byte[] buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + in.close(); + out.close(); + + Toast.makeText(getActivity(), targetFile.toString(), + Toast.LENGTH_LONG).show(); + return targetFile; + } catch (IOException e) { + Toast.makeText(getActivity(), getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Toast.LENGTH_LONG).show(); + return null; + } + } + + private class LogsReader extends AsyncTask { + + private static final int MAX_LOG_SIZE = 1000 * 1024; // 1000 KB + private MaterialDialog mProgressDialog; + + private long skipLargeFile(BufferedReader is, long length) throws IOException { + if (length < MAX_LOG_SIZE) + return 0; + + long skipped = length - MAX_LOG_SIZE; + long yetToSkip = skipped; + do { + yetToSkip -= is.skip(yetToSkip); + } while (yetToSkip > 0); + + int c; + do { + c = is.read(); + if (c == -1) + break; + skipped++; + } while (c != '\n'); + + return skipped; + + } + + @Override + protected void onPreExecute() { + mProgressDialog = new MaterialDialog.Builder(getActivity()).content(R.string.loading).progress(true, 0).show(); + } + + @Override + protected String doInBackground(File... log) { + Thread.currentThread().setPriority(Thread.NORM_PRIORITY + 2); + + StringBuilder llog = new StringBuilder(15 * 10 * 1024); + try { + File logfile = log[0]; + BufferedReader br; + br = new BufferedReader(new FileReader(logfile)); + long skipped = skipLargeFile(br, logfile.length()); + if (skipped > 0) { + llog.append("-----------------\n"); + llog.append("Log too long"); + llog.append("\n-----------------\n\n"); + } + + char[] temp = new char[1024]; + int read; + while ((read = br.read(temp)) > 0) { + llog.append(temp, 0, read); + } + br.close(); + } catch (IOException e) { + llog.append("Cannot read log"); + llog.append(e.getMessage()); + } + + return llog.toString(); + } + + @Override + protected void onPostExecute(String llog) { + mProgressDialog.dismiss(); + mTxtLog.setText(llog); + + if (llog.length() == 0) + mTxtLog.setText(R.string.log_is_empty); + } + + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/ModulesBookmark.java b/app/src/main/java/de/robv/android/xposed/installer/ModulesBookmark.java new file mode 100644 index 000000000..5adc3673f --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/ModulesBookmark.java @@ -0,0 +1,250 @@ +package de.robv.android.xposed.installer; + +import android.app.ListFragment; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.design.widget.Snackbar; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.ContextMenu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import de.robv.android.xposed.installer.repo.Module; +import de.robv.android.xposed.installer.repo.ModuleVersion; +import de.robv.android.xposed.installer.util.DownloadsUtil; +import de.robv.android.xposed.installer.util.RepoLoader; +import de.robv.android.xposed.installer.util.ThemeUtil; + +public class ModulesBookmark extends XposedBaseActivity { + + private static RepoLoader mRepoLoader; + private static View container; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeUtil.setTheme(this); + setContentView(R.layout.activity_container); + + mRepoLoader = RepoLoader.getInstance(); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setTitle(R.string.bookmarks); + ab.setDisplayHomeAsUpEnabled(true); + } + + setFloating(toolbar, 0); + + container = findViewById(R.id.container); + + if (savedInstanceState == null) { + getFragmentManager().beginTransaction().add(R.id.container, new ModulesBookmarkFragment()).commit(); + } + } + + public static class ModulesBookmarkFragment extends ListFragment implements AdapterView.OnItemClickListener, SharedPreferences.OnSharedPreferenceChangeListener { + + private List mBookmarkedModules = new ArrayList<>(); + private BookmarkModuleAdapter mAdapter; + private SharedPreferences mBookmarksPref; + private boolean changed; + private MenuItem mClickedMenuItem = null; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mBookmarksPref = getActivity().getSharedPreferences("bookmarks", MODE_PRIVATE); + mBookmarksPref.registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onResume() { + super.onResume(); + + if (changed) + getModules(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + mBookmarksPref.unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + getListView().setDivider(null); + getListView().setDividerHeight(getDp(6)); + getListView().setPadding(getDp(8), getDp(8), getDp(8), getDp(8)); + getListView().setOnItemClickListener(this); + getListView().setClipToPadding(false); + registerForContextMenu(getListView()); + setEmptyText(getString(R.string.no_bookmark_added)); + + mAdapter = new BookmarkModuleAdapter(getActivity()); + getModules(); + setListAdapter(mAdapter); + + setHasOptionsMenu(true); + } + + private void getModules() { + mAdapter.clear(); + mBookmarkedModules.clear(); + for (String s : mBookmarksPref.getAll().keySet()) { + boolean isBookmarked = mBookmarksPref.getBoolean(s, false); + + if (isBookmarked) { + Module m = mRepoLoader.getModule(s); + if (m != null) mBookmarkedModules.add(m); + } + } + Collections.sort(mBookmarkedModules, new Comparator() { + @Override + public int compare(Module mod1, Module mod2) { + return mod1.name.compareTo(mod2.name); + } + }); + mAdapter.addAll(mBookmarkedModules); + mAdapter.notifyDataSetChanged(); + } + + private int getDp(float value) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, metrics); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Intent detailsIntent = new Intent(getActivity(), DownloadDetailsActivity.class); + detailsIntent.setData(Uri.fromParts("package", mBookmarkedModules.get(position).packageName, null)); + startActivity(detailsIntent); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + changed = true; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + Module module = getItemFromContextMenuInfo(menuInfo); + if (module == null) + return; + + menu.setHeaderTitle(module.name); + getActivity().getMenuInflater().inflate(R.menu.context_menu_modules_bookmark, menu); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + final Module module = getItemFromContextMenuInfo( + item.getMenuInfo()); + if (module == null) + return false; + + final String pkg = module.packageName; + ModuleVersion mv = DownloadsUtil.getStableVersion(module); + + if (mv == null) + return false; + + mClickedMenuItem = item; + + switch (item.getItemId()) { + case R.id.install_bookmark: + DownloadsUtil.addModule(getActivity(), module.name, mv.downloadLink, new DownloadDetailsVersionsFragment.DownloadModuleCallback(mv)); + break; + case R.id.install_remove_bookmark: + DownloadsUtil.addModule(getActivity(), module.name, mv.downloadLink, new DownloadDetailsVersionsFragment.DownloadModuleCallback(mv) { + @Override + public void onDownloadFinished(Context context, DownloadsUtil.DownloadInfo info) { + super.onDownloadFinished(context, info); + remove(pkg); + } + }); + break; + case R.id.remove: + remove(pkg); + break; + } + + return false; + } + + private void remove(final String pkg) { + mBookmarksPref.edit().putBoolean(pkg, false).apply(); + + Snackbar.make(container, R.string.bookmark_removed, Snackbar.LENGTH_SHORT).setAction(R.string.undo, new View.OnClickListener() { + @Override + public void onClick(View v) { + mBookmarksPref.edit().putBoolean(pkg, true).apply(); + + getModules(); + } + }).show(); + + getModules(); + } + + private Module getItemFromContextMenuInfo(ContextMenu.ContextMenuInfo menuInfo) { + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; + int position = info.position - getListView().getHeaderViewsCount(); + return (position >= 0) ? (Module) getListAdapter().getItem(position) : null; + } + } + + private static class BookmarkModuleAdapter extends ArrayAdapter { + public BookmarkModuleAdapter(Context context) { + super(context, R.layout.list_item_module, R.id.title); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = super.getView(position, convertView, parent); + + view.findViewById(R.id.checkbox).setVisibility(View.GONE); + view.findViewById(R.id.version_name).setVisibility(View.GONE); + view.findViewById(R.id.icon).setVisibility(View.GONE); + + Module item = getItem(position); + + ((TextView) view.findViewById(R.id.title)).setText(item.name); + ((TextView) view.findViewById(R.id.description)) + .setText(item.summary); + + return view; + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/ModulesFragment.java b/app/src/main/java/de/robv/android/xposed/installer/ModulesFragment.java new file mode 100644 index 000000000..85a9c6fa0 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/ModulesFragment.java @@ -0,0 +1,565 @@ +package de.robv.android.xposed.installer; + +import android.Manifest; +import android.app.ListFragment; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.v13.app.FragmentCompat; +import android.support.v4.app.ActivityCompat; +import android.support.v7.app.ActionBar; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import de.robv.android.xposed.installer.installation.StatusInstallerFragment; +import de.robv.android.xposed.installer.repo.Module; +import de.robv.android.xposed.installer.repo.ModuleVersion; +import de.robv.android.xposed.installer.repo.ReleaseType; +import de.robv.android.xposed.installer.repo.RepoDb; +import de.robv.android.xposed.installer.repo.RepoDb.RowNotFoundException; +import de.robv.android.xposed.installer.util.DownloadsUtil; +import de.robv.android.xposed.installer.util.ModuleUtil; +import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; +import de.robv.android.xposed.installer.util.ModuleUtil.ModuleListener; +import de.robv.android.xposed.installer.util.NavUtil; +import de.robv.android.xposed.installer.util.RepoLoader; +import de.robv.android.xposed.installer.util.ThemeUtil; + +import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS; +import static de.robv.android.xposed.installer.XposedApp.WRITE_EXTERNAL_PERMISSION; + +public class ModulesFragment extends ListFragment implements ModuleListener { + public static final String SETTINGS_CATEGORY = "de.robv.android.xposed.category.MODULE_SETTINGS"; + public static final String PLAY_STORE_PACKAGE = "com.android.vending"; + public static final String PLAY_STORE_LINK = "https://play.google.com/store/apps/details?id=%s"; + public static final String XPOSED_REPO_LINK = "http://repo.xposed.info/module/%s"; + private static final String NOT_ACTIVE_NOTE_TAG = "NOT_ACTIVE_NOTE"; + private static String PLAY_STORE_LABEL = null; + private int installedXposedVersion; + private ModuleUtil mModuleUtil; + private ModuleAdapter mAdapter = null; + private PackageManager mPm = null; + private Runnable reloadModules = new Runnable() { + public void run() { + mAdapter.setNotifyOnChange(false); + mAdapter.clear(); + mAdapter.addAll(mModuleUtil.getModules().values()); + final Collator col = Collator.getInstance(Locale.getDefault()); + mAdapter.sort(new Comparator() { + @Override + public int compare(InstalledModule lhs, InstalledModule rhs) { + return col.compare(lhs.getAppName(), rhs.getAppName()); + } + }); + mAdapter.notifyDataSetChanged(); + } + }; + private MenuItem mClickedMenuItem = null; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mModuleUtil = ModuleUtil.getInstance(); + mPm = getActivity().getPackageManager(); + if (PLAY_STORE_LABEL == null) { + try { + ApplicationInfo ai = mPm.getApplicationInfo(PLAY_STORE_PACKAGE, + 0); + PLAY_STORE_LABEL = mPm.getApplicationLabel(ai).toString(); + } catch (NameNotFoundException ignored) { + } + } + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + installedXposedVersion = XposedApp.getInstalledXposedVersion(); + if (installedXposedVersion < 0 || XposedApp.getActiveXposedVersion() < 0 || StatusInstallerFragment.DISABLE_FILE.exists()) { + View notActiveNote = getActivity().getLayoutInflater().inflate(R.layout.xposed_not_active_note, getListView(), false); + if (installedXposedVersion < 0) { + ((TextView) notActiveNote.findViewById(android.R.id.title)).setText(R.string.framework_not_installed); + } + notActiveNote.setTag(NOT_ACTIVE_NOTE_TAG); + getListView().addHeaderView(notActiveNote); + } + + mAdapter = new ModuleAdapter(getActivity()); + reloadModules.run(); + setListAdapter(mAdapter); + setEmptyText(getActivity().getString(R.string.no_xposed_modules_found)); + registerForContextMenu(getListView()); + mModuleUtil.addListener(this); + + ActionBar actionBar = ((WelcomeActivity) getActivity()).getSupportActionBar(); + + DisplayMetrics metrics = getResources().getDisplayMetrics(); + int sixDp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6, metrics); + int eightDp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, metrics); + assert actionBar != null; + int toolBarDp = actionBar.getHeight() == 0 ? 196 : actionBar.getHeight(); + + getListView().setDivider(null); + getListView().setDividerHeight(sixDp); + getListView().setPadding(eightDp, toolBarDp + eightDp, eightDp, eightDp); + getListView().setClipToPadding(false); + + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + // TODO maybe enable again after checking the implementation + //inflater.inflate(R.menu.menu_modules, menu); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, + grantResults); + if (requestCode == WRITE_EXTERNAL_PERMISSION) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (mClickedMenuItem != null) { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + onOptionsItemSelected(mClickedMenuItem); + } + }, 500); + } + } else { + Toast.makeText(getActivity(), R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); + } + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.bookmarks) { + startActivity(new Intent(getActivity(), ModulesBookmark.class)); + return true; + } + + String backupPath = Environment.getExternalStorageDirectory() + + "/XposedInstaller"; + + File enabledModulesPath = new File(backupPath, "enabled_modules.list"); + File installedModulesPath = new File(backupPath, "installed_modules.list"); + File targetDir = new File(backupPath); + File listModules = new File(XposedApp.ENABLED_MODULES_LIST_FILE); + + mClickedMenuItem = item; + + if (checkPermissions()) + return false; + + switch (item.getItemId()) { + case R.id.export_enabled_modules: + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + return false; + } + + if (ModuleUtil.getInstance().getEnabledModules().isEmpty()) { + Toast.makeText(getActivity(), getString(R.string.no_enabled_modules), Toast.LENGTH_SHORT).show(); + return false; + } + + try { + if (!targetDir.exists()) + targetDir.mkdir(); + + FileInputStream in = new FileInputStream(listModules); + FileOutputStream out = new FileOutputStream(enabledModulesPath); + + byte[] buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + in.close(); + out.close(); + } catch (IOException e) { + Toast.makeText(getActivity(), getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Toast.LENGTH_LONG).show(); + return false; + } + + Toast.makeText(getActivity(), enabledModulesPath.toString(), Toast.LENGTH_LONG).show(); + return true; + case R.id.export_installed_modules: + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + Toast.makeText(getActivity(), R.string.sdcard_not_writable, Toast.LENGTH_LONG).show(); + return false; + } + Map installedModules = ModuleUtil.getInstance().getModules(); + + if (installedModules.isEmpty()) { + Toast.makeText(getActivity(), getString(R.string.no_installed_modules), Toast.LENGTH_SHORT).show(); + return false; + } + + try { + if (!targetDir.exists()) + targetDir.mkdir(); + + FileWriter fw = new FileWriter(installedModulesPath); + BufferedWriter bw = new BufferedWriter(fw); + PrintWriter fileOut = new PrintWriter(bw); + + Set keys = installedModules.keySet(); + for (Object key1 : keys) { + String packageName = (String) key1; + fileOut.println(packageName); + } + + fileOut.close(); + } catch (IOException e) { + Toast.makeText(getActivity(), getResources().getString(R.string.logs_save_failed) + "n" + e.getMessage(), Toast.LENGTH_LONG).show(); + return false; + } + + Toast.makeText(getActivity(), installedModulesPath.toString(), Toast.LENGTH_LONG).show(); + return true; + case R.id.import_installed_modules: + return importModules(installedModulesPath); + case R.id.import_enabled_modules: + return importModules(enabledModulesPath); + } + return super.onOptionsItemSelected(item); + } + + private boolean checkPermissions() { + if (ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + FragmentCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_PERMISSION); + return true; + } + return false; + } + + private boolean importModules(File path) { + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + Toast.makeText(getActivity(), R.string.sdcard_not_writable, Toast.LENGTH_LONG).show(); + return false; + } + InputStream ips = null; + RepoLoader repoLoader = RepoLoader.getInstance(); + List list = new ArrayList<>(); + if (!path.exists()) { + Toast.makeText(getActivity(), getString(R.string.no_backup_found), + Toast.LENGTH_LONG).show(); + return false; + } + try { + ips = new FileInputStream(path); + } catch (FileNotFoundException e) { + Log.e(XposedApp.TAG, "Could not open " + path, e); + } + + if (path.length() == 0) { + Toast.makeText(getActivity(), R.string.file_is_empty, Toast.LENGTH_LONG).show(); + return false; + } + + try { + assert ips != null; + InputStreamReader ipsr = new InputStreamReader(ips); + BufferedReader br = new BufferedReader(ipsr); + String line; + while ((line = br.readLine()) != null) { + Module m = repoLoader.getModule(line); + + if (m == null) { + Toast.makeText(getActivity(), getString(R.string.download_details_not_found, + line), Toast.LENGTH_SHORT).show(); + } else { + list.add(m); + } + } + br.close(); + } catch (ActivityNotFoundException | IOException e) { + Toast.makeText(getActivity(), e.toString(), Toast.LENGTH_SHORT).show(); + } + + for (Module m : list) { + ModuleVersion mv = null; + for (int i = 0; i < m.versions.size(); i++) { + ModuleVersion mvTemp = m.versions.get(i); + + if (mvTemp.relType == ReleaseType.STABLE) { + mv = mvTemp; + break; + } + } + + if (mv != null) { + DownloadsUtil.addModule(getActivity(), m.name, mv.downloadLink, new DownloadDetailsVersionsFragment.DownloadModuleCallback(mv)); + } + } + + return true; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mModuleUtil.removeListener(this); + setListAdapter(null); + mAdapter = null; + } + + @Override + public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module) { + getActivity().runOnUiThread(reloadModules); + } + + @Override + public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { + getActivity().runOnUiThread(reloadModules); + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + String packageName = (String) v.getTag(); + if (packageName == null) + return; + + if (packageName.equals(NOT_ACTIVE_NOTE_TAG)) { + ((WelcomeActivity) getActivity()).switchFragment(0); + return; + } + + Intent launchIntent = getSettingsIntent(packageName); + if (launchIntent != null) + startActivity(launchIntent); + else + Toast.makeText(getActivity(), + getActivity().getString(R.string.module_no_ui), + Toast.LENGTH_LONG).show(); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + InstalledModule installedModule = getItemFromContextMenuInfo(menuInfo); + if (installedModule == null) + return; + + menu.setHeaderTitle(installedModule.getAppName()); + getActivity().getMenuInflater().inflate(R.menu.context_menu_modules, menu); + + if (getSettingsIntent(installedModule.packageName) == null) + menu.removeItem(R.id.menu_launch); + + try { + String support = RepoDb + .getModuleSupport(installedModule.packageName); + if (NavUtil.parseURL(support) == null) + menu.removeItem(R.id.menu_support); + } catch (RowNotFoundException e) { + menu.removeItem(R.id.menu_download_updates); + menu.removeItem(R.id.menu_support); + } + + String installer = mPm.getInstallerPackageName(installedModule.packageName); + if (PLAY_STORE_LABEL != null && PLAY_STORE_PACKAGE.equals(installer)) + menu.findItem(R.id.menu_play_store).setTitle(PLAY_STORE_LABEL); + else + menu.removeItem(R.id.menu_play_store); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + InstalledModule module = getItemFromContextMenuInfo(item.getMenuInfo()); + if (module == null) + return false; + + switch (item.getItemId()) { + case R.id.menu_launch: + startActivity(getSettingsIntent(module.packageName)); + return true; + + case R.id.menu_download_updates: + Intent detailsIntent = new Intent(getActivity(), DownloadDetailsActivity.class); + detailsIntent.setData(Uri.fromParts("package", module.packageName, null)); + startActivity(detailsIntent); + return true; + + case R.id.menu_support: + NavUtil.startURL(getActivity(), Uri.parse(RepoDb.getModuleSupport(module.packageName))); + return true; + + case R.id.menu_play_store: + Intent i = new Intent(android.content.Intent.ACTION_VIEW); + i.setData(Uri.parse(String.format(PLAY_STORE_LINK, module.packageName))); + i.setPackage(PLAY_STORE_PACKAGE); + try { + startActivity(i); + } catch (ActivityNotFoundException e) { + i.setPackage(null); + startActivity(i); + } + return true; + + case R.id.menu_app_info: + startActivity(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", module.packageName, null))); + return true; + + case R.id.menu_uninstall: + startActivity(new Intent(Intent.ACTION_UNINSTALL_PACKAGE, Uri.fromParts("package", module.packageName, null))); + return true; + } + + return false; + } + + private InstalledModule getItemFromContextMenuInfo(ContextMenuInfo menuInfo) { + AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; + int position = info.position - getListView().getHeaderViewsCount(); + return (position >= 0) ? (InstalledModule) getListAdapter().getItem(position) : null; + } + + private Intent getSettingsIntent(String packageName) { + // taken from + // ApplicationPackageManager.getLaunchIntentForPackage(String) + // first looks for an Xposed-specific category, falls back to + // getLaunchIntentForPackage + PackageManager pm = getActivity().getPackageManager(); + + Intent intentToResolve = new Intent(Intent.ACTION_MAIN); + intentToResolve.addCategory(SETTINGS_CATEGORY); + intentToResolve.setPackage(packageName); + List ris = pm.queryIntentActivities(intentToResolve, 0); + + if (ris == null || ris.size() <= 0) { + return pm.getLaunchIntentForPackage(packageName); + } + + Intent intent = new Intent(intentToResolve); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setClassName(ris.get(0).activityInfo.packageName, ris.get(0).activityInfo.name); + return intent; + } + + private class ModuleAdapter extends ArrayAdapter { + public ModuleAdapter(Context context) { + super(context, R.layout.list_item_module, R.id.title); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = super.getView(position, convertView, parent); + + if (convertView == null) { + // The reusable view was created for the first time, set up the + // listener on the checkbox + ((CheckBox) view.findViewById(R.id.checkbox)).setOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + String packageName = (String) buttonView.getTag(); + boolean changed = mModuleUtil.isModuleEnabled(packageName) ^ isChecked; + if (changed) { + mModuleUtil.setModuleEnabled(packageName, isChecked); + mModuleUtil.updateModulesList(true); + } + } + }); + } + + InstalledModule item = getItem(position); + + TextView version = (TextView) view.findViewById(R.id.version_name); + version.setText(item.versionName); + + // Store the package name in some views' tag for later access + view.findViewById(R.id.checkbox).setTag(item.packageName); + view.setTag(item.packageName); + + ((ImageView) view.findViewById(R.id.icon)).setImageDrawable(item.getIcon()); + + TextView descriptionText = (TextView) view.findViewById(R.id.description); + if (!item.getDescription().isEmpty()) { + descriptionText.setText(item.getDescription()); + descriptionText.setTextColor(ThemeUtil.getThemeColor(getContext(), android.R.attr.textColorSecondary)); + } else { + descriptionText.setText(getString(R.string.module_empty_description)); + descriptionText.setTextColor(getResources().getColor(R.color.warning)); + } + + CheckBox checkbox = (CheckBox) view.findViewById(R.id.checkbox); + checkbox.setChecked(mModuleUtil.isModuleEnabled(item.packageName)); + TextView warningText = (TextView) view.findViewById(R.id.warning); + + if (item.minVersion == 0) { + checkbox.setEnabled(false); + warningText.setText(getString(R.string.no_min_version_specified)); + warningText.setVisibility(View.VISIBLE); + } else if (installedXposedVersion != 0 && item.minVersion > installedXposedVersion) { + checkbox.setEnabled(false); + warningText.setText(String.format(getString(R.string.warning_xposed_min_version), item.minVersion)); + warningText.setVisibility(View.VISIBLE); + } else if (item.minVersion < ModuleUtil.MIN_MODULE_VERSION) { + checkbox.setEnabled(false); + warningText.setText(String.format(getString(R.string.warning_min_version_too_low), item.minVersion, ModuleUtil.MIN_MODULE_VERSION)); + warningText.setVisibility(View.VISIBLE); + } else if (item.isInstalledOnExternalStorage()) { + checkbox.setEnabled(false); + warningText.setText(getString(R.string.warning_installed_on_external_storage)); + warningText.setVisibility(View.VISIBLE); + } else if (installedXposedVersion == 0) { + checkbox.setEnabled(false); + warningText.setText(getString(R.string.framework_not_installed)); + warningText.setVisibility(View.VISIBLE); + } else { + checkbox.setEnabled(true); + warningText.setVisibility(View.GONE); + } + return view; + } + } + +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/SettingsActivity.java b/app/src/main/java/de/robv/android/xposed/installer/SettingsActivity.java new file mode 100644 index 000000000..07bcf54f9 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/SettingsActivity.java @@ -0,0 +1,180 @@ +package de.robv.android.xposed.installer; + +import android.Manifest; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.widget.Toast; + +import com.afollestad.materialdialogs.folderselector.FolderChooserDialog; + +import java.io.File; +import java.io.IOException; + +import de.robv.android.xposed.installer.util.RepoLoader; +import de.robv.android.xposed.installer.util.ThemeUtil; + +public class SettingsActivity extends XposedBaseActivity implements FolderChooserDialog.FolderCallback { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeUtil.setTheme(this); + setContentView(R.layout.activity_container); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setTitle(R.string.nav_item_settings); + ab.setDisplayHomeAsUpEnabled(true); + } + + setFloating(toolbar, 0); + + if (savedInstanceState == null) { + getFragmentManager().beginTransaction() + .add(R.id.container, new SettingsFragment()).commit(); + } + + } + + @Override + public void onFolderSelection(@NonNull FolderChooserDialog dialog, @NonNull File folder) { + if (folder.canWrite()) { + XposedApp.getPreferences().edit().putString("download_location", folder.getPath()).apply(); + } else { + Toast.makeText(this, R.string.sdcard_not_writable, Toast.LENGTH_SHORT).show(); + } + } + + public static class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener, Preference.OnPreferenceClickListener { + private static final File mDisableResourcesFlag = new File(XposedApp.BASE_DIR + "conf/disable_resources"); + private Preference mClickedPreference; + private Preference downloadLocation; + + public SettingsFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.prefs); + + findPreference("release_type_global").setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + RepoLoader.getInstance().setReleaseTypeGlobal((String) newValue); + return true; + } + }); + + CheckBoxPreference prefDisableResources = (CheckBoxPreference) findPreference("disable_resources"); + prefDisableResources.setChecked(mDisableResourcesFlag.exists()); + prefDisableResources.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + boolean enabled = (Boolean) newValue; + if (enabled) { + try { + //noinspection ResultOfMethodCallIgnored + mDisableResourcesFlag.createNewFile(); + } catch (IOException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } else { + //noinspection ResultOfMethodCallIgnored + mDisableResourcesFlag.delete(); + } + return (enabled == mDisableResourcesFlag.exists()); + } + }); + + // TODO maybe enable again after checking the implementation + //downloadLocation = findPreference("download_location"); + //downloadLocation.setOnPreferenceClickListener(this); + } + + @Override + public void onResume() { + super.onResume(); + + getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + super.onPause(); + + getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key.equals("theme")) getActivity().recreate(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (preference.getKey().equals(downloadLocation.getKey())) { + if (checkPermissions()) { + mClickedPreference = downloadLocation; + return false; + } + + new FolderChooserDialog.Builder((SettingsActivity) getActivity()) + .cancelButton(android.R.string.cancel) + .initialPath(XposedApp.getDownloadPath()) + .show(); + } + + return true; + } + + private boolean checkPermissions() { + if (Build.VERSION.SDK_INT < 23) return false; + + if (ActivityCompat.checkSelfPermission(getContext(), + Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0); + return true; + } + return false; + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (mClickedPreference != null) { + new android.os.Handler().postDelayed(new Runnable() { + @Override + public void run() { + onPreferenceClick(mClickedPreference); + } + }, 500); + } + } else { + Toast.makeText(getActivity(), R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); + } + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/SupportActivity.java b/app/src/main/java/de/robv/android/xposed/installer/SupportActivity.java new file mode 100644 index 000000000..81a7f1677 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/SupportActivity.java @@ -0,0 +1,80 @@ +package de.robv.android.xposed.installer; + +import android.app.Fragment; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import de.robv.android.xposed.installer.util.NavUtil; +import de.robv.android.xposed.installer.util.ThemeUtil; + +public class SupportActivity extends XposedBaseActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeUtil.setTheme(this); + setContentView(R.layout.activity_container); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setTitle(R.string.nav_item_support); + ab.setDisplayHomeAsUpEnabled(true); + } + + setFloating(toolbar, 0); + + if (savedInstanceState == null) { + getFragmentManager().beginTransaction().add(R.id.container, new SupportFragment()).commit(); + } + } + + public static class SupportFragment extends Fragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.tab_support, container, false); + + View installerSupportView = v.findViewById(R.id.installerSupportView); + View faqView = v.findViewById(R.id.faqView); + View donateView = v.findViewById(R.id.donateView); + TextView txtModuleSupport = (TextView) v.findViewById(R.id.tab_support_module_description); + + txtModuleSupport.setText(getString(R.string.support_modules_description, + getString(R.string.module_support))); + + setupView(installerSupportView, R.string.about_support); + setupView(faqView, R.string.support_faq_url); + setupView(donateView, R.string.support_donate_url); + + return v; + } + + public void setupView(View v, final int url) { + v.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + NavUtil.startURL(getActivity(), getString(url)); + } + }); + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/WelcomeActivity.java b/app/src/main/java/de/robv/android/xposed/installer/WelcomeActivity.java new file mode 100644 index 000000000..f3c133517 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/WelcomeActivity.java @@ -0,0 +1,267 @@ +package de.robv.android.xposed.installer; + +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.support.design.widget.NavigationView; +import android.support.design.widget.Snackbar; +import android.support.v4.view.GravityCompat; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.widget.Toolbar; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.Transformation; +import android.widget.LinearLayout; + +import de.robv.android.xposed.installer.installation.StatusInstallerFragment; +import de.robv.android.xposed.installer.util.Loader; +import de.robv.android.xposed.installer.util.ModuleUtil; +import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; +import de.robv.android.xposed.installer.util.ModuleUtil.ModuleListener; +import de.robv.android.xposed.installer.util.RepoLoader; +import de.robv.android.xposed.installer.util.ThemeUtil; + +public class WelcomeActivity extends XposedBaseActivity implements NavigationView.OnNavigationItemSelectedListener, + ModuleListener, Loader.Listener { + + private static final String SELECTED_ITEM_ID = "SELECTED_ITEM_ID"; + private final Handler mDrawerHandler = new Handler(); + private RepoLoader mRepoLoader; + private DrawerLayout mDrawerLayout; + private int mPrevSelectedId; + private NavigationView mNavigationView; + private int mSelectedId; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeUtil.setTheme(this); + setContentView(R.layout.activity_welcome); + + mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + mNavigationView = (NavigationView) findViewById(R.id.navigation_view); + assert mNavigationView != null; + mNavigationView.setNavigationItemSelectedListener(this); + + ActionBarDrawerToggle mDrawerToggle = new ActionBarDrawerToggle(this, + mDrawerLayout, toolbar, R.string.navigation_drawer_open, + R.string.navigation_drawer_close) { + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + super.onDrawerSlide(drawerView, 0); // this disables the arrow @ completed state + } + + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + super.onDrawerSlide(drawerView, 0); // this disables the animation + } + }; + mDrawerLayout.addDrawerListener(mDrawerToggle); + mDrawerToggle.syncState(); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + mSelectedId = mNavigationView.getMenu().getItem(prefs.getInt("default_view", 0)).getItemId(); + mSelectedId = savedInstanceState == null ? mSelectedId : savedInstanceState.getInt(SELECTED_ITEM_ID); + mPrevSelectedId = mSelectedId; + mNavigationView.getMenu().findItem(mSelectedId).setChecked(true); + + if (savedInstanceState == null) { + mDrawerHandler.removeCallbacksAndMessages(null); + mDrawerHandler.postDelayed(new Runnable() { + @Override + public void run() { + navigate(mSelectedId); + } + }, 250); + + boolean openDrawer = prefs.getBoolean("open_drawer", false); + + if (openDrawer) + mDrawerLayout.openDrawer(GravityCompat.START); + else + mDrawerLayout.closeDrawers(); + } + + Bundle extras = getIntent().getExtras(); + if (extras != null) { + int value = extras.getInt("fragment", prefs.getInt("default_view", 0)); + switchFragment(value); + } + + mRepoLoader = RepoLoader.getInstance(); + ModuleUtil.getInstance().addListener(this); + mRepoLoader.addListener(this); + + notifyDataSetChanged(); + } + + public void switchFragment(int itemId) { + mSelectedId = mNavigationView.getMenu().getItem(itemId).getItemId(); + mNavigationView.getMenu().findItem(mSelectedId).setChecked(true); + mDrawerHandler.removeCallbacksAndMessages(null); + mDrawerHandler.postDelayed(new Runnable() { + @Override + public void run() { + navigate(mSelectedId); + } + }, 250); + mDrawerLayout.closeDrawers(); + } + + private void navigate(final int itemId) { + final View elevation = findViewById(R.id.elevation); + Fragment navFragment = null; + switch (itemId) { + case R.id.nav_item_framework: + mPrevSelectedId = itemId; + setTitle(R.string.app_name); + navFragment = new StatusInstallerFragment(); + break; + case R.id.nav_item_modules: + mPrevSelectedId = itemId; + setTitle(R.string.nav_item_modules); + navFragment = new ModulesFragment(); + break; + case R.id.nav_item_downloads: + mPrevSelectedId = itemId; + setTitle(R.string.nav_item_download); + navFragment = new DownloadFragment(); + break; + case R.id.nav_item_logs: + mPrevSelectedId = itemId; + setTitle(R.string.nav_item_logs); + navFragment = new LogsFragment(); + break; + case R.id.nav_item_settings: + startActivity(new Intent(this, SettingsActivity.class)); + mNavigationView.getMenu().findItem(mPrevSelectedId).setChecked(true); + return; + case R.id.nav_item_support: + startActivity(new Intent(this, SupportActivity.class)); + mNavigationView.getMenu().findItem(mPrevSelectedId).setChecked(true); + return; + case R.id.nav_item_about: + startActivity(new Intent(this, AboutActivity.class)); + mNavigationView.getMenu().findItem(mPrevSelectedId).setChecked(true); + return; + } + + final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(4)); + + if (navFragment != null) { + FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.setCustomAnimations(R.animator.fade_in, R.animator.fade_out); + try { + transaction.replace(R.id.content_frame, navFragment).commit(); + + if (elevation != null) { + Animation a = new Animation() { + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + elevation.setLayoutParams(params); + } + }; + a.setDuration(150); + elevation.startAnimation(a); + } + } catch (IllegalStateException ignored) { + } + } + } + + public int dp(float value) { + float density = getApplicationContext().getResources().getDisplayMetrics().density; + + if (value == 0) { + return 0; + } + return (int) Math.ceil(density * value); + } + + @Override + public boolean onNavigationItemSelected(MenuItem menuItem) { + menuItem.setChecked(true); + mSelectedId = menuItem.getItemId(); + mDrawerHandler.removeCallbacksAndMessages(null); + mDrawerHandler.postDelayed(new Runnable() { + @Override + public void run() { + navigate(mSelectedId); + } + }, 250); + mDrawerLayout.closeDrawers(); + return true; + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(SELECTED_ITEM_ID, mSelectedId); + } + + @Override + public void onBackPressed() { + if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) { + mDrawerLayout.closeDrawer(GravityCompat.START); + } else { + super.onBackPressed(); + } + } + + private void notifyDataSetChanged() { + View parentLayout = findViewById(R.id.content_frame); + String frameworkUpdateVersion = mRepoLoader.getFrameworkUpdateVersion(); + boolean moduleUpdateAvailable = mRepoLoader.hasModuleUpdates(); + + Fragment currentFragment = getFragmentManager().findFragmentById(R.id.content_frame); + if (currentFragment instanceof DownloadDetailsFragment) { + if (frameworkUpdateVersion != null) { + Snackbar.make(parentLayout, R.string.welcome_framework_update_available + " " + String.valueOf(frameworkUpdateVersion), Snackbar.LENGTH_LONG).show(); + } + } + + boolean snackBar = XposedApp.getPreferences().getBoolean("snack_bar", true); + + if (moduleUpdateAvailable && snackBar) { + Snackbar.make(parentLayout, R.string.modules_updates_available, Snackbar.LENGTH_LONG).setAction(getString(R.string.view), new View.OnClickListener() { + @Override + public void onClick(View view) { + switchFragment(2); + } + }).show(); + } + } + + @Override + public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { + notifyDataSetChanged(); + } + + @Override + public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module) { + notifyDataSetChanged(); + } + + @Override + public void onReloadDone(RepoLoader loader) { + notifyDataSetChanged(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + ModuleUtil.getInstance().removeListener(this); + mRepoLoader.removeListener(this); + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/XposedApp.java b/app/src/main/java/de/robv/android/xposed/installer/XposedApp.java new file mode 100644 index 000000000..abb821164 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/XposedApp.java @@ -0,0 +1,211 @@ +package de.robv.android.xposed.installer; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Application; +import android.app.Application.ActivityLifecycleCallbacks; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.FileUtils; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.support.v4.content.FileProvider; +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Method; + +import de.robv.android.xposed.installer.util.AssetUtil; +import de.robv.android.xposed.installer.util.DownloadsUtil; +import de.robv.android.xposed.installer.util.InstallZipUtil; +import de.robv.android.xposed.installer.util.InstallZipUtil.XposedProp; +import de.robv.android.xposed.installer.util.NotificationUtil; +import de.robv.android.xposed.installer.util.RepoLoader; + +public class XposedApp extends Application implements ActivityLifecycleCallbacks { + public static final String TAG = "XposedInstaller"; + + @SuppressLint("SdCardPath") + private static final String BASE_DIR_LEGACY = "/data/data/de.robv.android.xposed.installer/"; + + public static final String BASE_DIR = Build.VERSION.SDK_INT >= 24 + ? "/data/user_de/0/de.robv.android.xposed.installer/" : BASE_DIR_LEGACY; + + public static final String ENABLED_MODULES_LIST_FILE = XposedApp.BASE_DIR + "conf/enabled_modules.list"; + + private static final String[] XPOSED_PROP_FILES = new String[]{ + "/su/xposed/xposed.prop", // official systemless + "/system/xposed.prop", // classical + }; + + public static int WRITE_EXTERNAL_PERMISSION = 69; + private static XposedApp mInstance = null; + private static Thread mUiThread; + private static Handler mMainHandler; + private boolean mIsUiLoaded = false; + private SharedPreferences mPref; + private XposedProp mXposedProp; + + public static XposedApp getInstance() { + return mInstance; + } + + public static void runOnUiThread(Runnable action) { + if (Thread.currentThread() != mUiThread) { + mMainHandler.post(action); + } else { + action.run(); + } + } + + public static void postOnUiThread(Runnable action) { + mMainHandler.post(action); + } + + // This method is hooked by XposedBridge to return the current version + public static int getActiveXposedVersion() { + return -1; + } + + public static int getInstalledXposedVersion() { + XposedProp prop = getXposedProp(); + return prop != null ? prop.getVersionInt() : -1; + } + + public static XposedProp getXposedProp() { + synchronized (mInstance) { + return mInstance.mXposedProp; + } + } + + public static SharedPreferences getPreferences() { + return mInstance.mPref; + } + + public static void installApk(Context context, DownloadsUtil.DownloadInfo info) { + Intent installIntent = new Intent(Intent.ACTION_INSTALL_PACKAGE); + installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Uri uri; + if (Build.VERSION.SDK_INT >= 24) { + uri = FileProvider.getUriForFile(context, "de.robv.android.xposed.installer.fileprovider", new File(info.localFilename)); + installIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } else { + uri = Uri.fromFile(new File(info.localFilename)); + } + installIntent.setDataAndType(uri, DownloadsUtil.MIME_TYPE_APK); + installIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, context.getApplicationInfo().packageName); + context.startActivity(installIntent); + } + + public static String getDownloadPath() { + return getPreferences().getString("download_location", Environment.getExternalStorageDirectory() + "/XposedInstaller"); + } + + public void onCreate() { + super.onCreate(); + mInstance = this; + mUiThread = Thread.currentThread(); + mMainHandler = new Handler(); + + mPref = PreferenceManager.getDefaultSharedPreferences(this); + reloadXposedProp(); + createDirectories(); + NotificationUtil.init(); + AssetUtil.removeBusybox(); + + registerActivityLifecycleCallbacks(this); + } + + private void createDirectories() { + FileUtils.setPermissions(BASE_DIR, 00711, -1, -1); + mkdirAndChmod("conf", 00771); + mkdirAndChmod("log", 00777); + + if (Build.VERSION.SDK_INT >= 24) { + try { + Method deleteDir = FileUtils.class.getDeclaredMethod("deleteContentsAndDir", File.class); + deleteDir.invoke(null, new File(BASE_DIR_LEGACY, "bin")); + deleteDir.invoke(null, new File(BASE_DIR_LEGACY, "conf")); + deleteDir.invoke(null, new File(BASE_DIR_LEGACY, "log")); + } catch (ReflectiveOperationException e) { + Log.w(XposedApp.TAG, "Failed to delete obsolete directories", e); + } + } + } + + private void mkdirAndChmod(String dir, int permissions) { + dir = BASE_DIR + dir; + new File(dir).mkdir(); + FileUtils.setPermissions(dir, permissions, -1, -1); + } + + public void reloadXposedProp() { + XposedProp prop = null; + + for (String path : XPOSED_PROP_FILES) { + File file = new File(path); + if (file.canRead()) { + FileInputStream is = null; + try { + is = new FileInputStream(file); + prop = InstallZipUtil.parseXposedProp(is); + break; + } catch (IOException e) { + Log.e(XposedApp.TAG, "Could not read " + file.getPath(), e); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ignored) { + } + } + } + } + } + + synchronized (this) { + mXposedProp = prop; + } + } + + // TODO find a better way to trigger actions only when any UI is shown for the first time + @Override + public synchronized void onActivityCreated(Activity activity, Bundle savedInstanceState) { + if (mIsUiLoaded) + return; + + RepoLoader.getInstance().triggerFirstLoadIfNecessary(); + mIsUiLoaded = true; + } + + @Override + public synchronized void onActivityResumed(Activity activity) { + } + + @Override + public synchronized void onActivityPaused(Activity activity) { + } + + @Override + public void onActivityStarted(Activity activity) { + } + + @Override + public void onActivityStopped(Activity activity) { + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + } + + @Override + public void onActivityDestroyed(Activity activity) { + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/XposedBaseActivity.java b/app/src/main/java/de/robv/android/xposed/installer/XposedBaseActivity.java new file mode 100644 index 000000000..6c5d15d32 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/XposedBaseActivity.java @@ -0,0 +1,49 @@ +package de.robv.android.xposed.installer; + +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.StringRes; +import android.support.v7.app.AppCompatActivity; +import android.view.WindowManager; + +import de.robv.android.xposed.installer.util.ThemeUtil; + +public abstract class XposedBaseActivity extends AppCompatActivity { + public int mTheme = -1; + + @Override + protected void onCreate(Bundle savedInstanceBundle) { + super.onCreate(savedInstanceBundle); + ThemeUtil.setTheme(this); + } + + @Override + protected void onResume() { + super.onResume(); + + ThemeUtil.reloadTheme(this); + } + + @SuppressWarnings("deprecation") + public void setFloating(android.support.v7.widget.Toolbar toolbar, @StringRes int details) { + boolean isTablet = getResources().getBoolean(R.bool.isTablet); + if (isTablet) { + WindowManager.LayoutParams params = getWindow().getAttributes(); + params.height = getResources().getDimensionPixelSize(R.dimen.floating_height); + params.width = getResources().getDimensionPixelSize(R.dimen.floating_width); + params.alpha = 1.0f; + params.dimAmount = 0.6f; + params.flags |= 2; + getWindow().setAttributes(params); + + if (details != 0) { + toolbar.setTitle(details); + } + toolbar.setNavigationIcon(R.drawable.ic_close); + setFinishOnTouchOutside(true); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().setStatusBarColor(getResources().getColor(R.color.colorPrimaryDark)); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/installation/FlashCallback.java b/app/src/main/java/de/robv/android/xposed/installer/installation/FlashCallback.java new file mode 100644 index 000000000..d565766c7 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/installation/FlashCallback.java @@ -0,0 +1,23 @@ +package de.robv.android.xposed.installer.installation; + +import de.robv.android.xposed.installer.util.RootUtil; +import eu.chainfire.libsuperuser.Shell; + +public interface FlashCallback extends RootUtil.LineCallback { + void onStarted(); + void onDone(); + void onError(int exitCode, String error); + + int OK = 0; + int ERROR_GENERIC = 1; + + // SU errors + int ERROR_TIMEOUT = Shell.OnCommandResultListener.WATCHDOG_EXIT; + int ERROR_SHELL_DIED = Shell.OnCommandResultListener.SHELL_DIED; + int ERROR_NO_ROOT_ACCESS = Shell.OnCommandResultListener.SHELL_EXEC_FAILED; + + // ZIP errors + int ERROR_INVALID_ZIP = -100; + int ERROR_NOT_FLASHABLE_IN_APP = -101; + int ERROR_INSTALLER_NEEDS_UPDATE = -102; +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/installation/FlashDirectly.java b/app/src/main/java/de/robv/android/xposed/installer/installation/FlashDirectly.java new file mode 100644 index 000000000..e230d3fd7 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/installation/FlashDirectly.java @@ -0,0 +1,103 @@ +package de.robv.android.xposed.installer.installation; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.util.AssetUtil; +import de.robv.android.xposed.installer.util.FrameworkZips; +import de.robv.android.xposed.installer.util.InstallZipUtil.ZipCheckResult; +import de.robv.android.xposed.installer.util.RootUtil; + +import static de.robv.android.xposed.installer.util.InstallZipUtil.closeSilently; +import static de.robv.android.xposed.installer.util.InstallZipUtil.triggerError; +import static de.robv.android.xposed.installer.util.RootUtil.getShellPath; + +public class FlashDirectly extends Flashable { + private final boolean mSystemless; + + public FlashDirectly(File zipPath, FrameworkZips.Type type, String title, boolean systemless) { + super(zipPath, type, title); + mSystemless = systemless; + } + + public void flash(Context context, FlashCallback callback) { + ZipCheckResult zipCheck = openAndCheckZip(callback); + if (zipCheck == null) { + return; + } + + // Do additional checks. + ZipFile zip = zipCheck.getZip(); + if (!zipCheck.isFlashableInApp()) { + triggerError(callback, FlashCallback.ERROR_NOT_FLASHABLE_IN_APP); + closeSilently(zip); + return; + } + + // Extract update-binary. + ZipEntry entry = zip.getEntry("META-INF/com/google/android/update-binary"); + File updateBinaryFile = new File(XposedApp.getInstance().getCacheDir(), "update-binary"); + try { + AssetUtil.writeStreamToFile(zip.getInputStream(entry), updateBinaryFile, 0700); + } catch (IOException e) { + Log.e(XposedApp.TAG, "Could not extract update-binary", e); + triggerError(callback, FlashCallback.ERROR_INVALID_ZIP); + return; + } finally { + closeSilently(zip); + } + + // Execute the flash commands. + RootUtil rootUtil = new RootUtil(); + if (!rootUtil.startShell(callback)) { + return; + } + + callback.onStarted(); + + rootUtil.execute("export NO_UIPRINT=1", callback); + if (mSystemless) { + rootUtil.execute("export SYSTEMLESS=1", callback); + } + + int result = rootUtil.execute(getShellPath(updateBinaryFile) + " 2 1 " + getShellPath(mZipPath), callback); + if (result != FlashCallback.OK) { + triggerError(callback, result); + return; + } + + callback.onDone(); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public FlashDirectly createFromParcel(Parcel in) { + return new FlashDirectly(in); + } + + @Override + public FlashDirectly[] newArray(int size) { + return new FlashDirectly[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mSystemless ? 1 : 0); + } + + protected FlashDirectly(Parcel in) { + super(in); + mSystemless = in.readInt() == 1; + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/installation/FlashRecoveryAuto.java b/app/src/main/java/de/robv/android/xposed/installer/installation/FlashRecoveryAuto.java new file mode 100644 index 000000000..acc6bcf57 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/installation/FlashRecoveryAuto.java @@ -0,0 +1,94 @@ +package de.robv.android.xposed.installer.installation; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.File; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.util.FrameworkZips; +import de.robv.android.xposed.installer.util.InstallZipUtil.ZipCheckResult; +import de.robv.android.xposed.installer.util.RootUtil; + +import static de.robv.android.xposed.installer.util.InstallZipUtil.closeSilently; + +public class FlashRecoveryAuto extends Flashable { + public FlashRecoveryAuto(File zipPath, FrameworkZips.Type type, String title) { + super(zipPath, type, title); + } + + @Override + public void flash(Context context, FlashCallback callback) { + ZipCheckResult zipCheck = openAndCheckZip(callback); + if (zipCheck == null) { + return; + } else { + closeSilently(zipCheck.getZip()); + } + + final String zipName = mZipPath.getName(); + String cmd; + + // Execute the flash commands. + RootUtil rootUtil = new RootUtil(); + if (!rootUtil.startShell(callback)) { + return; + } + + callback.onStarted(); + + // Make sure /cache/recovery/ exists. + if (rootUtil.execute("ls /cache/recovery", null) != 0) { + callback.onLine(context.getString(R.string.file_creating_directory, "/cache/recovery")); + if (rootUtil.executeWithBusybox("mkdir /cache/recovery", callback) != 0) { + callback.onError(FlashCallback.ERROR_GENERIC, + context.getString(R.string.file_create_directory_failed, "/cache/recovery")); + return; + } + } + + // Copy the ZIP to /cache/recovery/. + callback.onLine(context.getString(R.string.file_copying, zipName)); + cmd = "cp -a " + RootUtil.getShellPath(mZipPath) + " /cache/recovery/" + zipName; + if (rootUtil.executeWithBusybox(cmd, callback) != 0) { + callback.onError(FlashCallback.ERROR_GENERIC, + context.getString(R.string.file_copy_failed, zipName, "/cache/recovery")); + return; + } + + // Write the flashing command to /cache/recovery/command. + callback.onLine(context.getString(R.string.file_writing_recovery_command)); + cmd = "echo --update_package=/cache/recovery/" + zipName + " > /cache/recovery/command"; + if (rootUtil.execute(cmd, callback) != 0) { + callback.onError(FlashCallback.ERROR_GENERIC, + context.getString(R.string.file_writing_recovery_command_failed)); + return; + } + + callback.onLine(context.getString(R.string.auto_flash_note, zipName)); + callback.onDone(); + } + + @Override + public RootUtil.RebootMode getRebootMode() { + return RootUtil.RebootMode.RECOVERY; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public FlashRecoveryAuto createFromParcel(Parcel in) { + return new FlashRecoveryAuto(in); + } + + @Override + public FlashRecoveryAuto[] newArray(int size) { + return new FlashRecoveryAuto[size]; + } + }; + + protected FlashRecoveryAuto(Parcel in) { + super(in); + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/installation/Flashable.java b/app/src/main/java/de/robv/android/xposed/installer/installation/Flashable.java new file mode 100644 index 000000000..05df14cb0 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/installation/Flashable.java @@ -0,0 +1,96 @@ +package de.robv.android.xposed.installer.installation; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.File; +import java.io.IOException; +import java.util.Set; +import java.util.zip.ZipFile; + +import de.robv.android.xposed.installer.util.FrameworkZips; +import de.robv.android.xposed.installer.util.InstallZipUtil; +import de.robv.android.xposed.installer.util.InstallZipUtil.ZipCheckResult; +import de.robv.android.xposed.installer.util.RootUtil; + +import static de.robv.android.xposed.installer.util.InstallZipUtil.closeSilently; +import static de.robv.android.xposed.installer.util.InstallZipUtil.reportMissingFeatures; +import static de.robv.android.xposed.installer.util.InstallZipUtil.triggerError; + +public abstract class Flashable implements Parcelable { + public static final String KEY = "flash"; + + protected final File mZipPath; + protected final FrameworkZips.Type mType; + protected final String mTitle; + + public Flashable(File zipPath, FrameworkZips.Type type, String title) { + mZipPath = zipPath; + mType = type; + mTitle = title; + } + + protected Flashable(Parcel in) { + mZipPath = (File) in.readSerializable(); + mType = (FrameworkZips.Type) in.readSerializable(); + mTitle = in.readString(); + } + + public abstract void flash(Context context, FlashCallback callback); + + protected ZipCheckResult openAndCheckZip(FlashCallback callback) { + // Open the ZIP file. + ZipFile zip; + try { + zip = new ZipFile(mZipPath); + } catch (IOException e) { + triggerError(callback, FlashCallback.ERROR_INVALID_ZIP, e.getLocalizedMessage()); + return null; + } + + // Do some checks. + ZipCheckResult zipCheck = InstallZipUtil.checkZip(zip); + if (!zipCheck.isValidZip()) { + triggerError(callback, FlashCallback.ERROR_INVALID_ZIP); + closeSilently(zip); + return null; + } + + if (zipCheck.hasXposedProp()) { + Set missingFeatures = zipCheck.getXposedProp().getMissingInstallerFeatures(); + if (!missingFeatures.isEmpty()) { + reportMissingFeatures(missingFeatures); + triggerError(callback, FlashCallback.ERROR_INSTALLER_NEEDS_UPDATE); + closeSilently(zip); + return null; + } + } + + return zipCheck; + } + + public FrameworkZips.Type getType() { + return mType; + } + + public String getTitle() { + return mTitle; + } + + public RootUtil.RebootMode getRebootMode() { + return RootUtil.RebootMode.NORMAL; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeSerializable(mZipPath); + dest.writeSerializable(mType); + dest.writeString(mTitle); + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/installation/InstallationActivity.java b/app/src/main/java/de/robv/android/xposed/installer/installation/InstallationActivity.java new file mode 100644 index 000000000..b64e0e90e --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/installation/InstallationActivity.java @@ -0,0 +1,382 @@ +package de.robv.android.xposed.installer.installation; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.app.Fragment; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.text.Spannable; +import android.text.style.ForegroundColorSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.LinearInterpolator; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import java.io.File; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.XposedBaseActivity; +import de.robv.android.xposed.installer.util.RootUtil; + +public class InstallationActivity extends XposedBaseActivity { + private static final int REBOOT_COUNTDOWN = 15000; + + private static final int MEDIUM_ANIM_TIME = XposedApp.getInstance().getResources() + .getInteger(android.R.integer.config_mediumAnimTime); + private static final int LONG_ANIM_TIME = XposedApp.getInstance().getResources() + .getInteger(android.R.integer.config_longAnimTime); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Flashable flashable = getIntent().getParcelableExtra(Flashable.KEY); + if (flashable == null) { + Log.e(XposedApp.TAG, InstallationActivity.class.getName() + ": Flashable is missing"); + finish(); + return; + } + + setContentView(R.layout.activity_container); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setTitle(flashable.getType().title); + ab.setSubtitle(flashable.getTitle()); + ab.setDisplayHomeAsUpEnabled(true); + } + + setFloating(toolbar, flashable.getType().title); + + if (savedInstanceState == null) { + InstallationFragment logFragment = new InstallationFragment(); + getFragmentManager().beginTransaction().replace(R.id.container, logFragment).commit(); + logFragment.startInstallation(this, flashable); + } + } + + public static class InstallationFragment extends Fragment implements FlashCallback { + private Flashable mFlashable; + private static final int TYPE_NONE = 0; + private static final int TYPE_ERROR = -1; + private static final int TYPE_OK = 1; + private TextView mLogText; + private ProgressBar mProgress; + private ImageView mConsoleResult; + private Button mBtnReboot; + private Button mBtnCancel; + + public void startInstallation(final Context context, final Flashable flashable) { + mFlashable = flashable; + new Thread("FlashZip") { + @Override + public void run() { + mFlashable.flash(context, InstallationFragment.this); + } + }.start(); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.activity_installation, container, false); + + mLogText = (TextView) view.findViewById(R.id.console); + mProgress = (ProgressBar) view.findViewById(R.id.progressBar); + mConsoleResult = (ImageView) view.findViewById(R.id.console_result); + mBtnReboot = (Button) view.findViewById(R.id.reboot); + mBtnCancel = (Button) view.findViewById(R.id.cancel); + + return view; + } + + @Override + public void onStarted() { + try { + Thread.sleep(LONG_ANIM_TIME * 3); + } catch (InterruptedException ignored) { + } + } + + @Override + public void onLine(final String line) { + try { + Thread.sleep(60); + } catch (InterruptedException ignored) { + } + XposedApp.postOnUiThread(new Runnable() { + @Override + public void run() { + appendText(line, TYPE_NONE); + } + }); + } + + @Override + public void onErrorLine(final String line) { + try { + Thread.sleep(60); + } catch (InterruptedException ignored) { + } + XposedApp.postOnUiThread(new Runnable() { + @Override + public void run() { + appendText(line, TYPE_ERROR); + } + }); + } + + private static ValueAnimator createExpandCollapseAnimator(final View view, final boolean expand) { + ValueAnimator animator = new ValueAnimator() { + @Override + public void start() { + view.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + int height = view.getMeasuredHeight(); + + int start = 0, end = 0; + if (expand) { + start = -height; + } else { + end = -height; + } + + setIntValues(start, end); + + super.start(); + } + }; + + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + final ViewGroup.MarginLayoutParams layoutParams = ((ViewGroup.MarginLayoutParams) view.getLayoutParams()); + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + layoutParams.bottomMargin = (Integer) animation.getAnimatedValue(); + view.requestLayout(); + } + }); + + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + view.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!expand) { + view.setVisibility(View.GONE); + } + } + }); + + return animator; + } + + @Override + public void onDone() { + XposedApp.getInstance().reloadXposedProp(); + try { + Thread.sleep(LONG_ANIM_TIME); + } catch (InterruptedException ignored) { + } + XposedApp.postOnUiThread(new Runnable() { + @Override + public void run() { + appendText("\n" + getString(R.string.file_done), TYPE_OK); + + // Fade in the result image. + mConsoleResult.setImageResource(R.drawable.ic_check_circle); + mConsoleResult.setVisibility(View.VISIBLE); + ObjectAnimator fadeInResult = ObjectAnimator.ofFloat(mConsoleResult, "alpha", 0.0f, 0.03f); + fadeInResult.setDuration(MEDIUM_ANIM_TIME * 2); + + // Collapse the whole bottom bar. + View buttomBar = getView().findViewById(R.id.buttonPanel); + Animator collapseBottomBar = createExpandCollapseAnimator(buttomBar, false); + collapseBottomBar.setDuration(MEDIUM_ANIM_TIME); + collapseBottomBar.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mProgress.setIndeterminate(false); + mProgress.setRotation(180); + mProgress.setMax(REBOOT_COUNTDOWN); + mProgress.setProgress(REBOOT_COUNTDOWN); + + mBtnReboot.setVisibility(View.VISIBLE); + mBtnCancel.setVisibility(View.VISIBLE); + } + }); + + Animator expandBottomBar = createExpandCollapseAnimator(buttomBar, true); + expandBottomBar.setDuration(MEDIUM_ANIM_TIME * 2); + expandBottomBar.setStartDelay(LONG_ANIM_TIME * 4); + + final ObjectAnimator countdownProgress = ObjectAnimator.ofInt(mProgress, "progress", REBOOT_COUNTDOWN, 0); + countdownProgress.setDuration(REBOOT_COUNTDOWN); + countdownProgress.setInterpolator(new LinearInterpolator()); + + final ValueAnimator countdownButton = ValueAnimator.ofInt(REBOOT_COUNTDOWN / 1000, 0); + countdownButton.setDuration(REBOOT_COUNTDOWN); + countdownButton.setInterpolator(new LinearInterpolator()); + + final String format = getString(R.string.countdown); + final RootUtil.RebootMode rebootMode = mFlashable.getRebootMode(); + final String action = getString(rebootMode.titleRes); + mBtnReboot.setText(String.format(format, action, REBOOT_COUNTDOWN / 1000)); + + countdownButton.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + private int minWidth = 0; + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mBtnReboot.setText(String.format(format, action, animation.getAnimatedValue())); + + // Make sure that the button width doesn't shrink. + if (mBtnReboot.getWidth() > minWidth) { + minWidth = mBtnReboot.getWidth(); + mBtnReboot.setMinimumWidth(minWidth); + } + } + }); + + countdownButton.addListener(new AnimatorListenerAdapter() { + private boolean canceled = false; + + @Override + public void onAnimationCancel(Animator animation) { + canceled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!canceled) { + mBtnReboot.callOnClick(); + } + } + }); + + mBtnReboot.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + countdownProgress.cancel(); + countdownButton.cancel(); + + RootUtil rootUtil = new RootUtil(); + if (!rootUtil.startShell(InstallationFragment.this) + || !rootUtil.reboot(rebootMode, InstallationFragment.this)) { + onError(FlashCallback.ERROR_GENERIC, getString(R.string.reboot_failed)); + } + } + }); + + mBtnCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + countdownProgress.cancel(); + countdownButton.cancel(); + + getActivity().finish(); + } + }); + + AnimatorSet as = new AnimatorSet(); + as.play(fadeInResult); + as.play(collapseBottomBar).with(fadeInResult); + as.play(expandBottomBar).after(collapseBottomBar); + as.play(countdownProgress).after(expandBottomBar); + as.play(countdownButton).after(expandBottomBar); + as.start(); + } + }); + } + + @Override + public void onError(final int exitCode, final String error) { + XposedApp.postOnUiThread(new Runnable() { + @Override + public void run() { + appendText(error, TYPE_ERROR); + + mConsoleResult.setImageResource(R.drawable.ic_error); + mConsoleResult.setVisibility(View.VISIBLE); + ObjectAnimator fadeInResult = ObjectAnimator.ofFloat(mConsoleResult, "alpha", 0.0f, 0.03f); + fadeInResult.setDuration(MEDIUM_ANIM_TIME * 2); + + View buttomBar = getView().findViewById(R.id.buttonPanel); + Animator collapseBottomBar = createExpandCollapseAnimator(buttomBar, false); + collapseBottomBar.setDuration(MEDIUM_ANIM_TIME); + + AnimatorSet as = new AnimatorSet(); + as.play(fadeInResult); + as.play(collapseBottomBar).with(fadeInResult); + as.start(); + } + }); + } + + @SuppressLint("SetTextI18n") + private void appendText(String text, int type) { + int color; + switch (type) { + case TYPE_ERROR: + color = ContextCompat.getColor(getActivity(), R.color.red_500); + break; + case TYPE_OK: + color = ContextCompat.getColor(getActivity(), R.color.darker_green); + break; + default: + mLogText.append(text); + mLogText.append("\n"); + return; + } + + int start = mLogText.length(); + mLogText.append(text); + int end = mLogText.length(); + ((Spannable) mLogText.getText()).setSpan(new ForegroundColorSpan(color), start, end, 0); + mLogText.append("\n"); + } + + private boolean isOkSystemless() { + boolean suPartition = new File("/su").exists() && new File("/data/su.img").exists(); + boolean m = Build.VERSION.SDK_INT >= 23; + + /* + TODO: Add toggle for user to force system installation + */ + + return m && suPartition; + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/installation/StatusInstallerFragment.java b/app/src/main/java/de/robv/android/xposed/installer/installation/StatusInstallerFragment.java new file mode 100644 index 000000000..29383f304 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/installation/StatusInstallerFragment.java @@ -0,0 +1,626 @@ +package de.robv.android.xposed.installer.installation; + +import android.annotation.SuppressLint; +import android.app.Fragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.design.widget.Snackbar; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.SwitchCompat; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Set; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.util.DownloadsUtil; +import de.robv.android.xposed.installer.util.DownloadsUtil.DownloadFinishedCallback; +import de.robv.android.xposed.installer.util.DownloadsUtil.DownloadInfo; +import de.robv.android.xposed.installer.util.FrameworkZips; +import de.robv.android.xposed.installer.util.FrameworkZips.FrameworkZip; +import de.robv.android.xposed.installer.util.FrameworkZips.LocalFrameworkZip; +import de.robv.android.xposed.installer.util.FrameworkZips.LocalZipLoader; +import de.robv.android.xposed.installer.util.FrameworkZips.OnlineFrameworkZip; +import de.robv.android.xposed.installer.util.FrameworkZips.OnlineZipLoader; +import de.robv.android.xposed.installer.util.InstallZipUtil; +import de.robv.android.xposed.installer.util.Loader; +import de.robv.android.xposed.installer.util.NavUtil; +import de.robv.android.xposed.installer.util.RootUtil; +import de.robv.android.xposed.installer.util.RunnableWithParam; + +public class StatusInstallerFragment extends Fragment { + public static final File DISABLE_FILE = new File(XposedApp.BASE_DIR + "conf/disabled"); + private boolean mShowOutdated = false; + + private static boolean checkClassExists(String className) { + try { + Class.forName(className); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.status_installer, container, false); + + // Available ZIPs + final SwipeRefreshLayout refreshLayout = (SwipeRefreshLayout) v.findViewById(R.id.swiperefreshlayout); + refreshLayout.setColorSchemeColors(getResources().getColor(R.color.colorPrimary)); + + ONLINE_ZIP_LOADER.setSwipeRefreshLayout(refreshLayout); + ONLINE_ZIP_LOADER.addListener(mOnlineZipListener); + ONLINE_ZIP_LOADER.triggerFirstLoadIfNecessary(); + + LOCAL_ZIP_LOADER.addListener(mLocalZipListener); + LOCAL_ZIP_LOADER.triggerFirstLoadIfNecessary(); + + refreshZipViews(v); + + // Disable switch + final SwitchCompat disableSwitch = (SwitchCompat) v.findViewById(R.id.disableSwitch); + disableSwitch.setChecked(!DISABLE_FILE.exists()); + disableSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (DISABLE_FILE.exists()) { + DISABLE_FILE.delete(); + Snackbar.make(disableSwitch, R.string.xposed_on_next_reboot, Snackbar.LENGTH_LONG).show(); + } else { + try { + DISABLE_FILE.createNewFile(); + Snackbar.make(disableSwitch, R.string.xposed_off_next_reboot, Snackbar.LENGTH_LONG).show(); + } catch (IOException e) { + Log.e(XposedApp.TAG, "Could not create " + DISABLE_FILE, e); + } + } + } + }); + + // Device info + TextView androidSdk = (TextView) v.findViewById(R.id.android_version); + TextView manufacturer = (TextView) v.findViewById(R.id.ic_manufacturer); + TextView cpu = (TextView) v.findViewById(R.id.cpu); + + androidSdk.setText(getString(R.string.android_sdk, Build.VERSION.RELEASE, getAndroidVersion(), Build.VERSION.SDK_INT)); + manufacturer.setText(getUIFramework()); + cpu.setText(FrameworkZips.ARCH); + + determineVerifiedBootState(v); + + // Known issues + refreshKnownIssue(v); + + // Display warning dialog to new users + if (!XposedApp.getPreferences().getBoolean("hide_install_warning", false)) { + new MaterialDialog.Builder(getActivity()) + .title(R.string.install_warning_title) + .content(R.string.install_warning) + .positiveText(android.R.string.ok) + .onPositive(new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + if (dialog.isPromptCheckBoxChecked()) { + XposedApp.getPreferences().edit().putBoolean("hide_install_warning", true).apply(); + } + } + }) + .checkBoxPromptRes(R.string.dont_show_again, false, null) + .cancelable(false) + .show(); + } + + return v; + } + + @Override + public void onResume() { + super.onResume(); + refreshInstallStatus(); + } + + private void refreshInstallStatus() { + View v = getView(); + TextView txtInstallError = (TextView) v.findViewById(R.id.framework_install_errors); + View txtInstallContainer = v.findViewById(R.id.status_container); + ImageView txtInstallIcon = (ImageView) v.findViewById(R.id.status_icon); + View disableWrapper = v.findViewById(R.id.disableView); + + // TODO This should probably compare the full version string, not just the number part. + int active = XposedApp.getActiveXposedVersion(); + int installed = XposedApp.getInstalledXposedVersion(); + if (installed < 0) { + txtInstallError.setText(R.string.framework_not_installed); + txtInstallError.setTextColor(getResources().getColor(R.color.warning)); + txtInstallContainer.setBackgroundColor(getResources().getColor(R.color.warning)); + txtInstallIcon.setImageDrawable(getResources().getDrawable(R.drawable.ic_error)); + disableWrapper.setVisibility(View.GONE); + } else if (installed != active) { + txtInstallError.setText(getString(R.string.framework_not_active, XposedApp.getXposedProp().getVersion())); + txtInstallError.setTextColor(getResources().getColor(R.color.amber_500)); + txtInstallContainer.setBackgroundColor(getResources().getColor(R.color.amber_500)); + txtInstallIcon.setImageDrawable(getResources().getDrawable(R.drawable.ic_warning)); + } else { + txtInstallError.setText(getString(R.string.framework_active, XposedApp.getXposedProp().getVersion())); + txtInstallError.setTextColor(getResources().getColor(R.color.darker_green)); + txtInstallContainer.setBackgroundColor(getResources().getColor(R.color.darker_green)); + txtInstallIcon.setImageDrawable(getResources().getDrawable(R.drawable.ic_check_circle)); + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + ONLINE_ZIP_LOADER.removeListener(mOnlineZipListener); + ONLINE_ZIP_LOADER.setSwipeRefreshLayout(null); + LOCAL_ZIP_LOADER.removeListener(mLocalZipListener); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mShowOutdated = XposedApp.getPreferences().getBoolean("framework_download_show_outdated", false); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_installer, menu); + menu.findItem(R.id.show_outdated).setChecked(mShowOutdated); + if (Build.VERSION.SDK_INT < 26) { + menu.findItem(R.id.dexopt_now).setVisible(false); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.reboot: + case R.id.soft_reboot: + case R.id.reboot_recovery: + final RootUtil.RebootMode mode = RootUtil.RebootMode.fromId(item.getItemId()); + confirmReboot(mode.titleRes, new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + RootUtil.reboot(mode, getActivity()); + } + }); + return true; + + case R.id.dexopt_now: + new MaterialDialog.Builder(getActivity()) + .title(R.string.dexopt_now) + .content(R.string.this_may_take_a_while) + .progress(true, 0) + .cancelable(false) + .showListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(final DialogInterface dialog) { + new Thread("dexopt") { + @Override + public void run() { + RootUtil rootUtil = new RootUtil(); + if (!rootUtil.startShell()) { + dialog.dismiss(); + NavUtil.showMessage(getActivity(), getString(R.string.root_failed)); + return; + } + + rootUtil.execute("cmd package bg-dexopt-job", null); + + dialog.dismiss(); + XposedApp.runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getActivity(), R.string.done, Toast.LENGTH_LONG).show(); + } + }); + } + }.start(); + } + }).show(); + return true; + + case R.id.show_outdated: + mShowOutdated = !item.isChecked(); + XposedApp.getPreferences().edit().putBoolean("framework_download_show_outdated", mShowOutdated).apply(); + item.setChecked(mShowOutdated); + refreshZipViews(getView()); + return true; + } + + return super.onOptionsItemSelected(item); + } + + private void confirmReboot(int contentTextId, MaterialDialog.SingleButtonCallback yesHandler) { + new MaterialDialog.Builder(getActivity()) + .content(R.string.reboot_confirmation) + .positiveText(contentTextId) + .negativeText(android.R.string.no) + .onPositive(yesHandler) + .show(); + } + + private File getCanonicalFile(File file) { + try { + return file.getCanonicalFile(); + } catch (IOException e) { + Log.e(XposedApp.TAG, "Failed to get canonical file for " + file.getAbsolutePath(), e); + return file; + } + } + + private String getPathWithCanonicalPath(File file, File canonical) { + if (file.equals(canonical)) { + return file.getAbsolutePath(); + } else { + return file.getAbsolutePath() + " \u2192 " + canonical.getAbsolutePath(); + } + } + + @SuppressLint("StringFormatInvalid") + private void refreshKnownIssue(View v) { + final String issueName; + final String issueLink; + final ApplicationInfo appInfo = getActivity().getApplicationInfo(); + final File baseDir = new File(XposedApp.BASE_DIR); + final File baseDirCanonical = getCanonicalFile(baseDir); + final File baseDirActual = new File(Build.VERSION.SDK_INT >= 24 ? appInfo.deviceProtectedDataDir : appInfo.dataDir); + final File baseDirActualCanonical = getCanonicalFile(baseDirActual); + final InstallZipUtil.XposedProp prop = XposedApp.getXposedProp(); + final Set missingFeatures = prop != null ? prop.getMissingInstallerFeatures() : null; + + if (missingFeatures != null && !missingFeatures.isEmpty()) { + InstallZipUtil.reportMissingFeatures(missingFeatures); + issueName = getString(R.string.installer_needs_update, getString(R.string.app_name)); + issueLink = getString(R.string.about_support); + } else if (new File("/system/framework/core.jar.jex").exists()) { + issueName = "Aliyun OS"; + issueLink = "https://forum.xda-developers.com/showpost.php?p=52289793&postcount=5"; + } else if (Build.VERSION.SDK_INT < 24 && (new File("/data/miui/DexspyInstaller.jar").exists() || checkClassExists("miui.dexspy.DexspyInstaller"))) { + issueName = "MIUI/Dexspy"; + issueLink = "https://forum.xda-developers.com/showpost.php?p=52291098&postcount=6"; + } else if (Build.VERSION.SDK_INT < 24 && new File("/system/framework/twframework.jar").exists()) { + issueName = "Samsung TouchWiz ROM"; + issueLink = "https://forum.xda-developers.com/showthread.php?t=3034811"; + } else if (!baseDirCanonical.equals(baseDirActualCanonical)) { + Log.e(XposedApp.TAG, "Base directory: " + getPathWithCanonicalPath(baseDir, baseDirCanonical)); + Log.e(XposedApp.TAG, "Expected: " + getPathWithCanonicalPath(baseDirActual, baseDirActualCanonical)); + issueName = getString(R.string.known_issue_wrong_base_directory, getPathWithCanonicalPath(baseDirActual, baseDirActualCanonical)); + issueLink = "https://github.com/rovo89/XposedInstaller/issues/395"; + } else if (!baseDir.exists()) { + issueName = getString(R.string.known_issue_missing_base_directory); + issueLink = "https://github.com/rovo89/XposedInstaller/issues/393"; + } else { + issueName = null; + issueLink = null; + } + + TextView txtKnownIssue = (TextView) v.findViewById(R.id.framework_known_issue); + if (issueName != null) { + txtKnownIssue.setText(getString(R.string.install_known_issue, issueName)); + txtKnownIssue.setVisibility(View.VISIBLE); + txtKnownIssue.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + NavUtil.startURL(getActivity(), issueLink); + } + }); + } else { + txtKnownIssue.setVisibility(View.GONE); + } + } + + private String getAndroidVersion() { + switch (Build.VERSION.SDK_INT) { + case 15: + return "Ice Cream Sandwich"; + case 16: + case 17: + case 18: + return "Jelly Bean"; + case 19: + return "KitKat"; + case 21: + case 22: + return "Lollipop"; + case 23: + return "Marshmallow"; + case 24: + case 25: + return "Nougat"; + case 26: + case 27: + return "Oreo"; + default: + return "unknown"; + } + } + + private String getUIFramework() { + String manufacturer = Character.toUpperCase(Build.MANUFACTURER.charAt(0)) + Build.MANUFACTURER.substring(1); + if (!Build.BRAND.equals(Build.MANUFACTURER)) { + manufacturer += " " + Character.toUpperCase(Build.BRAND.charAt(0)) + Build.BRAND.substring(1); + } + manufacturer += " " + Build.MODEL + " "; + if (manufacturer.contains("Samsung")) { + manufacturer += new File("/system/framework/twframework.jar").exists() ? "(TouchWiz)" : "(AOSP-based ROM)"; + } else if (manufacturer.contains("Xioami")) { + manufacturer += new File("/system/framework/framework-miui-res.apk").exists() ? "(MIUI)" : "(AOSP-based ROM)"; + } + return manufacturer; + } + + private void determineVerifiedBootState(View v) { + try { + Class c = Class.forName("android.os.SystemProperties"); + Method m = c.getDeclaredMethod("get", String.class, String.class); + m.setAccessible(true); + + String propSystemVerified = (String) m.invoke(null, "partition.system.verified", "0"); + String propState = (String) m.invoke(null, "ro.boot.verifiedbootstate", ""); + File fileDmVerityModule = new File("/sys/module/dm_verity"); + + boolean verified = !propSystemVerified.equals("0"); + boolean detected = !propState.isEmpty() || fileDmVerityModule.exists(); + + TextView tv = v.findViewById(R.id.dmverity); + if (verified) { + tv.setText(R.string.verified_boot_active); + tv.setTextColor(getResources().getColor(R.color.warning)); + } else if (detected) { + tv.setText(R.string.verified_boot_deactivated); + v.findViewById(R.id.dmverity_explanation).setVisibility(View.GONE); + } else { + v.findViewById(R.id.dmverity_row).setVisibility(View.GONE); + } + } catch (Exception e) { + Log.e(XposedApp.TAG, "Could not detect Verified Boot state", e); + } + } + + @UiThread + private void refreshZipViews(View view) { + LinearLayout zips = (LinearLayout) view.findViewById(R.id.zips); + zips.removeAllViews(); + TextView tvError = (TextView) view.findViewById(R.id.zips_load_error); + synchronized (FrameworkZips.class) { + boolean hasZips = false; + for (FrameworkZips.Type type : FrameworkZips.Type.values()) { + hasZips |= addZipViews(getActivity().getLayoutInflater(), zips, type); + } + + if (!FrameworkZips.hasLoadedOnlineZips()) { + tvError.setText(R.string.framework_zip_load_failed); + tvError.setVisibility(View.VISIBLE); + } else if (!hasZips) { + tvError.setText(R.string.framework_no_zips); + tvError.setVisibility(View.VISIBLE); + } else { + tvError.setVisibility(View.GONE); + } + } + } + + private boolean addZipViews(LayoutInflater inflater, ViewGroup root, FrameworkZips.Type type) { + ViewGroup container = null; + Set allTitles = FrameworkZips.getAllTitles(type); + for (String title : allTitles) { + OnlineFrameworkZip online = FrameworkZips.getOnline(title, type); + LocalFrameworkZip local = FrameworkZips.getLocal(title, type); + + boolean hasOnline = (online != null); + boolean hasLocal = (local != null); + FrameworkZip zip = hasOnline ? online : local; + boolean isOutdated = zip.isOutdated(); + + if (isOutdated && !mShowOutdated) { + continue; + } + + if (container == null) { + View card = inflater.inflate(R.layout.framework_zip_group, root, false); + TextView tv = (TextView) card.findViewById(android.R.id.title); + tv.setText(type.title); + container = (ViewGroup) card.findViewById(android.R.id.content); + root.addView(card); + } + + addZipView(inflater, container, zip, hasOnline, hasLocal, isOutdated); + } + + return !allTitles.isEmpty(); + } + + public void addZipView(LayoutInflater inflater, ViewGroup container, final FrameworkZip zip, + boolean hasOnline, boolean hasLocal, boolean isOutdated) { + View view = inflater.inflate(R.layout.framework_zip_item, container, false); + + TextView tvTitle = (TextView) view.findViewById(android.R.id.title); + tvTitle.setText(zip.title); + + ImageView ivStatus = (ImageView) view.findViewById(R.id.framework_zip_status); + if (!hasLocal) { + ivStatus.setImageResource(R.drawable.ic_cloud); + } else if (hasOnline) { + ivStatus.setImageResource(R.drawable.ic_cloud_download); + } else { + ivStatus.setImageResource(R.drawable.ic_cloud_off); + } + + if (isOutdated) { + int gray = Color.parseColor("#A0A0A0"); + tvTitle.setTextColor(gray); + ivStatus.setColorFilter(gray); + } + + view.setClickable(true); + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showActionDialog(getActivity(), zip.title, zip.type); + } + }); + container.addView(view); + } + + private void showActionDialog(final Context context, final String title, final FrameworkZips.Type type) { + final int ACTION_FLASH = 0; + final int ACTION_FLASH_RECOVERY = 1; + final int ACTION_SAVE = 2; + final int ACTION_DELETE = 3; + + boolean isDownloaded = FrameworkZips.hasLocal(title, type); + int itemCount = isDownloaded ? 3 : 2; + String[] texts = new String[itemCount]; + int[] ids = new int[itemCount]; + int i = 0; + + texts[i] = context.getString(type.text_flash); + ids[i++] = ACTION_FLASH; + + texts[i] = context.getString(type.text_flash_recovery); + ids[i++] = ACTION_FLASH_RECOVERY; + + /* + texts[i] = "Save to..."; + ids[i++] = ACTION_SAVE; + */ + + if (FrameworkZips.hasLocal(title, type)) { + texts[i] = context.getString(R.string.framework_delete); + ids[i++] = ACTION_DELETE; + } + + new MaterialDialog.Builder(context) + .title(title) + .items(texts) + .itemsIds(ids) + .itemsCallback(new MaterialDialog.ListCallback() { + @Override + public void onSelection(MaterialDialog dialog, View itemView, int position, CharSequence text) { + final int action = itemView.getId(); + + // Handle delete simple actions. + if (action == ACTION_DELETE) { + FrameworkZips.delete(context, title, type); + LOCAL_ZIP_LOADER.triggerReload(true); + return; + } + + // Handle actions that need a download first. + RunnableWithParam runAfterDownload = null; + if (action == ACTION_FLASH) { + runAfterDownload = new RunnableWithParam() { + @Override + public void run(File file) { + flash(context, new FlashDirectly(file, type, title, false)); + } + }; + } else if (action == ACTION_FLASH_RECOVERY) { + runAfterDownload = new RunnableWithParam() { + @Override + public void run(File file) { + flash(context, new FlashRecoveryAuto(file, type, title)); + } + }; + } else if (action == ACTION_SAVE) { + runAfterDownload = new RunnableWithParam() { + @Override + public void run(File file) { + saveTo(context, file); + } + }; + } + + LocalFrameworkZip local = FrameworkZips.getLocal(title, type); + if (local != null) { + runAfterDownload.run(local.path); + } else { + download(context, title, type, runAfterDownload); + } + } + }) + .show(); + } + + private void download(Context context, String title, FrameworkZips.Type type, final RunnableWithParam callback) { + OnlineFrameworkZip zip = FrameworkZips.getOnline(title, type); + new DownloadsUtil.Builder(context) + .setTitle(zip.title) + .setUrl(zip.url) + .setDestinationFromUrl(DownloadsUtil.DOWNLOAD_FRAMEWORK) + .setCallback(new DownloadFinishedCallback() { + @Override + public void onDownloadFinished(Context context, DownloadInfo info) { + LOCAL_ZIP_LOADER.triggerReload(true); + callback.run(new File(info.localFilename)); + } + }) + .setMimeType(DownloadsUtil.MIME_TYPES.ZIP) + .setDialog(true) + .download(); + } + + private static void flash(Context context, Flashable flashable) { + Intent install = new Intent(context, InstallationActivity.class); + install.putExtra(Flashable.KEY, flashable); + context.startActivity(install); + } + + private static void saveTo(Context context, File file) { + Toast.makeText(context, "Not implemented yet", Toast.LENGTH_SHORT).show(); + } + + private void refreshZipViewsOnUiThread() { + XposedApp.runOnUiThread(new Runnable() { + @Override + public void run() { + refreshZipViews(getView()); + } + }); + } + + private static final OnlineZipLoader ONLINE_ZIP_LOADER = OnlineZipLoader.getInstance(); + private final Loader.Listener mOnlineZipListener = new Loader.Listener() { + @Override + public void onReloadDone(OnlineZipLoader loader) { + refreshZipViewsOnUiThread(); + } + }; + + private static final LocalZipLoader LOCAL_ZIP_LOADER = LocalZipLoader.getInstance(); + private final Loader.Listener mLocalZipListener = new Loader.Listener() { + @Override + public void onReloadDone(LocalZipLoader loader) { + refreshZipViewsOnUiThread(); + } + }; +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/receivers/DownloadReceiver.java b/app/src/main/java/de/robv/android/xposed/installer/receivers/DownloadReceiver.java new file mode 100644 index 000000000..ee2ed9a35 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/receivers/DownloadReceiver.java @@ -0,0 +1,19 @@ +package de.robv.android.xposed.installer.receivers; + +import android.app.DownloadManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import de.robv.android.xposed.installer.util.DownloadsUtil; + +public class DownloadReceiver extends BroadcastReceiver { + @Override + public void onReceive(final Context context, final Intent intent) { + String action = intent.getAction(); + if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) { + long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); + DownloadsUtil.triggerDownloadFinishedCallback(context, downloadId); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/receivers/PackageChangeReceiver.java b/app/src/main/java/de/robv/android/xposed/installer/receivers/PackageChangeReceiver.java new file mode 100644 index 000000000..c98c73be5 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/receivers/PackageChangeReceiver.java @@ -0,0 +1,66 @@ +package de.robv.android.xposed.installer.receivers; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import de.robv.android.xposed.installer.util.ModuleUtil; +import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; +import de.robv.android.xposed.installer.util.NotificationUtil; + +public class PackageChangeReceiver extends BroadcastReceiver { + private final static ModuleUtil mModuleUtil = ModuleUtil.getInstance(); + + private static String getPackageName(Intent intent) { + Uri uri = intent.getData(); + return (uri != null) ? uri.getSchemeSpecificPart() : null; + } + + @Override + public void onReceive(final Context context, final Intent intent) { + if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED) && intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) + // Ignore existing packages being removed in order to be updated + return; + + String packageName = getPackageName(intent); + if (packageName == null) + return; + + if (intent.getAction().equals(Intent.ACTION_PACKAGE_CHANGED)) { + // make sure that the change is for the complete package, not only a + // component + String[] components = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST); + if (components != null) { + boolean isForPackage = false; + for (String component : components) { + if (packageName.equals(component)) { + isForPackage = true; + break; + } + } + if (!isForPackage) + return; + } + } + + InstalledModule module = ModuleUtil.getInstance().reloadSingleModule(packageName); + if (module == null + || intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)) { + // Package being removed, disable it if it was a previously active + // Xposed mod + if (mModuleUtil.isModuleEnabled(packageName)) { + mModuleUtil.setModuleEnabled(packageName, false); + mModuleUtil.updateModulesList(false); + } + return; + } + + if (mModuleUtil.isModuleEnabled(packageName)) { + mModuleUtil.updateModulesList(false); + NotificationUtil.showModulesUpdatedNotification(); + } else { + NotificationUtil.showNotActivatedNotification(packageName, module.getAppName()); + } + } +} diff --git a/src/de/robv/android/xposed/installer/repo/Module.java b/app/src/main/java/de/robv/android/xposed/installer/repo/Module.java similarity index 90% rename from src/de/robv/android/xposed/installer/repo/Module.java rename to app/src/main/java/de/robv/android/xposed/installer/repo/Module.java index 8d416a4b3..d1f5b7185 100644 --- a/src/de/robv/android/xposed/installer/repo/Module.java +++ b/app/src/main/java/de/robv/android/xposed/installer/repo/Module.java @@ -1,13 +1,16 @@ package de.robv.android.xposed.installer.repo; +import android.util.Pair; + import java.util.ArrayList; import java.util.LinkedList; import java.util.List; -import android.util.Pair; - public class Module { public final Repository repository; + public final List> moreInfo = new LinkedList>(); + public final List versions = new ArrayList(); + public final List screenshots = new ArrayList(); public String packageName; public String name; public String summary; @@ -15,13 +18,10 @@ public class Module { public boolean descriptionIsHtml = false; public String author; public String support; - public final List> moreInfo = new LinkedList>(); - public final List versions = new ArrayList(); - public final List screenshots = new ArrayList(); public long created = -1; public long updated = -1; - /*package*/ Module(Repository repository) { + /* package */ Module(Repository repository) { this.repository = repository; } } diff --git a/src/de/robv/android/xposed/installer/repo/ModuleVersion.java b/app/src/main/java/de/robv/android/xposed/installer/repo/ModuleVersion.java similarity index 88% rename from src/de/robv/android/xposed/installer/repo/ModuleVersion.java rename to app/src/main/java/de/robv/android/xposed/installer/repo/ModuleVersion.java index 082fbb21a..96f67c11b 100644 --- a/src/de/robv/android/xposed/installer/repo/ModuleVersion.java +++ b/app/src/main/java/de/robv/android/xposed/installer/repo/ModuleVersion.java @@ -1,6 +1,5 @@ package de.robv.android.xposed.installer.repo; - public class ModuleVersion { public final Module module; public String name; @@ -12,7 +11,7 @@ public class ModuleVersion { public ReleaseType relType = ReleaseType.STABLE; public long uploaded = -1; - /*package*/ ModuleVersion(Module module) { + /* package */ ModuleVersion(Module module) { this.module = module; } } diff --git a/app/src/main/java/de/robv/android/xposed/installer/repo/ReleaseType.java b/app/src/main/java/de/robv/android/xposed/installer/repo/ReleaseType.java new file mode 100644 index 000000000..3c18c2c97 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/repo/ReleaseType.java @@ -0,0 +1,39 @@ +package de.robv.android.xposed.installer.repo; + +import de.robv.android.xposed.installer.R; + +public enum ReleaseType { + STABLE(R.string.reltype_stable), + BETA(R.string.reltype_beta), + EXPERIMENTAL(R.string.reltype_experimental); + + private static final ReleaseType[] sValuesCache = values(); + private final int mTitleId; + + ReleaseType(int titleId) { + mTitleId = titleId; + } + + public static ReleaseType fromString(String value) { + if (value == null) { + return STABLE; + } + switch (value) { + case "stable": + default: + return STABLE; + case "beta": + return BETA; + case "experimental": + return EXPERIMENTAL; + } + } + + public static ReleaseType fromOrdinal(int ordinal) { + return sValuesCache[ordinal]; + } + + public int getTitleId() { + return mTitleId; + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/repo/RepoDb.java b/app/src/main/java/de/robv/android/xposed/installer/repo/RepoDb.java new file mode 100644 index 000000000..55d5cd16b --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/repo/RepoDb.java @@ -0,0 +1,480 @@ +package de.robv.android.xposed.installer.repo; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.os.Build; +import android.text.TextUtils; +import android.util.Pair; + +import java.io.File; +import java.util.LinkedHashMap; +import java.util.Map; + +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.repo.RepoDbDefinitions.InstalledModulesColumns; +import de.robv.android.xposed.installer.repo.RepoDbDefinitions.InstalledModulesUpdatesColumns; +import de.robv.android.xposed.installer.repo.RepoDbDefinitions.ModuleVersionsColumns; +import de.robv.android.xposed.installer.repo.RepoDbDefinitions.ModulesColumns; +import de.robv.android.xposed.installer.repo.RepoDbDefinitions.MoreInfoColumns; +import de.robv.android.xposed.installer.repo.RepoDbDefinitions.OverviewColumns; +import de.robv.android.xposed.installer.repo.RepoDbDefinitions.OverviewColumnsIndexes; +import de.robv.android.xposed.installer.repo.RepoDbDefinitions.RepositoriesColumns; +import de.robv.android.xposed.installer.util.ModuleUtil; +import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; +import de.robv.android.xposed.installer.util.RepoLoader; + +public final class RepoDb extends SQLiteOpenHelper { + public static final int SORT_STATUS = 0; + public static final int SORT_UPDATED = 1; + public static final int SORT_CREATED = 2; + + private static SQLiteDatabase sDb; + + private RepoDb(Context context) { + super(context, getDbPath(context), null, RepoDbDefinitions.DATABASE_VERSION); + } + + private static String getDbPath(Context context) { + if (Build.VERSION.SDK_INT >= 21) { + return new File(context.getNoBackupFilesDir(), RepoDbDefinitions.DATABASE_NAME).getPath(); + } else { + return RepoDbDefinitions.DATABASE_NAME; + } + } + + static { + RepoDb instance = new RepoDb(XposedApp.getInstance()); + sDb = instance.getWritableDatabase(); + sDb.execSQL("PRAGMA foreign_keys=ON"); + instance.createTempTables(sDb); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_REPOSITORIES); + db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MODULES); + db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MODULE_VERSIONS); + db.execSQL(RepoDbDefinitions.SQL_CREATE_INDEX_MODULE_VERSIONS_MODULE_ID); + db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MORE_INFO); + + RepoLoader.getInstance().clear(false); + } + + private void createTempTables(SQLiteDatabase db) { + db.execSQL(RepoDbDefinitions.SQL_CREATE_TEMP_TABLE_INSTALLED_MODULES); + db.execSQL(RepoDbDefinitions.SQL_CREATE_TEMP_VIEW_INSTALLED_MODULES_UPDATES); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // This is only a cache, so simply drop & recreate the tables + db.execSQL("DROP TABLE IF EXISTS " + RepositoriesColumns.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + ModulesColumns.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + ModuleVersionsColumns.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + MoreInfoColumns.TABLE_NAME); + + db.execSQL("DROP TABLE IF EXISTS " + InstalledModulesColumns.TABLE_NAME); + db.execSQL("DROP VIEW IF EXISTS " + InstalledModulesUpdatesColumns.VIEW_NAME); + + onCreate(db); + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + onUpgrade(db, oldVersion, newVersion); + } + + public static void beginTransation() { + sDb.beginTransaction(); + } + + public static void setTransactionSuccessful() { + sDb.setTransactionSuccessful(); + } + + public static void endTransation() { + sDb.endTransaction(); + } + + private static String getString(String table, String searchColumn, String searchValue, String resultColumn) { + String[] projection = new String[]{resultColumn}; + String where = searchColumn + " = ?"; + String[] whereArgs = new String[]{searchValue}; + Cursor c = sDb.query(table, projection, where, whereArgs, null, null, null, "1"); + if (c.moveToFirst()) { + String result = c.getString(c.getColumnIndexOrThrow(resultColumn)); + c.close(); + return result; + } else { + c.close(); + throw new RowNotFoundException("Could not find " + table + "." + searchColumn + " with value '" + searchValue + "'"); + } + } + + public static long insertRepository(String url) { + ContentValues values = new ContentValues(); + values.put(RepositoriesColumns.URL, url); + return sDb.insertOrThrow(RepositoriesColumns.TABLE_NAME, null, values); + } + + public static void deleteRepositories() { + if (sDb != null) + sDb.delete(RepositoriesColumns.TABLE_NAME, null, null); + } + + public static Map getRepositories() { + Map result = new LinkedHashMap(1); + + String[] projection = new String[]{ + RepositoriesColumns._ID, + RepositoriesColumns.URL, + RepositoriesColumns.TITLE, + RepositoriesColumns.PARTIAL_URL, + RepositoriesColumns.VERSION, + }; + + Cursor c = sDb.query(RepositoriesColumns.TABLE_NAME, projection, null, null, null, null, RepositoriesColumns._ID); + while (c.moveToNext()) { + Repository repo = new Repository(); + long id = c.getLong(c.getColumnIndexOrThrow(RepositoriesColumns._ID)); + repo.url = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.URL)); + repo.name = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.TITLE)); + repo.partialUrl = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.PARTIAL_URL)); + repo.version = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.VERSION)); + result.put(id, repo); + } + c.close(); + + return result; + } + + public static void updateRepository(long repoId, Repository repository) { + ContentValues values = new ContentValues(); + values.put(RepositoriesColumns.TITLE, repository.name); + values.put(RepositoriesColumns.PARTIAL_URL, repository.partialUrl); + values.put(RepositoriesColumns.VERSION, repository.version); + sDb.update(RepositoriesColumns.TABLE_NAME, values, RepositoriesColumns._ID + " = ?", new String[]{Long.toString(repoId)}); + } + + public static void updateRepositoryVersion(long repoId, String version) { + ContentValues values = new ContentValues(); + values.put(RepositoriesColumns.VERSION, version); + sDb.update(RepositoriesColumns.TABLE_NAME, values, RepositoriesColumns._ID + " = ?", new String[]{Long.toString(repoId)}); + } + + public static long insertModule(long repoId, Module mod) { + ContentValues values = new ContentValues(); + values.put(ModulesColumns.REPO_ID, repoId); + values.put(ModulesColumns.PKGNAME, mod.packageName); + values.put(ModulesColumns.TITLE, mod.name); + values.put(ModulesColumns.SUMMARY, mod.summary); + values.put(ModulesColumns.DESCRIPTION, mod.description); + values.put(ModulesColumns.DESCRIPTION_IS_HTML, mod.descriptionIsHtml); + values.put(ModulesColumns.AUTHOR, mod.author); + values.put(ModulesColumns.SUPPORT, mod.support); + values.put(ModulesColumns.CREATED, mod.created); + values.put(ModulesColumns.UPDATED, mod.updated); + + ModuleVersion latestVersion = RepoLoader.getInstance().getLatestVersion(mod); + + sDb.beginTransaction(); + try { + long moduleId = sDb.insertOrThrow(ModulesColumns.TABLE_NAME, null, values); + + long latestVersionId = -1; + for (ModuleVersion version : mod.versions) { + long versionId = insertModuleVersion(moduleId, version); + if (latestVersion == version) + latestVersionId = versionId; + } + + if (latestVersionId > -1) { + values = new ContentValues(); + values.put(ModulesColumns.LATEST_VERSION, latestVersionId); + sDb.update(ModulesColumns.TABLE_NAME, values, ModulesColumns._ID + " = ?", new String[]{Long.toString(moduleId)}); + } + + for (Pair moreInfoEntry : mod.moreInfo) { + insertMoreInfo(moduleId, moreInfoEntry.first, moreInfoEntry.second); + } + + // TODO Add mod.screenshots + + sDb.setTransactionSuccessful(); + return moduleId; + + } finally { + sDb.endTransaction(); + } + } + + private static long insertModuleVersion(long moduleId, ModuleVersion version) { + ContentValues values = new ContentValues(); + values.put(ModuleVersionsColumns.MODULE_ID, moduleId); + values.put(ModuleVersionsColumns.NAME, version.name); + values.put(ModuleVersionsColumns.CODE, version.code); + values.put(ModuleVersionsColumns.DOWNLOAD_LINK, version.downloadLink); + values.put(ModuleVersionsColumns.MD5SUM, version.md5sum); + values.put(ModuleVersionsColumns.CHANGELOG, version.changelog); + values.put(ModuleVersionsColumns.CHANGELOG_IS_HTML, version.changelogIsHtml); + values.put(ModuleVersionsColumns.RELTYPE, version.relType.ordinal()); + values.put(ModuleVersionsColumns.UPLOADED, version.uploaded); + return sDb.insertOrThrow(ModuleVersionsColumns.TABLE_NAME, null, + values); + } + + private static long insertMoreInfo(long moduleId, String title, String value) { + ContentValues values = new ContentValues(); + values.put(MoreInfoColumns.MODULE_ID, moduleId); + values.put(MoreInfoColumns.LABEL, title); + values.put(MoreInfoColumns.VALUE, value); + return sDb.insertOrThrow(MoreInfoColumns.TABLE_NAME, null, values); + } + + public static void deleteAllModules(long repoId) { + sDb.delete(ModulesColumns.TABLE_NAME, ModulesColumns.REPO_ID + " = ?", new String[]{Long.toString(repoId)}); + } + + public static void deleteModule(long repoId, String packageName) { + sDb.delete(ModulesColumns.TABLE_NAME, ModulesColumns.REPO_ID + " = ? AND " + ModulesColumns.PKGNAME + " = ?", new String[]{Long.toString(repoId), packageName}); + } + + public static Module getModuleByPackageName(String packageName) { + // The module itself + String[] projection = new String[]{ + ModulesColumns._ID, + ModulesColumns.REPO_ID, + ModulesColumns.PKGNAME, + ModulesColumns.TITLE, + ModulesColumns.SUMMARY, + ModulesColumns.DESCRIPTION, + ModulesColumns.DESCRIPTION_IS_HTML, + ModulesColumns.AUTHOR, + ModulesColumns.SUPPORT, + ModulesColumns.CREATED, + ModulesColumns.UPDATED, + }; + + String where = ModulesColumns.PREFERRED + " = 1 AND " + ModulesColumns.PKGNAME + " = ?"; + String[] whereArgs = new String[]{packageName}; + + Cursor c = sDb.query(ModulesColumns.TABLE_NAME, projection, where, whereArgs, null, null, null, "1"); + if (!c.moveToFirst()) { + c.close(); + return null; + } + + long moduleId = c.getLong(c.getColumnIndexOrThrow(ModulesColumns._ID)); + long repoId = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.REPO_ID)); + + Module mod = new Module(RepoLoader.getInstance().getRepository(repoId)); + mod.packageName = c.getString(c.getColumnIndexOrThrow(ModulesColumns.PKGNAME)); + mod.name = c.getString(c.getColumnIndexOrThrow(ModulesColumns.TITLE)); + mod.summary = c.getString(c.getColumnIndexOrThrow(ModulesColumns.SUMMARY)); + mod.description = c.getString(c.getColumnIndexOrThrow(ModulesColumns.DESCRIPTION)); + mod.descriptionIsHtml = c.getInt(c.getColumnIndexOrThrow(ModulesColumns.DESCRIPTION_IS_HTML)) > 0; + mod.author = c.getString(c.getColumnIndexOrThrow(ModulesColumns.AUTHOR)); + mod.support = c.getString(c.getColumnIndexOrThrow(ModulesColumns.SUPPORT)); + mod.created = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.CREATED)); + mod.updated = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.UPDATED)); + + c.close(); + + // Versions + projection = new String[]{ + ModuleVersionsColumns.NAME, + ModuleVersionsColumns.CODE, ModuleVersionsColumns.DOWNLOAD_LINK, + ModuleVersionsColumns.MD5SUM, ModuleVersionsColumns.CHANGELOG, + ModuleVersionsColumns.CHANGELOG_IS_HTML, + ModuleVersionsColumns.RELTYPE, + ModuleVersionsColumns.UPLOADED, + }; + + where = ModuleVersionsColumns.MODULE_ID + " = ?"; + whereArgs = new String[]{Long.toString(moduleId)}; + + c = sDb.query(ModuleVersionsColumns.TABLE_NAME, projection, where, whereArgs, null, null, null); + while (c.moveToNext()) { + ModuleVersion version = new ModuleVersion(mod); + version.name = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.NAME)); + version.code = c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.CODE)); + version.downloadLink = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.DOWNLOAD_LINK)); + version.md5sum = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.MD5SUM)); + version.changelog = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.CHANGELOG)); + version.changelogIsHtml = c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.CHANGELOG_IS_HTML)) > 0; + version.relType = ReleaseType.fromOrdinal(c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.RELTYPE))); + version.uploaded = c.getLong(c.getColumnIndexOrThrow(ModuleVersionsColumns.UPLOADED)); + mod.versions.add(version); + } + c.close(); + + // MoreInfo + projection = new String[]{ + MoreInfoColumns.LABEL, + MoreInfoColumns.VALUE, + }; + + where = MoreInfoColumns.MODULE_ID + " = ?"; + whereArgs = new String[]{Long.toString(moduleId)}; + + c = sDb.query(MoreInfoColumns.TABLE_NAME, projection, where, whereArgs, null, null, MoreInfoColumns._ID); + while (c.moveToNext()) { + String label = c.getString(c.getColumnIndexOrThrow(MoreInfoColumns.LABEL)); + String value = c.getString(c.getColumnIndexOrThrow(MoreInfoColumns.VALUE)); + mod.moreInfo.add(new Pair<>(label, value)); + } + c.close(); + + return mod; + } + + public static String getModuleSupport(String packageName) { + return getString(ModulesColumns.TABLE_NAME, ModulesColumns.PKGNAME, packageName, ModulesColumns.SUPPORT); + } + + public static void updateModuleLatestVersion(String packageName) { + int maxShownReleaseType = RepoLoader.getInstance().getMaxShownReleaseType(packageName).ordinal(); + sDb.execSQL("UPDATE " + ModulesColumns.TABLE_NAME + + " SET " + ModulesColumns.LATEST_VERSION + + " = (SELECT " + ModuleVersionsColumns._ID + " FROM " + ModuleVersionsColumns.TABLE_NAME + " AS v" + + " WHERE v." + ModuleVersionsColumns.MODULE_ID + + " = " + ModulesColumns.TABLE_NAME + "." + ModulesColumns._ID + + " AND reltype <= ? LIMIT 1)" + + " WHERE " + ModulesColumns.PKGNAME + " = ?", + new Object[]{maxShownReleaseType, packageName}); + } + + public static void updateAllModulesLatestVersion() { + sDb.beginTransaction(); + try { + String[] projection = new String[]{ModulesColumns.PKGNAME}; + Cursor c = sDb.query(true, ModulesColumns.TABLE_NAME, projection, null, null, null, null, null, null); + while (c.moveToNext()) { + updateModuleLatestVersion(c.getString(0)); + } + c.close(); + sDb.setTransactionSuccessful(); + } finally { + sDb.endTransaction(); + } + } + + public static long insertInstalledModule(InstalledModule installed) { + ContentValues values = new ContentValues(); + values.put(InstalledModulesColumns.PKGNAME, installed.packageName); + values.put(InstalledModulesColumns.VERSION_CODE, installed.versionCode); + values.put(InstalledModulesColumns.VERSION_NAME, installed.versionName); + return sDb.insertOrThrow(InstalledModulesColumns.TABLE_NAME, null, values); + } + + public static void deleteInstalledModule(String packageName) { + sDb.delete(InstalledModulesColumns.TABLE_NAME, InstalledModulesColumns.PKGNAME + " = ?", new String[]{packageName}); + } + + public static void deleteAllInstalledModules() { + sDb.delete(InstalledModulesColumns.TABLE_NAME, null, null); + } + + public static Cursor queryModuleOverview(int sortingOrder, + CharSequence filterText) { + // Columns + String[] projection = new String[]{ + "m." + ModulesColumns._ID, + "m." + ModulesColumns.PKGNAME, + "m." + ModulesColumns.TITLE, + "m." + ModulesColumns.SUMMARY, + "m." + ModulesColumns.CREATED, + "m." + ModulesColumns.UPDATED, + + "v." + ModuleVersionsColumns.NAME + " AS " + OverviewColumns.LATEST_VERSION, + "i." + InstalledModulesColumns.VERSION_NAME + " AS " + OverviewColumns.INSTALLED_VERSION, + + "(CASE WHEN m." + ModulesColumns.PKGNAME + " = '" + ModuleUtil.getInstance().getFrameworkPackageName() + + "' THEN 1 ELSE 0 END) AS " + OverviewColumns.IS_FRAMEWORK, + + "(CASE WHEN i." + InstalledModulesColumns.VERSION_NAME + " IS NOT NULL" + + " THEN 1 ELSE 0 END) AS " + OverviewColumns.IS_INSTALLED, + + "(CASE WHEN v." + ModuleVersionsColumns.CODE + " > " + InstalledModulesColumns.VERSION_CODE + + " THEN 1 ELSE 0 END) AS " + OverviewColumns.HAS_UPDATE, + }; + + // Conditions + String where = ModulesColumns.PREFERRED + " = 1"; + String whereArgs[] = null; + if (!TextUtils.isEmpty(filterText)) { + where += " AND (m." + ModulesColumns.TITLE + " LIKE ?" + + " OR m." + ModulesColumns.SUMMARY + " LIKE ?" + + " OR m." + ModulesColumns.DESCRIPTION + " LIKE ?" + + " OR m." + ModulesColumns.AUTHOR + " LIKE ?)"; + String filterTextArg = "%" + filterText + "%"; + whereArgs = new String[]{filterTextArg, filterTextArg, filterTextArg, filterTextArg}; + } + + // Sorting order + StringBuilder sbOrder = new StringBuilder(); + if (sortingOrder == SORT_CREATED) { + sbOrder.append(OverviewColumns.CREATED); + sbOrder.append(" DESC,"); + } else if (sortingOrder == SORT_UPDATED) { + sbOrder.append(OverviewColumns.UPDATED); + sbOrder.append(" DESC,"); + } + sbOrder.append(OverviewColumns.IS_FRAMEWORK); + sbOrder.append(" DESC, "); + sbOrder.append(OverviewColumns.HAS_UPDATE); + sbOrder.append(" DESC, "); + sbOrder.append(OverviewColumns.IS_INSTALLED); + sbOrder.append(" DESC, "); + sbOrder.append("m."); + sbOrder.append(OverviewColumns.TITLE); + sbOrder.append(" COLLATE NOCASE, "); + sbOrder.append("m."); + sbOrder.append(OverviewColumns.PKGNAME); + + // Query + Cursor c = sDb.query( + ModulesColumns.TABLE_NAME + " AS m" + + " LEFT JOIN " + ModuleVersionsColumns.TABLE_NAME + " AS v" + + " ON v." + ModuleVersionsColumns._ID + " = m." + ModulesColumns.LATEST_VERSION + + " LEFT JOIN " + InstalledModulesColumns.TABLE_NAME + " AS i" + + " ON i." + InstalledModulesColumns.PKGNAME + " = m." + ModulesColumns.PKGNAME, + projection, where, whereArgs, null, null, sbOrder.toString()); + + // Cache column indexes + OverviewColumnsIndexes.fillFromCursor(c); + + return c; + } + + public static String getFrameworkUpdateVersion() { + return getFirstUpdate(true); + } + + public static boolean hasModuleUpdates() { + return getFirstUpdate(false) != null; + } + + private static String getFirstUpdate(boolean framework) { + String[] projection = new String[]{InstalledModulesUpdatesColumns.LATEST_NAME}; + String where = ModulesColumns.PKGNAME + (framework ? " = ?" : " != ?"); + String[] whereArgs = new String[]{ModuleUtil.getInstance().getFrameworkPackageName()}; + Cursor c = sDb.query(InstalledModulesUpdatesColumns.VIEW_NAME, projection, where, whereArgs, null, null, null, "1"); + String latestVersion = null; + if (c.moveToFirst()) + latestVersion = c.getString(c.getColumnIndexOrThrow(InstalledModulesUpdatesColumns.LATEST_NAME)); + c.close(); + return latestVersion; + } + + public static class RowNotFoundException extends RuntimeException { + private static final long serialVersionUID = -396324186622439535L; + + public RowNotFoundException(String reason) { + super(reason); + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/repo/RepoDbDefinitions.java b/app/src/main/java/de/robv/android/xposed/installer/repo/RepoDbDefinitions.java new file mode 100644 index 000000000..54f3b2af2 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/repo/RepoDbDefinitions.java @@ -0,0 +1,218 @@ +package de.robv.android.xposed.installer.repo; + +import android.database.Cursor; +import android.provider.BaseColumns; + +public class RepoDbDefinitions { + public static final int DATABASE_VERSION = 4; + public static final String DATABASE_NAME = "repo_cache.db"; + + + ////////////////////////////////////////////////////////////////////////// + public static interface RepositoriesColumns extends BaseColumns { + public static final String TABLE_NAME = "repositories"; + + public static final String URL = "url"; + public static final String TITLE = "title"; + public static final String PARTIAL_URL = "partial_url"; + public static final String VERSION = "version"; + } + static final String SQL_CREATE_TABLE_REPOSITORIES = + "CREATE TABLE " + RepositoriesColumns.TABLE_NAME + " (" + + RepositoriesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + RepositoriesColumns.URL + " TEXT NOT NULL, " + + RepositoriesColumns.TITLE + " TEXT, " + + RepositoriesColumns.PARTIAL_URL + " TEXT, " + + RepositoriesColumns.VERSION + " TEXT, " + + "UNIQUE (" + RepositoriesColumns.URL + ") ON CONFLICT REPLACE)"; + + + ////////////////////////////////////////////////////////////////////////// + public static interface ModulesColumns extends BaseColumns { + public static final String TABLE_NAME = "modules"; + + public static final String REPO_ID = "repo_id"; + public static final String PKGNAME = "pkgname"; + public static final String TITLE = "title"; + public static final String SUMMARY = "summary"; + public static final String DESCRIPTION = "description"; + public static final String DESCRIPTION_IS_HTML = "description_is_html"; + public static final String AUTHOR = "author"; + public static final String SUPPORT = "support"; + public static final String CREATED = "created"; + public static final String UPDATED = "updated"; + + public static final String PREFERRED = "preferred"; + public static final String LATEST_VERSION = "latest_version_id"; + } + static final String SQL_CREATE_TABLE_MODULES = + "CREATE TABLE " + ModulesColumns.TABLE_NAME + " (" + + ModulesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + + ModulesColumns.REPO_ID + " INTEGER NOT NULL REFERENCES " + + RepositoriesColumns.TABLE_NAME + " ON DELETE CASCADE, " + + ModulesColumns.PKGNAME + " TEXT NOT NULL, " + + ModulesColumns.TITLE + " TEXT NOT NULL, " + + ModulesColumns.SUMMARY + " TEXT, " + + ModulesColumns.DESCRIPTION + " TEXT, " + + ModulesColumns.DESCRIPTION_IS_HTML + " INTEGER DEFAULT 0, " + + ModulesColumns.AUTHOR + " TEXT, " + + ModulesColumns.SUPPORT + " TEXT, " + + ModulesColumns.CREATED + " INTEGER DEFAULT -1, " + + ModulesColumns.UPDATED + " INTEGER DEFAULT -1, " + + ModulesColumns.PREFERRED + " INTEGER DEFAULT 1, " + + ModulesColumns.LATEST_VERSION + " INTEGER REFERENCES " + ModuleVersionsColumns.TABLE_NAME + ", " + + "UNIQUE (" + ModulesColumns.PKGNAME + ", " + ModulesColumns.REPO_ID + ") ON CONFLICT REPLACE)"; + + + ////////////////////////////////////////////////////////////////////////// + public static interface ModuleVersionsColumns extends BaseColumns { + public static final String TABLE_NAME = "module_versions"; + public static final String IDX_MODULE_ID = "module_versions_module_id_idx"; + + public static final String MODULE_ID = "module_id"; + public static final String NAME = "name"; + public static final String CODE = "code"; + public static final String DOWNLOAD_LINK = "download_link"; + public static final String MD5SUM = "md5sum"; + public static final String CHANGELOG = "changelog"; + public static final String CHANGELOG_IS_HTML = "changelog_is_html"; + public static final String RELTYPE = "reltype"; + public static final String UPLOADED = "uploaded"; + } + static final String SQL_CREATE_TABLE_MODULE_VERSIONS = + "CREATE TABLE " + ModuleVersionsColumns.TABLE_NAME + " (" + + ModuleVersionsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + ModuleVersionsColumns.MODULE_ID + " INTEGER NOT NULL REFERENCES " + + ModulesColumns.TABLE_NAME + " ON DELETE CASCADE, " + + ModuleVersionsColumns.NAME + " TEXT NOT NULL, " + + ModuleVersionsColumns.CODE + " INTEGER NOT NULL, " + + ModuleVersionsColumns.DOWNLOAD_LINK + " TEXT, " + + ModuleVersionsColumns.MD5SUM + " TEXT, " + + ModuleVersionsColumns.CHANGELOG + " TEXT, " + + ModuleVersionsColumns.CHANGELOG_IS_HTML + " INTEGER DEFAULT 0, " + + ModuleVersionsColumns.RELTYPE + " INTEGER DEFAULT 0, " + + ModuleVersionsColumns.UPLOADED + " INTEGER DEFAULT -1)"; + static final String SQL_CREATE_INDEX_MODULE_VERSIONS_MODULE_ID = + "CREATE INDEX " + ModuleVersionsColumns.IDX_MODULE_ID + " ON " + + ModuleVersionsColumns.TABLE_NAME + " (" + + ModuleVersionsColumns.MODULE_ID + ")"; + + + ////////////////////////////////////////////////////////////////////////// + public static interface MoreInfoColumns extends BaseColumns { + public static final String TABLE_NAME = "more_info"; + + public static final String MODULE_ID = "module_id"; + public static final String LABEL = "label"; + public static final String VALUE = "value"; + } + static final String SQL_CREATE_TABLE_MORE_INFO = + "CREATE TABLE " + MoreInfoColumns.TABLE_NAME + " (" + + MoreInfoColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + MoreInfoColumns.MODULE_ID + " INTEGER NOT NULL REFERENCES " + + ModulesColumns.TABLE_NAME + " ON DELETE CASCADE, " + + MoreInfoColumns.LABEL + " TEXT NOT NULL, " + + MoreInfoColumns.VALUE + " TEXT)"; + + + ////////////////////////////////////////////////////////////////////////// + public static interface InstalledModulesColumns { + public static final String TABLE_NAME = "installed_modules"; + + public static final String PKGNAME = "pkgname"; + public static final String VERSION_CODE = "version_code"; + public static final String VERSION_NAME = "version_name"; + } + static final String SQL_CREATE_TEMP_TABLE_INSTALLED_MODULES = + "CREATE TEMP TABLE " + InstalledModulesColumns.TABLE_NAME + " (" + + InstalledModulesColumns.PKGNAME + " TEXT PRIMARY KEY ON CONFLICT REPLACE, " + + InstalledModulesColumns.VERSION_CODE + " INTEGER NOT NULL, " + + InstalledModulesColumns.VERSION_NAME + " TEXT)"; + + + ////////////////////////////////////////////////////////////////////////// + public static interface InstalledModulesUpdatesColumns { + public static final String VIEW_NAME = InstalledModulesColumns.TABLE_NAME + "_updates"; + + public static final String MODULE_ID = "module_id"; + public static final String PKGNAME = "pkgname"; + public static final String INSTALLED_CODE = "installed_code"; + public static final String INSTALLED_NAME = "installed_name"; + public static final String LATEST_ID = "latest_id"; + public static final String LATEST_CODE = "latest_code"; + public static final String LATEST_NAME = "latest_name"; + } + static final String SQL_CREATE_TEMP_VIEW_INSTALLED_MODULES_UPDATES = + "CREATE TEMP VIEW " + InstalledModulesUpdatesColumns.VIEW_NAME + " AS SELECT " + + "m." + ModulesColumns._ID + " AS " + InstalledModulesUpdatesColumns.MODULE_ID + ", " + + "i." + InstalledModulesColumns.PKGNAME + " AS " + InstalledModulesUpdatesColumns.PKGNAME + ", " + + "i." + InstalledModulesColumns.VERSION_CODE + " AS " + InstalledModulesUpdatesColumns.INSTALLED_CODE + ", " + + "i." + InstalledModulesColumns.VERSION_NAME + " AS " + InstalledModulesUpdatesColumns.INSTALLED_NAME + ", " + + "v." + ModuleVersionsColumns._ID + " AS " + InstalledModulesUpdatesColumns.LATEST_ID + ", " + + "v." + ModuleVersionsColumns.CODE + " AS " + InstalledModulesUpdatesColumns.LATEST_CODE + ", " + + "v." + ModuleVersionsColumns.NAME + " AS " + InstalledModulesUpdatesColumns.LATEST_NAME + + " FROM " + InstalledModulesColumns.TABLE_NAME + " AS i" + + " INNER JOIN " + ModulesColumns.TABLE_NAME + " AS m" + + " ON m." + ModulesColumns.PKGNAME + " = i." + InstalledModulesColumns.PKGNAME + + " INNER JOIN " + ModuleVersionsColumns.TABLE_NAME + " AS v" + + " ON v." + ModuleVersionsColumns._ID + " = m." + ModulesColumns.LATEST_VERSION + + " WHERE " + InstalledModulesUpdatesColumns.LATEST_CODE + + " > " + InstalledModulesUpdatesColumns.INSTALLED_CODE + + " AND " + ModulesColumns.PREFERRED + " = 1"; + + + ////////////////////////////////////////////////////////////////////////// + public interface OverviewColumns extends BaseColumns { + public static final String PKGNAME = ModulesColumns.PKGNAME; + public static final String TITLE = ModulesColumns.TITLE; + public static final String SUMMARY = ModulesColumns.SUMMARY; + public static final String CREATED = ModulesColumns.CREATED; + public static final String UPDATED = ModulesColumns.UPDATED; + + public static final String INSTALLED_VERSION = "installed_version"; + public static final String LATEST_VERSION = "latest_version"; + + public static final String IS_FRAMEWORK = "is_framework"; + public static final String IS_INSTALLED = "is_installed"; + public static final String HAS_UPDATE = "has_update"; + } + + public static class OverviewColumnsIndexes { + private OverviewColumnsIndexes() {} + + public static int PKGNAME = -1; + public static int TITLE = -1; + public static int SUMMARY = -1; + public static int CREATED = -1; + public static int UPDATED = -1; + + public static int INSTALLED_VERSION = -1; + public static int LATEST_VERSION = -1; + + public static int IS_FRAMEWORK = -1; + public static int IS_INSTALLED = -1; + public static int HAS_UPDATE = -1; + + private static boolean isFilled = false; + + public static void fillFromCursor(Cursor cursor) { + if (isFilled || cursor == null) + return; + + PKGNAME = cursor.getColumnIndexOrThrow(OverviewColumns.PKGNAME); + TITLE = cursor.getColumnIndexOrThrow(OverviewColumns.TITLE); + SUMMARY = cursor.getColumnIndexOrThrow(OverviewColumns.SUMMARY); + CREATED = cursor.getColumnIndexOrThrow(OverviewColumns.CREATED); + UPDATED = cursor.getColumnIndexOrThrow(OverviewColumns.UPDATED); + INSTALLED_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.INSTALLED_VERSION); + LATEST_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.LATEST_VERSION); + INSTALLED_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.INSTALLED_VERSION); + IS_FRAMEWORK = cursor.getColumnIndexOrThrow(OverviewColumns.IS_FRAMEWORK); + IS_INSTALLED = cursor.getColumnIndexOrThrow(OverviewColumns.IS_INSTALLED); + HAS_UPDATE = cursor.getColumnIndexOrThrow(OverviewColumns.HAS_UPDATE); + + isFilled = true; + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/repo/RepoParser.java b/app/src/main/java/de/robv/android/xposed/installer/repo/RepoParser.java new file mode 100644 index 000000000..f7956e7f3 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/repo/RepoParser.java @@ -0,0 +1,337 @@ +package de.robv.android.xposed.installer.repo; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LevelListDrawable; +import android.os.AsyncTask; +import android.text.Html; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.util.Log; +import android.util.Pair; +import android.widget.TextView; + +import com.squareup.picasso.Picasso; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.IOException; +import java.io.InputStream; + +import de.robv.android.xposed.installer.R; + +public class RepoParser { + public final static String TAG = "XposedRepoParser"; + protected final static String NS = null; + protected final XmlPullParser parser; + protected RepoParserCallback mCallback; + private boolean mRepoEventTriggered = false; + + protected RepoParser(InputStream is, RepoParserCallback callback) throws XmlPullParserException, IOException { + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + parser = factory.newPullParser(); + parser.setInput(is, null); + parser.nextTag(); + mCallback = callback; + } + + public static void parse(InputStream is, RepoParserCallback callback) throws XmlPullParserException, IOException { + new RepoParser(is, callback).readRepo(); + } + + public static Spanned parseSimpleHtml(final Context c, String source, final TextView textView) { + source = source.replaceAll("
  • ", "\t\u0095 "); + source = source.replaceAll("
  • ", "
    "); + Spanned html = Html.fromHtml(source, new Html.ImageGetter() { + @Override + public Drawable getDrawable(String source) { + LevelListDrawable d = new LevelListDrawable(); + @SuppressWarnings("deprecation") + Drawable empty = c.getResources().getDrawable(R.drawable.ic_no_image); + d.addLevel(0, 0, empty); + assert empty != null; + d.setBounds(0, 0, empty.getIntrinsicWidth(), empty.getIntrinsicHeight()); + new ImageGetterAsyncTask(c, source, d).execute(textView); + + return d; + } + }, null); + + // trim trailing newlines + int len = html.length(); + int end = len; + for (int i = len - 1; i >= 0; i--) { + if (html.charAt(i) != '\n') + break; + end = i; + } + + if (end == len) + return html; + else + return new SpannableStringBuilder(html, 0, end); + } + + protected void readRepo() throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, NS, "repository"); + Repository repository = new Repository(); + repository.isPartial = "true".equals(parser.getAttributeValue(NS, "partial")); + repository.partialUrl = parser.getAttributeValue(NS, "partial-url"); + repository.version = parser.getAttributeValue(NS, "version"); + + while (parser.nextTag() == XmlPullParser.START_TAG) { + String tagName = parser.getName(); + switch (tagName) { + case "name": + repository.name = parser.nextText(); + break; + case "module": + triggerRepoEvent(repository); + Module module = readModule(repository); + if (module != null) + mCallback.onNewModule(module); + break; + case "remove-module": + triggerRepoEvent(repository); + String packageName = readRemoveModule(); + if (packageName != null) + mCallback.onRemoveModule(packageName); + break; + default: + skip(true); + break; + } + } + + mCallback.onCompleted(repository); + } + + private void triggerRepoEvent(Repository repository) { + if (mRepoEventTriggered) + return; + + mCallback.onRepositoryMetadata(repository); + mRepoEventTriggered = true; + } + + protected Module readModule(Repository repository) throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, NS, "module"); + final int startDepth = parser.getDepth(); + + Module module = new Module(repository); + module.packageName = parser.getAttributeValue(NS, "package"); + if (module.packageName == null) { + logError("no package name defined"); + leave(startDepth); + return null; + } + + module.created = parseTimestamp("created"); + module.updated = parseTimestamp("updated"); + + while (parser.nextTag() == XmlPullParser.START_TAG) { + String tagName = parser.getName(); + switch (tagName) { + case "name": + module.name = parser.nextText(); + break; + case "author": + module.author = parser.nextText(); + break; + case "summary": + module.summary = parser.nextText(); + break; + case "description": + String isHtml = parser.getAttributeValue(NS, "html"); + if (isHtml != null && isHtml.equals("true")) + module.descriptionIsHtml = true; + module.description = parser.nextText(); + break; + case "screenshot": + module.screenshots.add(parser.nextText()); + break; + case "moreinfo": + String label = parser.getAttributeValue(NS, "label"); + String role = parser.getAttributeValue(NS, "role"); + String value = parser.nextText(); + module.moreInfo.add(new Pair<>(label, value)); + + if (role != null && role.contains("support")) + module.support = value; + break; + case "version": + ModuleVersion version = readModuleVersion(module); + if (version != null) + module.versions.add(version); + break; + default: + skip(true); + break; + } + } + + if (module.name == null) { + logError("packages need at least a name"); + return null; + } + + return module; + } + + private long parseTimestamp(String attName) { + String value = parser.getAttributeValue(NS, attName); + if (value == null) + return -1; + try { + return Long.parseLong(value) * 1000L; + } catch (NumberFormatException ex) { + return -1; + } + } + + protected ModuleVersion readModuleVersion(Module module) throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, NS, "version"); + final int startDepth = parser.getDepth(); + ModuleVersion version = new ModuleVersion(module); + + version.uploaded = parseTimestamp("uploaded"); + + while (parser.nextTag() == XmlPullParser.START_TAG) { + String tagName = parser.getName(); + switch (tagName) { + case "name": + version.name = parser.nextText(); + break; + case "code": + try { + version.code = Integer.parseInt(parser.nextText()); + } catch (NumberFormatException nfe) { + logError(nfe.getMessage()); + leave(startDepth); + return null; + } + break; + case "reltype": + version.relType = ReleaseType.fromString(parser.nextText()); + break; + case "download": + version.downloadLink = parser.nextText(); + break; + case "md5sum": + version.md5sum = parser.nextText(); + break; + case "changelog": + String isHtml = parser.getAttributeValue(NS, "html"); + if (isHtml != null && isHtml.equals("true")) + version.changelogIsHtml = true; + version.changelog = parser.nextText(); + break; + case "branch": + // obsolete + skip(false); + break; + default: + skip(true); + break; + } + } + + return version; + } + + protected String readRemoveModule() throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, NS, "remove-module"); + final int startDepth = parser.getDepth(); + + String packageName = parser.getAttributeValue(NS, "package"); + if (packageName == null) { + logError("no package name defined"); + leave(startDepth); + return null; + } + + return packageName; + } + + protected void skip(boolean showWarning) throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, null, null); + if (showWarning) + Log.w(TAG, "skipping unknown/erronous tag: " + parser.getPositionDescription()); + int level = 1; + while (level > 0) { + int eventType = parser.next(); + if (eventType == XmlPullParser.END_TAG) { + level--; + } else if (eventType == XmlPullParser.START_TAG) { + level++; + } + } + } + + protected void leave(int targetDepth) throws XmlPullParserException, IOException { + Log.w(TAG, "leaving up to level " + targetDepth + ": " + parser.getPositionDescription()); + while (parser.getDepth() > targetDepth) { + //noinspection StatementWithEmptyBody + while (parser.next() != XmlPullParser.END_TAG) { + // do nothing + } + } + } + + protected void logError(String error) { + Log.e(TAG, parser.getPositionDescription() + ": " + error); + } + + public interface RepoParserCallback { + void onRepositoryMetadata(Repository repository); + void onNewModule(Module module); + void onRemoveModule(String packageName); + void onCompleted(Repository repository); + } + + static class ImageGetterAsyncTask extends AsyncTask { + private LevelListDrawable levelListDrawable; + private Context context; + private String source; + private TextView t; + + public ImageGetterAsyncTask(Context context, String source, LevelListDrawable levelListDrawable) { + this.context = context; + this.source = source; + this.levelListDrawable = levelListDrawable; + } + + @Override + protected Bitmap doInBackground(TextView... params) { + t = params[0]; + try { + return Picasso.with(context).load(source).get(); + } catch (Exception e) { + return null; + } + } + + @Override + protected void onPostExecute(final Bitmap bitmap) { + try { + Drawable d = new BitmapDrawable(context.getResources(), bitmap); + Point size = new Point(); + ((Activity) context).getWindowManager().getDefaultDisplay().getSize(size); + int multiplier = size.x / bitmap.getWidth(); + if (multiplier <= 0) multiplier = 1; + levelListDrawable.addLevel(1, 1, d); + levelListDrawable.setBounds(0, 0, bitmap.getWidth() * multiplier, bitmap.getHeight() * multiplier); + levelListDrawable.setLevel(1); + t.setText(t.getText()); + } catch (Exception ignored) { /* Like a null bitmap, etc. */ + } + } + } + +} diff --git a/src/de/robv/android/xposed/installer/repo/Repository.java b/app/src/main/java/de/robv/android/xposed/installer/repo/Repository.java similarity index 86% rename from src/de/robv/android/xposed/installer/repo/Repository.java rename to app/src/main/java/de/robv/android/xposed/installer/repo/Repository.java index a223bdd5c..e404b68ad 100644 --- a/src/de/robv/android/xposed/installer/repo/Repository.java +++ b/app/src/main/java/de/robv/android/xposed/installer/repo/Repository.java @@ -1,6 +1,5 @@ package de.robv.android.xposed.installer.repo; - public class Repository { public String name; public String url; @@ -8,5 +7,5 @@ public class Repository { public String partialUrl; public String version; - /*package*/ Repository() {}; + /*package*/ Repository() {} } diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/AssetUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/AssetUtil.java new file mode 100644 index 000000000..63b2bc826 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/AssetUtil.java @@ -0,0 +1,70 @@ +package de.robv.android.xposed.installer.util; + +import android.content.res.AssetManager; +import android.os.Build; +import android.os.FileUtils; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import de.robv.android.xposed.installer.XposedApp; + +public class AssetUtil { + public static final File BUSYBOX_FILE = new File(XposedApp.getInstance().getCacheDir(), "busybox-xposed"); + + @SuppressWarnings("deprecation") + public static String getBinariesFolder() { + if (Build.CPU_ABI.startsWith("arm")) { + return "arm/"; + } else if (Build.CPU_ABI.startsWith("x86")) { + return "x86/"; + } else { + return null; + } + } + + public static File writeAssetToFile(AssetManager assets, String assetName, File targetFile, int mode) { + try { + if (assets == null) + assets = XposedApp.getInstance().getAssets(); + InputStream in = assets.open(assetName); + writeStreamToFile(in, targetFile, mode);; + return targetFile; + } catch (IOException e) { + Log.e(XposedApp.TAG, "could not extract asset", e); + if (targetFile != null) + targetFile.delete(); + + return null; + } + } + + public static void writeStreamToFile(InputStream in, File targetFile, int mode) throws IOException { + FileOutputStream out = new FileOutputStream(targetFile); + + byte[] buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + in.close(); + out.close(); + + FileUtils.setPermissions(targetFile.getAbsolutePath(), mode, -1, -1); + } + + public synchronized static void extractBusybox() { + if (BUSYBOX_FILE.exists()) + return; + + AssetManager assets = null; + writeAssetToFile(assets, getBinariesFolder() + "busybox-xposed", BUSYBOX_FILE, 00700); + } + + public synchronized static void removeBusybox() { + BUSYBOX_FILE.delete(); + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/DownloadsUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/DownloadsUtil.java new file mode 100644 index 000000000..46594565c --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/DownloadsUtil.java @@ -0,0 +1,662 @@ +package de.robv.android.xposed.installer.util; + +import android.app.DownloadManager; +import android.app.DownloadManager.Query; +import android.app.DownloadManager.Request; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.support.annotation.NonNull; +import android.support.annotation.UiThread; +import android.support.v4.content.ContextCompat; +import android.support.v4.os.EnvironmentCompat; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; +import com.afollestad.materialdialogs.MaterialDialog.SingleButtonCallback; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.repo.Module; +import de.robv.android.xposed.installer.repo.ModuleVersion; +import de.robv.android.xposed.installer.repo.ReleaseType; + +public class DownloadsUtil { + public static final String MIME_TYPE_APK = "application/vnd.android.package-archive"; + public static final String MIME_TYPE_ZIP = "application/zip"; + private static final Map mCallbacks = new HashMap<>(); + private static final XposedApp mApp = XposedApp.getInstance(); + private static final SharedPreferences mPref = mApp + .getSharedPreferences("download_cache", Context.MODE_PRIVATE); + + public static class Builder { + private final Context mContext; + private String mTitle = null; + private String mUrl = null; + private DownloadFinishedCallback mCallback = null; + private MIME_TYPES mMimeType = MIME_TYPES.APK; + private File mDestination = null; + private boolean mDialog = false; + + public Builder(Context context) { + mContext = context; + } + + public Builder setTitle(String title) { + mTitle = title; + return this; + } + + public Builder setUrl(String url) { + mUrl = url; + return this; + } + + public Builder setCallback(DownloadFinishedCallback callback) { + mCallback = callback; + return this; + } + + public Builder setMimeType(MIME_TYPES mimeType) { + mMimeType = mimeType; + return this; + } + + public Builder setDestination(File file) { + mDestination = file; + return this; + } + + public Builder setDestinationFromUrl(String subDir) { + if (mUrl == null) { + throw new IllegalStateException("URL must be set first"); + } + return setDestination(getDownloadTargetForUrl(subDir, mUrl)); + } + + public Builder setDialog(boolean dialog) { + mDialog = dialog; + return this; + } + + public DownloadInfo download() { + return add(this); + } + } + + public static String DOWNLOAD_FRAMEWORK = "framework"; + public static String DOWNLOAD_MODULES = "modules"; + + public static File[] getDownloadDirs(String subDir) { + Context context = XposedApp.getInstance(); + ArrayList dirs = new ArrayList<>(2); + for (File dir : ContextCompat.getExternalCacheDirs(context)) { + if (dir != null && EnvironmentCompat.getStorageState(dir).equals(Environment.MEDIA_MOUNTED)) { + dirs.add(new File(new File(dir, "downloads"), subDir)); + } + } + dirs.add(new File(new File(context.getCacheDir(), "downloads"), subDir)); + return dirs.toArray(new File[dirs.size()]); + } + + public static File getDownloadTarget(String subDir, String filename) { + return new File(getDownloadDirs(subDir)[0], filename); + } + + public static File getDownloadTargetForUrl(String subDir, String url) { + return getDownloadTarget(subDir, Uri.parse(url).getLastPathSegment()); + } + + public static DownloadInfo addModule(Context context, String title, String url, DownloadFinishedCallback callback) { + return new Builder(context) + .setTitle(title) + .setUrl(url) + .setDestinationFromUrl(DownloadsUtil.DOWNLOAD_MODULES) + .setCallback(callback) + .setMimeType(MIME_TYPES.APK) + .download(); + } + + private static DownloadInfo add(Builder b) { + Context context = b.mContext; + removeAllForUrl(context, b.mUrl); + + if (!b.mDialog) { + synchronized (mCallbacks) { + mCallbacks.put(b.mUrl, b.mCallback); + } + } + + Request request = new Request(Uri.parse(b.mUrl)); + request.setTitle(b.mTitle); + request.setMimeType(b.mMimeType.toString()); + if (b.mDestination != null) { + b.mDestination.getParentFile().mkdirs(); + removeAllForLocalFile(context, b.mDestination); + request.setDestinationUri(Uri.fromFile(b.mDestination)); + } + request.setNotificationVisibility(Request.VISIBILITY_VISIBLE); + + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + long id = dm.enqueue(request); + + if (b.mDialog) { + showDownloadDialog(b, id); + } + + return getById(context, id); + } + + private static void showDownloadDialog(final Builder b, final long id) { + final Context context = b.mContext; + final DownloadDialog dialog = new DownloadDialog(new MaterialDialog.Builder(context) + .title(b.mTitle) + .content(R.string.download_view_waiting) + .progress(false, 0, true) + .progressNumberFormat(context.getString(R.string.download_progress)) + .canceledOnTouchOutside(false) + .negativeText(R.string.download_view_cancel) + .onNegative(new SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + dialog.cancel(); + } + }) + .cancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + removeById(context, id); + } + }) + ); + dialog.setShowProcess(false); + dialog.show(); + + new Thread("DownloadDialog") { + @Override + public void run() { + while (true) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + return; + } + + final DownloadInfo info = getById(context, id); + if (info == null) { + dialog.cancel(); + return; + } else if (info.status == DownloadManager.STATUS_FAILED) { + dialog.cancel(); + XposedApp.runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(context, + context.getString(R.string.download_view_failed, info.reason), + Toast.LENGTH_LONG).show(); + } + }); + return; + } else if (info.status == DownloadManager.STATUS_SUCCESSFUL) { + dialog.dismiss(); + // Hack to reset stat information. + new File(info.localFilename).setExecutable(false); + if (b.mCallback != null) { + b.mCallback.onDownloadFinished(context, info); + } + return; + } + + XposedApp.runOnUiThread(new Runnable() { + @Override + public void run() { + if (info.totalSize <= 0 || info.status != DownloadManager.STATUS_RUNNING) { + dialog.setContent(R.string.download_view_waiting); + dialog.setShowProcess(false); + } else { + dialog.setContent(R.string.download_running); + dialog.setProgress(info.bytesDownloaded / 1024); + dialog.setMaxProgress(info.totalSize / 1024); + dialog.setShowProcess(true); + } + } + }); + } + } + }.start(); + } + + private static class DownloadDialog extends MaterialDialog { + public DownloadDialog(Builder builder) { + super(builder); + } + + @UiThread + public void setShowProcess(boolean show) { + int visibility = show ? View.VISIBLE : View.GONE; + mProgress.setVisibility(visibility); + mProgressLabel.setVisibility(visibility); + mProgressMinMax.setVisibility(visibility); + } + } + + public static ModuleVersion getStableVersion(Module m) { + for (int i = 0; i < m.versions.size(); i++) { + ModuleVersion mvTemp = m.versions.get(i); + + if (mvTemp.relType == ReleaseType.STABLE) { + return mvTemp; + } + } + return null; + } + + public static DownloadInfo getById(Context context, long id) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + Cursor c = dm.query(new Query().setFilterById(id)); + if (!c.moveToFirst()) { + c.close(); + return null; + } + + int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI); + int columnTitle = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE); + int columnLastMod = c.getColumnIndexOrThrow( + DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP); + int columnLocalUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI); + int columnStatus = c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS); + int columnTotalSize = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES); + int columnBytesDownloaded = c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR); + int columnReason = c.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON); + + int status = c.getInt(columnStatus); + String localFilename = getFilenameFromUri(c.getString(columnLocalUri)); + if (status == DownloadManager.STATUS_SUCCESSFUL && !new File(localFilename).isFile()) { + dm.remove(id); + c.close(); + return null; + } + + DownloadInfo info = new DownloadInfo(id, c.getString(columnUri), + c.getString(columnTitle), c.getLong(columnLastMod), + localFilename, status, + c.getInt(columnTotalSize), c.getInt(columnBytesDownloaded), + c.getInt(columnReason)); + c.close(); + return info; + } + + public static DownloadInfo getLatestForUrl(Context context, String url) { + List all = getAllForUrl(context, url); + return all.isEmpty() ? null : all.get(0); + } + + public static List getAllForUrl(Context context, String url) { + DownloadManager dm = (DownloadManager) context + .getSystemService(Context.DOWNLOAD_SERVICE); + Cursor c = dm.query(new Query()); + int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); + int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI); + int columnTitle = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE); + int columnLastMod = c.getColumnIndexOrThrow( + DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP); + int columnLocalUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI); + int columnStatus = c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS); + int columnTotalSize = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES); + int columnBytesDownloaded = c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR); + int columnReason = c.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON); + + List downloads = new ArrayList<>(); + while (c.moveToNext()) { + if (!url.equals(c.getString(columnUri))) + continue; + + int status = c.getInt(columnStatus); + String localFilename = getFilenameFromUri(c.getString(columnLocalUri)); + if (status == DownloadManager.STATUS_SUCCESSFUL && !new File(localFilename).isFile()) { + dm.remove(c.getLong(columnId)); + continue; + } + + downloads.add(new DownloadInfo(c.getLong(columnId), + c.getString(columnUri), c.getString(columnTitle), + c.getLong(columnLastMod), localFilename, + status, c.getInt(columnTotalSize), + c.getInt(columnBytesDownloaded), c.getInt(columnReason))); + } + c.close(); + + Collections.sort(downloads); + return downloads; + } + + public static void removeById(Context context, long id) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + dm.remove(id); + } + + public static void removeAllForUrl(Context context, String url) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + Cursor c = dm.query(new Query()); + int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); + int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI); + + List idsList = new ArrayList<>(1); + while (c.moveToNext()) { + if (url.equals(c.getString(columnUri))) + idsList.add(c.getLong(columnId)); + } + c.close(); + + if (idsList.isEmpty()) + return; + + long ids[] = new long[idsList.size()]; + for (int i = 0; i < ids.length; i++) + ids[i] = idsList.get(i); + + dm.remove(ids); + } + + public static void removeAllForLocalFile(Context context, File file) { + file.delete(); + + String filename; + try { + filename = file.getCanonicalPath(); + } catch (IOException e) { + Log.w(XposedApp.TAG, "Could not resolve path for " + file.getAbsolutePath(), e); + return; + } + + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + Cursor c = dm.query(new Query()); + int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); + int columnLocalUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI); + + List idsList = new ArrayList<>(1); + while (c.moveToNext()) { + String itemFilename = getFilenameFromUri(c.getString(columnLocalUri)); + if (itemFilename != null) { + if (filename.equals(itemFilename)) { + idsList.add(c.getLong(columnId)); + } else { + try { + if (filename.equals(new File(itemFilename).getCanonicalPath())) { + idsList.add(c.getLong(columnId)); + } + } catch (IOException ignored) { + } + } + } + } + c.close(); + + if (idsList.isEmpty()) + return; + + long ids[] = new long[idsList.size()]; + for (int i = 0; i < ids.length; i++) + ids[i] = idsList.get(i); + + dm.remove(ids); + } + + public static void removeOutdated(Context context, long cutoff) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + Cursor c = dm.query(new Query()); + int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); + int columnLastMod = c.getColumnIndexOrThrow( + DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP); + + List idsList = new ArrayList<>(); + while (c.moveToNext()) { + if (c.getLong(columnLastMod) < cutoff) + idsList.add(c.getLong(columnId)); + } + c.close(); + + if (idsList.isEmpty()) + return; + + long ids[] = new long[idsList.size()]; + for (int i = 0; i < ids.length; i++) + ids[i] = idsList.get(0); + + dm.remove(ids); + } + + public static void triggerDownloadFinishedCallback(Context context, long id) { + DownloadInfo info = getById(context, id); + if (info == null || info.status != DownloadManager.STATUS_SUCCESSFUL) + return; + + DownloadFinishedCallback callback; + synchronized (mCallbacks) { + callback = mCallbacks.get(info.url); + } + + if (callback == null) + return; + + // Hack to reset stat information. + new File(info.localFilename).setExecutable(false); + callback.onDownloadFinished(context, info); + } + + private static String getFilenameFromUri(String uriString) { + if (uriString == null) { + return null; + } + Uri uri = Uri.parse(uriString); + if (uri.getScheme().equals("file")) { + return uri.getPath(); + } else if (uri.getScheme().equals("content")) { + Context context = XposedApp.getInstance(); + Cursor c = null; + try { + c = context.getContentResolver().query(uri, new String[]{MediaStore.Files.FileColumns.DATA}, null, null, null); + c.moveToFirst(); + return c.getString(c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA)); + } finally { + if (c != null) { + c.close(); + } + } + } else { + throw new UnsupportedOperationException("Unexpected URI: " + uriString); + } + } + + public static SyncDownloadInfo downloadSynchronously(String url, File target) { + final boolean useNotModifiedTags = target.exists(); + + URLConnection connection = null; + InputStream in = null; + FileOutputStream out = null; + try { + connection = new URL(url).openConnection(); + connection.setDoOutput(false); + connection.setConnectTimeout(30000); + connection.setReadTimeout(30000); + + if (connection instanceof HttpURLConnection) { + // Disable transparent gzip encoding for gzipped files + if (url.endsWith(".gz")) { + connection.addRequestProperty("Accept-Encoding", "identity"); + } + + if (useNotModifiedTags) { + String modified = mPref.getString("download_" + url + "_modified", null); + String etag = mPref.getString("download_" + url + "_etag", null); + + if (modified != null) { + connection.addRequestProperty("If-Modified-Since", modified); + } + if (etag != null) { + connection.addRequestProperty("If-None-Match", etag); + } + } + } + + connection.connect(); + + if (connection instanceof HttpURLConnection) { + HttpURLConnection httpConnection = (HttpURLConnection) connection; + int responseCode = httpConnection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { + return new SyncDownloadInfo(SyncDownloadInfo.STATUS_NOT_MODIFIED, null); + } else if (responseCode < 200 || responseCode >= 300) { + return new SyncDownloadInfo(SyncDownloadInfo.STATUS_FAILED, + mApp.getString(R.string.repo_download_failed_http, + url, responseCode, + httpConnection.getResponseMessage())); + } + } + + in = connection.getInputStream(); + out = new FileOutputStream(target); + byte buf[] = new byte[1024]; + int read; + while ((read = in.read(buf)) != -1) { + out.write(buf, 0, read); + } + + if (connection instanceof HttpURLConnection) { + HttpURLConnection httpConnection = (HttpURLConnection) connection; + String modified = httpConnection.getHeaderField("Last-Modified"); + String etag = httpConnection.getHeaderField("ETag"); + + mPref.edit() + .putString("download_" + url + "_modified", modified) + .putString("download_" + url + "_etag", etag).apply(); + } + + return new SyncDownloadInfo(SyncDownloadInfo.STATUS_SUCCESS, null); + + } catch (Throwable t) { + return new SyncDownloadInfo(SyncDownloadInfo.STATUS_FAILED, + mApp.getString(R.string.repo_download_failed, url, + t.getMessage())); + + } finally { + if (connection != null && connection instanceof HttpURLConnection) + ((HttpURLConnection) connection).disconnect(); + if (in != null) + try { + in.close(); + } catch (IOException ignored) { + } + if (out != null) + try { + out.close(); + } catch (IOException ignored) { + } + } + } + + public static void clearCache(String url) { + if (url != null) { + mPref.edit().remove("download_" + url + "_modified") + .remove("download_" + url + "_etag").apply(); + } else { + mPref.edit().clear().apply(); + } + } + + public enum MIME_TYPES { + APK { + public String toString() { + return MIME_TYPE_APK; + } + + public String getExtension() { + return ".apk"; + } + }, + ZIP { + public String toString() { + return MIME_TYPE_ZIP; + } + + public String getExtension() { + return ".zip"; + } + }; + + public String getExtension() { + return null; + } + } + + public interface DownloadFinishedCallback { + void onDownloadFinished(Context context, DownloadInfo info); + } + + public static class DownloadInfo implements Comparable { + public final long id; + public final String url; + public final String title; + public final long lastModification; + public final String localFilename; + public final int status; + public final int totalSize; + public final int bytesDownloaded; + public final int reason; + + private DownloadInfo(long id, String url, String title, long lastModification, String localFilename, int status, int totalSize, int bytesDownloaded, int reason) { + this.id = id; + this.url = url; + this.title = title; + this.lastModification = lastModification; + this.localFilename = localFilename; + this.status = status; + this.totalSize = totalSize; + this.bytesDownloaded = bytesDownloaded; + this.reason = reason; + } + + @Override + public int compareTo(@NonNull DownloadInfo another) { + int compare = (int) (another.lastModification + - this.lastModification); + if (compare != 0) + return compare; + return this.url.compareTo(another.url); + } + } + + public static class SyncDownloadInfo { + public static final int STATUS_SUCCESS = 0; + public static final int STATUS_NOT_MODIFIED = 1; + public static final int STATUS_FAILED = 2; + + public final int status; + public final String errorMessage; + + private SyncDownloadInfo(int status, String errorMessage) { + this.status = status; + this.errorMessage = errorMessage; + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/FrameworkZips.java b/app/src/main/java/de/robv/android/xposed/installer/util/FrameworkZips.java new file mode 100644 index 000000000..6a6ba23a5 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/FrameworkZips.java @@ -0,0 +1,486 @@ +package de.robv.android.xposed.installer.util; + +import android.content.Context; +import android.os.Build; +import android.support.annotation.StringRes; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.util.DownloadsUtil.SyncDownloadInfo; +import de.robv.android.xposed.installer.util.InstallZipUtil.XposedProp; +import de.robv.android.xposed.installer.util.InstallZipUtil.ZipCheckResult; + +public final class FrameworkZips { + public static final String ARCH = getArch(); + public static final String SDK = Integer.toString(Build.VERSION.SDK_INT); + + private static final File ONLINE_FILE = new File(XposedApp.getInstance().getCacheDir(), "framework.json"); + private static final String ONLINE_URL = "http://dl-xda.xposed.info/framework.json"; + + public enum Type { + INSTALLER(R.string.install_update, R.string.framework_install, R.string.framework_install_recovery), + UNINSTALLER(R.string.uninstall, R.string.uninstall, R.string.framework_uninstall_recovery); + + public final int title; + public final int text_flash; + public final int text_flash_recovery; + + Type(@StringRes int title, @StringRes int text_flash, @StringRes int text_flash_recovery) { + this.title = title; + this.text_flash = text_flash; + this.text_flash_recovery = text_flash_recovery; + } + } + private static final int TYPE_COUNT = Type.values().length; + + @SuppressWarnings("rawtypes") + private static final Map[] EMPTY_MAP_ARRAY = new Map[TYPE_COUNT]; + static { + Arrays.fill(EMPTY_MAP_ARRAY, Collections.emptyMap()); + } + + private static Map[] sOnline = emptyMapArray(); + private static Map>[] sLocal = emptyMapArray(); + + @SuppressWarnings("unchecked") + public static Map[] emptyMapArray() { + return (Map[]) EMPTY_MAP_ARRAY; + } + + public static class FrameworkZip { + public String title; + public Type type = Type.INSTALLER; + + public boolean isOutdated() { + return true; + } + } + + public static class OnlineFrameworkZip extends FrameworkZip { + public String url; + public boolean current = true; + + public boolean isOutdated() { + return !current; + } + } + + public static class LocalFrameworkZip extends FrameworkZip { + public File path; + } + + @WorkerThread + private static void refreshOnline() { + Map[] zips = getOnline(); + synchronized (FrameworkZips.class) { + sOnline = zips; + } + } + + // TODO provide user feedback in case of errors + private static Map[] getOnline() { + String text; + try { + text = fileToString(ONLINE_FILE); + } catch (FileNotFoundException e) { + return emptyMapArray(); + } catch (IOException e) { + Log.e(XposedApp.TAG, "Could not read " + ONLINE_FILE, e); + return emptyMapArray(); + } + + try { + JSONObject json = new JSONObject(text); + + //noinspection unchecked + Map[] zipsArray = new Map[TYPE_COUNT]; + for (int i = 0; i < TYPE_COUNT; i++) { + zipsArray[i] = new LinkedHashMap<>(); + } + + JSONArray jsonZips = json.getJSONArray("zips"); + for (int i = 0; i < jsonZips.length(); i++) { + parseZipSpec(jsonZips.getJSONObject(i), zipsArray); + } + + return zipsArray; + } catch (JSONException e) { + Log.e(XposedApp.TAG, "Could not parse " + ONLINE_URL, e); + return emptyMapArray(); + } + } + + private static String fileToString(File file) throws IOException { + Reader reader = null; + try { + reader = new FileReader(file); + StringBuilder sb = new StringBuilder((int) file.length()); + char[] buffer = new char[8192]; + int read; + while ((read = reader.read(buffer, 0, buffer.length)) > 0) { + sb.append(buffer, 0, read); + } + return sb.toString(); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException ignored) {} + } + } + } + + private static void parseZipSpec(JSONObject jsonZip, Map[] zipsArray) throws JSONException { + if (!contains(jsonZip, "archs", ARCH) || !contains(jsonZip, "sdks", SDK)) { + return; + } + + String titleTemplate = jsonZip.getString("title"); + String urlTemplate = jsonZip.getString("url"); + boolean current = jsonZip.optBoolean("current", false); + String typeString = jsonZip.optString("type", null); + Type type; + if (typeString == null) { + type = Type.INSTALLER; + } else if (typeString.equals("uninstaller")) { + type = Type.UNINSTALLER; + } else { + Log.w(XposedApp.TAG, "Unsupported framework zip type: " + typeString); + return; + } + Map zips = zipsArray[type.ordinal()]; + + Map attributes = new HashMap<>(3); + + JSONArray jsonVersions = jsonZip.optJSONArray("versions"); + if (jsonVersions != null) { + Set excludes = Collections.emptySet(); + JSONArray jsonExcludes = jsonZip.optJSONArray("exclude"); + if (jsonExcludes != null) { + excludes = new HashSet<>(); + for (int i = 0; i < jsonExcludes.length(); i++) { + JSONObject jsonExclude = jsonExcludes.getJSONObject(i); + if (contains(jsonExclude, "archs", ARCH) && contains(jsonExclude, "sdks", SDK)) { + JSONArray jsonExcludeVersions = jsonExclude.getJSONArray("versions"); + for (int j = 0; j < jsonExcludeVersions.length(); j++) { + excludes.add(jsonExcludeVersions.getString(j)); + } + } + } + } + + for (int i = 0; i < jsonVersions.length(); i++) { + JSONObject versionData = jsonVersions.getJSONObject(i); + String version = versionData.getString("version"); + if (excludes.contains(version)) { + continue; + } + + attributes.clear(); + attributes.put("arch", ARCH); + attributes.put("sdk", SDK); + parseAttributes(versionData, attributes); + + addZip(zips, titleTemplate, urlTemplate, attributes, + versionData.optBoolean("current", current), type); + } + } else { + attributes.put("arch", ARCH); + attributes.put("sdk", SDK); + addZip(zips, titleTemplate, urlTemplate, attributes, current, type); + } + } + + private static boolean contains(JSONObject obj, String key, String value) throws JSONException { + JSONArray array = obj.optJSONArray(key); + if (array == null) { + return true; + } + for (int i = 0; i < array.length(); i++) { + if (array.getString(i).equals(value)) { + return true; + } + } + return false; + } + + private static void parseAttributes(JSONObject obj, Map attributes) throws JSONException { + if (obj != null) { + Iterator it = obj.keys(); + while (it.hasNext()) { + String key = it.next(); + Object value = obj.get(key); + if (value instanceof String) { + attributes.put(key, (String) value); + } + } + } + } + + private static void addZip(Map zips, String titleTemplate, String urlTemplate, + Map attributes, boolean current, Type type) { + String title = replacePlaceholders(titleTemplate, attributes); + if (!zips.containsKey(title)) { + OnlineFrameworkZip zip = new OnlineFrameworkZip(); + zip.title = title; + zip.url = replacePlaceholders(urlTemplate, attributes); + zip.current = current; + zip.type = type; + zips.put(zip.title, zip); + } + } + + private static String replacePlaceholders(String template, Map values) { + if (!template.contains("$(")) { + return template; + } + + StringBuilder sb = new StringBuilder(template); + for (Entry entry : values.entrySet()) { + String search = "$(" + entry.getKey() + ")"; + int length = search.length(); + int index; + while ((index = sb.indexOf(search)) != -1) { + sb.replace(index, index + length, entry.getValue()); + } + } + return sb.toString(); + } + + @WorkerThread + private static void refreshLocal() { + //noinspection unchecked + Map>[] zipsArray = new Map[TYPE_COUNT]; + for (int i = 0; i < TYPE_COUNT; i++) { + zipsArray[i] = new TreeMap<>(); + } + + for (File dir : DownloadsUtil.getDownloadDirs(DownloadsUtil.DOWNLOAD_FRAMEWORK)) { + if (!dir.isDirectory()) { + continue; + } + for (String filename : dir.list()) { + if (!filename.endsWith(".zip")) { + continue; + } + LocalFrameworkZip zip = analyze(new File(dir, filename)); + if (zip != null) { + Map> zips = zipsArray[zip.type.ordinal()]; + List list = zips.get(zip.title); + if (list == null) { + list = new ArrayList<>(1); + zips.put(zip.title, list); + } + list.add(zip); + } + } + } + synchronized (FrameworkZips.class) { + sLocal = zipsArray; + } + } + + // TODO Replace this with a proper way to report loading failures to the users. + public static boolean hasLoadedOnlineZips() { + return sOnline != EMPTY_MAP_ARRAY; + } + + public static Set getAllTitles(Type type) { + Set result = new LinkedHashSet<>(sOnline[type.ordinal()].keySet()); + result.addAll(sLocal[type.ordinal()].keySet()); + return result; + } + + public static OnlineFrameworkZip getOnline(String title, Type type) { + return sOnline[type.ordinal()].get(title); + } + + public static LocalFrameworkZip getLocal(String title, Type type) { + List all = sLocal[type.ordinal()].get(title); + return all != null ? all.get(0) : null; + } + + public static boolean hasLocal(String title, Type type) { + return sLocal[type.ordinal()].containsKey(title); + } + + public static List getAllLocal(String title, Type type) { + List all = sLocal[type.ordinal()].get(title); + return all != null ? all : Collections.emptyList(); + } + + public static void delete(Context context, String title, Type type) { + OnlineFrameworkZip online = getOnline(title, type); + if (online != null) { + DownloadsUtil.removeAllForUrl(context, online.url); + } + + List locals = getAllLocal(title, type); + for (LocalFrameworkZip local : locals) { + DownloadsUtil.removeAllForLocalFile(context, local.path); + } + } + + @WorkerThread + private static LocalFrameworkZip analyze(File file) { + String filename = file.getName(); + ZipFile zipFile = null; + try { + zipFile = new ZipFile(file); + ZipCheckResult zcr = InstallZipUtil.checkZip(zipFile); + if (!zcr.isValidZip()) { + return null; + } + + LocalFrameworkZip zip = new LocalFrameworkZip(); + ZipEntry entry; + if ((entry = zipFile.getEntry("system/xposed.prop")) != null) { + XposedProp prop = InstallZipUtil.parseXposedProp(zipFile.getInputStream(entry)); + if (prop == null || !prop.isCompatible()) { + Log.w(XposedApp.TAG, "ZIP file is not compatible: " + file); + return null; + } + zip.title = "Version " + prop.getVersion(); + } else if (filename.startsWith("xposed-uninstaller-")) { + // TODO provide more information inside uninstaller ZIPs + zip.type = Type.UNINSTALLER; + zip.title = "Uninstaller"; + int start = "xposed-uninstaller-".length(); + int end = filename.lastIndexOf('-'); + if (start < end) { + zip.title += " (" + filename.substring(start, end) + ")"; + } + } else { + return null; + } + + zip.path = file; + return zip; + } catch (IOException e) { + Log.e(XposedApp.TAG, "Errors while checking " + file, e); + return null; + } finally { + if (zipFile != null) { + InstallZipUtil.closeSilently(zipFile); + } + } + } + + @SuppressWarnings("deprecation") + private static String getArch() { + if (Build.CPU_ABI.equals("arm64-v8a")) { + return "arm64"; + } else if (Build.CPU_ABI.equals("x86_64")) { + return "x86_64"; + } else if (Build.CPU_ABI.equals("mips64")) { + return "mips64"; + } else if (Build.CPU_ABI.startsWith("x86") || Build.CPU_ABI2.startsWith("x86")) { + return "x86"; + } else if (Build.CPU_ABI.startsWith("mips")) { + return "mips"; + } else if (Build.CPU_ABI.startsWith("armeabi-v5") || Build.CPU_ABI.startsWith("armeabi-v6")) { + return "armv5"; + } else { + return "arm"; + } + } + + private FrameworkZips() { + } + + public static class OnlineZipLoader extends OnlineLoader { + private static OnlineZipLoader sInstance = new OnlineZipLoader(); + + public static OnlineZipLoader getInstance() { + return sInstance; + } + + @Override + protected synchronized void onFirstLoad() { + new Thread("OnlineZipInit") { + @Override + public void run() { + refreshOnline(); + notifyListeners(); + } + }.start(); + } + + @Override + protected boolean onReload() { + SyncDownloadInfo info = DownloadsUtil.downloadSynchronously(ONLINE_URL, ONLINE_FILE); + switch (info.status) { + case SyncDownloadInfo.STATUS_NOT_MODIFIED: + return false; + + case SyncDownloadInfo.STATUS_FAILED: + onClear(); + return true; + + case SyncDownloadInfo.STATUS_SUCCESS: + default: + refreshOnline(); + return true; + } + } + + @Override + protected void onClear() { + super.onClear(); + synchronized (this) { + ONLINE_FILE.delete(); + } + synchronized (FrameworkZips.class) { + sOnline = emptyMapArray(); + } + } + } + + public static class LocalZipLoader extends Loader { + private static LocalZipLoader sInstance = new LocalZipLoader(); + + public static LocalZipLoader getInstance() { + return sInstance; + } + + @Override + protected boolean onReload() { + refreshLocal(); + return true; + } + + @Override + protected void onClear() { + synchronized (FrameworkZips.class) { + sLocal = emptyMapArray(); + } + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/HashUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/HashUtil.java new file mode 100644 index 000000000..163d63505 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/HashUtil.java @@ -0,0 +1,56 @@ +package de.robv.android.xposed.installer.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class HashUtil { + public static final String hash(String input, String algorithm) { + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + byte[] messageDigest = md.digest(input.getBytes()); + return toHexString(messageDigest); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + public static final String md5(String input) { + return hash(input, "MD5"); + } + + public static final String hash(File file, String algorithm) throws IOException { + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + InputStream is = new FileInputStream(file); + byte[] buffer = new byte[8192]; + int read = 0; + while ((read = is.read(buffer)) > 0) { + md.update(buffer, 0, read); + } + is.close(); + byte[] messageDigest = md.digest(); + return toHexString(messageDigest); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + public static final String md5(File input) throws IOException { + return hash(input, "MD5"); + } + + private static String toHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + int unsignedB = b & 0xff; + if (unsignedB < 0x10) + sb.append("0"); + sb.append(Integer.toHexString(unsignedB)); + } + return sb.toString(); + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/InstallZipUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/InstallZipUtil.java new file mode 100644 index 000000000..d123f01ad --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/InstallZipUtil.java @@ -0,0 +1,215 @@ +package de.robv.android.xposed.installer.util; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Build; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import de.robv.android.xposed.installer.BuildConfig; +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.installation.FlashCallback; + +public final class InstallZipUtil { + private static final Set FEATURES = new HashSet<>(); + + static { + FEATURES.add("fbe_aware"); // BASE_DIR in /data/user_de/0 on SDK24+ + } + + public static class ZipCheckResult { + private final ZipFile mZip; + private boolean mValidZip = false; + private boolean mFlashableInApp = false; + private XposedProp mXposedProp = null; + + public ZipFile getZip() { + return mZip; + } + + public boolean isValidZip() { + return mValidZip; + } + + public boolean isFlashableInApp() { + return mFlashableInApp; + } + + public boolean hasXposedProp() { + return mXposedProp != null; + } + + public XposedProp getXposedProp() { + return mXposedProp; + } + + private ZipCheckResult(ZipFile zip) { + mZip = zip; + } + } + + public static ZipCheckResult checkZip(ZipFile zip) { + ZipCheckResult result = new ZipCheckResult(zip); + + // Check for update-binary. + if (zip.getEntry("META-INF/com/google/android/update-binary") == null) { + return result; + } + + result.mValidZip = true; + + // Check whether the file can be flashed directly in the app. + if (zip.getEntry("META-INF/com/google/android/flash-script.sh") != null) { + result.mFlashableInApp = true; + } + + + ZipEntry xposedPropEntry = zip.getEntry("system/xposed.prop"); + if (xposedPropEntry != null) { + try { + result.mXposedProp = parseXposedProp(zip.getInputStream(xposedPropEntry)); + } catch (IOException e) { + Log.e(XposedApp.TAG, "Failed to read system/xposed.prop from " + zip.getName(), e); + } + } + + return result; + } + + public static class XposedProp { + private String mVersion = null; + private int mVersionInt = 0; + private String mArch = null; + private int mMinSdk = 0; + private int mMaxSdk = 0; + private Set mRequires = new HashSet<>(); + + private boolean isComplete() { + return mVersion != null + && mVersionInt > 0 + && mArch != null + && mMinSdk > 0 + && mMaxSdk > 0; + } + + public String getVersion() { + return mVersion; + } + + public int getVersionInt() { + return mVersionInt; + } + + public boolean isArchCompatible() { + return FrameworkZips.ARCH.equals(mArch); + } + + public boolean isSdkCompatible() { + return mMinSdk <= Build.VERSION.SDK_INT && Build.VERSION.SDK_INT <= mMaxSdk; + } + + public Set getMissingInstallerFeatures() { + Set missing = new TreeSet<>(mRequires); + missing.removeAll(FEATURES); + return missing; + } + + public boolean isCompatible() { + return isSdkCompatible() && isArchCompatible(); + } + } + + public static XposedProp parseXposedProp(InputStream is) throws IOException { + XposedProp prop = new XposedProp(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + String line; + while ((line = reader.readLine()) != null) { + String[] parts = line.split("=", 2); + if (parts.length != 2) { + continue; + } + + String key = parts[0].trim(); + if (key.charAt(0) == '#') { + continue; + } + + String value = parts[1].trim(); + + if (key.equals("version")) { + prop.mVersion = value; + prop.mVersionInt = ModuleUtil.extractIntPart(value); + } else if (key.equals("arch")) { + prop.mArch = value; + } else if (key.equals("minsdk")) { + prop.mMinSdk = Integer.parseInt(value); + } else if (key.equals("maxsdk")) { + prop.mMaxSdk = Integer.parseInt(value); + } else if (key.startsWith("requires:")) { + prop.mRequires.add(key.substring(9)); + } + } + reader.close(); + return prop.isComplete() ? prop : null; + } + + public static String messageForError(int code, Object... args) { + Context context = XposedApp.getInstance(); + switch (code) { + case FlashCallback.ERROR_TIMEOUT: + return context.getString(R.string.flash_error_timeout); + + case FlashCallback.ERROR_SHELL_DIED: + return context.getString(R.string.flash_error_shell_died); + + case FlashCallback.ERROR_NO_ROOT_ACCESS: + return context.getString(R.string.root_failed); + + case FlashCallback.ERROR_INVALID_ZIP: + String message = context.getString(R.string.flash_error_invalid_zip); + if (args.length > 0) { + message += "\n" + args[0]; + } + return message; + + case FlashCallback.ERROR_NOT_FLASHABLE_IN_APP: + return context.getString(R.string.flash_error_not_flashable_in_app); + + case FlashCallback.ERROR_INSTALLER_NEEDS_UPDATE: + Resources res = context.getResources(); + return res.getString(R.string.installer_needs_update, res.getString(R.string.app_name)); + + default: + return context.getString(R.string.flash_error_default, code); + } + } + + public static void triggerError(FlashCallback callback, int code, Object... args) { + callback.onError(code, messageForError(code, args)); + } + + public static void closeSilently(ZipFile z) { + try { + z.close(); + } catch (IOException ignored) { + } + } + + public static void reportMissingFeatures(Set missingFeatures) { + Log.e(XposedApp.TAG, "Installer version: " + BuildConfig.VERSION_NAME); + Log.e(XposedApp.TAG, "Missing installer features: " + missingFeatures); + } + + private InstallZipUtil() { + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/Loader.java b/app/src/main/java/de/robv/android/xposed/installer/util/Loader.java new file mode 100644 index 000000000..68e63d054 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/Loader.java @@ -0,0 +1,143 @@ +package de.robv.android.xposed.installer.util; + +import android.support.v4.widget.SwipeRefreshLayout; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import de.robv.android.xposed.installer.XposedApp; + +public abstract class Loader implements SwipeRefreshLayout.OnRefreshListener { + protected final String CLASS_NAME = getClass().getSimpleName(); + private boolean mIsLoading = false; + private boolean mReloadTriggeredOnce = false; + private final List> mListeners = new CopyOnWriteArrayList<>(); + private SwipeRefreshLayout mSwipeRefreshLayout; + + public void triggerReload(final boolean force) { + synchronized (this) { + if (!mReloadTriggeredOnce) { + onFirstLoad(); + mReloadTriggeredOnce = true; + } + } + + if (!force && !shouldUpdate()) { + return; + } + + synchronized (this) { + if (mIsLoading) { + return; + } + mIsLoading = true; + updateProgressIndicator(); + } + + new Thread("Reload" + CLASS_NAME) { + public void run() { + boolean hasChanged = onReload(); + if (hasChanged) { + notifyListeners(); + } + + synchronized (this) { + mIsLoading = false; + updateProgressIndicator(); + } + } + }.start(); + } + + protected synchronized void onFirstLoad() { + // Empty by default. + } + + protected boolean shouldUpdate() { + return true; + } + + protected abstract boolean onReload(); + + public void clear(boolean notify) { + synchronized (this) { + // TODO Stop reloading repository when it should be cleared + if (mIsLoading) { + return; + } + onClear(); + } + + if (notify) { + notifyListeners(); + } + } + + protected abstract void onClear(); + + public void triggerFirstLoadIfNecessary() { + synchronized (this) { + if (mReloadTriggeredOnce) { + return; + } + } + triggerReload(false); + } + + public synchronized boolean isLoading() { + return mIsLoading; + } + + public interface Listener { + void onReloadDone(T loader); + } + + public void addListener(Listener listener) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + } + } + + public void removeListener(Listener listener) { + mListeners.remove(listener); + } + + protected void notifyListeners() { + for (Listener listener : mListeners) { + //noinspection unchecked + listener.onReloadDone((T) this); + } + } + + public synchronized void setSwipeRefreshLayout(SwipeRefreshLayout swipeRefreshLayout) { + this.mSwipeRefreshLayout = swipeRefreshLayout; + if (swipeRefreshLayout == null) { + return; + } + + swipeRefreshLayout.setRefreshing(mIsLoading); + swipeRefreshLayout.setOnRefreshListener(this); + } + + @Override + public void onRefresh() { + triggerReload(true); + } + + private synchronized void updateProgressIndicator() { + if (mSwipeRefreshLayout == null) { + return; + } + + XposedApp.runOnUiThread(new Runnable() { + @Override + public void run() { + synchronized (Loader.this) { + if (mSwipeRefreshLayout != null) { + mSwipeRefreshLayout.setRefreshing(mIsLoading); + } + } + } + }); + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/ModuleUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/ModuleUtil.java new file mode 100644 index 000000000..9aa8e1de3 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/ModuleUtil.java @@ -0,0 +1,367 @@ +package de.robv.android.xposed.installer.util; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.FileUtils; +import android.util.Log; +import android.widget.Toast; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +import de.robv.android.xposed.installer.ModulesFragment; +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.repo.ModuleVersion; +import de.robv.android.xposed.installer.repo.RepoDb; + +public final class ModuleUtil { + // xposedminversion below this + private static final String MODULES_LIST_FILE = XposedApp.BASE_DIR + "conf/modules.list"; + private static final String PLAY_STORE_PACKAGE = "com.android.vending"; + public static int MIN_MODULE_VERSION = 2; // reject modules with + private static ModuleUtil mInstance = null; + private final XposedApp mApp; + private final PackageManager mPm; + private final String mFrameworkPackageName; + private final List mListeners = new CopyOnWriteArrayList(); + private SharedPreferences mPref; + private InstalledModule mFramework = null; + private Map mInstalledModules; + private boolean mIsReloading = false; + private Toast mToast; + + private ModuleUtil() { + mApp = XposedApp.getInstance(); + mPref = mApp.getSharedPreferences("enabled_modules", Context.MODE_PRIVATE); + mPm = mApp.getPackageManager(); + mFrameworkPackageName = mApp.getPackageName(); + } + + public static synchronized ModuleUtil getInstance() { + if (mInstance == null) { + mInstance = new ModuleUtil(); + mInstance.reloadInstalledModules(); + } + return mInstance; + } + + public static int extractIntPart(String str) { + int result = 0, length = str.length(); + for (int offset = 0; offset < length; offset++) { + char c = str.charAt(offset); + if ('0' <= c && c <= '9') + result = result * 10 + (c - '0'); + else + break; + } + return result; + } + + public void reloadInstalledModules() { + synchronized (this) { + if (mIsReloading) + return; + mIsReloading = true; + } + + Map modules = new HashMap(); + RepoDb.beginTransation(); + try { + RepoDb.deleteAllInstalledModules(); + + for (PackageInfo pkg : mPm.getInstalledPackages(PackageManager.GET_META_DATA)) { + ApplicationInfo app = pkg.applicationInfo; + if (!app.enabled) + continue; + + InstalledModule installed = null; + if (app.metaData != null && app.metaData.containsKey("xposedmodule")) { + installed = new InstalledModule(pkg, false); + modules.put(pkg.packageName, installed); + } else if (isFramework(pkg.packageName)) { + mFramework = installed = new InstalledModule(pkg, true); + } + + if (installed != null) + RepoDb.insertInstalledModule(installed); + } + + RepoDb.setTransactionSuccessful(); + } finally { + RepoDb.endTransation(); + } + + mInstalledModules = modules; + synchronized (this) { + mIsReloading = false; + } + + for (ModuleListener listener : mListeners) { + listener.onInstalledModulesReloaded(mInstance); + } + } + + public InstalledModule reloadSingleModule(String packageName) { + PackageInfo pkg; + try { + pkg = mPm.getPackageInfo(packageName, PackageManager.GET_META_DATA); + } catch (NameNotFoundException e) { + RepoDb.deleteInstalledModule(packageName); + InstalledModule old = mInstalledModules.remove(packageName); + if (old != null) { + for (ModuleListener listener : mListeners) { + listener.onSingleInstalledModuleReloaded(mInstance, packageName, null); + } + } + return null; + } + + ApplicationInfo app = pkg.applicationInfo; + if (app.enabled && app.metaData != null && app.metaData.containsKey("xposedmodule")) { + InstalledModule module = new InstalledModule(pkg, false); + RepoDb.insertInstalledModule(module); + mInstalledModules.put(packageName, module); + for (ModuleListener listener : mListeners) { + listener.onSingleInstalledModuleReloaded(mInstance, packageName, + module); + } + return module; + } else { + RepoDb.deleteInstalledModule(packageName); + InstalledModule old = mInstalledModules.remove(packageName); + if (old != null) { + for (ModuleListener listener : mListeners) { + listener.onSingleInstalledModuleReloaded(mInstance, packageName, null); + } + } + return null; + } + } + + public InstalledModule getFramework() { + return mFramework; + } + + public String getFrameworkPackageName() { + return mFrameworkPackageName; + } + + public boolean isFramework(String packageName) { + return mFrameworkPackageName.equals(packageName); + } + + public InstalledModule getModule(String packageName) { + return mInstalledModules.get(packageName); + } + + public Map getModules() { + return mInstalledModules; + } + + public void setModuleEnabled(String packageName, boolean enabled) { + if (enabled) + mPref.edit().putInt(packageName, 1).apply(); + else + mPref.edit().remove(packageName).apply(); + } + + public boolean isModuleEnabled(String packageName) { + return mPref.contains(packageName); + } + + public List getEnabledModules() { + LinkedList result = new LinkedList(); + + for (String packageName : mPref.getAll().keySet()) { + InstalledModule module = getModule(packageName); + if (module != null) + result.add(module); + else + setModuleEnabled(packageName, false); + } + + return result; + } + + public synchronized void updateModulesList(boolean showToast) { + try { + Log.i(XposedApp.TAG, "updating modules.list"); + int installedXposedVersion = XposedApp.getInstalledXposedVersion(); + + PrintWriter modulesList = new PrintWriter(MODULES_LIST_FILE); + PrintWriter enabledModulesList = new PrintWriter(XposedApp.ENABLED_MODULES_LIST_FILE); + + List enabledModules = getEnabledModules(); + for (InstalledModule module : enabledModules) { + if (module.minVersion > installedXposedVersion || module.minVersion < MIN_MODULE_VERSION) + continue; + + modulesList.println(module.app.sourceDir); + try { + String installer = mPm.getInstallerPackageName(module.app.packageName); + if (!PLAY_STORE_PACKAGE.equals(installer)) { + enabledModulesList.println(module.app.packageName); + } + } catch (IllegalArgumentException ignored) { + // In rare cases, the package might not be installed anymore at this point, + // so the PackageManager can't return its installer package name. + } + } + modulesList.close(); + enabledModulesList.close(); + + FileUtils.setPermissions(MODULES_LIST_FILE, 00664, -1, -1); + FileUtils.setPermissions(XposedApp.ENABLED_MODULES_LIST_FILE, 00664, -1, -1); + + if (showToast) + showToast(R.string.xposed_module_list_updated); + } catch (IOException e) { + Log.e(XposedApp.TAG, "cannot write " + MODULES_LIST_FILE, e); + Toast.makeText(mApp, "cannot write " + MODULES_LIST_FILE + e, Toast.LENGTH_SHORT).show(); + } + } + + private void showToast(int message) { + if (mToast != null) { + mToast.cancel(); + mToast = null; + } + mToast = Toast.makeText(mApp, mApp.getString(message), Toast.LENGTH_SHORT); + mToast.show(); + } + + public void addListener(ModuleListener listener) { + if (!mListeners.contains(listener)) + mListeners.add(listener); + } + + public void removeListener(ModuleListener listener) { + mListeners.remove(listener); + } + + public interface ModuleListener { + /** + * Called whenever one (previously or now) installed module has been + * reloaded + */ + void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module); + + /** + * Called whenever all installed modules have been reloaded + */ + void onInstalledModulesReloaded(ModuleUtil moduleUtil); + } + + public class InstalledModule { + private static final int FLAG_FORWARD_LOCK = 1 << 29; + public final String packageName; + public final boolean isFramework; + public final String versionName; + public final int versionCode; + public final int minVersion; + public ApplicationInfo app; + private String appName; // loaded lazyily + private String description; // loaded lazyily + + private Drawable.ConstantState iconCache = null; + + private InstalledModule(PackageInfo pkg, boolean isFramework) { + this.app = pkg.applicationInfo; + this.packageName = pkg.packageName; + this.isFramework = isFramework; + this.versionName = pkg.versionName; + this.versionCode = pkg.versionCode; + + if (isFramework) { + this.minVersion = 0; + this.description = ""; + } else { + Object minVersionRaw = app.metaData.get("xposedminversion"); + if (minVersionRaw instanceof Integer) { + this.minVersion = (Integer) minVersionRaw; + } else if (minVersionRaw instanceof String) { + this.minVersion = extractIntPart((String) minVersionRaw); + } else { + this.minVersion = 0; + } + } + } + + public boolean isInstalledOnExternalStorage() { + return (app.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0; + } + + /** + * @hide + */ + public boolean isForwardLocked() { + return (app.flags & FLAG_FORWARD_LOCK) != 0; + } + + public String getAppName() { + if (appName == null) + appName = app.loadLabel(mPm).toString(); + return appName; + } + + public String getDescription() { + if (this.description == null) { + Object descriptionRaw = app.metaData.get("xposeddescription"); + String descriptionTmp = null; + if (descriptionRaw instanceof String) { + descriptionTmp = ((String) descriptionRaw).trim(); + } else if (descriptionRaw instanceof Integer) { + try { + int resId = (Integer) descriptionRaw; + if (resId != 0) + descriptionTmp = mPm.getResourcesForApplication(app).getString(resId).trim(); + } catch (Exception ignored) { + } + } + this.description = (descriptionTmp != null) ? descriptionTmp : ""; + } + return this.description; + } + + public boolean isUpdate(ModuleVersion version) { + return (version != null) && version.code > versionCode; + } + + public Drawable getIcon() { + if (iconCache != null) + return iconCache.newDrawable(); + + Intent mIntent = new Intent(Intent.ACTION_MAIN); + mIntent.addCategory(ModulesFragment.SETTINGS_CATEGORY); + mIntent.setPackage(app.packageName); + List ris = mPm.queryIntentActivities(mIntent, 0); + + Drawable result; + if (ris == null || ris.size() <= 0) + result = app.loadIcon(mPm); + else + result = ris.get(0).activityInfo.loadIcon(mPm); + iconCache = result.getConstantState(); + + return result; + } + + @Override + public String toString() { + return getAppName(); + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/NavUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/NavUtil.java new file mode 100644 index 000000000..2daca66b0 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/NavUtil.java @@ -0,0 +1,63 @@ +package de.robv.android.xposed.installer.util; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.provider.Browser; +import android.support.annotation.AnyThread; +import android.support.annotation.NonNull; +import android.support.customtabs.CustomTabsIntent; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.URLSpan; +import android.text.util.Linkify; + +import com.afollestad.materialdialogs.MaterialDialog; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; + +public final class NavUtil { + + public static Uri parseURL(String str) { + if (str == null || str.isEmpty()) + return null; + + Spannable spannable = new SpannableString(str); + Linkify.addLinks(spannable, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES); + URLSpan spans[] = spannable.getSpans(0, spannable.length(), URLSpan.class); + return (spans.length > 0) ? Uri.parse(spans[0].getURL()) : null; + } + + public static void startURL(Activity activity, Uri uri) { + if (!XposedApp.getPreferences().getBoolean("chrome_tabs", true)) { + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName()); + activity.startActivity(intent); + return; + } + + CustomTabsIntent.Builder customTabsIntent = new CustomTabsIntent.Builder(); + customTabsIntent.setShowTitle(true); + customTabsIntent.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary)); + customTabsIntent.build().launchUrl(activity, uri); + } + + public static void startURL(Activity activity, String url) { + startURL(activity, parseURL(url)); + } + + @AnyThread + public static void showMessage(final @NonNull Context context, final CharSequence message) { + XposedApp.runOnUiThread(new Runnable() { + @Override + public void run() { + new MaterialDialog.Builder(context) + .content(message) + .positiveText(android.R.string.ok) + .show(); + } + }); + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/NotificationUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/NotificationUtil.java new file mode 100644 index 000000000..5953b754b --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/NotificationUtil.java @@ -0,0 +1,177 @@ +package de.robv.android.xposed.installer.util; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import android.widget.Toast; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.WelcomeActivity; +import de.robv.android.xposed.installer.XposedApp; + +public final class NotificationUtil { + public static final int NOTIFICATION_MODULE_NOT_ACTIVATED_YET = 0; + public static final int NOTIFICATION_MODULES_UPDATED = 1; + private static final int PENDING_INTENT_OPEN_MODULES = 0; + private static final int PENDING_INTENT_OPEN_INSTALL = 1; + private static final int PENDING_INTENT_SOFT_REBOOT = 2; + private static final int PENDING_INTENT_REBOOT = 3; + private static final int PENDING_INTENT_ACTIVATE_MODULE_AND_REBOOT = 4; + private static final int PENDING_INTENT_ACTIVATE_MODULE = 5; + private static Context sContext = null; + private static NotificationManager sNotificationManager; + + public static void init() { + if (sContext != null) + throw new IllegalStateException( + "NotificationUtil has already been initialized"); + + sContext = XposedApp.getInstance(); + sNotificationManager = (NotificationManager) sContext.getSystemService(Context.NOTIFICATION_SERVICE); + } + + public static void cancel(int id) { + sNotificationManager.cancel(id); + } + + public static void cancelAll() { + sNotificationManager.cancelAll(); + } + + public static void showNotActivatedNotification(String packageName, + String appName) { + Intent intent = new Intent(sContext, WelcomeActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra("fragment", 1); + + PendingIntent pModulesTab = PendingIntent.getActivity(sContext, PENDING_INTENT_OPEN_MODULES, intent, PendingIntent.FLAG_UPDATE_CURRENT); + + String title = sContext.getString(R.string.module_is_not_activated_yet); + NotificationCompat.Builder builder = new NotificationCompat.Builder(sContext).setContentTitle(title).setContentText(appName) + .setTicker(title).setContentIntent(pModulesTab) + .setVibrate(new long[]{0}).setAutoCancel(true) + .setSmallIcon(R.drawable.ic_notification) + .setColor(sContext.getResources().getColor(R.color.colorPrimary)); + + if (Build.VERSION.SDK_INT >= 21) + builder.setPriority(2); + + Intent iActivateAndReboot = new Intent(sContext, RebootReceiver.class); + iActivateAndReboot.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE, packageName); + PendingIntent pActivateAndReboot = PendingIntent.getBroadcast(sContext, + PENDING_INTENT_ACTIVATE_MODULE_AND_REBOOT, iActivateAndReboot, + PendingIntent.FLAG_UPDATE_CURRENT); + + Intent iActivate = new Intent(sContext, RebootReceiver.class); + iActivate.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE, packageName); + iActivate.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE_AND_RETURN, true); + PendingIntent pActivate = PendingIntent.getBroadcast(sContext, + PENDING_INTENT_ACTIVATE_MODULE, iActivate, + PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle(); + notiStyle.setBigContentTitle(title); + notiStyle.bigText(sContext.getString( + R.string.module_is_not_activated_yet_detailed, appName)); + builder.setStyle(notiStyle); + + // Only show the quick activation button if any module has been + // enabled before, + // to ensure that the user know the way to disable the module later. + if (!ModuleUtil.getInstance().getEnabledModules().isEmpty()) { + builder.addAction(R.drawable.ic_menu_refresh, sContext.getString(R.string.activate_and_reboot), pActivateAndReboot); + builder.addAction(R.drawable.ic_save, sContext.getString(R.string.activate_only), pActivate); + } + + sNotificationManager.notify(packageName, + NOTIFICATION_MODULE_NOT_ACTIVATED_YET, builder.build()); + } + + @SuppressWarnings("deprecation") + public static void showModulesUpdatedNotification() { + Intent intent = new Intent(sContext, WelcomeActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra("fragment", 0); + + PendingIntent pInstallTab = PendingIntent.getActivity(sContext, + PENDING_INTENT_OPEN_INSTALL, intent, + PendingIntent.FLAG_UPDATE_CURRENT); + + String title = sContext + .getString(R.string.xposed_module_updated_notification_title); + String message = sContext + .getString(R.string.xposed_module_updated_notification); + NotificationCompat.Builder builder = new NotificationCompat.Builder( + sContext).setContentTitle(title).setContentText(message) + .setTicker(title).setContentIntent(pInstallTab) + .setVibrate(new long[]{0}).setAutoCancel(true) + .setSmallIcon(R.drawable.ic_notification) + .setColor(sContext.getResources().getColor(R.color.colorPrimary)); + + if (Build.VERSION.SDK_INT >= 21) + builder.setPriority(2); + + Intent iSoftReboot = new Intent(sContext, RebootReceiver.class); + iSoftReboot.putExtra(RebootReceiver.EXTRA_SOFT_REBOOT, true); + PendingIntent pSoftReboot = PendingIntent.getBroadcast(sContext, + PENDING_INTENT_SOFT_REBOOT, iSoftReboot, + PendingIntent.FLAG_UPDATE_CURRENT); + + Intent iReboot = new Intent(sContext, RebootReceiver.class); + PendingIntent pReboot = PendingIntent.getBroadcast(sContext, + PENDING_INTENT_REBOOT, iReboot, + PendingIntent.FLAG_UPDATE_CURRENT); + + builder.addAction(0, sContext.getString(R.string.reboot), pReboot); + builder.addAction(0, sContext.getString(R.string.soft_reboot), + pSoftReboot); + + sNotificationManager.notify(null, NOTIFICATION_MODULES_UPDATED, builder.build()); + } + + public static class RebootReceiver extends BroadcastReceiver { + public static String EXTRA_SOFT_REBOOT = "soft"; + public static String EXTRA_ACTIVATE_MODULE = "activate_module"; + public static String EXTRA_ACTIVATE_MODULE_AND_RETURN = "activate_module_and_return"; + + @Override + public void onReceive(Context context, Intent intent) { + /* + * Close the notification bar in order to see the toast that module + * was enabled successfully. Furthermore, if SU permissions haven't + * been granted yet, the SU dialog will be prompted behind the + * expanded notification panel and is therefore not visible to the + * user. + */ + sContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + cancelAll(); + + if (intent.hasExtra(EXTRA_ACTIVATE_MODULE)) { + String packageName = intent.getStringExtra(EXTRA_ACTIVATE_MODULE); + ModuleUtil moduleUtil = ModuleUtil.getInstance(); + moduleUtil.setModuleEnabled(packageName, true); + moduleUtil.updateModulesList(false); + Toast.makeText(sContext, R.string.module_activated, Toast.LENGTH_SHORT).show(); + + if (intent.hasExtra(EXTRA_ACTIVATE_MODULE_AND_RETURN)) return; + } + + RootUtil rootUtil = new RootUtil(); + if (!rootUtil.startShell()) { + Log.e(XposedApp.TAG, "Could not start root shell"); + return; + } + + boolean isSoftReboot = intent.getBooleanExtra(EXTRA_SOFT_REBOOT, false); + rootUtil.reboot(isSoftReboot ? RootUtil.RebootMode.SOFT : RootUtil.RebootMode.NORMAL, + new RootUtil.LogLineCallback()); + + AssetUtil.removeBusybox(); + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/OnlineLoader.java b/app/src/main/java/de/robv/android/xposed/installer/util/OnlineLoader.java new file mode 100644 index 000000000..c0b8d897e --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/OnlineLoader.java @@ -0,0 +1,45 @@ +package de.robv.android.xposed.installer.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.support.annotation.CallSuper; + +import de.robv.android.xposed.installer.XposedApp; + +public abstract class OnlineLoader extends Loader { + protected SharedPreferences mPref = XposedApp.getPreferences(); + protected String mPrefKeyLastUpdateCheck = CLASS_NAME + "_last_update_check"; + protected int mUpdateFrequency = 24 * 60 * 60 * 1000; + + private static final ConnectivityManager sConMgr + = (ConnectivityManager) XposedApp.getInstance().getSystemService(Context.CONNECTIVITY_SERVICE); + + protected boolean shouldUpdate() { + long now = System.currentTimeMillis(); + long lastUpdateCheck = mPref.getLong(mPrefKeyLastUpdateCheck, 0); + if (now < lastUpdateCheck + mUpdateFrequency) { + return false; + } + + NetworkInfo netInfo = sConMgr.getActiveNetworkInfo(); + if (netInfo == null || !netInfo.isConnected()) { + return false; + } + + mPref.edit().putLong(mPrefKeyLastUpdateCheck, now).apply(); + return true; + } + + @CallSuper + @Override + protected void onClear() { + resetLastUpdateCheck(); + } + + public void resetLastUpdateCheck() { + mPref.edit().remove(mPrefKeyLastUpdateCheck).apply(); + } + +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/PrefixedSharedPreferences.java b/app/src/main/java/de/robv/android/xposed/installer/util/PrefixedSharedPreferences.java new file mode 100644 index 000000000..0a5831408 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/PrefixedSharedPreferences.java @@ -0,0 +1,160 @@ +package de.robv.android.xposed.installer.util; + +import android.annotation.SuppressLint; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class PrefixedSharedPreferences implements SharedPreferences { + private final SharedPreferences mBase; + private final String mPrefix; + + public PrefixedSharedPreferences(SharedPreferences base, String prefix) { + mBase = base; + mPrefix = prefix + "_"; + } + + public static void injectToPreferenceManager(PreferenceManager manager, String prefix) { + SharedPreferences prefixedPrefs = new PrefixedSharedPreferences(manager.getSharedPreferences(), prefix); + + try { + Field fieldSharedPref = PreferenceManager.class.getDeclaredField("mSharedPreferences"); + fieldSharedPref.setAccessible(true); + fieldSharedPref.set(manager, prefixedPrefs); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + @Override + public Map getAll() { + Map baseResult = mBase.getAll(); + Map prefixedResult = new HashMap(baseResult); + for (Entry entry : baseResult.entrySet()) { + prefixedResult.put(mPrefix + entry.getKey(), entry.getValue()); + } + return prefixedResult; + } + + @Override + public String getString(String key, String defValue) { + return mBase.getString(mPrefix + key, defValue); + } + + @Override + public Set getStringSet(String key, Set defValues) { + return mBase.getStringSet(mPrefix + key, defValues); + } + + @Override + public int getInt(String key, int defValue) { + return mBase.getInt(mPrefix + key, defValue); + } + + @Override + public long getLong(String key, long defValue) { + return mBase.getLong(mPrefix + key, defValue); + } + + @Override + public float getFloat(String key, float defValue) { + return mBase.getFloat(mPrefix + key, defValue); + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + return mBase.getBoolean(mPrefix + key, defValue); + } + + @Override + public boolean contains(String key) { + return mBase.contains(mPrefix + key); + } + + @SuppressLint("CommitPrefEdits") + @Override + public Editor edit() { + return new EditorImpl(mBase.edit()); + } + + @Override + public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException("listeners are not supported in this implementation"); + } + + @Override + public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException("listeners are not supported in this implementation"); + } + + private class EditorImpl implements Editor { + private final Editor mEditorBase; + + public EditorImpl(Editor base) { + mEditorBase = base; + } + + @Override + public Editor putString(String key, String value) { + mEditorBase.putString(mPrefix + key, value); + return this; + } + + @Override + public Editor putStringSet(String key, Set values) { + mEditorBase.putStringSet(mPrefix + key, values); + return this; + } + + @Override + public Editor putInt(String key, int value) { + mEditorBase.putInt(mPrefix + key, value); + return this; + } + + @Override + public Editor putLong(String key, long value) { + mEditorBase.putLong(mPrefix + key, value); + return this; + } + + @Override + public Editor putFloat(String key, float value) { + mEditorBase.putFloat(mPrefix + key, value); + return this; + } + + @Override + public Editor putBoolean(String key, boolean value) { + mEditorBase.putBoolean(mPrefix + key, value); + return this; + } + + @Override + public Editor remove(String key) { + mEditorBase.remove(mPrefix + key); + return this; + } + + @Override + public Editor clear() { + mEditorBase.clear(); + return this; + } + + @Override + public boolean commit() { + return mEditorBase.commit(); + } + + @Override + public void apply() { + mEditorBase.apply(); + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/RepoLoader.java b/app/src/main/java/de/robv/android/xposed/installer/util/RepoLoader.java new file mode 100644 index 000000000..32799c0b4 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/RepoLoader.java @@ -0,0 +1,312 @@ +package de.robv.android.xposed.installer.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.zip.GZIPInputStream; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.repo.Module; +import de.robv.android.xposed.installer.repo.ModuleVersion; +import de.robv.android.xposed.installer.repo.ReleaseType; +import de.robv.android.xposed.installer.repo.RepoDb; +import de.robv.android.xposed.installer.repo.RepoParser; +import de.robv.android.xposed.installer.repo.RepoParser.RepoParserCallback; +import de.robv.android.xposed.installer.repo.Repository; +import de.robv.android.xposed.installer.util.DownloadsUtil.SyncDownloadInfo; + +public class RepoLoader extends OnlineLoader { + private static final String DEFAULT_REPOSITORIES = "http://dl.xposed.info/repo/full.xml.gz"; + private static RepoLoader mInstance = null; + private static final XposedApp sApp = XposedApp.getInstance(); + private final Map mLocalReleaseTypesCache = new HashMap<>(); + private SharedPreferences mModulePref; + private Map mRepositories = null; + private ReleaseType mGlobalReleaseType; + + private RepoLoader() { + mInstance = this; + mPref = sApp.getSharedPreferences("repo", Context.MODE_PRIVATE); + mPrefKeyLastUpdateCheck = "last_update_check"; + mModulePref = sApp.getSharedPreferences("module_settings", Context.MODE_PRIVATE); + mGlobalReleaseType = ReleaseType.fromString(XposedApp.getPreferences().getString("release_type_global", "stable")); + refreshRepositories(); + } + + public static synchronized RepoLoader getInstance() { + if (mInstance == null) + new RepoLoader(); + return mInstance; + } + + public boolean refreshRepositories() { + mRepositories = RepoDb.getRepositories(); + + // Unlikely case (usually only during initial load): DB state doesn't + // fit to configuration + boolean needReload = false; + String[] config = mPref.getString("repositories", DEFAULT_REPOSITORIES).split("\\|"); + if (mRepositories.size() != config.length) { + needReload = true; + } else { + int i = 0; + for (Repository repo : mRepositories.values()) { + if (!repo.url.equals(config[i++])) { + needReload = true; + break; + } + } + } + + if (!needReload) + return false; + + clear(false); + for (String url : config) { + RepoDb.insertRepository(url); + } + mRepositories = RepoDb.getRepositories(); + return true; + } + + public void setReleaseTypeGlobal(String relTypeString) { + ReleaseType relType = ReleaseType.fromString(relTypeString); + if (mGlobalReleaseType == relType) + return; + + mGlobalReleaseType = relType; + + // Updating the latest version for all modules takes a moment + new Thread("DBUpdate") { + @Override + public void run() { + RepoDb.updateAllModulesLatestVersion(); + notifyListeners(); + } + }.start(); + } + + public void setReleaseTypeLocal(String packageName, String relTypeString) { + ReleaseType relType = (!TextUtils.isEmpty(relTypeString)) ? ReleaseType.fromString(relTypeString) : null; + + if (getReleaseTypeLocal(packageName) == relType) + return; + + synchronized (mLocalReleaseTypesCache) { + mLocalReleaseTypesCache.put(packageName, relType); + } + + RepoDb.updateModuleLatestVersion(packageName); + notifyListeners(); + } + + private ReleaseType getReleaseTypeLocal(String packageName) { + synchronized (mLocalReleaseTypesCache) { + if (mLocalReleaseTypesCache.containsKey(packageName)) + return mLocalReleaseTypesCache.get(packageName); + + String value = mModulePref.getString(packageName + "_release_type", + null); + ReleaseType result = (!TextUtils.isEmpty(value)) ? ReleaseType.fromString(value) : null; + mLocalReleaseTypesCache.put(packageName, result); + return result; + } + } + + public Repository getRepository(long repoId) { + return mRepositories.get(repoId); + } + + public Module getModule(String packageName) { + return RepoDb.getModuleByPackageName(packageName); + } + + public ModuleVersion getLatestVersion(Module module) { + if (module == null || module.versions.isEmpty()) + return null; + + for (ModuleVersion version : module.versions) { + if (version.downloadLink != null && isVersionShown(version)) + return version; + } + return null; + } + + public boolean isVersionShown(ModuleVersion version) { + return version.relType + .ordinal() <= getMaxShownReleaseType(version.module.packageName).ordinal(); + } + + public ReleaseType getMaxShownReleaseType(String packageName) { + ReleaseType localSetting = getReleaseTypeLocal(packageName); + if (localSetting != null) + return localSetting; + else + return mGlobalReleaseType; + } + + @Override + protected void onClear() { + super.onClear(); + RepoDb.deleteRepositories(); + mRepositories = new LinkedHashMap<>(0); + DownloadsUtil.clearCache(null); + } + + public void setRepositories(String... repos) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < repos.length; i++) { + if (i > 0) + sb.append("|"); + sb.append(repos[i]); + } + mPref.edit().putString("repositories", sb.toString()).apply(); + if (refreshRepositories()) + triggerReload(true); + } + + public boolean hasModuleUpdates() { + return RepoDb.hasModuleUpdates(); + } + + public String getFrameworkUpdateVersion() { + return RepoDb.getFrameworkUpdateVersion(); + } + + private File getRepoCacheFile(String repo) { + String filename = "repo_" + HashUtil.md5(repo) + ".xml"; + if (repo.endsWith(".gz")) + filename += ".gz"; + return new File(sApp.getCacheDir(), filename); + } + + @Override + protected boolean onReload() { + final List messages = new LinkedList<>(); + + boolean hasChanged = downloadAndParseFiles(messages); + if (!messages.isEmpty()) { + XposedApp.runOnUiThread(new Runnable() { + public void run() { + for (String message : messages) { + Toast.makeText(sApp, message, Toast.LENGTH_LONG).show(); + } + } + }); + } + + return hasChanged; + } + + private boolean downloadAndParseFiles(List messages) { + // These variables don't need to be atomic, just mutable + final AtomicBoolean hasChanged = new AtomicBoolean(false); + final AtomicInteger insertCounter = new AtomicInteger(); + final AtomicInteger deleteCounter = new AtomicInteger(); + + for (Entry repoEntry : mRepositories.entrySet()) { + final long repoId = repoEntry.getKey(); + final Repository repo = repoEntry.getValue(); + + String url = (repo.partialUrl != null && repo.version != null) ? String.format(repo.partialUrl, repo.version) : repo.url; + + File cacheFile = getRepoCacheFile(url); + SyncDownloadInfo info = DownloadsUtil.downloadSynchronously(url, + cacheFile); + + Log.i(XposedApp.TAG, String.format( + "Downloaded %s with status %d (error: %s), size %d bytes", + url, info.status, info.errorMessage, cacheFile.length())); + + if (info.status != SyncDownloadInfo.STATUS_SUCCESS) { + if (info.errorMessage != null) + messages.add(info.errorMessage); + continue; + } + + InputStream in = null; + RepoDb.beginTransation(); + try { + in = new FileInputStream(cacheFile); + if (url.endsWith(".gz")) + in = new GZIPInputStream(in); + + RepoParser.parse(in, new RepoParserCallback() { + @Override + public void onRepositoryMetadata(Repository repository) { + if (!repository.isPartial) { + RepoDb.deleteAllModules(repoId); + hasChanged.set(true); + } + } + + @Override + public void onNewModule(Module module) { + RepoDb.insertModule(repoId, module); + hasChanged.set(true); + insertCounter.incrementAndGet(); + } + + @Override + public void onRemoveModule(String packageName) { + RepoDb.deleteModule(repoId, packageName); + hasChanged.set(true); + deleteCounter.decrementAndGet(); + } + + @Override + public void onCompleted(Repository repository) { + if (!repository.isPartial) { + RepoDb.updateRepository(repoId, repository); + repo.name = repository.name; + repo.partialUrl = repository.partialUrl; + repo.version = repository.version; + } else { + RepoDb.updateRepositoryVersion(repoId, repository.version); + repo.version = repository.version; + } + + Log.i(XposedApp.TAG, String.format( + "Updated repository %s to version %s (%d new / %d removed modules)", + repo.url, repo.version, insertCounter.get(), + deleteCounter.get())); + } + }); + + RepoDb.setTransactionSuccessful(); + } catch (Throwable t) { + Log.e(XposedApp.TAG, "Cannot load repository from " + url, t); + messages.add(sApp.getString(R.string.repo_load_failed, url, t.getMessage())); + DownloadsUtil.clearCache(url); + } finally { + if (in != null) + try { + in.close(); + } catch (IOException ignored) { + } + cacheFile.delete(); + RepoDb.endTransation(); + } + } + + // TODO Set ModuleColumns.PREFERRED for modules which appear in multiple + // repositories + return hasChanged.get(); + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/RootUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/RootUtil.java new file mode 100644 index 000000000..f6267651d --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/RootUtil.java @@ -0,0 +1,322 @@ +package de.robv.android.xposed.installer.util; + +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.support.annotation.IdRes; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; +import android.text.TextUtils; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.installation.FlashCallback; +import eu.chainfire.libsuperuser.Shell; +import eu.chainfire.libsuperuser.Shell.OnCommandResultListener; + +import static de.robv.android.xposed.installer.util.InstallZipUtil.triggerError; + +public class RootUtil { + private Shell.Interactive mShell = null; + private HandlerThread mCallbackThread = null; + + private boolean mCommandRunning = false; + private int mLastExitCode = -1; + private LineCallback mCallback = null; + + private static final String EMULATED_STORAGE_SOURCE; + private static final String EMULATED_STORAGE_TARGET; + + static { + EMULATED_STORAGE_SOURCE = getEmulatedStorageVariable("EMULATED_STORAGE_SOURCE"); + EMULATED_STORAGE_TARGET = getEmulatedStorageVariable("EMULATED_STORAGE_TARGET"); + } + + public interface LineCallback { + void onLine(String line); + void onErrorLine(String line); + } + + public static class CollectingLineCallback implements LineCallback { + protected List mLines = new LinkedList<>(); + + @Override + public void onLine(String line) { + mLines.add(line); + } + + @Override + public void onErrorLine(String line) { + mLines.add(line); + } + + @Override + public String toString() { + return TextUtils.join("\n", mLines); + } + } + + public static class LogLineCallback implements LineCallback { + @Override + public void onLine(String line) { + Log.i(XposedApp.TAG, line); + } + + @Override + public void onErrorLine(String line) { + Log.e(XposedApp.TAG, line); + } + } + + private static String getEmulatedStorageVariable(String variable) { + String result = System.getenv(variable); + if (result != null) { + result = getCanonicalPath(new File(result)); + if (!result.endsWith("/")) { + result += "/"; + } + } + return result; + } + + + private final Shell.OnCommandResultListener mOpenListener = new Shell.OnCommandResultListener() { + @Override + public void onCommandResult(int commandCode, int exitCode, List output) { + mStdoutListener.onCommandResult(commandCode, exitCode); + } + }; + + private final Shell.OnCommandLineListener mStdoutListener = new Shell.OnCommandLineListener() { + public void onLine(String line) { + if (mCallback != null) { + mCallback.onLine(line); + } + } + + @Override + public void onCommandResult(int commandCode, int exitCode) { + mLastExitCode = exitCode; + synchronized (mCallbackThread) { + mCommandRunning = false; + mCallbackThread.notifyAll(); + } + } + }; + + private final Shell.OnCommandLineListener mStderrListener = new Shell.OnCommandLineListener() { + @Override + public void onLine(String line) { + if (mCallback != null) { + mCallback.onErrorLine(line); + } + } + + @Override + public void onCommandResult(int commandCode, int exitCode) { + // Not called for STDERR listener. + } + }; + + private void waitForCommandFinished() { + synchronized (mCallbackThread) { + while (mCommandRunning) { + try { + mCallbackThread.wait(); + } catch (InterruptedException ignored) { + } + } + } + + if (mLastExitCode == OnCommandResultListener.WATCHDOG_EXIT || mLastExitCode == OnCommandResultListener.SHELL_DIED) { + dispose(); + } + } + + /** + * Starts an interactive shell with root permissions. Does nothing if + * already running. + * + * @return true if root access is available, false otherwise + */ + public synchronized boolean startShell() { + if (mShell != null) { + if (mShell.isRunning()) { + return true; + } else { + dispose(); + } + } + + mCallbackThread = new HandlerThread("su callback listener"); + mCallbackThread.start(); + + mCommandRunning = true; + mShell = new Shell.Builder().useSU() + .setHandler(new Handler(mCallbackThread.getLooper())) + .setOnSTDERRLineListener(mStderrListener) + .open(mOpenListener); + + waitForCommandFinished(); + + if (mLastExitCode != OnCommandResultListener.SHELL_RUNNING) { + dispose(); + return false; + } + + return true; + } + + public boolean startShell(FlashCallback flashCallback) { + if (!startShell()) { + triggerError(flashCallback, FlashCallback.ERROR_NO_ROOT_ACCESS); + return false; + } + return true; + } + + /** + * Closes all resources related to the shell. + */ + public synchronized void dispose() { + if (mShell == null) { + return; + } + + try { + mShell.close(); + } catch (Exception ignored) { + } + mShell = null; + + mCallbackThread.quit(); + mCallbackThread = null; + } + + public synchronized int execute(String command, LineCallback callback) { + if (mShell == null) { + throw new IllegalStateException("shell is not running"); + } + + mCallback = callback; + mCommandRunning = true; + mShell.addCommand(command, 0, mStdoutListener); + waitForCommandFinished(); + + return mLastExitCode; + } + + public int executeWithBusybox(String command, LineCallback callback) { + AssetUtil.extractBusybox(); + return execute(AssetUtil.BUSYBOX_FILE.getAbsolutePath() + " " + command, callback); + } + + private static String getCanonicalPath(File file) { + try { + return file.getCanonicalPath(); + } catch (IOException e) { + Log.w(XposedApp.TAG, "Could not get canonical path for " + file); + return file.getAbsolutePath(); + } + } + + public static String getShellPath(File file) { + return getShellPath(getCanonicalPath(file)); + } + + public static String getShellPath(String path) { + if (EMULATED_STORAGE_SOURCE != null && EMULATED_STORAGE_TARGET != null + && path.startsWith(EMULATED_STORAGE_TARGET)) { + path = EMULATED_STORAGE_SOURCE + path.substring(EMULATED_STORAGE_TARGET.length()); + } + return path; + } + + @Override + protected void finalize() throws Throwable { + dispose(); + } + + public enum RebootMode { + NORMAL(R.string.reboot), + SOFT(R.string.soft_reboot), + RECOVERY(R.string.reboot_recovery); + + public final int titleRes; + + RebootMode(@StringRes int titleRes) { + this.titleRes = titleRes; + } + + public static RebootMode fromId(@IdRes int id) { + switch (id) { + case R.id.reboot: + return NORMAL; + case R.id.soft_reboot: + return SOFT; + case R.id.reboot_recovery: + return RECOVERY; + default: + throw new IllegalArgumentException(); + } + } + } + + public static boolean reboot(RebootMode mode, @NonNull Context context) { + RootUtil rootUtil = new RootUtil(); + if (!rootUtil.startShell()) { + NavUtil.showMessage(context, context.getString(R.string.root_failed)); + return false; + } + + LineCallback callback = new CollectingLineCallback(); + if (!rootUtil.reboot(mode, callback)) { + StringBuilder message = new StringBuilder(callback.toString()); + if (message.length() > 0) { + message.append("\n\n"); + } + message.append(context.getString(R.string.reboot_failed)); + NavUtil.showMessage(context, message); + return false; + } + + return true; + } + + public boolean reboot(RebootMode mode, LineCallback callback) { + switch (mode) { + case NORMAL: + return reboot(callback); + case SOFT: + return softReboot(callback); + case RECOVERY: + return rebootToRecovery(callback); + default: + throw new IllegalArgumentException(); + } + } + + private boolean reboot(LineCallback callback) { + return executeWithBusybox("reboot", callback) == 0; + } + + private boolean softReboot(LineCallback callback) { + return execute("setprop ctl.restart surfaceflinger; setprop ctl.restart zygote", callback) == 0; + } + + private boolean rebootToRecovery(LineCallback callback) { + // Create a flag used by some kernels to boot into recovery. + if (execute("ls /cache/recovery", null) != 0) { + executeWithBusybox("mkdir /cache/recovery", callback); + } + executeWithBusybox("touch /cache/recovery/boot", callback); + + return executeWithBusybox("reboot recovery", callback) == 0; + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/RunnableWithParam.java b/app/src/main/java/de/robv/android/xposed/installer/util/RunnableWithParam.java new file mode 100644 index 000000000..6b06814ca --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/RunnableWithParam.java @@ -0,0 +1,5 @@ +package de.robv.android.xposed.installer.util; + +public interface RunnableWithParam { + public void run(T param); +} diff --git a/src/de/robv/android/xposed/installer/util/ThemeUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/ThemeUtil.java similarity index 73% rename from src/de/robv/android/xposed/installer/util/ThemeUtil.java rename to app/src/main/java/de/robv/android/xposed/installer/util/ThemeUtil.java index a844d930b..77f690b66 100644 --- a/src/de/robv/android/xposed/installer/util/ThemeUtil.java +++ b/app/src/main/java/de/robv/android/xposed/installer/util/ThemeUtil.java @@ -3,18 +3,19 @@ import android.content.Context; import android.content.res.Resources.Theme; import android.content.res.TypedArray; + import de.robv.android.xposed.installer.R; import de.robv.android.xposed.installer.XposedApp; import de.robv.android.xposed.installer.XposedBaseActivity; public final class ThemeUtil { - private ThemeUtil() {}; - private static int[] THEMES = new int[] { - R.style.Theme_Light, - R.style.Theme_Dark, - R.style.Theme_Dark_Black, - }; + R.style.Theme_XposedInstaller_Light, + R.style.Theme_XposedInstaller_Dark, + R.style.Theme_XposedInstaller_Dark_Black, }; + + private ThemeUtil() { + } public static int getSelectTheme() { int theme = XposedApp.getPreferences().getInt("theme", 0); @@ -30,11 +31,11 @@ public static void reloadTheme(XposedBaseActivity activity) { int theme = getSelectTheme(); if (theme != activity.mTheme) activity.recreate(); - } + } - public static int getThemeColor(Context context, int id) { - Theme theme = context.getTheme(); - TypedArray a = theme.obtainStyledAttributes(new int[] {id}); + public static int getThemeColor(Context context, int id) { + Theme theme = context.getTheme(); + TypedArray a = theme.obtainStyledAttributes(new int[] { id }); int result = a.getColor(0, 0); a.recycle(); return result; diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabActivityHelper.java b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabActivityHelper.java new file mode 100644 index 000000000..6905a679b --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabActivityHelper.java @@ -0,0 +1,165 @@ +package de.robv.android.xposed.installer.util.chrome; + +import android.app.Activity; +import android.net.Uri; +import android.os.Bundle; +import android.support.customtabs.CustomTabsClient; +import android.support.customtabs.CustomTabsIntent; +import android.support.customtabs.CustomTabsServiceConnection; +import android.support.customtabs.CustomTabsSession; + +import java.util.List; + +/** + * This is a helper class to manage the connection to the Custom Tabs Service. + */ +public class CustomTabActivityHelper implements ServiceConnectionCallback { + private CustomTabsSession mCustomTabsSession; + private CustomTabsClient mClient; + private CustomTabsServiceConnection mConnection; + private ConnectionCallback mConnectionCallback; + + /** + * Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening + * it on a WebView. + * + * @param activity The host activity. + * @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available. + * @param uri the Uri to be opened. + * @param fallback a CustomTabFallback to be used if Custom Tabs is not + * available. + */ + public static void openCustomTab(Activity activity, CustomTabsIntent customTabsIntent, Uri uri, CustomTabFallback fallback) { + String packageName = CustomTabsHelper.getPackageNameToUse(activity); + + // If we cant find a package name, it means theres no browser that + // supports + // Chrome Custom Tabs installed. So, we fallback to the webview + if (packageName == null) { + if (fallback != null) { + fallback.openUri(activity, uri); + } + } else { + customTabsIntent.intent.setPackage(packageName); + customTabsIntent.launchUrl(activity, uri); + } + } + + /** + * Unbinds the Activity from the Custom Tabs Service. + * + * @param activity the activity that is connected to the service. + */ + public void unbindCustomTabsService(Activity activity) { + if (mConnection == null) + return; + activity.unbindService(mConnection); + mClient = null; + mCustomTabsSession = null; + mConnection = null; + } + + /** + * Creates or retrieves an exiting CustomTabsSession. + * + * @return a CustomTabsSession. + */ + public CustomTabsSession getSession() { + if (mClient == null) { + mCustomTabsSession = null; + } else if (mCustomTabsSession == null) { + mCustomTabsSession = mClient.newSession(null); + } + return mCustomTabsSession; + } + + /** + * Register a Callback to be called when connected or disconnected from the + * Custom Tabs Service. + * + * @param connectionCallback + */ + public void setConnectionCallback(ConnectionCallback connectionCallback) { + this.mConnectionCallback = connectionCallback; + } + + /** + * Binds the Activity to the Custom Tabs Service. + * + * @param activity + * the activity to be binded to the service. + */ + public void bindCustomTabsService(Activity activity) { + if (mClient != null) + return; + + String packageName = CustomTabsHelper.getPackageNameToUse(activity); + if (packageName == null) + return; + + mConnection = new ServiceConnection(this); + CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection); + } + + /** + * @return true if call to mayLaunchUrl was accepted. + * @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}. + */ + public boolean mayLaunchUrl(Uri uri, Bundle extras, + List otherLikelyBundles) { + if (mClient == null) + return false; + + CustomTabsSession session = getSession(); + if (session == null) + return false; + + return session.mayLaunchUrl(uri, extras, otherLikelyBundles); + } + + @Override + public void onServiceConnected(CustomTabsClient client) { + mClient = client; + mClient.warmup(0L); + if (mConnectionCallback != null) + mConnectionCallback.onCustomTabsConnected(); + } + + @Override + public void onServiceDisconnected() { + mClient = null; + mCustomTabsSession = null; + if (mConnectionCallback != null) + mConnectionCallback.onCustomTabsDisconnected(); + } + + /** + * A Callback for when the service is connected or disconnected. Use those + * callbacks to handle UI changes when the service is connected or + * disconnected. + */ + public interface ConnectionCallback { + /** + * Called when the service is connected. + */ + void onCustomTabsConnected(); + + /** + * Called when the service is disconnected. + */ + void onCustomTabsDisconnected(); + } + + /** + * To be used as a fallback to open the Uri when Custom Tabs is not + * available. + */ + public interface CustomTabFallback { + /** + * @param activity The Activity that wants to open the Uri. + * @param uri The uri to be opened by the fallback. + */ + void openUri(Activity activity, Uri uri); + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabsHelper.java b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabsHelper.java new file mode 100644 index 000000000..1c843a42d --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabsHelper.java @@ -0,0 +1,128 @@ +package de.robv.android.xposed.installer.util.chrome; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class for Custom Tabs. + */ +public class CustomTabsHelper { + static final String STABLE_PACKAGE = "com.android.chrome"; + static final String BETA_PACKAGE = "com.chrome.beta"; + static final String DEV_PACKAGE = "com.chrome.dev"; + static final String LOCAL_PACKAGE = "com.google.android.apps.chrome"; + private static final String TAG = "CustomTabsHelper"; + private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE = "android.support.customtabs.extra.KEEP_ALIVE"; + private static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService"; + + private static String sPackageNameToUse; + + private CustomTabsHelper() { + } + + /** + * Goes through all apps that handle VIEW intents and have a warmup service. + * Picks the one chosen by the user if there is one, otherwise makes a best + * effort to return a valid package name. + *

    + * This is not threadsafe. + * + * @param context {@link Context} to use for accessing {@link PackageManager}. + * @return The package name recommended to use for connecting to custom tabs + * related components. + */ + public static String getPackageNameToUse(Context context) { + if (sPackageNameToUse != null) + return sPackageNameToUse; + + PackageManager pm = context.getPackageManager(); + // Get default VIEW intent handler. + Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); + ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0); + String defaultViewHandlerPackageName = null; + if (defaultViewHandlerInfo != null) { + defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName; + } + + // Get all apps that can handle VIEW intents. + List resolvedActivityList = pm.queryIntentActivities(activityIntent, 0); + List packagesSupportingCustomTabs = new ArrayList<>(); + for (ResolveInfo info : resolvedActivityList) { + Intent serviceIntent = new Intent(); + serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION); + serviceIntent.setPackage(info.activityInfo.packageName); + if (pm.resolveService(serviceIntent, 0) != null) { + packagesSupportingCustomTabs.add(info.activityInfo.packageName); + } + } + + // Now packagesSupportingCustomTabs contains all apps that can handle + // both VIEW intents + // and service calls. + if (packagesSupportingCustomTabs.isEmpty()) { + sPackageNameToUse = null; + } else if (packagesSupportingCustomTabs.size() == 1) { + sPackageNameToUse = packagesSupportingCustomTabs.get(0); + } else if (!TextUtils.isEmpty(defaultViewHandlerPackageName) + && !hasSpecializedHandlerIntents(context, activityIntent) + && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) { + sPackageNameToUse = defaultViewHandlerPackageName; + } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) { + sPackageNameToUse = STABLE_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) { + sPackageNameToUse = BETA_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) { + sPackageNameToUse = DEV_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) { + sPackageNameToUse = LOCAL_PACKAGE; + } + return sPackageNameToUse; + } + + /** + * Used to check whether there is a specialized handler for a given intent. + * + * @param intent The intent to check with. + * @return Whether there is a specialized handler for the given intent. + */ + private static boolean hasSpecializedHandlerIntents(Context context, + Intent intent) { + try { + PackageManager pm = context.getPackageManager(); + List handlers = pm.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER); + if (handlers == null || handlers.size() == 0) { + return false; + } + for (ResolveInfo resolveInfo : handlers) { + IntentFilter filter = resolveInfo.filter; + if (filter == null) + continue; + if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) + continue; + if (resolveInfo.activityInfo == null) + continue; + return true; + } + } catch (RuntimeException e) { + Log.e(TAG, "Runtime exception while getting specialized handlers"); + } + return false; + } + + /** + * @return All possible chrome package names that provide custom tabs + * feature. + */ + public static String[] getPackages() { + return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE}; + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabsURLSpan.java b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabsURLSpan.java new file mode 100644 index 000000000..9bb9d6f49 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabsURLSpan.java @@ -0,0 +1,26 @@ +package de.robv.android.xposed.installer.util.chrome; + +import android.app.Activity; +import android.text.style.URLSpan; +import android.view.View; + +import de.robv.android.xposed.installer.util.NavUtil; + +/** + * Created by Nikola D. on 12/23/2015. + */ +public class CustomTabsURLSpan extends URLSpan { + + private Activity activity; + + public CustomTabsURLSpan(Activity activity, String url) { + super(url); + this.activity = activity; + } + + @Override + public void onClick(View widget) { + String url = getURL(); + NavUtil.startURL(activity, url); + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/chrome/LinkTransformationMethod.java b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/LinkTransformationMethod.java new file mode 100644 index 000000000..589d8d23a --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/LinkTransformationMethod.java @@ -0,0 +1,50 @@ +package de.robv.android.xposed.installer.util.chrome; + +import android.app.Activity; +import android.graphics.Rect; +import android.text.Spannable; +import android.text.Spanned; +import android.text.method.TransformationMethod; +import android.text.style.URLSpan; +import android.text.util.Linkify; +import android.view.View; +import android.widget.TextView; + +/** + * Created by Nikola D. on 12/23/2015. + */ +public class LinkTransformationMethod implements TransformationMethod { + + private Activity activity; + + public LinkTransformationMethod(Activity activity) { + this.activity = activity; + } + + @Override + public CharSequence getTransformation(CharSequence source, View view) { + if (view instanceof TextView) { + TextView textView = (TextView) view; + Linkify.addLinks(textView, Linkify.WEB_URLS); + if (textView.getText() == null || !(textView.getText() instanceof Spannable)) { + return source; + } + Spannable text = (Spannable) textView.getText(); + URLSpan[] spans = text.getSpans(0, textView.length(), URLSpan.class); + for (int i = spans.length - 1; i >= 0; i--) { + URLSpan oldSpan = spans[i]; + int start = text.getSpanStart(oldSpan); + int end = text.getSpanEnd(oldSpan); + String url = oldSpan.getURL(); + text.removeSpan(oldSpan); + text.setSpan(new CustomTabsURLSpan(activity, url), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + return text; + } + return source; + } + + @Override + public void onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction, Rect previouslyFocusedRect) { + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/chrome/ServiceConnection.java b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/ServiceConnection.java new file mode 100644 index 000000000..f90a1e0f6 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/ServiceConnection.java @@ -0,0 +1,34 @@ +package de.robv.android.xposed.installer.util.chrome; + +import android.content.ComponentName; +import android.support.customtabs.CustomTabsClient; +import android.support.customtabs.CustomTabsServiceConnection; + +import java.lang.ref.WeakReference; + +/** + * Implementation for the CustomTabsServiceConnection that avoids leaking the + * ServiceConnectionCallback + */ +public class ServiceConnection extends CustomTabsServiceConnection { + // A weak reference to the ServiceConnectionCallback to avoid leaking it. + private WeakReference mConnectionCallback; + + public ServiceConnection(ServiceConnectionCallback connectionCallback) { + mConnectionCallback = new WeakReference<>(connectionCallback); + } + + @Override + public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) { + ServiceConnectionCallback connectionCallback = mConnectionCallback.get(); + if (connectionCallback != null) + connectionCallback.onServiceConnected(client); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + ServiceConnectionCallback connectionCallback = mConnectionCallback.get(); + if (connectionCallback != null) + connectionCallback.onServiceDisconnected(); + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/chrome/ServiceConnectionCallback.java b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/ServiceConnectionCallback.java new file mode 100644 index 000000000..1a7c38d8d --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/ServiceConnectionCallback.java @@ -0,0 +1,22 @@ +package de.robv.android.xposed.installer.util.chrome; + +import android.support.customtabs.CustomTabsClient; + +/** + * Callback for events when connecting and disconnecting from Custom Tabs + * Service. + */ +public interface ServiceConnectionCallback { + /** + * Called when the service is connected. + * + * @param client + * a CustomTabsClient + */ + void onServiceConnected(CustomTabsClient client); + + /** + * Called when the service is disconnected. + */ + void onServiceDisconnected(); +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/widget/DownloadView.java b/app/src/main/java/de/robv/android/xposed/installer/widget/DownloadView.java new file mode 100644 index 000000000..33741edef --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/widget/DownloadView.java @@ -0,0 +1,213 @@ +package de.robv.android.xposed.installer.widget; + +import android.app.DownloadManager; +import android.app.Fragment; +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.util.DownloadsUtil; +import de.robv.android.xposed.installer.util.DownloadsUtil.DownloadFinishedCallback; +import de.robv.android.xposed.installer.util.DownloadsUtil.DownloadInfo; + +public class DownloadView extends LinearLayout { + public static String mClickedUrl; + private final Button btnDownload; + private final Button btnDownloadCancel; + private final Button btnInstall; + private final ProgressBar progressBar; + private final TextView txtInfo; + public Fragment fragment; + private DownloadInfo mInfo = null; + private String mUrl = null; + private final Runnable refreshViewRunnable = new Runnable() { + @Override + public void run() { + if (mUrl == null) { + btnDownload.setVisibility(View.GONE); + btnDownloadCancel.setVisibility(View.GONE); + btnInstall.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtInfo.setVisibility(View.VISIBLE); + txtInfo.setText(R.string.download_view_no_url); + } else if (mInfo == null) { + btnDownload.setVisibility(View.VISIBLE); + btnDownloadCancel.setVisibility(View.GONE); + btnInstall.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtInfo.setVisibility(View.GONE); + } else { + switch (mInfo.status) { + case DownloadManager.STATUS_PENDING: + case DownloadManager.STATUS_PAUSED: + case DownloadManager.STATUS_RUNNING: + btnDownload.setVisibility(View.GONE); + btnDownloadCancel.setVisibility(View.VISIBLE); + btnInstall.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + txtInfo.setVisibility(View.VISIBLE); + if (mInfo.totalSize <= 0 || mInfo.status != DownloadManager.STATUS_RUNNING) { + progressBar.setIndeterminate(true); + txtInfo.setText(R.string.download_view_waiting); + } else { + progressBar.setIndeterminate(false); + progressBar.setMax(mInfo.totalSize); + progressBar.setProgress(mInfo.bytesDownloaded); + txtInfo.setText(getContext().getString( + R.string.download_view_running, + mInfo.bytesDownloaded / 1024, + mInfo.totalSize / 1024)); + } + break; + + case DownloadManager.STATUS_FAILED: + btnDownload.setVisibility(View.VISIBLE); + btnDownloadCancel.setVisibility(View.GONE); + btnInstall.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtInfo.setVisibility(View.VISIBLE); + txtInfo.setText(getContext().getString( + R.string.download_view_failed, mInfo.reason)); + break; + + case DownloadManager.STATUS_SUCCESSFUL: + btnDownload.setVisibility(View.GONE); + btnDownloadCancel.setVisibility(View.GONE); + btnInstall.setVisibility(View.VISIBLE); + progressBar.setVisibility(View.GONE); + txtInfo.setVisibility(View.VISIBLE); + txtInfo.setText(R.string.download_view_successful); + break; + } + } + } + }; + private String mTitle = null; + private DownloadFinishedCallback mCallback = null; + + public DownloadView(Context context, final AttributeSet attrs) { + super(context, attrs); + setFocusable(false); + setOrientation(LinearLayout.VERTICAL); + + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.download_view, this, true); + + btnDownload = (Button) findViewById(R.id.btnDownload); + btnDownloadCancel = (Button) findViewById(R.id.btnDownloadCancel); + btnInstall = (Button) findViewById(R.id.btnInstall); + + btnDownload.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mInfo = DownloadsUtil.addModule(getContext(), mTitle, mUrl, mCallback); + refreshViewFromUiThread(); + + if (mInfo != null) + new DownloadMonitor().start(); + } + }); + + btnDownloadCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mInfo == null) + return; + + DownloadsUtil.removeById(getContext(), mInfo.id); + // UI update will happen automatically by the DownloadMonitor + } + }); + + btnInstall.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mCallback == null) + return; + + mCallback.onDownloadFinished(getContext(), mInfo); + } + }); + + progressBar = (ProgressBar) findViewById(R.id.progress); + txtInfo = (TextView) findViewById(R.id.txtInfo); + + refreshViewFromUiThread(); + } + + private void refreshViewFromUiThread() { + refreshViewRunnable.run(); + } + + private void refreshView() { + post(refreshViewRunnable); + } + + public String getUrl() { + return mUrl; + } + + public void setUrl(String url) { + mUrl = url; + + if (mUrl != null) + mInfo = DownloadsUtil.getLatestForUrl(getContext(), mUrl); + else + mInfo = null; + + refreshView(); + } + + public String getTitle() { + return mTitle; + } + + public void setTitle(String title) { + this.mTitle = title; + } + + public DownloadFinishedCallback getDownloadFinishedCallback() { + return mCallback; + } + + public void setDownloadFinishedCallback(DownloadFinishedCallback downloadFinishedCallback) { + this.mCallback = downloadFinishedCallback; + } + + private class DownloadMonitor extends Thread { + public DownloadMonitor() { + super("DownloadMonitor"); + } + + @Override + public void run() { + while (true) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + return; + } + + try { + mInfo = DownloadsUtil.getById(getContext(), mInfo.id); + } catch (NullPointerException ignored) { + } + + refreshView(); + if (mInfo == null) + return; + + if (mInfo.status != DownloadManager.STATUS_PENDING + && mInfo.status != DownloadManager.STATUS_PAUSED + && mInfo.status != DownloadManager.STATUS_RUNNING) + return; + } + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/widget/IconListPreference.java b/app/src/main/java/de/robv/android/xposed/installer/widget/IconListPreference.java new file mode 100644 index 000000000..409f67a5b --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/widget/IconListPreference.java @@ -0,0 +1,118 @@ +package de.robv.android.xposed.installer.widget; + +/* +* Copyright (C) 2013 The Android Open Source Project +* +* 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. +*/ + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.preference.ListPreference; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckedTextView; +import android.widget.ImageView; +import android.widget.ListAdapter; + +import java.util.ArrayList; +import java.util.List; + +import de.robv.android.xposed.installer.R; + +public class IconListPreference extends ListPreference { + + private List mEntryDrawables = new ArrayList<>(); + + public IconListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.IconListPreference, 0, 0); + + CharSequence[] drawables; + + try { + drawables = a.getTextArray(R.styleable.IconListPreference_icons); + } finally { + a.recycle(); + } + + for (CharSequence drawable : drawables) { + int resId = context.getResources().getIdentifier(drawable.toString(), "mipmap", context.getPackageName()); + + Drawable d = context.getResources().getDrawable(resId); + + mEntryDrawables.add(d); + } + + setWidgetLayoutResource(R.layout.color_icon_preview); + } + + protected ListAdapter createListAdapter() { + final String selectedValue = getValue(); + int selectedIndex = findIndexOfValue(selectedValue); + return new AppArrayAdapter(getContext(), R.layout.icon_preference_item, getEntries(), mEntryDrawables, selectedIndex); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + + String selectedValue = getValue(); + int selectedIndex = findIndexOfValue(selectedValue); + + Drawable drawable = mEntryDrawables.get(selectedIndex); + + ((ImageView) view.findViewById(R.id.preview)).setImageDrawable(drawable); + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + builder.setAdapter(createListAdapter(), this); + super.onPrepareDialogBuilder(builder); + } + + public class AppArrayAdapter extends ArrayAdapter { + private List mImageDrawables = null; + private int mSelectedIndex = 0; + + public AppArrayAdapter(Context context, int textViewResourceId, + CharSequence[] objects, List imageDrawables, + int selectedIndex) { + super(context, textViewResourceId, objects); + mSelectedIndex = selectedIndex; + mImageDrawables = imageDrawables; + } + + @Override + @SuppressLint("ViewHolder") + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = ((Activity) getContext()).getLayoutInflater(); + View view = inflater.inflate(R.layout.icon_preference_item, parent, false); + CheckedTextView textView = (CheckedTextView) view.findViewById(R.id.label); + textView.setText(getItem(position)); + textView.setChecked(position == mSelectedIndex); + + ImageView imageView = (ImageView) view.findViewById(R.id.icon); + imageView.setImageDrawable(mImageDrawables.get(position)); + return view; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/widget/IntegerListPreference.java b/app/src/main/java/de/robv/android/xposed/installer/widget/IntegerListPreference.java new file mode 100644 index 000000000..1b61e7d69 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/widget/IntegerListPreference.java @@ -0,0 +1,62 @@ +package de.robv.android.xposed.installer.widget; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.AttributeSet; + +import com.afollestad.materialdialogs.prefs.MaterialListPreference; + +public class IntegerListPreference extends MaterialListPreference { + public IntegerListPreference(Context context) { + super(context); + } + + public IntegerListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public static int getIntValue(String value) { + if (value == null) + return 0; + + return (int) ((value.startsWith("0x")) + ? Long.parseLong(value.substring(2), 16) + : Long.parseLong(value)); + } + + @Override + public void setValue(String value) { + super.setValue(value); + notifyChanged(); + } + + @Override + protected boolean persistString(String value) { + return value != null && persistInt(getIntValue(value)); + + } + + @Override + protected String getPersistedString(String defaultReturnValue) { + SharedPreferences pref = getPreferenceManager().getSharedPreferences(); + String key = getKey(); + if (!shouldPersist() || !pref.contains(key)) + return defaultReturnValue; + + return String.valueOf(pref.getInt(key, 0)); + } + + @Override + public int findIndexOfValue(String value) { + CharSequence[] entryValues = getEntryValues(); + int intValue = getIntValue(value); + if (value != null && entryValues != null) { + for (int i = entryValues.length - 1; i >= 0; i--) { + if (getIntValue(entryValues[i].toString()) == intValue) { + return i; + } + } + } + return -1; + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/widget/ListPreferenceSummaryFix.java b/app/src/main/java/de/robv/android/xposed/installer/widget/ListPreferenceSummaryFix.java new file mode 100644 index 000000000..c7b154e2d --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/widget/ListPreferenceSummaryFix.java @@ -0,0 +1,22 @@ +package de.robv.android.xposed.installer.widget; + +import android.content.Context; +import android.util.AttributeSet; + +import com.afollestad.materialdialogs.prefs.MaterialListPreference; + +public class ListPreferenceSummaryFix extends MaterialListPreference { + public ListPreferenceSummaryFix(Context context) { + super(context); + } + + public ListPreferenceSummaryFix(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void setValue(String value) { + super.setValue(value); + notifyChanged(); + } +} \ No newline at end of file diff --git a/app/src/main/res/animator/fade_in.xml b/app/src/main/res/animator/fade_in.xml new file mode 100644 index 000000000..eec5f8950 --- /dev/null +++ b/app/src/main/res/animator/fade_in.xml @@ -0,0 +1,8 @@ + + diff --git a/app/src/main/res/animator/fade_out.xml b/app/src/main/res/animator/fade_out.xml new file mode 100644 index 000000000..155d82d69 --- /dev/null +++ b/app/src/main/res/animator/fade_out.xml @@ -0,0 +1,8 @@ + + diff --git a/app/src/main/res/drawable-anydpi/ic_action_share.xml b/app/src/main/res/drawable-anydpi/ic_action_share.xml new file mode 100644 index 000000000..d404e01bf --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_action_share.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_android.xml b/app/src/main/res/drawable-anydpi/ic_android.xml new file mode 100644 index 000000000..475ece58a --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_android.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_bookmark.xml b/app/src/main/res/drawable-anydpi/ic_bookmark.xml new file mode 100644 index 000000000..0a84b01a0 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_bookmark.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_bookmark_outline.xml b/app/src/main/res/drawable-anydpi/ic_bookmark_outline.xml new file mode 100644 index 000000000..38d373467 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_bookmark_outline.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_check_circle.xml b/app/src/main/res/drawable-anydpi/ic_check_circle.xml new file mode 100644 index 000000000..6ab5a1757 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_check_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_chip.xml b/app/src/main/res/drawable-anydpi/ic_chip.xml new file mode 100644 index 000000000..fa986516d --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_chip.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_close.xml b/app/src/main/res/drawable-anydpi/ic_close.xml new file mode 100644 index 000000000..ff5c6f1f8 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_cloud.xml b/app/src/main/res/drawable-anydpi/ic_cloud.xml new file mode 100644 index 000000000..0ca5119d0 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_cloud.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_cloud_download.xml b/app/src/main/res/drawable-anydpi/ic_cloud_download.xml new file mode 100644 index 000000000..846ad3947 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_cloud_download.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_cloud_off.xml b/app/src/main/res/drawable-anydpi/ic_cloud_off.xml new file mode 100644 index 000000000..1e753cf4c --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_cloud_off.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_delete.xml b/app/src/main/res/drawable-anydpi/ic_delete.xml new file mode 100644 index 000000000..684d9e14e --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_description.xml b/app/src/main/res/drawable-anydpi/ic_description.xml new file mode 100644 index 000000000..47fe87009 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_description.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_donate.xml b/app/src/main/res/drawable-anydpi/ic_donate.xml new file mode 100644 index 000000000..2d64a828d --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_donate.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_error.xml b/app/src/main/res/drawable-anydpi/ic_error.xml new file mode 100644 index 000000000..b45b40fa5 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_error.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_github.xml b/app/src/main/res/drawable-anydpi/ic_github.xml new file mode 100644 index 000000000..83be6c04e --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_github.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_help.xml b/app/src/main/res/drawable-anydpi/ic_help.xml new file mode 100644 index 000000000..3902588ff --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_help.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_info.xml b/app/src/main/res/drawable-anydpi/ic_info.xml new file mode 100644 index 000000000..2d66b0bf6 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_info.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_language.xml b/app/src/main/res/drawable-anydpi/ic_language.xml new file mode 100644 index 000000000..85b37743a --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_language.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_menu_refresh.xml b/app/src/main/res/drawable-anydpi/ic_menu_refresh.xml new file mode 100644 index 000000000..0ca6efe7b --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_menu_refresh.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_menu_search.xml b/app/src/main/res/drawable-anydpi/ic_menu_search.xml new file mode 100644 index 000000000..3063609d8 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_menu_search.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_menu_sort.xml b/app/src/main/res/drawable-anydpi/ic_menu_sort.xml new file mode 100644 index 000000000..ae9823151 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_menu_sort.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_nav_about.xml b/app/src/main/res/drawable-anydpi/ic_nav_about.xml new file mode 100644 index 000000000..57ab7a702 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_nav_about.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_nav_downloads.xml b/app/src/main/res/drawable-anydpi/ic_nav_downloads.xml new file mode 100644 index 000000000..4518e0e33 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_nav_downloads.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_nav_install.xml b/app/src/main/res/drawable-anydpi/ic_nav_install.xml new file mode 100644 index 000000000..bf0085b6f --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_nav_install.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable-anydpi/ic_nav_logs.xml b/app/src/main/res/drawable-anydpi/ic_nav_logs.xml new file mode 100644 index 000000000..fc4c9f32b --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_nav_logs.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_nav_modules.xml b/app/src/main/res/drawable-anydpi/ic_nav_modules.xml new file mode 100644 index 000000000..80d25de23 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_nav_modules.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_nav_settings.xml b/app/src/main/res/drawable-anydpi/ic_nav_settings.xml new file mode 100644 index 000000000..5372b524f --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_nav_settings.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_nav_support.xml b/app/src/main/res/drawable-anydpi/ic_nav_support.xml new file mode 100644 index 000000000..3ba8726d1 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_nav_support.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_no_connection.xml b/app/src/main/res/drawable-anydpi/ic_no_connection.xml new file mode 100644 index 000000000..0c234a818 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_no_connection.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable-anydpi/ic_no_image.xml b/app/src/main/res/drawable-anydpi/ic_no_image.xml new file mode 100644 index 000000000..53e0a6de2 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_no_image.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_notification.xml b/app/src/main/res/drawable-anydpi/ic_notification.xml new file mode 100644 index 000000000..9e0699c21 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_notification.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_person.xml b/app/src/main/res/drawable-anydpi/ic_person.xml new file mode 100644 index 000000000..234d73d8d --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_person.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_save.xml b/app/src/main/res/drawable-anydpi/ic_save.xml new file mode 100644 index 000000000..a71d98393 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_save.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_scroll_down.xml b/app/src/main/res/drawable-anydpi/ic_scroll_down.xml new file mode 100644 index 000000000..29f6ca4bc --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_scroll_down.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_scroll_top.xml b/app/src/main/res/drawable-anydpi/ic_scroll_top.xml new file mode 100644 index 000000000..08f75dc51 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_scroll_top.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_send.xml b/app/src/main/res/drawable-anydpi/ic_send.xml new file mode 100644 index 000000000..890d89494 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_send.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_warning.xml b/app/src/main/res/drawable-anydpi/ic_warning.xml new file mode 100644 index 000000000..9d06ad046 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_warning.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_warning_grey.xml b/app/src/main/res/drawable-anydpi/ic_warning_grey.xml new file mode 100644 index 000000000..70c96d861 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_warning_grey.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/background_card_black.xml b/app/src/main/res/drawable/background_card_black.xml new file mode 100644 index 000000000..63cc99e37 --- /dev/null +++ b/app/src/main/res/drawable/background_card_black.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/background_card_dark.xml b/app/src/main/res/drawable/background_card_dark.xml new file mode 100644 index 000000000..4c88a51d1 --- /dev/null +++ b/app/src/main/res/drawable/background_card_dark.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/background_card_light.xml b/app/src/main/res/drawable/background_card_light.xml new file mode 100644 index 000000000..026baa95f --- /dev/null +++ b/app/src/main/res/drawable/background_card_light.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/background_card_normal_black.xml b/app/src/main/res/drawable/background_card_normal_black.xml similarity index 72% rename from res/drawable/background_card_normal_black.xml rename to app/src/main/res/drawable/background_card_normal_black.xml index 36fc1a0bb..0f59ec586 100644 --- a/res/drawable/background_card_normal_black.xml +++ b/app/src/main/res/drawable/background_card_normal_black.xml @@ -3,8 +3,9 @@ - + @@ -14,14 +15,16 @@ - + - diff --git a/res/drawable/background_card_normal_dark.xml b/app/src/main/res/drawable/background_card_normal_dark.xml similarity index 72% rename from res/drawable/background_card_normal_dark.xml rename to app/src/main/res/drawable/background_card_normal_dark.xml index e4752ebda..8817501fa 100644 --- a/res/drawable/background_card_normal_dark.xml +++ b/app/src/main/res/drawable/background_card_normal_dark.xml @@ -3,8 +3,9 @@ - + @@ -14,14 +15,16 @@ - + - diff --git a/res/drawable/background_card_normal_light.xml b/app/src/main/res/drawable/background_card_normal_light.xml similarity index 72% rename from res/drawable/background_card_normal_light.xml rename to app/src/main/res/drawable/background_card_normal_light.xml index 171173b20..4740f5a91 100644 --- a/res/drawable/background_card_normal_light.xml +++ b/app/src/main/res/drawable/background_card_normal_light.xml @@ -3,8 +3,9 @@ - + @@ -14,14 +15,16 @@ - + - diff --git a/app/src/main/res/drawable/background_card_pressed_black.xml b/app/src/main/res/drawable/background_card_pressed_black.xml new file mode 100644 index 000000000..a9dfaf498 --- /dev/null +++ b/app/src/main/res/drawable/background_card_pressed_black.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/background_card_pressed_dark.xml b/app/src/main/res/drawable/background_card_pressed_dark.xml new file mode 100644 index 000000000..37d662742 --- /dev/null +++ b/app/src/main/res/drawable/background_card_pressed_dark.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/background_card_pressed_light.xml b/app/src/main/res/drawable/background_card_pressed_light.xml new file mode 100644 index 000000000..471984f04 --- /dev/null +++ b/app/src/main/res/drawable/background_card_pressed_light.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_flash.xml b/app/src/main/res/drawable/ic_flash.xml new file mode 100644 index 000000000..a3c81cc38 --- /dev/null +++ b/app/src/main/res/drawable/ic_flash.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_phone.xml b/app/src/main/res/drawable/ic_phone.xml new file mode 100644 index 000000000..af764eff1 --- /dev/null +++ b/app/src/main/res/drawable/ic_phone.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_verified.xml b/app/src/main/res/drawable/ic_verified.xml new file mode 100644 index 000000000..d136718a7 --- /dev/null +++ b/app/src/main/res/drawable/ic_verified.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/toolbar_shadow.xml b/app/src/main/res/drawable/toolbar_shadow.xml new file mode 100644 index 000000000..7a7a56833 --- /dev/null +++ b/app/src/main/res/drawable/toolbar_shadow.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-v23/list_item_download.xml b/app/src/main/res/layout-v23/list_item_download.xml new file mode 100644 index 000000000..494534e72 --- /dev/null +++ b/app/src/main/res/layout-v23/list_item_download.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_container.xml b/app/src/main/res/layout/activity_container.xml new file mode 100644 index 000000000..593b14b38 --- /dev/null +++ b/app/src/main/res/layout/activity_container.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_download_details.xml b/app/src/main/res/layout/activity_download_details.xml new file mode 100644 index 000000000..f80ad4051 --- /dev/null +++ b/app/src/main/res/layout/activity_download_details.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + diff --git a/res/layout/activity_download_details_not_found.xml b/app/src/main/res/layout/activity_download_details_not_found.xml similarity index 52% rename from res/layout/activity_download_details_not_found.xml rename to app/src/main/res/layout/activity_download_details_not_found.xml index fe33e010b..cfb8b4a05 100644 --- a/res/layout/activity_download_details_not_found.xml +++ b/app/src/main/res/layout/activity_download_details_not_found.xml @@ -1,12 +1,16 @@ + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center" + android:orientation="vertical" + android:padding="8dp" + tools:context=".DownloadDetailsActivity"> + + + android:textAppearance="?android:attr/textAppearanceMedium"/>