diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..ae8d2d3
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,441 @@
+root = true
+
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = tab
+insert_final_newline = true
+trim_trailing_whitespace = true
+max_line_length = 120
+ij_smart_tabs = true
+ij_continuation_indent_size = 4
+ij_formatter_off_tag = @formatter:off
+ij_formatter_on_tag = @formatter:on
+ij_formatter_tags_enabled = true
+ij_visual_guides = none
+ij_wrap_on_typing = false
+
+
+[*.java]
+ij_java_align_consecutive_assignments = false
+ij_java_align_consecutive_variable_declarations = false
+ij_java_align_group_field_declarations = false
+ij_java_align_multiline_annotation_parameters = false
+ij_java_align_multiline_array_initializer_expression = false
+ij_java_align_multiline_assignment = false
+ij_java_align_multiline_binary_operation = false
+ij_java_align_multiline_chained_methods = false
+ij_java_align_multiline_extends_list = false
+ij_java_align_multiline_for = true
+ij_java_align_multiline_method_parentheses = false
+ij_java_align_multiline_parameters = true
+ij_java_align_multiline_parameters_in_calls = false
+ij_java_align_multiline_parenthesized_expression = false
+ij_java_align_multiline_records = true
+ij_java_align_multiline_resources = true
+ij_java_align_multiline_ternary_operation = false
+ij_java_align_multiline_text_blocks = false
+ij_java_align_multiline_throws_list = false
+ij_java_align_subsequent_simple_methods = false
+ij_java_align_throws_keyword = false
+ij_java_align_types_in_multi_catch = true
+ij_java_annotation_parameter_wrap = off
+ij_java_array_initializer_new_line_after_left_brace = false
+ij_java_array_initializer_right_brace_on_new_line = false
+ij_java_array_initializer_wrap = off
+ij_java_assert_statement_colon_on_next_line = false
+ij_java_assert_statement_wrap = off
+ij_java_assignment_wrap = off
+ij_java_binary_operation_sign_on_next_line = false
+ij_java_binary_operation_wrap = off
+ij_java_blank_lines_after_anonymous_class_header = 0
+ij_java_blank_lines_after_class_header = 0
+ij_java_blank_lines_after_imports = 1
+ij_java_blank_lines_after_package = 1
+ij_java_blank_lines_around_class = 1
+ij_java_blank_lines_around_field = 0
+ij_java_blank_lines_around_field_in_interface = 0
+ij_java_blank_lines_around_initializer = 1
+ij_java_blank_lines_around_method = 1
+ij_java_blank_lines_around_method_in_interface = 1
+ij_java_blank_lines_before_class_end = 0
+ij_java_blank_lines_before_imports = 1
+ij_java_blank_lines_before_method_body = 0
+ij_java_blank_lines_before_package = 0
+ij_java_block_brace_style = end_of_line
+ij_java_block_comment_add_space = false
+ij_java_block_comment_at_first_column = true
+ij_java_builder_methods = none
+ij_java_call_parameters_new_line_after_left_paren = false
+ij_java_call_parameters_right_paren_on_new_line = false
+ij_java_call_parameters_wrap = off
+ij_java_case_statement_on_separate_line = true
+ij_java_catch_on_new_line = false
+ij_java_class_annotation_wrap = split_into_lines
+ij_java_class_brace_style = end_of_line
+ij_java_class_count_to_use_import_on_demand = 99
+ij_java_class_names_in_javadoc = 1
+ij_java_do_not_indent_top_level_class_members = false
+ij_java_do_not_wrap_after_single_annotation = false
+ij_java_do_not_wrap_after_single_annotation_in_parameter = false
+ij_java_do_while_brace_force = never
+ij_java_doc_add_blank_line_after_description = true
+ij_java_doc_add_blank_line_after_param_comments = false
+ij_java_doc_add_blank_line_after_return = false
+ij_java_doc_add_p_tag_on_empty_lines = true
+ij_java_doc_align_exception_comments = true
+ij_java_doc_align_param_comments = true
+ij_java_doc_do_not_wrap_if_one_line = false
+ij_java_doc_enable_formatting = true
+ij_java_doc_enable_leading_asterisks = true
+ij_java_doc_indent_on_continuation = false
+ij_java_doc_keep_empty_lines = true
+ij_java_doc_keep_empty_parameter_tag = true
+ij_java_doc_keep_empty_return_tag = true
+ij_java_doc_keep_empty_throws_tag = true
+ij_java_doc_keep_invalid_tags = true
+ij_java_doc_param_description_on_new_line = false
+ij_java_doc_preserve_line_breaks = false
+ij_java_doc_use_throws_not_exception_tag = true
+ij_java_else_on_new_line = false
+ij_java_enum_constants_wrap = off
+ij_java_extends_keyword_wrap = off
+ij_java_extends_list_wrap = off
+ij_java_field_annotation_wrap = split_into_lines
+ij_java_finally_on_new_line = false
+ij_java_for_brace_force = never
+ij_java_for_statement_new_line_after_left_paren = false
+ij_java_for_statement_right_paren_on_new_line = false
+ij_java_for_statement_wrap = off
+ij_java_generate_final_locals = false
+ij_java_generate_final_parameters = false
+ij_java_if_brace_force = never
+ij_java_imports_layout = $android.**, $androidx.**, $com.**, $junit.**, $net.**, $org.**, $java.**, $javax.**, $*, |, android.**, |, androidx.**, |, com.**, |, junit.**, |, net.**, |, org.**, |, java.**, |, javax.**, |, *, |
+ij_java_indent_case_from_switch = true
+ij_java_insert_inner_class_imports = false
+ij_java_insert_override_annotation = true
+ij_java_keep_blank_lines_before_right_brace = 0
+ij_java_keep_blank_lines_between_package_declaration_and_header = 1
+ij_java_keep_blank_lines_in_code = 1
+ij_java_keep_blank_lines_in_declarations = 1
+ij_java_keep_builder_methods_indents = false
+ij_java_keep_control_statement_in_one_line = true
+ij_java_keep_first_column_comment = true
+ij_java_keep_indents_on_empty_lines = false
+ij_java_keep_line_breaks = true
+ij_java_keep_multiple_expressions_in_one_line = false
+ij_java_keep_simple_blocks_in_one_line = false
+ij_java_keep_simple_classes_in_one_line = false
+ij_java_keep_simple_lambdas_in_one_line = false
+ij_java_keep_simple_methods_in_one_line = false
+ij_java_label_indent_absolute = false
+ij_java_label_indent_size = 0
+ij_java_lambda_brace_style = end_of_line
+ij_java_layout_static_imports_separately = false
+ij_java_line_comment_add_space = false
+ij_java_line_comment_add_space_on_reformat = false
+ij_java_line_comment_at_first_column = true
+ij_java_method_annotation_wrap = split_into_lines
+ij_java_method_brace_style = end_of_line
+ij_java_method_call_chain_wrap = off
+ij_java_method_parameters_new_line_after_left_paren = false
+ij_java_method_parameters_right_paren_on_new_line = false
+ij_java_method_parameters_wrap = off
+ij_java_modifier_list_wrap = false
+ij_java_multi_catch_types_wrap = normal
+ij_java_names_count_to_use_import_on_demand = 99
+ij_java_new_line_after_lparen_in_annotation = false
+ij_java_new_line_after_lparen_in_record_header = false
+ij_java_parameter_annotation_wrap = off
+ij_java_parentheses_expression_new_line_after_left_paren = false
+ij_java_parentheses_expression_right_paren_on_new_line = false
+ij_java_place_assignment_sign_on_next_line = false
+ij_java_prefer_longer_names = true
+ij_java_prefer_parameters_wrap = false
+ij_java_record_components_wrap = normal
+ij_java_repeat_synchronized = true
+ij_java_replace_instanceof_and_cast = false
+ij_java_replace_null_check = true
+ij_java_replace_sum_lambda_with_method_ref = true
+ij_java_resource_list_new_line_after_left_paren = false
+ij_java_resource_list_right_paren_on_new_line = false
+ij_java_resource_list_wrap = off
+ij_java_rparen_on_new_line_in_annotation = false
+ij_java_rparen_on_new_line_in_record_header = false
+ij_java_space_after_closing_angle_bracket_in_type_argument = false
+ij_java_space_after_colon = true
+ij_java_space_after_comma = true
+ij_java_space_after_comma_in_type_arguments = true
+ij_java_space_after_for_semicolon = true
+ij_java_space_after_quest = true
+ij_java_space_after_type_cast = true
+ij_java_space_before_annotation_array_initializer_left_brace = false
+ij_java_space_before_annotation_parameter_list = false
+ij_java_space_before_array_initializer_left_brace = false
+ij_java_space_before_catch_keyword = true
+ij_java_space_before_catch_left_brace = true
+ij_java_space_before_catch_parentheses = true
+ij_java_space_before_class_left_brace = true
+ij_java_space_before_colon = true
+ij_java_space_before_colon_in_foreach = true
+ij_java_space_before_comma = false
+ij_java_space_before_do_left_brace = true
+ij_java_space_before_else_keyword = true
+ij_java_space_before_else_left_brace = true
+ij_java_space_before_finally_keyword = true
+ij_java_space_before_finally_left_brace = true
+ij_java_space_before_for_left_brace = true
+ij_java_space_before_for_parentheses = true
+ij_java_space_before_for_semicolon = false
+ij_java_space_before_if_left_brace = true
+ij_java_space_before_if_parentheses = true
+ij_java_space_before_method_call_parentheses = false
+ij_java_space_before_method_left_brace = true
+ij_java_space_before_method_parentheses = false
+ij_java_space_before_opening_angle_bracket_in_type_parameter = false
+ij_java_space_before_quest = true
+ij_java_space_before_switch_left_brace = true
+ij_java_space_before_switch_parentheses = true
+ij_java_space_before_synchronized_left_brace = true
+ij_java_space_before_synchronized_parentheses = true
+ij_java_space_before_try_left_brace = true
+ij_java_space_before_try_parentheses = true
+ij_java_space_before_type_parameter_list = false
+ij_java_space_before_while_keyword = true
+ij_java_space_before_while_left_brace = true
+ij_java_space_before_while_parentheses = true
+ij_java_space_inside_one_line_enum_braces = false
+ij_java_space_within_empty_array_initializer_braces = false
+ij_java_space_within_empty_method_call_parentheses = false
+ij_java_space_within_empty_method_parentheses = false
+ij_java_spaces_around_additive_operators = true
+ij_java_spaces_around_annotation_eq = true
+ij_java_spaces_around_assignment_operators = true
+ij_java_spaces_around_bitwise_operators = true
+ij_java_spaces_around_equality_operators = true
+ij_java_spaces_around_lambda_arrow = true
+ij_java_spaces_around_logical_operators = true
+ij_java_spaces_around_method_ref_dbl_colon = false
+ij_java_spaces_around_multiplicative_operators = true
+ij_java_spaces_around_relational_operators = true
+ij_java_spaces_around_shift_operators = true
+ij_java_spaces_around_type_bounds_in_type_parameters = true
+ij_java_spaces_around_unary_operator = false
+ij_java_spaces_within_angle_brackets = false
+ij_java_spaces_within_annotation_parentheses = false
+ij_java_spaces_within_array_initializer_braces = false
+ij_java_spaces_within_braces = false
+ij_java_spaces_within_brackets = false
+ij_java_spaces_within_cast_parentheses = false
+ij_java_spaces_within_catch_parentheses = false
+ij_java_spaces_within_for_parentheses = false
+ij_java_spaces_within_if_parentheses = false
+ij_java_spaces_within_method_call_parentheses = false
+ij_java_spaces_within_method_parentheses = false
+ij_java_spaces_within_parentheses = false
+ij_java_spaces_within_record_header = false
+ij_java_spaces_within_switch_parentheses = false
+ij_java_spaces_within_synchronized_parentheses = false
+ij_java_spaces_within_try_parentheses = false
+ij_java_spaces_within_while_parentheses = false
+ij_java_special_else_if_treatment = true
+ij_java_subclass_name_suffix = Impl
+ij_java_ternary_operation_signs_on_next_line = false
+ij_java_ternary_operation_wrap = off
+ij_java_test_name_suffix = Test
+ij_java_throws_keyword_wrap = off
+ij_java_throws_list_wrap = off
+ij_java_use_external_annotations = false
+ij_java_use_fq_class_names = false
+ij_java_use_relative_indents = false
+ij_java_use_single_class_imports = true
+ij_java_variable_annotation_wrap = off
+ij_java_visibility = public
+ij_java_while_brace_force = never
+ij_java_while_on_new_line = false
+ij_java_wrap_comments = false
+ij_java_wrap_first_method_in_call_chain = false
+ij_java_wrap_long_lines = false
+
+
+[{*.kt,*.kts}]
+ij_kotlin_align_in_columns_case_branch = false
+ij_kotlin_align_multiline_binary_operation = false
+ij_kotlin_align_multiline_extends_list = false
+ij_kotlin_align_multiline_method_parentheses = false
+ij_kotlin_align_multiline_parameters = false
+ij_kotlin_align_multiline_parameters_in_calls = false
+ij_kotlin_allow_trailing_comma = false
+ij_kotlin_allow_trailing_comma_on_call_site = false
+ij_kotlin_assignment_wrap = normal
+ij_kotlin_blank_lines_after_class_header = 1
+ij_kotlin_blank_lines_around_block_when_branches = 0
+ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
+ij_kotlin_block_comment_add_space = false
+ij_kotlin_block_comment_at_first_column = true
+ij_kotlin_call_parameters_new_line_after_left_paren = true
+ij_kotlin_call_parameters_right_paren_on_new_line = true
+ij_kotlin_call_parameters_wrap = on_every_item
+ij_kotlin_catch_on_new_line = false
+ij_kotlin_class_annotation_wrap = split_into_lines
+ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
+ij_kotlin_continuation_indent_for_chained_calls = false
+ij_kotlin_continuation_indent_for_expression_bodies = false
+ij_kotlin_continuation_indent_in_argument_lists = false
+ij_kotlin_continuation_indent_in_elvis = false
+ij_kotlin_continuation_indent_in_if_conditions = false
+ij_kotlin_continuation_indent_in_parameter_lists = false
+ij_kotlin_continuation_indent_in_supertype_lists = false
+ij_kotlin_else_on_new_line = false
+ij_kotlin_enum_constants_wrap = on_every_item
+ij_kotlin_extends_list_wrap = normal
+ij_kotlin_field_annotation_wrap = split_into_lines
+ij_kotlin_finally_on_new_line = false
+ij_kotlin_if_rparen_on_new_line = true
+ij_kotlin_import_nested_classes = true
+ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
+ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
+ij_kotlin_keep_blank_lines_before_right_brace = 0
+ij_kotlin_keep_blank_lines_in_code = 1
+ij_kotlin_keep_blank_lines_in_declarations = 1
+ij_kotlin_keep_first_column_comment = true
+ij_kotlin_keep_indents_on_empty_lines = false
+ij_kotlin_keep_line_breaks = false
+ij_kotlin_lbrace_on_next_line = false
+ij_kotlin_line_break_after_multiline_when_entry = false
+ij_kotlin_line_comment_add_space = false
+ij_kotlin_line_comment_add_space_on_reformat = false
+ij_kotlin_line_comment_at_first_column = true
+ij_kotlin_method_annotation_wrap = split_into_lines
+ij_kotlin_method_call_chain_wrap = on_every_item
+ij_kotlin_method_parameters_new_line_after_left_paren = true
+ij_kotlin_method_parameters_right_paren_on_new_line = true
+ij_kotlin_method_parameters_wrap = on_every_item
+ij_kotlin_name_count_to_use_star_import = 2147483647
+ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
+ij_kotlin_parameter_annotation_wrap = normal
+ij_kotlin_space_after_comma = true
+ij_kotlin_space_after_extend_colon = true
+ij_kotlin_space_after_type_colon = true
+ij_kotlin_space_before_catch_parentheses = true
+ij_kotlin_space_before_comma = false
+ij_kotlin_space_before_extend_colon = false
+ij_kotlin_space_before_for_parentheses = true
+ij_kotlin_space_before_if_parentheses = true
+ij_kotlin_space_before_lambda_arrow = true
+ij_kotlin_space_before_type_colon = false
+ij_kotlin_space_before_when_parentheses = true
+ij_kotlin_space_before_while_parentheses = true
+ij_kotlin_spaces_around_additive_operators = true
+ij_kotlin_spaces_around_assignment_operators = true
+ij_kotlin_spaces_around_equality_operators = true
+ij_kotlin_spaces_around_function_type_arrow = true
+ij_kotlin_spaces_around_logical_operators = true
+ij_kotlin_spaces_around_multiplicative_operators = true
+ij_kotlin_spaces_around_range = false
+ij_kotlin_spaces_around_relational_operators = true
+ij_kotlin_spaces_around_unary_operator = false
+ij_kotlin_spaces_around_when_arrow = true
+ij_kotlin_use_custom_formatting_for_modifiers = true
+ij_kotlin_variable_annotation_wrap = normal
+ij_kotlin_while_on_new_line = false
+ij_kotlin_wrap_elvis_expressions = 1
+ij_kotlin_wrap_expression_body_functions = 1
+ij_kotlin_wrap_first_method_in_call_chain = true
+
+
+[*.xml]
+ij_xml_align_attributes = false
+ij_xml_align_text = false
+ij_xml_attribute_wrap = normal
+ij_xml_block_comment_add_space = false
+ij_xml_block_comment_at_first_column = true
+ij_xml_keep_blank_lines = 1
+ij_xml_keep_indents_on_empty_lines = false
+ij_xml_keep_line_breaks = false
+ij_xml_keep_line_breaks_in_text = false
+ij_xml_keep_whitespaces = false
+ij_xml_keep_whitespaces_around_cdata = preserve
+ij_xml_keep_whitespaces_inside_cdata = false
+ij_xml_line_comment_at_first_column = true
+ij_xml_space_after_tag_name = false
+ij_xml_space_around_equals_in_attribute = false
+ij_xml_space_inside_empty_tag = false
+ij_xml_text_wrap = normal
+ij_xml_use_custom_settings = true
+
+
+[*.properties]
+ij_properties_align_group_field_declarations = false
+ij_properties_keep_blank_lines = true
+ij_properties_key_value_delimiter = equals
+ij_properties_spaces_around_key_value_delimiter = false
+
+
+[{*.markdown,*.md}]
+ij_markdown_force_one_space_after_blockquote_symbol = true
+ij_markdown_force_one_space_after_header_symbol = true
+ij_markdown_force_one_space_after_list_bullet = true
+ij_markdown_force_one_space_between_words = true
+ij_markdown_insert_quote_arrows_on_wrap = true
+ij_markdown_keep_indents_on_empty_lines = false
+ij_markdown_keep_line_breaks_inside_text_blocks = true
+ij_markdown_max_lines_around_block_elements = 1
+ij_markdown_max_lines_around_header = 1
+ij_markdown_max_lines_between_paragraphs = 1
+ij_markdown_min_lines_around_block_elements = 1
+ij_markdown_min_lines_around_header = 1
+ij_markdown_min_lines_between_paragraphs = 1
+ij_markdown_wrap_text_if_long = true
+ij_markdown_wrap_text_inside_blockquotes = true
+
+
+[*.yml]
+indent_size = 2
+ij_yaml_align_values_properties = do_not_align
+ij_yaml_autoinsert_sequence_marker = true
+ij_yaml_block_mapping_on_new_line = false
+ij_yaml_indent_sequence_value = true
+ij_yaml_keep_indents_on_empty_lines = false
+ij_yaml_keep_line_breaks = true
+ij_yaml_sequence_on_new_line = false
+ij_yaml_space_before_colon = false
+ij_yaml_spaces_within_braces = true
+ij_yaml_spaces_within_brackets = true
+
+
+[{*.bash,*.sh,*.zsh}]
+ij_shell_binary_ops_start_line = false
+ij_shell_keep_column_alignment_padding = false
+ij_shell_minify_program = false
+ij_shell_redirect_followed_by_space = false
+ij_shell_switch_cases_indented = false
+ij_shell_use_unix_line_separator = true
+
+
+[*.json]
+ij_json_array_wrapping = split_into_lines
+ij_json_keep_blank_lines_in_code = 0
+ij_json_keep_indents_on_empty_lines = true
+ij_json_keep_line_breaks = true
+ij_json_keep_trailing_comma = false
+ij_json_object_wrapping = split_into_lines
+ij_json_property_alignment = do_not_align
+ij_json_space_after_colon = true
+ij_json_space_after_comma = true
+ij_json_space_before_colon = false
+ij_json_space_before_comma = false
+ij_json_spaces_within_braces = false
+ij_json_spaces_within_brackets = false
+ij_json_wrap_long_lines = false
+
+
+[.editorconfig]
+ij_editorconfig_align_group_field_declarations = true
+ij_editorconfig_space_after_colon = false
+ij_editorconfig_space_after_comma = true
+ij_editorconfig_space_before_colon = false
+ij_editorconfig_space_before_comma = false
+ij_editorconfig_spaces_around_assignment_operators = true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c12e55b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+.gradle
+build/
+
+local.properties
+
+*.log
+.idea/
+.cxx
+*.iml
+
+*.DS_Store
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..32bcb54
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,190 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ Copyright 2024 Blink Commerce Private Limited (formerly known as Grofers India Private Limited)
+
+ 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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8dbec42
--- /dev/null
+++ b/README.md
@@ -0,0 +1,187 @@
+
+
+![Droid Dex](./assets/logo.png)
+
+
+
+## Introduction
+
+Droid Dex is a powerful tool crafted to enhance the performance of your Android applications, ultimately elevating the
+user experience. With a focus on addressing key performance issues, it is your solution for addressing prevalent
+challenges like Jerky(Janky) Scrolling, Out of Memory errors (OOMs), High Battery Consumption, and instances of
+Application Not Responding (ANR).
+
+It classifies and lets you analyze Android Device Performance across various parameters like:
+
+| PARAMETER | DESCRIPTION |
+|----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------|
+| [CPU](./droid-dex/src/main/kotlin/com/blinkit/droiddex/cpu/CpuPerformanceManager.kt)
| Total RAM, Core Count, CPU Frequency |
+| [MEMORY](./droid-dex/src/main/kotlin/com/blinkit/droiddex/memory/MemoryPerformanceManager.kt)
| Heap Limit, Heap Remaining, Available RAM |
+| [NETWORK](./droid-dex/src/main/kotlin/com/blinkit/droiddex/network/NetworkPerformanceManager.kt)
| Bandwidth Strength, Download Speed, Signal Strength |
+| [STORAGE](./droid-dex/src/main/kotlin/com/blinkit/droiddex/storage/StoragePerformanceManager.kt)
| Available Storage |
+| [BATTERY](./droid-dex/src/main/kotlin/com/blinkit/droiddex/battery/BatteryPerformanceManager.kt)
| Percentage Remaining, If Phone is Charging or Not |
+
+into various [levels](./droid-dex/src/main/kotlin/com/blinkit/droiddex/constants/PerformanceLevel.kt): EXCELLENT, HIGH,
+AVERAGE, LOW
+
+It is a compact library accompanied by extensive in-line documentation, providing users with the opportunity to delve
+into the code, comprehend each line thoroughly, and, ideally, contribute to its development.
+
+## Use Cases
+
+1. Consider a scenario where background polling of an API is necessary. In this context, the `BATTERY` level becomes a
+ crucial factor, as frequent polling can significantly drain the device's battery. To address this concern, you can
+ optimize the process using the following code snippet:
+
+ ```Kotlin
+ DroidDex.getPerformanceLevelLd(PerformanceClass.BATTERY).observe(this) {
+ // Adjust the polling time interval
+ }
+ ```
+
+2. Consider a scenario where you need to tailor the image quality for users based on their devices. In this context, the
+ `NETWORK` condition plays a crucial role in decision-making, as achieving better image quality typically involves
+ larger file sizes and increased data transfer. However, `MEMORY` is also a consideration, as higher-quality images
+ generate heavier bitmaps, consuming more memory. To optimize this process, you can use the following code snippet:
+
+ ```Kotlin
+ DroidDex.getWeightedPerformanceLevelLd(PerformanceClass.NETWORK to 2F, PerformanceClass.MEMORY to 1F).observe(this) {
+ // Implement image quality optimization
+ }
+ ```
+
+## Usage
+
+Initialize the library in your Application class using the following code snippet:
+
+```Kotlin
+DroidDex.init(this, BuildConfig.DEBUG)
+```
+
+The first parameter requires the `Application Context`, while the second parameter determines whether the user is in
+`Debug Mode`, enabling extensive logging.
+
+1. To get performance level for single/multiple parameters:
+
+ ```Kotlin
+ DroidDex.getPerformanceLevel(params)
+ ```
+
+ For observing the changes:
+
+ ```Kotlin
+ DroidDex.getPerformanceLevelLd(params).observe(this) {
+ }
+ ```
+
+ Replace `params` with comma separated list of `Performance Class(es)`.
+
+ Example:
+ ```Kotlin
+ DroidDex.getPerformanceLevel(PerformanceClass.CPU, PerformanceClass.MEMORY)
+ ```
+
+2. To get performance level for multiple parameters with unequal weights:
+
+ ```Kotlin
+ DroidDex.getWeightedPerformanceLevel(params)
+ ```
+
+ For observing the changes:
+
+ ```Kotlin
+ DroidDex.getWeightedPerformanceLevelLd(params).observe(this) {
+ }
+ ```
+
+ Replace `params` with comma separated list of `Performance Classes` to their `Weights`.
+
+ Example:
+ ```Kotlin
+ DroidDex.getWeightedPerformanceLevelLd(PerformanceClass.CPU to 2F, PerformanceClass.MEMORY to 1F).observe(this) {
+ }
+ ```
+
+
+
+
+
+See [Example Project](https://github.com/grofers/droid-dex/tree/main/example) for Further Usage
+
+## Download
+
+
+Kotlin DSL
+
+Add this to your root `settings.gradle.kts`
+
+```Kotlin
+dependencyResolutionManagement {
+ repositories {
+ maven {
+ url = URI("https://maven.pkg.github.com/grofers/*")
+ credentials {
+ username = "Blinkit"
+ password = GITHUB_PERSONAL_ACCESS_TOKEN
+ }
+ }
+ }
+}
+```
+
+And add this dependency to your project level `build.gradle.kts`:
+
+```Kotlin
+dependencies {
+ implementation("com.blinkit.kits:droid-dex:x.y.z")
+}
+```
+
+
+
+
+Groovy
+
+Add this to your root `settings.gradle`
+
+```Groovy
+dependencyResolutionManagement {
+ repositories {
+ maven {
+ url "https://maven.pkg.github.com/grofers/*"
+ credentials {
+ username = "Blinkit"
+ password = GITHUB_PERSONAL_ACCESS_TOKEN
+ }
+ }
+ }
+}
+```
+
+And add this dependency to your project level `build.gradle`:
+
+```Groovy
+dependencies {
+ implementation "com.blinkit.kits:droid-dex:x.y.z"
+}
+```
+
+
+
+## License
+
+
+Copyright 2024 Blink Commerce Private Limited (formerly known as Grofers India Private Limited)
+
+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.
+
diff --git a/assets/example.png b/assets/example.png
new file mode 100644
index 0000000..a0c9fbb
Binary files /dev/null and b/assets/example.png differ
diff --git a/assets/logo.png b/assets/logo.png
new file mode 100644
index 0000000..bf3a80d
Binary files /dev/null and b/assets/logo.png differ
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..3cf5684
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,8 @@
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
+
+ alias(libs.plugins.jetbrains.kotlin.android) apply false
+
+ alias(libs.plugins.maven.publish) apply false
+}
diff --git a/droid-dex/.gitignore b/droid-dex/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/droid-dex/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/droid-dex/build.gradle.kts b/droid-dex/build.gradle.kts
new file mode 100644
index 0000000..5cdd314
--- /dev/null
+++ b/droid-dex/build.gradle.kts
@@ -0,0 +1,64 @@
+@file:Suppress("UnstableApiUsage")
+
+import com.vanniktech.maven.publish.AndroidSingleVariantLibrary
+import java.net.URI
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrains.kotlin.android)
+
+ alias(libs.plugins.maven.publish)
+}
+
+
+publishing {
+ repositories {
+ maven {
+ name = "Blinkit"
+ url = URI("https://maven.pkg.github.com/grofers/droid-dex")
+ credentials {
+ username = "Blinkit"
+ password = System.getenv("READ_ARTIFACTS_TOKEN")
+ }
+ }
+ }
+}
+
+
+mavenPublishing {
+ configure(AndroidSingleVariantLibrary("release", sourcesJar = true, publishJavadocJar = true))
+}
+
+
+android {
+ namespace = "com.blinkit.droiddex"
+
+ compileSdk = libs.versions.compileSdk.get().toInt()
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes { release { isMinifyEnabled = false } }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ }
+ kotlinOptions { jvmTarget = libs.versions.java.get() }
+}
+
+
+dependencies {
+ implementation(libs.timber)
+
+ implementation(libs.bundles.core)
+
+ implementation(libs.kotlinx.coroutines.android)
+
+ implementation(libs.bundles.lifecycle)
+
+ implementation(libs.androidx.core.performance)
+}
diff --git a/droid-dex/consumer-rules.pro b/droid-dex/consumer-rules.pro
new file mode 100644
index 0000000..424633e
--- /dev/null
+++ b/droid-dex/consumer-rules.pro
@@ -0,0 +1 @@
+-keepattributes SourceFile,LineNumberTable
diff --git a/droid-dex/src/main/AndroidManifest.xml b/droid-dex/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f2cb9ae
--- /dev/null
+++ b/droid-dex/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/DroidDex.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/DroidDex.kt
new file mode 100644
index 0000000..3d54c25
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/DroidDex.kt
@@ -0,0 +1,98 @@
+package com.blinkit.droiddex
+
+import android.content.Context
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.blinkit.droiddex.constants.PerformanceClass
+import com.blinkit.droiddex.constants.PerformanceClass.Companion.name
+import com.blinkit.droiddex.constants.PerformanceLevel
+import com.blinkit.droiddex.factory.factory.PerformanceManagerFactory
+import com.blinkit.droiddex.utils.Logger
+import com.blinkit.droiddex.utils.getPerformanceLevelLdWithWeights
+import com.blinkit.droiddex.utils.getPerformanceLevelWithWeights
+
+object DroidDex {
+
+ private lateinit var performanceManagerFactory: PerformanceManagerFactory
+
+ private val logger by lazy { Logger() }
+
+ fun init(applicationContext: Context, isInDebugMode: Boolean = false) {
+ if (::performanceManagerFactory.isInitialized) {
+ logger.logError(IllegalStateException("Droid Dex is already initialized"))
+ return
+ }
+ logger.isInDebugMode = isInDebugMode
+
+ try {
+ performanceManagerFactory = PerformanceManagerFactory(applicationContext, isInDebugMode)
+ } catch (e: Exception) {
+ logger.logError(e)
+ }
+ }
+
+ /**
+ * @param classes spread list of performance classes
+ * @return Average PerformanceLevel for the input classes
+ *
+ * Steps for getting PerformanceLevel:
+ * - Check if the class has been initialized or not
+ * - Get the PerformanceLevel of individual PerformanceClass
+ * - Ignore the ones for which the PerformanceLevel is UNKNOWN
+ * - Take average of all these PerformanceClass
+ */
+ fun getPerformanceLevel(@PerformanceClass vararg classes: Int): PerformanceLevel =
+ getWeightedPerformanceLevel(*classes.map { Pair(it, 1F) }.toTypedArray())
+
+ /**
+ * @param classes spread list of performance classes
+ * @return LiveData of Average PerformanceLevel for the input classes
+ *
+ * Steps for getting PerformanceLevel:
+ * - Check if the class has been initialized or not
+ * - Get the PerformanceLevel of individual PerformanceClass
+ * - Ignore the ones for which the PerformanceLevel is UNKNOWN
+ * - Take average of all these PerformanceClass
+ */
+ fun getPerformanceLevelLd(@PerformanceClass vararg classes: Int): LiveData =
+ getWeightedPerformanceLevelLd(*classes.map { Pair(it, 1F) }.toTypedArray())
+
+ /**
+ * @param classes spread list of performance classes mapped with weight
+ * @return Weighted Average PerformanceLevel for the input classes
+ *
+ * Steps for getting PerformanceLevel:
+ * - Check if the class has been initialized or not
+ * - Get the PerformanceLevel of individual PerformanceClass
+ * - Ignore the ones for which the PerformanceLevel is UNKNOWN
+ * - Take weighted average of all these PerformanceClass
+ */
+ fun getWeightedPerformanceLevel(vararg classes: Pair<@PerformanceClass Int, Float>): PerformanceLevel =
+ checkInitialized(*classes.map { it.first }.toIntArray()) ?: getPerformanceLevelWithWeights(classes.map {
+ Pair(performanceManagerFactory.getPerformanceLevel(it.first), it.second)
+ }).also { logger.logPerformanceLevelResult(*classes, performanceLevel = it) }
+
+ /**
+ * @param classes spread list of performance classes mapped with weight
+ * @return LiveData of Weighted Average PerformanceLevel for the input classes
+ *
+ * Steps for getting PerformanceLevel:
+ * - Check if the class has been initialized or not
+ * - Get the PerformanceLevel of individual PerformanceClass
+ * - Ignore the ones for which the PerformanceLevel is UNKNOWN
+ * - Take weighted average of all these PerformanceClass
+ */
+ fun getWeightedPerformanceLevelLd(vararg classes: Pair<@PerformanceClass Int, Float>): LiveData =
+ checkInitialized(*classes.map { it.first }.toIntArray())?.let { MutableLiveData(it) }
+ ?: getPerformanceLevelLdWithWeights(classes.map {
+ Pair(performanceManagerFactory.getPerformanceLevelLd(it.first), it.second)
+ }) { logger.logPerformanceLevelResult(*classes, performanceLevel = it) }
+
+ private fun checkInitialized(@PerformanceClass vararg classes: Int): PerformanceLevel? {
+ if (::performanceManagerFactory.isInitialized) return null
+
+ val classesNames = classes.joinToString(", ") { it.name() }
+ logger.logError(UninitializedPropertyAccessException("Droid Dex is not initialized for parameters: $classesNames"))
+ return PerformanceLevel.UNKNOWN
+ }
+}
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/battery/BatteryPerformanceManager.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/battery/BatteryPerformanceManager.kt
new file mode 100644
index 0000000..53c0980
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/battery/BatteryPerformanceManager.kt
@@ -0,0 +1,50 @@
+package com.blinkit.droiddex.battery
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.BatteryManager
+import com.blinkit.droiddex.constants.PerformanceClass
+import com.blinkit.droiddex.constants.PerformanceLevel
+import com.blinkit.droiddex.factory.base.PerformanceManager
+import com.blinkit.droiddex.factory.providers.PerformanceManagerProvider
+
+internal class BatteryPerformanceManager(
+ private val applicationContext: Context, isInDebugMode: Boolean
+): PerformanceManager(isInDebugMode) {
+
+ override fun getPerformanceClass() = PerformanceClass.BATTERY
+
+ override fun getDelayInSecs() = DELAY_IN_SECS
+
+ override fun measurePerformanceLevel(): PerformanceLevel {
+ val batteryStatusIntent =
+ IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { applicationContext.registerReceiver(null, it) }
+ batteryStatusIntent ?: return PerformanceLevel.UNKNOWN
+
+ val isCharging = batteryStatusIntent.getIntExtra(BatteryManager.EXTRA_STATUS, -1).let {
+ it == BatteryManager.BATTERY_STATUS_CHARGING || it == BatteryManager.BATTERY_STATUS_FULL
+ }.also { logDebug("IS BATTERY CHARGING: $it") }
+
+ val batteryPercentage = batteryStatusIntent.let { intent ->
+ val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1).takeUnless { it == -1 } ?: return@let -1F
+ val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1).takeUnless { it == -1 } ?: return@let -1F
+ level * 100 / scale.toFloat()
+ }.also { logDebug("BATTERY PERCENTAGE: $it%") }
+
+ return when {
+ batteryPercentage >= 80 || (isCharging && batteryPercentage >= 70) -> PerformanceLevel.EXCELLENT
+ batteryPercentage >= 55 || (isCharging && batteryPercentage >= 50) -> PerformanceLevel.HIGH
+ batteryPercentage >= 40 || (isCharging && batteryPercentage >= 35) -> PerformanceLevel.AVERAGE
+ else -> PerformanceLevel.LOW
+ }
+ }
+
+ companion object: PerformanceManagerProvider {
+
+ override fun create(applicationContext: Context, isInDebugMode: Boolean): PerformanceManager =
+ BatteryPerformanceManager(applicationContext, isInDebugMode)
+
+ private const val DELAY_IN_SECS = 60F
+ }
+}
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/constants/PerformanceClass.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/constants/PerformanceClass.kt
new file mode 100644
index 0000000..18668e3
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/constants/PerformanceClass.kt
@@ -0,0 +1,43 @@
+package com.blinkit.droiddex.constants
+
+import androidx.annotation.IntDef
+import androidx.annotation.Keep
+
+@Keep
+@IntDef(
+ PerformanceClass.CPU,
+ PerformanceClass.MEMORY,
+ PerformanceClass.STORAGE,
+ PerformanceClass.NETWORK,
+ PerformanceClass.BATTERY
+)
+@Retention(AnnotationRetention.SOURCE)
+@Target(
+ AnnotationTarget.PROPERTY,
+ AnnotationTarget.VALUE_PARAMETER,
+ AnnotationTarget.CLASS,
+ AnnotationTarget.TYPE,
+ AnnotationTarget.FUNCTION
+)
+annotation class PerformanceClass {
+
+ companion object {
+
+ const val CPU = 0
+ const val MEMORY = 1
+ const val STORAGE = 2
+ const val NETWORK = 3
+ const val BATTERY = 4
+
+ internal fun values(): List = listOf(CPU, MEMORY, STORAGE, NETWORK, BATTERY)
+
+ fun @PerformanceClass Int.name() = when (this) {
+ CPU -> "CPU"
+ MEMORY -> "MEMORY"
+ STORAGE -> "STORAGE"
+ NETWORK -> "NETWORK"
+ BATTERY -> "BATTERY"
+ else -> this.toString()
+ }
+ }
+}
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/constants/PerformanceLevel.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/constants/PerformanceLevel.kt
new file mode 100644
index 0000000..618f432
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/constants/PerformanceLevel.kt
@@ -0,0 +1,15 @@
+package com.blinkit.droiddex.constants
+
+import androidx.annotation.Keep
+
+@Keep
+enum class PerformanceLevel(val level: Int) {
+
+ UNKNOWN(0), LOW(1), AVERAGE(2), HIGH(3), EXCELLENT(4);
+
+ companion object {
+
+ internal fun getPerformanceLevel(level: Int? = null): PerformanceLevel =
+ PerformanceLevel.values().find { it.level == level } ?: UNKNOWN
+ }
+}
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/cpu/CpuPerformanceManager.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/cpu/CpuPerformanceManager.kt
new file mode 100644
index 0000000..b05fefb
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/cpu/CpuPerformanceManager.kt
@@ -0,0 +1,93 @@
+package com.blinkit.droiddex.cpu
+
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.core.performance.DevicePerformance
+import com.blinkit.droiddex.constants.PerformanceClass
+import com.blinkit.droiddex.constants.PerformanceLevel
+import com.blinkit.droiddex.cpu.utils.CpuInfoManager
+import com.blinkit.droiddex.factory.base.PerformanceManager
+import com.blinkit.droiddex.factory.providers.PerformanceManagerProvider
+import com.blinkit.droiddex.utils.getApproxHeapLimitInMB
+import com.blinkit.droiddex.utils.getTotalRamInGB
+import java.util.Locale
+
+/**
+ * Inspired By Telegram's CPU Division
+ */
+internal class CpuPerformanceManager(
+ private val applicationContext: Context, isInDebugMode: Boolean
+): PerformanceManager(isInDebugMode) {
+
+ private val cpuInfoManager by lazy { CpuInfoManager(logger) }
+
+ private val lowSocModels = intArrayOf(
+ -1775228513, // EXYNOS 850
+ 802464304, // EXYNOS 7872
+ 802464333, // EXYNOS 7880
+ 802464302, // EXYNOS 7870
+ 2067362118, // MSM8953
+ 2067362060, // MSM8937
+ 2067362084, // MSM8940
+ 2067362241, // MSM8992
+ 2067362117, // MSM8952
+ 2067361998, // MSM8917
+ -1853602818 // SDM439
+ )
+
+ private val excellentMediaPerformanceClasses =
+ intArrayOf(Build.VERSION_CODES.S, Build.VERSION_CODES.S_V2, Build.VERSION_CODES.TIRAMISU)
+
+ override fun getPerformanceClass() = PerformanceClass.CPU
+
+ override fun getDelayInSecs() = DELAY_IN_SECS
+
+ override fun measurePerformanceLevel(): PerformanceLevel {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ val socModel = getBuildSocModel()
+ if (lowSocModels.any { it == socModel }) {
+ logInfo("SOC MODEL: $socModel IN LIST OF LOW SOC MODELS")
+ return PerformanceLevel.LOW
+ }
+ }
+
+ val coresCount = cpuInfoManager.noOfCores.also { logger.logDebug("CORE COUNT: $it") }
+
+ val maxCpuFreq = cpuInfoManager.maxCpuFreqInMHz
+
+ val ramInGB = getTotalRamInGB(applicationContext, logger)
+
+ val androidVersion = getAndroidVersion()
+
+ val mediaPerformanceClass = getMediaPerformanceClass()
+
+ val approxHeapLimitInMB = getApproxHeapLimitInMB(applicationContext, logger)
+
+ return if (mediaPerformanceClass in excellentMediaPerformanceClasses || ramInGB >= 12) {
+ PerformanceLevel.EXCELLENT
+ } else if (androidVersion < 21 || coresCount <= 2 || approxHeapLimitInMB <= 100 || coresCount <= 4 && maxCpuFreq <= 1250 || coresCount <= 4 && maxCpuFreq <= 1600 && approxHeapLimitInMB <= 128 && androidVersion <= 21 || coresCount <= 4 && maxCpuFreq <= 1300 && approxHeapLimitInMB <= 128 && androidVersion <= 24 || ramInGB <= 2) {
+ PerformanceLevel.LOW
+ } else if (coresCount < 8 || approxHeapLimitInMB <= 160 || maxCpuFreq <= 2055 || coresCount == 8 && androidVersion <= 23 || ramInGB <= 6) {
+ PerformanceLevel.AVERAGE
+ } else {
+ PerformanceLevel.HIGH
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.S)
+ private fun getBuildSocModel() = Build.SOC_MODEL.uppercase(Locale.getDefault()).hashCode()
+
+ private fun getMediaPerformanceClass() =
+ DevicePerformance.create(applicationContext).mediaPerformanceClass.also { logDebug("MEDIA PERFORMANCE CLASS: $it") }
+
+ private fun getAndroidVersion() = Build.VERSION.SDK_INT.also { logDebug("ANDROID VERSION: $it") }
+
+ companion object: PerformanceManagerProvider {
+
+ override fun create(applicationContext: Context, isInDebugMode: Boolean): PerformanceManager =
+ CpuPerformanceManager(applicationContext, isInDebugMode)
+
+ private const val DELAY_IN_SECS = 60F
+ }
+}
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/cpu/utils/CpuInfoManager.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/cpu/utils/CpuInfoManager.kt
new file mode 100644
index 0000000..1fe041e
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/cpu/utils/CpuInfoManager.kt
@@ -0,0 +1,86 @@
+package com.blinkit.droiddex.cpu.utils
+
+import com.blinkit.droiddex.utils.Logger
+import com.blinkit.droiddex.utils.average
+import java.io.BufferedReader
+import java.io.File
+import java.io.FileReader
+import java.lang.Integer.max
+import java.util.regex.Pattern
+
+internal class CpuInfoManager(private val logger: Logger) {
+
+ private val coresFreqList = mutableListOf()
+
+ val noOfCores: Int by lazy {
+ max(
+ runCatching {
+ val files = File(CPU_INFO_PATH).listFiles { pathname -> Pattern.matches("cpu[0-9]+", pathname.name) }
+ max(1, files?.size ?: 1)
+ }.getOrDefault(1), Runtime.getRuntime().availableProcessors()
+ )
+ }
+
+ val currentCpuUsage: Int
+ get() = (coresFreqList.map { it.currentUsagePercent }.average()?.toInt()
+ ?: 0).also { logger.logDebug("CURRENT USAGE: ${it}%") }
+
+ val maxCpuFreqInMHz: Int
+ get() = ((coresFreqList.map { it.max }.average()?.toLong()?.takeIf { it > 0 }?.div(1000)?.toInt())
+ ?: Int.MAX_VALUE).also { logger.logDebug("MAX CPU FREQUENCY: $it MHz") }
+
+ val minCpuFreqInMHz: Int
+ get() = ((coresFreqList.map { it.min }.average()?.toLong()?.takeIf { it > 0 }?.div(1000)?.toInt())
+ ?: Int.MAX_VALUE).also { logger.logDebug("MIN CPU FREQUENCY: $it MHz") }
+
+ init {
+ for (i in 0 until noOfCores) coresFreqList.add(CoreFreq(i))
+ }
+
+ private class CoreFreq(private val index: Int) {
+
+ var min = 0L
+ get() = field.takeIf { it > 0L } ?: getMinFreq().also { field = it }
+ private set
+
+ var max = 0L
+ get() = field.takeIf { it > 0L } ?: getMaxFreq().also { field = it }
+ private set
+
+ private var cur = 0L
+
+ init {
+ min = getMinFreq()
+ max = getMaxFreq()
+ }
+
+ val currentUsagePercent: Int
+ get() {
+ cur = getCurFreq()
+
+ var percent = 0
+ if (max - min > 0 && max > 0 && cur > 0) {
+ percent = ((cur - min) * 100 / (max - min)).toInt()
+ }
+
+ return max(percent, 0)
+ }
+
+ private fun getCurFreq() = readFile("${CPU_INFO_PATH}cpu$index/cpufreq/scaling_cur_freq")
+
+ private fun getMinFreq() = readFile("${CPU_INFO_PATH}cpu$index/cpufreq/cpuinfo_min_freq")
+
+ private fun getMaxFreq() = readFile("${CPU_INFO_PATH}cpu$index/cpufreq/cpuinfo_max_freq")
+
+ private fun readFile(path: String): Long = try {
+ BufferedReader(FileReader(path)).use { reader -> reader.readLine()?.toLongOrNull() }
+ } catch (e: Exception) {
+ null
+ } ?: 0
+ }
+
+ companion object {
+
+ private const val CPU_INFO_PATH = "/sys/devices/system/cpu/"
+ }
+}
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/factory/base/PerformanceManager.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/factory/base/PerformanceManager.kt
new file mode 100644
index 0000000..8f9c5a7
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/factory/base/PerformanceManager.kt
@@ -0,0 +1,44 @@
+package com.blinkit.droiddex.factory.base
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.blinkit.droiddex.constants.PerformanceClass
+import com.blinkit.droiddex.constants.PerformanceLevel
+import com.blinkit.droiddex.utils.Logger
+import com.blinkit.droiddex.utils.runAsyncPeriodically
+
+internal abstract class PerformanceManager(private val isInDebugMode: Boolean) {
+
+ private val _performanceLevelLd = MutableLiveData(PerformanceLevel.UNKNOWN)
+ val performanceLevelLd: LiveData
+ get() = _performanceLevelLd
+
+ protected val logger by lazy { Logger(isInDebugMode, getPerformanceClass()) }
+
+ fun init() {
+ runAsyncPeriodically({
+ try {
+ measurePerformanceLevel().also {
+ val hasPerformanceLevelChanged = performanceLevelLd.value != it
+ if (hasPerformanceLevelChanged) _performanceLevelLd.postValue(it)
+ logger.logPerformanceLevelChange(it, hasPerformanceLevelChanged)
+ }
+ } catch (e: Exception) {
+ logger.logError(e)
+ }
+ }, delayInSecs = getDelayInSecs())
+ }
+
+ @PerformanceClass
+ protected abstract fun getPerformanceClass(): Int
+
+ protected abstract fun getDelayInSecs(): Float
+
+ protected abstract fun measurePerformanceLevel(): PerformanceLevel
+
+ protected fun logInfo(message: String) = logger.logInfo(message)
+
+ protected fun logDebug(message: String) = logger.logDebug(message)
+
+ protected fun logError(throwable: Throwable) = logger.logError(throwable)
+}
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/factory/factory/PerformanceManagerFactory.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/factory/factory/PerformanceManagerFactory.kt
new file mode 100644
index 0000000..4faf69a
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/factory/factory/PerformanceManagerFactory.kt
@@ -0,0 +1,39 @@
+package com.blinkit.droiddex.factory.factory
+
+import android.content.Context
+import androidx.lifecycle.LiveData
+import com.blinkit.droiddex.battery.BatteryPerformanceManager
+import com.blinkit.droiddex.constants.PerformanceClass
+import com.blinkit.droiddex.constants.PerformanceLevel
+import com.blinkit.droiddex.cpu.CpuPerformanceManager
+import com.blinkit.droiddex.factory.base.PerformanceManager
+import com.blinkit.droiddex.memory.MemoryPerformanceManager
+import com.blinkit.droiddex.network.NetworkPerformanceManager
+import com.blinkit.droiddex.storage.StoragePerformanceManager
+
+internal class PerformanceManagerFactory(private val applicationContext: Context, private val isInDebugMode: Boolean) {
+
+ private val performanceManagerMap = mutableMapOf<@PerformanceClass Int, PerformanceManager>()
+
+ init {
+ PerformanceClass.values().forEach { getOrPut(it) }
+ }
+
+ fun getPerformanceLevel(@PerformanceClass performanceClass: Int): PerformanceLevel =
+ getOrPut(performanceClass).performanceLevelLd.value ?: PerformanceLevel.UNKNOWN
+
+ fun getPerformanceLevelLd(@PerformanceClass performanceClass: Int): LiveData =
+ getOrPut(performanceClass).performanceLevelLd
+
+ private fun getOrPut(@PerformanceClass performanceClass: Int): PerformanceManager =
+ performanceManagerMap.getOrPut(performanceClass) {
+ when (performanceClass) {
+ PerformanceClass.CPU -> CpuPerformanceManager.create(applicationContext, isInDebugMode)
+ PerformanceClass.MEMORY -> MemoryPerformanceManager.create(applicationContext, isInDebugMode)
+ PerformanceClass.STORAGE -> StoragePerformanceManager.create(applicationContext, isInDebugMode)
+ PerformanceClass.NETWORK -> NetworkPerformanceManager.create(applicationContext, isInDebugMode)
+ PerformanceClass.BATTERY -> BatteryPerformanceManager.create(applicationContext, isInDebugMode)
+ else -> throw IllegalArgumentException("NO SUCH PERFORMANCE CLASS EXISTS: $performanceClass")
+ }.apply { init() }
+ }
+}
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/factory/providers/PerformanceManagerProvider.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/factory/providers/PerformanceManagerProvider.kt
new file mode 100644
index 0000000..127cb54
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/factory/providers/PerformanceManagerProvider.kt
@@ -0,0 +1,9 @@
+package com.blinkit.droiddex.factory.providers
+
+import android.content.Context
+import com.blinkit.droiddex.factory.base.PerformanceManager
+
+internal interface PerformanceManagerProvider {
+
+ fun create(applicationContext: Context, isInDebugMode: Boolean): PerformanceManager
+}
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/memory/MemoryPerformanceManager.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/memory/MemoryPerformanceManager.kt
new file mode 100644
index 0000000..61eebb6
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/memory/MemoryPerformanceManager.kt
@@ -0,0 +1,51 @@
+package com.blinkit.droiddex.memory
+
+import android.content.Context
+import com.blinkit.droiddex.constants.PerformanceClass
+import com.blinkit.droiddex.constants.PerformanceLevel
+import com.blinkit.droiddex.factory.base.PerformanceManager
+import com.blinkit.droiddex.factory.providers.PerformanceManagerProvider
+import com.blinkit.droiddex.utils.getApproxHeapLimitInMB
+import com.blinkit.droiddex.utils.getApproxHeapRemainingInMB
+import com.blinkit.droiddex.utils.getAvailableRamInGB
+import com.blinkit.droiddex.utils.getMemoryInfo
+
+internal class MemoryPerformanceManager(
+ private val applicationContext: Context, isInDebugMode: Boolean
+): PerformanceManager(isInDebugMode) {
+
+ override fun getPerformanceClass() = PerformanceClass.MEMORY
+
+ override fun getDelayInSecs() = DELAY_IN_SECS
+
+ override fun measurePerformanceLevel(): PerformanceLevel {
+ if (getMemoryInfo(applicationContext, logger).lowMemory) {
+ logInfo("DEVICE HAS LOW MEMORY")
+ return PerformanceLevel.LOW
+ }
+
+ val availableRamInGB = getAvailableRamInGB(applicationContext, logger)
+
+ val approxHeapLimitInMB = getApproxHeapLimitInMB(applicationContext, logger)
+
+ val approxHeapRemainingInMB = getApproxHeapRemainingInMB(applicationContext, logger)
+
+ return if (approxHeapRemainingInMB <= 64 || approxHeapLimitInMB < 128) {
+ PerformanceLevel.LOW
+ } else if (approxHeapRemainingInMB <= 128 || approxHeapLimitInMB < 256 || availableRamInGB <= 2) {
+ PerformanceLevel.AVERAGE
+ } else if (approxHeapRemainingInMB <= 256 || availableRamInGB <= 3) {
+ PerformanceLevel.HIGH
+ } else {
+ PerformanceLevel.EXCELLENT
+ }
+ }
+
+ companion object: PerformanceManagerProvider {
+
+ override fun create(applicationContext: Context, isInDebugMode: Boolean): PerformanceManager =
+ MemoryPerformanceManager(applicationContext, isInDebugMode)
+
+ private const val DELAY_IN_SECS = 10F
+ }
+}
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/network/NetworkPerformanceManager.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/network/NetworkPerformanceManager.kt
new file mode 100644
index 0000000..5bfaf7e
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/network/NetworkPerformanceManager.kt
@@ -0,0 +1,205 @@
+package com.blinkit.droiddex.network
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
+import android.net.wifi.WifiManager
+import android.os.Build
+import android.telephony.CellInfoCdma
+import android.telephony.CellInfoGsm
+import android.telephony.CellInfoLte
+import android.telephony.CellInfoWcdma
+import android.telephony.TelephonyManager
+import androidx.annotation.IntRange
+import androidx.annotation.RequiresApi
+import com.blinkit.droiddex.constants.PerformanceClass
+import com.blinkit.droiddex.constants.PerformanceLevel
+import com.blinkit.droiddex.factory.base.PerformanceManager
+import com.blinkit.droiddex.factory.providers.PerformanceManagerProvider
+import com.blinkit.droiddex.network.utils.BandwidthManager
+import com.blinkit.droiddex.utils.getPerformanceLevelWithWeights
+
+internal class NetworkPerformanceManager(
+ private val applicationContext: Context, isInDebugMode: Boolean
+): PerformanceManager(isInDebugMode) {
+
+ private val bandwidthManager by lazy { BandwidthManager(logger) }
+
+ override fun getPerformanceClass() = PerformanceClass.NETWORK
+
+ override fun getDelayInSecs() = DELAY_IN_SECS
+
+ override fun measurePerformanceLevel(): PerformanceLevel {
+ val bandwidthAverage = bandwidthManager.addSampleAndRecalculateBandwidthAverage()
+
+ if (isInternetConnected().not()) {
+ logInfo("DEVICE HAS NO INTERNET")
+ return PerformanceLevel.LOW
+ }
+
+ val bandwidthAverageStrengthLevel = getBandwidthAverageStrengthLevel(bandwidthAverage)
+ val downloadSpeedLevel = getDownloadSpeedStrengthLevel()
+ val signalStrengthLevel = getSignalStrengthLevel()
+
+ return getPerformanceLevelWithWeights(mutableListOf>().apply {
+ bandwidthAverageStrengthLevel?.let { add(Pair(it, 2F)) }
+ downloadSpeedLevel?.let { add(Pair(it, 1F)) }
+ signalStrengthLevel?.let { add(Pair(it, 1F)) }
+ })
+ }
+
+ private fun isInternetConnected(): Boolean {
+ val connectivityManager = getConnectivityManager() ?: return false
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
+ capabilities?.let {
+ it.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || it.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
+ }
+ } else {
+ @Suppress("DEPRECATION") connectivityManager.activeNetworkInfo?.isConnected
+ } ?: false
+ }
+
+ private fun getBandwidthAverageStrengthLevel(bandwidthAverage: Double) = when {
+ bandwidthAverage <= 0 -> null
+ bandwidthAverage < AVERAGE_BANDWIDTH_THRESHOLD -> PerformanceLevel.LOW
+ bandwidthAverage < HIGH_BANDWIDTH_THRESHOLD -> PerformanceLevel.AVERAGE
+ bandwidthAverage < EXCELLENT_BANDWIDTH_THRESHOLD -> PerformanceLevel.HIGH
+ else -> PerformanceLevel.EXCELLENT
+ }?.also { logDebug("BANDWIDTH AVERAGE STRENGTH TYPE: ${it.name}") }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun getDownloadSpeed(): Int {
+ val connectivityManager = getConnectivityManager() ?: return 0
+ val network = connectivityManager.activeNetwork ?: return 0
+ val networkCapabilities = connectivityManager.getNetworkCapabilities(network)
+ return (networkCapabilities?.linkDownstreamBandwidthKbps ?: 0).also { logDebug("DOWNLOAD SPEED: $it Kb/s") }
+ }
+
+ private fun getDownloadSpeedStrengthLevel() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ val downloadSpeed = getDownloadSpeed()
+ when {
+ downloadSpeed >= EXCELLENT_DOWNLOAD_SPEED_THRESHOLD -> PerformanceLevel.EXCELLENT
+ downloadSpeed >= HIGH_DOWNLOAD_SPEED_THRESHOLD -> PerformanceLevel.HIGH
+ downloadSpeed >= AVERAGE_DOWNLOAD_SPEED_THRESHOLD -> PerformanceLevel.AVERAGE
+ else -> PerformanceLevel.LOW
+ }.also { logDebug("DOWNLOAD SPEED TYPE: ${it.name}") }
+ } else null
+
+ private fun getNetworkType(): NetworkType {
+ val connectivityManager = getConnectivityManager() ?: return NetworkType.UNKNOWN
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ val activeNetwork = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
+ ?: return NetworkType.UNKNOWN
+ when {
+ activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> NetworkType.WIFI
+ activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> NetworkType.CELLULAR
+ else -> NetworkType.UNKNOWN
+ }
+ } else {
+ @Suppress("DEPRECATION") when (connectivityManager.activeNetworkInfo?.type) {
+ ConnectivityManager.TYPE_WIFI -> NetworkType.WIFI
+ ConnectivityManager.TYPE_MOBILE -> NetworkType.CELLULAR
+ else -> NetworkType.UNKNOWN
+ }
+ }.also { logDebug("NETWORK TYPE: ${it.name}") }
+ }
+
+ @IntRange(from = 0, to = 4)
+ private fun getWifiSignalLevel(): Int {
+ val wifiManger =
+ applicationContext.applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager ?: return 0
+ @Suppress("DEPRECATION") val wifiInfo = wifiManger.connectionInfo
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ wifiManger.calculateSignalLevel(wifiInfo.rssi)
+ } else {
+ @Suppress("DEPRECATION") WifiManager.calculateSignalLevel(wifiInfo.rssi, 5)
+ }.also { logDebug("WIFI SIGNAL LEVEL: $it") }
+ }
+
+ @IntRange(from = 0, to = 4)
+ private fun getCellularInternetStrength(): Int {
+ val telephonyManager =
+ applicationContext.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager ?: return 0
+
+ val signalLevel = (try {
+ when (val info = telephonyManager.allCellInfo?.firstOrNull()) {
+ is CellInfoLte -> info.cellSignalStrength.level
+ is CellInfoGsm -> info.cellSignalStrength.level
+ is CellInfoCdma -> info.cellSignalStrength.level
+ is CellInfoWcdma -> info.cellSignalStrength.level
+ else -> 0
+ }
+ } catch (e: SecurityException) {
+ 0
+ }).also { logDebug("CELLULAR SIGNAL LEVEL: $it") }
+
+ val networkGeneration = (try {
+ val networkType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ telephonyManager.dataNetworkType
+ } else {
+ @Suppress("DEPRECATION") telephonyManager.networkType
+ }
+ @Suppress("DEPRECATION") when (networkType) {
+ TelephonyManager.NETWORK_TYPE_GPRS, TelephonyManager.NETWORK_TYPE_EDGE, TelephonyManager.NETWORK_TYPE_CDMA, TelephonyManager.NETWORK_TYPE_1xRTT, TelephonyManager.NETWORK_TYPE_IDEN -> NetworkGeneration.NETWORK_2G
+ TelephonyManager.NETWORK_TYPE_UMTS, TelephonyManager.NETWORK_TYPE_EVDO_0, TelephonyManager.NETWORK_TYPE_EVDO_A, TelephonyManager.NETWORK_TYPE_HSDPA, TelephonyManager.NETWORK_TYPE_HSUPA, TelephonyManager.NETWORK_TYPE_HSPA, TelephonyManager.NETWORK_TYPE_EVDO_B, TelephonyManager.NETWORK_TYPE_EHRPD, TelephonyManager.NETWORK_TYPE_HSPAP -> NetworkGeneration.NETWORK_3G
+ TelephonyManager.NETWORK_TYPE_LTE -> NetworkGeneration.NETWORK_4G
+ TelephonyManager.NETWORK_TYPE_NR -> NetworkGeneration.NETWORK_5G
+ else -> NetworkGeneration.UNKNOWN
+ }
+ } catch (e: SecurityException) {
+ NetworkGeneration.UNKNOWN
+ }).also { logDebug("CELLULAR NETWORK GENERATION: ${it.name}") }
+
+ return when (networkGeneration) {
+ NetworkGeneration.UNKNOWN -> 0
+ NetworkGeneration.NETWORK_2G -> (1 + signalLevel) / 2
+ NetworkGeneration.NETWORK_3G -> (2 + signalLevel) / 2
+ NetworkGeneration.NETWORK_4G -> (3 + signalLevel) / 2
+ NetworkGeneration.NETWORK_5G -> (4 + signalLevel) / 2
+ }.also { logDebug("CELLULAR SIGNAL STRENGTH: $it") }
+ }
+
+ private fun categorizeSignalThreshold(signal: Int): PerformanceLevel = when {
+ signal >= EXCELLENT_SIGNAL_THRESHOLD -> PerformanceLevel.EXCELLENT
+ signal >= HIGH_SIGNAL_THRESHOLD -> PerformanceLevel.HIGH
+ signal >= AVERAGE_SIGNAL_THRESHOLD -> PerformanceLevel.AVERAGE
+ else -> PerformanceLevel.LOW
+ }
+
+ private fun getSignalStrengthLevel() = when (getNetworkType()) {
+ NetworkType.WIFI -> categorizeSignalThreshold(getWifiSignalLevel())
+ NetworkType.CELLULAR -> categorizeSignalThreshold(getCellularInternetStrength())
+ else -> null
+ }?.also { logDebug("SIGNAL STRENGTH TYPE: ${it.name}") }
+
+ private fun getConnectivityManager(): ConnectivityManager? =
+ applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: run {
+ logger.logError(Throwable("CONNECTIVITY MANAGER IS NULL"))
+ null
+ }
+
+ private enum class NetworkType { UNKNOWN, CELLULAR, WIFI }
+
+ private enum class NetworkGeneration { UNKNOWN, NETWORK_2G, NETWORK_3G, NETWORK_4G, NETWORK_5G }
+
+ companion object: PerformanceManagerProvider {
+
+ override fun create(applicationContext: Context, isInDebugMode: Boolean): PerformanceManager =
+ NetworkPerformanceManager(applicationContext, isInDebugMode)
+
+ private const val DELAY_IN_SECS = 2.5F
+
+ private const val EXCELLENT_BANDWIDTH_THRESHOLD = 2000F // 2 Mbps
+ private const val HIGH_BANDWIDTH_THRESHOLD = 550F // 0.55 Mbps
+ private const val AVERAGE_BANDWIDTH_THRESHOLD = 150F // 0.15 Mbps
+
+ private const val EXCELLENT_DOWNLOAD_SPEED_THRESHOLD = 10000 // 10 Mbps
+ private const val HIGH_DOWNLOAD_SPEED_THRESHOLD = 5000 // 5 Mbps
+ private const val AVERAGE_DOWNLOAD_SPEED_THRESHOLD = 2000 // 2 Mbps
+
+ private const val EXCELLENT_SIGNAL_THRESHOLD = 4
+ private const val HIGH_SIGNAL_THRESHOLD = 3
+ private const val AVERAGE_SIGNAL_THRESHOLD = 2
+ }
+}
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/network/utils/BandwidthManager.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/network/utils/BandwidthManager.kt
new file mode 100644
index 0000000..f11df56
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/network/utils/BandwidthManager.kt
@@ -0,0 +1,91 @@
+package com.blinkit.droiddex.network.utils
+
+import android.net.TrafficStats
+import android.os.SystemClock
+import com.blinkit.droiddex.utils.Logger
+import kotlin.math.ceil
+import kotlin.math.exp
+import kotlin.math.ln
+
+/**
+ * Inspired By Facebook's Network Connection Class
+ */
+internal class BandwidthManager(private val logger: Logger) {
+
+ private var lastReadTime: Long = SystemClock.elapsedRealtime()
+
+ private var lastDownloadBytes: Long = TrafficStats.getTotalRxBytes()
+
+ private val downloadBandwidth = ExponentialGeometricAverage(DECAY_CONSTANT)
+
+ fun addSampleAndRecalculateBandwidthAverage(): Double {
+ val currentBytes = TrafficStats.getTotalRxBytes()
+ val currentTime = SystemClock.elapsedRealtime()
+
+ addBandwidth(currentBytes - lastDownloadBytes, currentTime - lastReadTime)
+
+ lastReadTime = currentTime
+ lastDownloadBytes = currentBytes
+
+ return downloadBandwidth.average.also { logger.logDebug("BANDWIDTH AVERAGE: $it Kb/s") }
+ }
+
+ private fun addBandwidth(bytes: Long, timeInMs: Long) {
+ val bandwidth = ((bytes * 1.0) / timeInMs) * BYTES_TO_BITS
+ if (timeInMs == 0L || bandwidth < BANDWIDTH_LOWER_BOUND) {
+ return
+ }
+
+ downloadBandwidth.addValue(bandwidth)
+ }
+
+ private class ExponentialGeometricAverage(private val decayConstant: Double) {
+
+ var average = -1.0
+ private set
+
+ private val cutOver: Int = if (decayConstant == 0.0) Int.MAX_VALUE else ceil(1 / decayConstant).toInt()
+
+ private var count = 0
+
+ // Adds a new value to the moving average
+ fun addValue(value: Double) {
+ val keepConstant = 1 - decayConstant
+ average = if (count > cutOver) {
+ exp(keepConstant * ln(average) + decayConstant * ln(value))
+ } else if (count > 0) {
+ val retained = keepConstant * count / (count + 1.0)
+ val newcomer = 1.0 - retained
+ exp(retained * ln(average) + newcomer * ln(value))
+ } else {
+ value
+ }
+ count++
+ }
+
+ // Resets the moving average
+ fun reset() {
+ average = -1.0
+ count = 0
+ }
+ }
+
+ companion object {
+
+ private const val BYTES_TO_BITS = 8
+
+ /**
+ * The factor used to calculate the current bandwidth
+ * depending upon the previous calculated value for bandwidth.
+ *
+ * The smaller this value is, the less responsive to new samples the moving average becomes.
+ */
+ private const val DECAY_CONSTANT = 0.05
+
+ /**
+ * The lower bound for measured bandwidth in bits/ms. Readings
+ * lower than this are treated as effectively zero (therefore ignored).
+ */
+ private const val BANDWIDTH_LOWER_BOUND = 10
+ }
+}
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/storage/StoragePerformanceManager.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/storage/StoragePerformanceManager.kt
new file mode 100644
index 0000000..446aa16
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/storage/StoragePerformanceManager.kt
@@ -0,0 +1,47 @@
+package com.blinkit.droiddex.storage
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Environment
+import com.blinkit.droiddex.constants.PerformanceClass
+import com.blinkit.droiddex.constants.PerformanceLevel
+import com.blinkit.droiddex.factory.base.PerformanceManager
+import com.blinkit.droiddex.factory.providers.PerformanceManagerProvider
+import com.blinkit.droiddex.utils.convertBytesToGB
+
+internal class StoragePerformanceManager(isInDebugMode: Boolean): PerformanceManager(isInDebugMode) {
+
+ override fun getPerformanceClass() = PerformanceClass.STORAGE
+
+ override fun getDelayInSecs() = DELAY_IN_SECS
+
+ override fun measurePerformanceLevel() = with(getStorageLeftInGB()) {
+ when {
+ this > 16 -> PerformanceLevel.EXCELLENT
+ this > 8 -> PerformanceLevel.HIGH
+ this > 4 -> PerformanceLevel.AVERAGE
+ this > 0 -> PerformanceLevel.LOW
+ else -> PerformanceLevel.UNKNOWN
+ }
+ }
+
+ @SuppressLint("UsableSpace")
+ private fun getStorageLeftInGB(): Float {
+ val totalStorageLeft = try {
+ convertBytesToGB(Environment.getExternalStorageDirectory().usableSpace)
+ } catch (e: SecurityException) {
+ logError(e)
+ 0F
+ }
+
+ return totalStorageLeft.also { logDebug("TOTAL STORAGE LEFT: $it GB") }
+ }
+
+ companion object: PerformanceManagerProvider {
+
+ override fun create(applicationContext: Context, isInDebugMode: Boolean): PerformanceManager =
+ StoragePerformanceManager(isInDebugMode)
+
+ private const val DELAY_IN_SECS = 600F
+ }
+}
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/utils/Logger.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/utils/Logger.kt
new file mode 100644
index 0000000..812896f
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/utils/Logger.kt
@@ -0,0 +1,38 @@
+package com.blinkit.droiddex.utils
+
+import com.blinkit.droiddex.constants.PerformanceClass
+import com.blinkit.droiddex.constants.PerformanceClass.Companion.name
+import com.blinkit.droiddex.constants.PerformanceLevel
+import timber.log.Timber
+
+internal class Logger(var isInDebugMode: Boolean = false, @PerformanceClass private val performanceClass: Int? = null) {
+
+ fun logPerformanceLevelChange(level: PerformanceLevel, hasPerformanceLevelChanged: Boolean) {
+ if (hasPerformanceLevelChanged || isInDebugMode) logInfo("PERFORMANCE LEVEL: ${level.name}")
+ }
+
+ fun logPerformanceLevelResult(
+ vararg classes: Pair<@PerformanceClass Int, Float>, performanceLevel: PerformanceLevel
+ ) {
+ logDebug("PERFORMANCE CLASSES: ${
+ classes.joinToString(", ") { it.first.name() + if (it.second != 1F) "(WEIGHT: ${it.second})" else "" }
+ } | PERFORMANCE LEVEL: $performanceLevel")
+ }
+
+ fun logInfo(message: String) {
+ getTimber().i(beautifyMessage(message))
+ }
+
+ fun logDebug(message: String) {
+ getTimber().d(beautifyMessage(message))
+ }
+
+ fun logError(exception: Throwable) {
+ getTimber().e(exception)
+ }
+
+ private fun getTimber() = Timber.tag("DROID-DEX")
+
+ private fun beautifyMessage(message: String): String =
+ performanceClass?.let { "${it.name()} | $message" } ?: message
+}
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/utils/MemoryUtils.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/utils/MemoryUtils.kt
new file mode 100644
index 0000000..92b5a35
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/utils/MemoryUtils.kt
@@ -0,0 +1,49 @@
+package com.blinkit.droiddex.utils
+
+import android.app.ActivityManager
+import android.content.Context
+import kotlin.math.min
+
+internal fun getMemoryInfo(applicationContext: Context, logger: Logger): ActivityManager.MemoryInfo =
+ ActivityManager.MemoryInfo().also { getActivityManager(applicationContext, logger)?.getMemoryInfo(it) }
+
+/**
+ * Detailed Explanation
+ *
+ * Max Memory gives the actual limit for the heap
+ * Memory Class gives the limit we should be respecting
+ *
+ * In most cases for non-rooted devices, memory class is a better representation of the limit that should be made.
+ * But, it can be changed in rooted devices or devices with developer options switched on. Max Memory cannot be changed.
+ * So to get a better limit, we check minimum of memory class and max memory.
+ */
+internal fun getApproxHeapLimitInMB(applicationContext: Context, logger: Logger): Float {
+ val memoryClassInMB = getActivityManager(applicationContext, logger)?.memoryClass?.toFloat() ?: Float.MAX_VALUE
+ val maxMemoryInMB = convertBytesToMB(Runtime.getRuntime().maxMemory())
+
+ logger.logDebug("MEMORY CLASS: $memoryClassInMB MB, MAX MEMORY: $maxMemoryInMB MB")
+
+ return min(memoryClassInMB, maxMemoryInMB).also { logger.logDebug("APPROXIMATE HEAP LIMIT: $it MB") }
+}
+
+internal fun getApproxHeapRemainingInMB(applicationContext: Context, logger: Logger): Float {
+ val heapLimitInMB = getApproxHeapLimitInMB(applicationContext, logger)
+ val heapUsedInMB = convertBytesToMB(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())
+
+ logger.logDebug("APPROXIMATE HEAP USED: $heapUsedInMB MB")
+
+ return (heapLimitInMB - heapUsedInMB).also { logger.logDebug("APPROXIMATE HEAP REMAINING: $it MB") }
+}
+
+internal fun getTotalRamInGB(applicationContext: Context, logger: Logger) =
+ convertBytesToGB(getMemoryInfo(applicationContext, logger).totalMem).also { logger.logDebug("TOTAL RAM: $it GB") }
+
+internal fun getAvailableRamInGB(applicationContext: Context, logger: Logger) = convertBytesToGB(
+ getMemoryInfo(applicationContext, logger).availMem
+).also { logger.logDebug("AVAILABLE RAM: $it GB") }
+
+private fun getActivityManager(applicationContext: Context, logger: Logger): ActivityManager? =
+ applicationContext.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager ?: run {
+ logger.logError(Throwable("ACTIVITY MANAGER IS NULL"))
+ null
+ }
diff --git a/droid-dex/src/main/kotlin/com/blinkit/droiddex/utils/Utils.kt b/droid-dex/src/main/kotlin/com/blinkit/droiddex/utils/Utils.kt
new file mode 100644
index 0000000..c15f822
--- /dev/null
+++ b/droid-dex/src/main/kotlin/com/blinkit/droiddex/utils/Utils.kt
@@ -0,0 +1,65 @@
+package com.blinkit.droiddex.utils
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.ProcessLifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.blinkit.droiddex.constants.PerformanceLevel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlin.math.ceil
+import kotlin.math.roundToInt
+
+internal fun convertBytesToMB(value: Long): Float = (value * 1.0F) / (1024 * 1024)
+
+internal fun convertBytesToGB(value: Long): Float = convertBytesToMB(value) / 1024
+
+internal fun floor(value: Float): Int = kotlin.math.floor(value).roundToInt()
+
+internal fun List.average() = if (isNotEmpty()) ceil(sumOf { it.toDouble() } / size) else null
+
+internal fun getPerformanceLevelWithWeights(performanceLevelWithWeights: List>): PerformanceLevel {
+ var weightedSum = 0F
+ var totalWeight = 0F
+
+ performanceLevelWithWeights.filter { it.first != PerformanceLevel.UNKNOWN }.forEach { (level, weight) ->
+ weightedSum += level.level * weight
+ totalWeight += weight
+ }
+
+ return PerformanceLevel.getPerformanceLevel(if (totalWeight != 0F) floor(weightedSum / totalWeight) else 0)
+}
+
+internal fun getPerformanceLevelLdWithWeights(
+ performanceLevelLdWithWeights: List, Float>>, onChanged: (PerformanceLevel) -> Unit
+): LiveData {
+ return MediatorLiveData().apply {
+ performanceLevelLdWithWeights.forEach { performanceLevelLdWithWeight ->
+ addSource(performanceLevelLdWithWeight.first) {
+ val performanceLevel = getPerformanceLevelWithWeights(performanceLevelLdWithWeights.mapNotNull {
+ it.first.value?.let { performanceLevel -> Pair(performanceLevel, it.second) }
+ })
+ if (value != performanceLevel) {
+ value = performanceLevel
+ onChanged(performanceLevel)
+ }
+ }
+ }
+ }
+}
+
+internal fun runAsyncPeriodically(block: () -> Unit, delayInSecs: Float) = with(ProcessLifecycleOwner.get()) {
+ block()
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ while (true) {
+ withContext(Dispatchers.IO) { block() }
+ delay((delayInSecs * 1000).toLong())
+ }
+ }
+ }
+}
diff --git a/example/.gitignore b/example/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/example/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/example/build.gradle.kts b/example/build.gradle.kts
new file mode 100644
index 0000000..e9a8c63
--- /dev/null
+++ b/example/build.gradle.kts
@@ -0,0 +1,49 @@
+@file:Suppress("UnstableApiUsage")
+
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.jetbrains.kotlin.android)
+}
+
+
+android {
+ namespace = "com.blinkit.droiddexexample"
+
+ buildFeatures { viewBinding = true }
+
+ compileSdk = libs.versions.compileSdk.get().toInt()
+
+ defaultConfig {
+ applicationId = "com.blinkit.droiddexexample"
+
+ minSdk = 24
+ targetSdk = libs.versions.targetSdk.get().toInt()
+
+ versionCode = 1
+ versionName = "1.0"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ }
+ kotlinOptions { jvmTarget = libs.versions.java.get() }
+}
+
+
+dependencies {
+ implementation(project(":droid-dex"))
+
+ implementation(libs.timber)
+
+ implementation(libs.bundles.core)
+
+ implementation(libs.bundles.ui)
+}
diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..69fc45f
--- /dev/null
+++ b/example/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/src/main/ic_launcher-playstore.png b/example/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..e8e00ff
Binary files /dev/null and b/example/src/main/ic_launcher-playstore.png differ
diff --git a/example/src/main/kotlin/com/blinkit/droiddexexample/ExampleApplication.kt b/example/src/main/kotlin/com/blinkit/droiddexexample/ExampleApplication.kt
new file mode 100644
index 0000000..908fe6e
--- /dev/null
+++ b/example/src/main/kotlin/com/blinkit/droiddexexample/ExampleApplication.kt
@@ -0,0 +1,16 @@
+package com.blinkit.droiddexexample
+
+import android.app.Application
+import com.blinkit.droiddex.DroidDex
+import timber.log.Timber
+
+class ExampleApplication: Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+
+ Timber.plant(Timber.DebugTree())
+
+ DroidDex.init(this, BuildConfig.DEBUG)
+ }
+}
diff --git a/example/src/main/kotlin/com/blinkit/droiddexexample/main/MainActivity.kt b/example/src/main/kotlin/com/blinkit/droiddexexample/main/MainActivity.kt
new file mode 100644
index 0000000..e158f1c
--- /dev/null
+++ b/example/src/main/kotlin/com/blinkit/droiddexexample/main/MainActivity.kt
@@ -0,0 +1,69 @@
+package com.blinkit.droiddexexample.main
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.blinkit.droiddex.DroidDex
+import com.blinkit.droiddex.constants.PerformanceClass
+import com.blinkit.droiddex.constants.PerformanceClass.Companion.name
+import com.blinkit.droiddexexample.R
+import com.blinkit.droiddexexample.databinding.ActivityMainBinding
+import com.blinkit.droiddexexample.utils.dpToPx
+import com.blinkit.droiddexexample.views.ItemView
+
+class MainActivity: AppCompatActivity() {
+
+ private val binding: ActivityMainBinding by lazy { ActivityMainBinding.inflate(layoutInflater) }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(binding.root)
+
+ binding.scrollView.setOnScrollChangeListener { _, _, scrollY, _, _ ->
+ binding.logo.elevation = (if (scrollY > 0.dpToPx()) 2 else 0).dpToPx()
+ }
+
+ binding.headingIndividual.setTextAppearance(R.style.TextAppearanceBold)
+ binding.headingAggregate.setTextAppearance(R.style.TextAppearanceBold)
+ binding.headingWeightedAggregate.setTextAppearance(R.style.TextAppearanceBold)
+
+ setupClasses(binding.cpu, PerformanceClass.CPU)
+ setupClasses(binding.memory, PerformanceClass.MEMORY)
+ setupClasses(binding.network, PerformanceClass.NETWORK)
+ setupClasses(binding.storage, PerformanceClass.STORAGE)
+ setupClasses(binding.battery, PerformanceClass.BATTERY)
+
+ setupClasses(binding.cpuAndMemory, PerformanceClass.CPU, PerformanceClass.MEMORY)
+ setupClasses(binding.networkAndStorage, PerformanceClass.NETWORK, PerformanceClass.STORAGE)
+ setupClasses(
+ binding.cpuMemoryAndNetwork, PerformanceClass.CPU, PerformanceClass.MEMORY, PerformanceClass.NETWORK
+ )
+
+ setupWeightedClasses(
+ binding.memoryAndNetworkWeighted, PerformanceClass.MEMORY to 2F, PerformanceClass.NETWORK to 1F
+ )
+ setupWeightedClasses(
+ binding.cpuAndBatteryWeighted, PerformanceClass.CPU to 2F, PerformanceClass.BATTERY to 3F
+ )
+ }
+
+ private fun setupClasses(item: ItemView, @PerformanceClass vararg performanceClasses: Int) {
+ DroidDex.getPerformanceLevelLd(*performanceClasses).observe(this) { level ->
+ item.set(level, performanceClasses.joinToString(" + ") { getClassName(it) })
+ }
+ }
+
+ private fun setupWeightedClasses(
+ item: ItemView, vararg performanceClassesWithWeights: Pair<@PerformanceClass Int, Float>
+ ) {
+ DroidDex.getWeightedPerformanceLevelLd(*performanceClassesWithWeights).observe(this) { level ->
+ item.set(level, performanceClassesWithWeights.joinToString(" +\n") {
+ getClassName(it.first) + " * " + it.second
+ })
+ }
+ }
+
+ private fun getClassName(@PerformanceClass performanceClass: Int) = when (performanceClass) {
+ PerformanceClass.CPU -> performanceClass.name()
+ else -> performanceClass.name().lowercase().replaceFirstChar { it.uppercase() }
+ }
+}
diff --git a/example/src/main/kotlin/com/blinkit/droiddexexample/utils/Extensions.kt b/example/src/main/kotlin/com/blinkit/droiddexexample/utils/Extensions.kt
new file mode 100644
index 0000000..56a248d
--- /dev/null
+++ b/example/src/main/kotlin/com/blinkit/droiddexexample/utils/Extensions.kt
@@ -0,0 +1,20 @@
+package com.blinkit.droiddexexample.utils
+
+import android.content.Context
+import android.content.res.Resources
+import androidx.annotation.ColorInt
+import com.blinkit.droiddex.constants.PerformanceLevel
+import com.blinkit.droiddexexample.R
+
+fun Int.dpToPx() = (this * Resources.getSystem().displayMetrics.density)
+
+@ColorInt
+fun PerformanceLevel.getColor(context: Context) = context.getColor(
+ when (this) {
+ PerformanceLevel.EXCELLENT -> R.color.excellent
+ PerformanceLevel.HIGH -> R.color.high
+ PerformanceLevel.AVERAGE -> R.color.average
+ PerformanceLevel.LOW -> R.color.low
+ PerformanceLevel.UNKNOWN -> android.R.color.black
+ }
+)
diff --git a/example/src/main/kotlin/com/blinkit/droiddexexample/views/ItemView.kt b/example/src/main/kotlin/com/blinkit/droiddexexample/views/ItemView.kt
new file mode 100644
index 0000000..394a757
--- /dev/null
+++ b/example/src/main/kotlin/com/blinkit/droiddexexample/views/ItemView.kt
@@ -0,0 +1,72 @@
+package com.blinkit.droiddexexample.views
+
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.graphics.drawable.GradientDrawable
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.animation.doOnCancel
+import androidx.core.animation.doOnEnd
+import androidx.core.view.updatePadding
+import com.blinkit.droiddex.constants.PerformanceLevel
+import com.blinkit.droiddexexample.R
+import com.blinkit.droiddexexample.databinding.LayoutItemBinding
+import com.blinkit.droiddexexample.utils.dpToPx
+import com.blinkit.droiddexexample.utils.getColor
+import kotlin.math.abs
+
+class ItemView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+): ConstraintLayout(context, attrs, defStyleAttr) {
+
+ private val binding: LayoutItemBinding by lazy { LayoutItemBinding.inflate(LayoutInflater.from(context), this) }
+
+ private var currentProgress = 0
+
+ private var animation: ObjectAnimator? = null
+ set(value) {
+ field?.cancel()
+ field = value
+ }
+
+ init {
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
+ updatePadding(bottom = 8.dpToPx().toInt())
+ }
+
+ background = GradientDrawable().apply {
+ shape = GradientDrawable.RECTANGLE
+ setColor(context.getColor(android.R.color.white))
+ cornerRadius = 12.dpToPx()
+ }
+ }
+
+ fun set(level: PerformanceLevel, classText: String) {
+ binding.progressBar.progressDrawable = getProgressBackground(level)
+
+ val newProgress = level.level * 25
+ animation = ObjectAnimator.ofInt(binding.progressBar, "progress", currentProgress, newProgress).apply {
+ duration = abs(newProgress - currentProgress) * 5L
+ doOnCancel { currentProgress = newProgress }
+ doOnEnd {
+ currentProgress = newProgress
+ binding.progressBar.progress = newProgress
+ }
+ start()
+ }
+
+ binding.level.text = level.name.lowercase().replaceFirstChar { it.uppercase() }
+ binding.level.setTextAppearance(R.style.TextAppearanceLight)
+
+ binding.className.text = classText
+ binding.className.setTextAppearance(R.style.TextAppearanceMedium)
+ }
+
+ private fun getProgressBackground(level: PerformanceLevel) = GradientDrawable().apply {
+ shape = GradientDrawable.RING
+ setColor(level.getColor(context))
+ }
+}
diff --git a/example/src/main/res/drawable-v24/ic_launcher_background.xml b/example/src/main/res/drawable-v24/ic_launcher_background.xml
new file mode 100644
index 0000000..56d85ac
--- /dev/null
+++ b/example/src/main/res/drawable-v24/ic_launcher_background.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/example/src/main/res/drawable/ic_launcher_foreground.xml b/example/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..7557419
--- /dev/null
+++ b/example/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/src/main/res/drawable/info_icon.xml b/example/src/main/res/drawable/info_icon.xml
new file mode 100644
index 0000000..ba10678
--- /dev/null
+++ b/example/src/main/res/drawable/info_icon.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/src/main/res/drawable/logo.xml b/example/src/main/res/drawable/logo.xml
new file mode 100644
index 0000000..4c57edc
--- /dev/null
+++ b/example/src/main/res/drawable/logo.xml
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/src/main/res/drawable/logo_background.xml b/example/src/main/res/drawable/logo_background.xml
new file mode 100644
index 0000000..a058773
--- /dev/null
+++ b/example/src/main/res/drawable/logo_background.xml
@@ -0,0 +1,16 @@
+
+
+ -
+
+
+
+
+
+
+
diff --git a/example/src/main/res/font/poppins.xml b/example/src/main/res/font/poppins.xml
new file mode 100644
index 0000000..de125bd
--- /dev/null
+++ b/example/src/main/res/font/poppins.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/src/main/res/font/poppins_bold.ttf b/example/src/main/res/font/poppins_bold.ttf
new file mode 100644
index 0000000..00559ee
Binary files /dev/null and b/example/src/main/res/font/poppins_bold.ttf differ
diff --git a/example/src/main/res/font/poppins_extrabold.ttf b/example/src/main/res/font/poppins_extrabold.ttf
new file mode 100644
index 0000000..df70936
Binary files /dev/null and b/example/src/main/res/font/poppins_extrabold.ttf differ
diff --git a/example/src/main/res/font/poppins_extralight.ttf b/example/src/main/res/font/poppins_extralight.ttf
new file mode 100644
index 0000000..e76ec69
Binary files /dev/null and b/example/src/main/res/font/poppins_extralight.ttf differ
diff --git a/example/src/main/res/font/poppins_light.ttf b/example/src/main/res/font/poppins_light.ttf
new file mode 100644
index 0000000..bc36bcc
Binary files /dev/null and b/example/src/main/res/font/poppins_light.ttf differ
diff --git a/example/src/main/res/font/poppins_medium.ttf b/example/src/main/res/font/poppins_medium.ttf
new file mode 100644
index 0000000..6bcdcc2
Binary files /dev/null and b/example/src/main/res/font/poppins_medium.ttf differ
diff --git a/example/src/main/res/font/poppins_regular.ttf b/example/src/main/res/font/poppins_regular.ttf
new file mode 100644
index 0000000..9f0c71b
Binary files /dev/null and b/example/src/main/res/font/poppins_regular.ttf differ
diff --git a/example/src/main/res/font/poppins_semibold.ttf b/example/src/main/res/font/poppins_semibold.ttf
new file mode 100644
index 0000000..74c726e
Binary files /dev/null and b/example/src/main/res/font/poppins_semibold.ttf differ
diff --git a/example/src/main/res/font/poppins_thin.ttf b/example/src/main/res/font/poppins_thin.ttf
new file mode 100644
index 0000000..03e7366
Binary files /dev/null and b/example/src/main/res/font/poppins_thin.ttf differ
diff --git a/example/src/main/res/layout/activity_main.xml b/example/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..9c1bf85
--- /dev/null
+++ b/example/src/main/res/layout/activity_main.xml
@@ -0,0 +1,188 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/src/main/res/layout/layout_item.xml b/example/src/main/res/layout/layout_item.xml
new file mode 100644
index 0000000..c34db99
--- /dev/null
+++ b/example/src/main/res/layout/layout_item.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..ac07c2f
--- /dev/null
+++ b/example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..ac07c2f
--- /dev/null
+++ b/example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/example/src/main/res/mipmap-hdpi/ic_launcher.webp b/example/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..d728ab2
Binary files /dev/null and b/example/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/example/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/example/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..c0d9a75
Binary files /dev/null and b/example/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/example/src/main/res/mipmap-mdpi/ic_launcher.webp b/example/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..ebff995
Binary files /dev/null and b/example/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/example/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/example/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..7203d68
Binary files /dev/null and b/example/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/example/src/main/res/mipmap-xhdpi/ic_launcher.webp b/example/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..88e3d73
Binary files /dev/null and b/example/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/example/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/example/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..6b58fc4
Binary files /dev/null and b/example/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/example/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/example/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..c1b5938
Binary files /dev/null and b/example/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..37df203
Binary files /dev/null and b/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/example/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/example/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..f08e20d
Binary files /dev/null and b/example/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..774110b
Binary files /dev/null and b/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/example/src/main/res/values-v23/themes.xml b/example/src/main/res/values-v23/themes.xml
new file mode 100644
index 0000000..69a940e
--- /dev/null
+++ b/example/src/main/res/values-v23/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/example/src/main/res/values-v31/themes.xml b/example/src/main/res/values-v31/themes.xml
new file mode 100644
index 0000000..86640b8
--- /dev/null
+++ b/example/src/main/res/values-v31/themes.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/example/src/main/res/values/colors.xml b/example/src/main/res/values/colors.xml
new file mode 100644
index 0000000..54d09ed
--- /dev/null
+++ b/example/src/main/res/values/colors.xml
@@ -0,0 +1,13 @@
+
+
+ #F4F6FB
+
+ #D1E0FF
+
+ #EF4F5F
+ #FAD975
+ #BBD959
+ #3AB757
+
+ #1F1F1F
+
diff --git a/example/src/main/res/values/strings.xml b/example/src/main/res/values/strings.xml
new file mode 100644
index 0000000..76c8498
--- /dev/null
+++ b/example/src/main/res/values/strings.xml
@@ -0,0 +1,11 @@
+
+ Droid Dex
+
+ Droid Dex
+
+ -
+
+ Individual
+ Aggregate
+ Weighted Aggregate
+
diff --git a/example/src/main/res/values/themes.xml b/example/src/main/res/values/themes.xml
new file mode 100644
index 0000000..a14d7e4
--- /dev/null
+++ b/example/src/main/res/values/themes.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..b4f52f6
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,62 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+
+# Gradle Properties https://docs.gradle.org/current/userguide/build_environment.html#sec:configuring_jvm_memory
+
+# This line enables Gradle's caching mechanism, which caches build information to speed up subsequent builds.
+org.gradle.caching=true
+# This line disables debugging information for Gradle's caching mechanism.
+org.gradle.caching.debug=false
+
+# This line enables the Gradle Daemon, which is a long-lived background process that can speed up Gradle builds by keeping parts of the build process in memory.
+org.gradle.daemon=true
+
+# This line enables parallel execution of tasks in Gradle, allowing multiple tasks to run concurrently, which can speed up the build process.
+org.gradle.parallel=true
+
+# This line sets the warning mode to "summary" which means that Gradle will provide a summary of warnings during the build.
+org.gradle.warning.mode=summary
+
+# Enabled Configuration Cache
+org.gradle.configuration-cache=true
+org.gradle.configuration-cache-problems=warn
+
+
+# Maven Publishing
+GROUP=com.blinkit.kits
+POM_ARTIFACT_ID=droid-dex
+VERSION_NAME=1.0.0
+
+POM_NAME=Droid Dex
+POM_DESCRIPTION=Classification and Analysis of Android Device Performance
+POM_INCEPTION_YEAR=2024
+POM_URL=https://github.com/grofers/droid-dex
+
+POM_LICENSE_NAME=The Apache Software License, Version 2.0
+POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt
+POM_LICENSE_DIST=repo
+
+POM_SCM_URL=https://github.com/grofers/droid-dex
+POM_SCM_CONNECTION=scm:git:git://github.com/grofers/droid-dex.git
+POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/grofers/droid-dex.git
+
+POM_DEVELOPER_ID=Blinkit
+POM_DEVELOPER_NAME=Blink Commerce Private Limited (formerly known as Grofers India Private Limited)
+POM_DEVELOPER_URL=https://github.com/grofers/
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..26d245c
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,58 @@
+[versions]
+java = "11"
+
+minSdk = "21"
+targetSdk = "34"
+compileSdk = "34"
+
+agp = "7.4.2"
+kotlin = "1.8.21"
+maven-publish = "0.27.0"
+
+timber = "5.0.1"
+
+core-ktx = "1.7.0"
+app-compat = "1.6.0"
+
+material = "1.9.0"
+constraint-layout = "2.1.4"
+
+kotlinx-coroutines = "1.6.4"
+
+lifecycle = "2.5.1"
+
+core-performance = "1.0.0-alpha02"
+
+
+[libraries]
+timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
+
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "app-compat" }
+
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+constraint-layout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraint-layout" }
+
+kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
+
+androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
+
+androidx-core-performance = { group = "androidx.core", name = "core-performance", version.ref = "core-performance" }
+
+
+[bundles]
+core = ["androidx-core-ktx", "androidx-appcompat"]
+
+ui = ["material", "constraint-layout"]
+
+lifecycle = ["androidx-lifecycle-process", "androidx-lifecycle-runtime-ktx"]
+
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+
+jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+
+maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..ccbffef
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Jan 03 19:34:15 IST 2024
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# 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
+#
+# https://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.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/localPublish.sh b/localPublish.sh
new file mode 100755
index 0000000..33a2927
--- /dev/null
+++ b/localPublish.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+./gradlew clean
+./gradlew droid-dex:assembleRelease
+
+echo -e "\n\nPublishing to LOCAL. BEWARE. Waiting for 2 seconds before continuing\n\n"
+sleep 2
+
+./gradlew droid-dex:publishMavenPublicationToMavenLocal
diff --git a/remotePublish.sh b/remotePublish.sh
new file mode 100755
index 0000000..6fa4828
--- /dev/null
+++ b/remotePublish.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+./gradlew clean
+./gradlew droid-dex:assembleRelease
+
+echo -e "\n\nPublishing to PRODUCTION. BEWARE. Waiting for 5 seconds before continuing\n\n"
+sleep 5
+
+./gradlew droid-dex:publishMavenPublicationToBlinkitRepository
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..c55c660
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,23 @@
+@file:Suppress("UnstableApiUsage")
+
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "Droid Dex"
+
+include(":droid-dex")
+
+include(":example")