├── .editorconfig ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── workflows │ ├── android.yml │ └── publish-release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── RELEASING.md ├── assets ├── logo.png └── logo_512.png ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── java │ └── Dependencies.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── logcat ├── api │ └── logcat.api ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── logcat │ │ ├── AndroidLogcatLogger.kt │ │ ├── LogPriority.kt │ │ ├── Logcat.kt │ │ ├── LogcatLogger.kt │ │ ├── PrintLogger.kt │ │ └── Throwables.kt │ └── test │ └── java │ └── logcat │ ├── LogcatTest.kt │ └── TestLogcatLogger.kt ├── sample ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── squareup │ │ └── logcat │ │ └── sample │ │ ├── ExampleApplication.kt │ │ └── MainActivity.kt │ └── res │ ├── layout │ └── main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ └── mipmap-xxxhdpi │ └── ic_launcher.png └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | # IntelliJ allows to export the code style in the .editorconfig format. These are the settings 2 | # from the most recent settings for SquareAndroid from here: 3 | # 4 | # https://github.com/square/java-code-styles 5 | # 6 | # Every time someone changes Square's code style we should update the config here, too. These 7 | # settings override the selected code style. This means nobody needs to import the code style 8 | # anymore and we can change settings for all developers here at once. 9 | # 10 | # Some keys were manually changed: 11 | # * insert_final_newline (is false, changed to true, expected by KtLint) 12 | # * ktlint_ignore_back_ticked_identifier (enabled, see comment below) 13 | # * root (see comment below) 14 | 15 | # Tells tools to stop searching upwards. 16 | root = true 17 | 18 | [*] 19 | charset = utf-8 20 | end_of_line = lf 21 | indent_size = 2 22 | indent_style = space 23 | insert_final_newline = true 24 | max_line_length = 100 25 | tab_width = 2 26 | ij_continuation_indent_size = 4 27 | ij_formatter_off_tag = @formatter:off 28 | ij_formatter_on_tag = @formatter:on 29 | ij_formatter_tags_enabled = false 30 | ij_smart_tabs = false 31 | ij_wrap_on_typing = false 32 | 33 | # Back-ticked method names are allowed to exceed the line length. That's especially helpful for 34 | # test methods with descriptive names. 35 | ktlint_ignore_back_ticked_identifier=true 36 | 37 | [*.java] 38 | ij_java_align_consecutive_assignments = false 39 | ij_java_align_consecutive_variable_declarations = false 40 | ij_java_align_group_field_declarations = false 41 | ij_java_align_multiline_annotation_parameters = false 42 | ij_java_align_multiline_array_initializer_expression = false 43 | ij_java_align_multiline_assignment = false 44 | ij_java_align_multiline_binary_operation = false 45 | ij_java_align_multiline_chained_methods = false 46 | ij_java_align_multiline_extends_list = false 47 | ij_java_align_multiline_for = false 48 | ij_java_align_multiline_method_parentheses = false 49 | ij_java_align_multiline_parameters = false 50 | ij_java_align_multiline_parameters_in_calls = false 51 | ij_java_align_multiline_parenthesized_expression = false 52 | ij_java_align_multiline_records = true 53 | ij_java_align_multiline_resources = true 54 | ij_java_align_multiline_ternary_operation = false 55 | ij_java_align_multiline_text_blocks = false 56 | ij_java_align_multiline_throws_list = false 57 | ij_java_align_subsequent_simple_methods = false 58 | ij_java_align_throws_keyword = false 59 | ij_java_annotation_parameter_wrap = off 60 | ij_java_array_initializer_new_line_after_left_brace = true 61 | ij_java_array_initializer_right_brace_on_new_line = true 62 | ij_java_array_initializer_wrap = normal 63 | ij_java_assert_statement_colon_on_next_line = false 64 | ij_java_assert_statement_wrap = normal 65 | ij_java_assignment_wrap = normal 66 | ij_java_binary_operation_sign_on_next_line = true 67 | ij_java_binary_operation_wrap = on_every_item 68 | ij_java_blank_lines_after_anonymous_class_header = 0 69 | ij_java_blank_lines_after_class_header = 0 70 | ij_java_blank_lines_after_imports = 1 71 | ij_java_blank_lines_after_package = 1 72 | ij_java_blank_lines_around_class = 1 73 | ij_java_blank_lines_around_field = 0 74 | ij_java_blank_lines_around_field_in_interface = 0 75 | ij_java_blank_lines_around_initializer = 1 76 | ij_java_blank_lines_around_method = 1 77 | ij_java_blank_lines_around_method_in_interface = 1 78 | ij_java_blank_lines_before_class_end = 0 79 | ij_java_blank_lines_before_imports = 1 80 | ij_java_blank_lines_before_method_body = 0 81 | ij_java_blank_lines_before_package = 0 82 | ij_java_block_brace_style = end_of_line 83 | ij_java_block_comment_at_first_column = false 84 | ij_java_call_parameters_new_line_after_left_paren = false 85 | ij_java_call_parameters_right_paren_on_new_line = false 86 | ij_java_call_parameters_wrap = normal 87 | ij_java_case_statement_on_separate_line = true 88 | ij_java_catch_on_new_line = false 89 | ij_java_class_annotation_wrap = normal 90 | ij_java_class_brace_style = end_of_line 91 | ij_java_class_count_to_use_import_on_demand = 999 92 | ij_java_class_names_in_javadoc = 3 93 | ij_java_do_not_indent_top_level_class_members = false 94 | ij_java_do_not_wrap_after_single_annotation = false 95 | ij_java_do_while_brace_force = if_multiline 96 | ij_java_doc_add_blank_line_after_description = true 97 | ij_java_doc_add_blank_line_after_param_comments = false 98 | ij_java_doc_add_blank_line_after_return = false 99 | ij_java_doc_add_p_tag_on_empty_lines = false 100 | ij_java_doc_align_exception_comments = false 101 | ij_java_doc_align_param_comments = false 102 | ij_java_doc_do_not_wrap_if_one_line = true 103 | ij_java_doc_enable_formatting = true 104 | ij_java_doc_enable_leading_asterisks = true 105 | ij_java_doc_indent_on_continuation = false 106 | ij_java_doc_keep_empty_lines = true 107 | ij_java_doc_keep_empty_parameter_tag = false 108 | ij_java_doc_keep_empty_return_tag = false 109 | ij_java_doc_keep_empty_throws_tag = true 110 | ij_java_doc_keep_invalid_tags = true 111 | ij_java_doc_param_description_on_new_line = false 112 | ij_java_doc_preserve_line_breaks = true 113 | ij_java_doc_use_throws_not_exception_tag = true 114 | ij_java_else_on_new_line = false 115 | ij_java_enum_constants_wrap = off 116 | ij_java_extends_keyword_wrap = normal 117 | ij_java_extends_list_wrap = normal 118 | ij_java_field_annotation_wrap = normal 119 | ij_java_finally_on_new_line = false 120 | ij_java_for_brace_force = never 121 | ij_java_for_statement_new_line_after_left_paren = false 122 | ij_java_for_statement_right_paren_on_new_line = false 123 | ij_java_for_statement_wrap = normal 124 | ij_java_generate_final_locals = false 125 | ij_java_generate_final_parameters = false 126 | ij_java_if_brace_force = if_multiline 127 | ij_java_imports_layout = *,|,$* 128 | ij_java_indent_case_from_switch = true 129 | ij_java_insert_inner_class_imports = false 130 | ij_java_insert_override_annotation = true 131 | ij_java_keep_blank_lines_before_right_brace = 0 132 | ij_java_keep_blank_lines_between_package_declaration_and_header = 2 133 | ij_java_keep_blank_lines_in_code = 1 134 | ij_java_keep_blank_lines_in_declarations = 1 135 | ij_java_keep_control_statement_in_one_line = true 136 | ij_java_keep_first_column_comment = false 137 | ij_java_keep_indents_on_empty_lines = false 138 | ij_java_keep_line_breaks = true 139 | ij_java_keep_multiple_expressions_in_one_line = false 140 | ij_java_keep_simple_blocks_in_one_line = false 141 | ij_java_keep_simple_classes_in_one_line = false 142 | ij_java_keep_simple_lambdas_in_one_line = false 143 | ij_java_keep_simple_methods_in_one_line = false 144 | ij_java_label_indent_absolute = false 145 | ij_java_label_indent_size = 0 146 | ij_java_lambda_brace_style = end_of_line 147 | ij_java_layout_static_imports_separately = true 148 | ij_java_line_comment_add_space = false 149 | ij_java_line_comment_at_first_column = false 150 | ij_java_method_annotation_wrap = normal 151 | ij_java_method_brace_style = end_of_line 152 | ij_java_method_call_chain_wrap = on_every_item 153 | ij_java_method_parameters_new_line_after_left_paren = false 154 | ij_java_method_parameters_right_paren_on_new_line = false 155 | ij_java_method_parameters_wrap = normal 156 | ij_java_modifier_list_wrap = false 157 | ij_java_names_count_to_use_import_on_demand = 999 158 | ij_java_new_line_after_lparen_in_record_header = false 159 | ij_java_parameter_annotation_wrap = normal 160 | ij_java_parentheses_expression_new_line_after_left_paren = false 161 | ij_java_parentheses_expression_right_paren_on_new_line = false 162 | ij_java_place_assignment_sign_on_next_line = false 163 | ij_java_prefer_longer_names = true 164 | ij_java_prefer_parameters_wrap = false 165 | ij_java_record_components_wrap = normal 166 | ij_java_repeat_synchronized = true 167 | ij_java_replace_instanceof_and_cast = false 168 | ij_java_replace_null_check = true 169 | ij_java_replace_sum_lambda_with_method_ref = true 170 | ij_java_resource_list_new_line_after_left_paren = false 171 | ij_java_resource_list_right_paren_on_new_line = false 172 | ij_java_resource_list_wrap = normal 173 | ij_java_rparen_on_new_line_in_record_header = false 174 | ij_java_space_after_closing_angle_bracket_in_type_argument = false 175 | ij_java_space_after_colon = true 176 | ij_java_space_after_comma = true 177 | ij_java_space_after_comma_in_type_arguments = true 178 | ij_java_space_after_for_semicolon = true 179 | ij_java_space_after_quest = true 180 | ij_java_space_after_type_cast = true 181 | ij_java_space_before_annotation_array_initializer_left_brace = false 182 | ij_java_space_before_annotation_parameter_list = false 183 | ij_java_space_before_array_initializer_left_brace = true 184 | ij_java_space_before_catch_keyword = true 185 | ij_java_space_before_catch_left_brace = true 186 | ij_java_space_before_catch_parentheses = true 187 | ij_java_space_before_class_left_brace = true 188 | ij_java_space_before_colon = true 189 | ij_java_space_before_colon_in_foreach = true 190 | ij_java_space_before_comma = false 191 | ij_java_space_before_do_left_brace = true 192 | ij_java_space_before_else_keyword = true 193 | ij_java_space_before_else_left_brace = true 194 | ij_java_space_before_finally_keyword = true 195 | ij_java_space_before_finally_left_brace = true 196 | ij_java_space_before_for_left_brace = true 197 | ij_java_space_before_for_parentheses = true 198 | ij_java_space_before_for_semicolon = false 199 | ij_java_space_before_if_left_brace = true 200 | ij_java_space_before_if_parentheses = true 201 | ij_java_space_before_method_call_parentheses = false 202 | ij_java_space_before_method_left_brace = true 203 | ij_java_space_before_method_parentheses = false 204 | ij_java_space_before_opening_angle_bracket_in_type_parameter = false 205 | ij_java_space_before_quest = true 206 | ij_java_space_before_switch_left_brace = true 207 | ij_java_space_before_switch_parentheses = true 208 | ij_java_space_before_synchronized_left_brace = true 209 | ij_java_space_before_synchronized_parentheses = true 210 | ij_java_space_before_try_left_brace = true 211 | ij_java_space_before_try_parentheses = true 212 | ij_java_space_before_type_parameter_list = false 213 | ij_java_space_before_while_keyword = true 214 | ij_java_space_before_while_left_brace = true 215 | ij_java_space_before_while_parentheses = true 216 | ij_java_space_inside_one_line_enum_braces = false 217 | ij_java_space_within_empty_array_initializer_braces = false 218 | ij_java_space_within_empty_method_call_parentheses = false 219 | ij_java_space_within_empty_method_parentheses = false 220 | ij_java_spaces_around_additive_operators = true 221 | ij_java_spaces_around_assignment_operators = true 222 | ij_java_spaces_around_bitwise_operators = true 223 | ij_java_spaces_around_equality_operators = true 224 | ij_java_spaces_around_lambda_arrow = true 225 | ij_java_spaces_around_logical_operators = true 226 | ij_java_spaces_around_method_ref_dbl_colon = false 227 | ij_java_spaces_around_multiplicative_operators = true 228 | ij_java_spaces_around_relational_operators = true 229 | ij_java_spaces_around_shift_operators = true 230 | ij_java_spaces_around_type_bounds_in_type_parameters = true 231 | ij_java_spaces_around_unary_operator = false 232 | ij_java_spaces_within_angle_brackets = false 233 | ij_java_spaces_within_annotation_parentheses = false 234 | ij_java_spaces_within_array_initializer_braces = true 235 | ij_java_spaces_within_braces = false 236 | ij_java_spaces_within_brackets = false 237 | ij_java_spaces_within_cast_parentheses = false 238 | ij_java_spaces_within_catch_parentheses = false 239 | ij_java_spaces_within_for_parentheses = false 240 | ij_java_spaces_within_if_parentheses = false 241 | ij_java_spaces_within_method_call_parentheses = false 242 | ij_java_spaces_within_method_parentheses = false 243 | ij_java_spaces_within_parentheses = false 244 | ij_java_spaces_within_switch_parentheses = false 245 | ij_java_spaces_within_synchronized_parentheses = false 246 | ij_java_spaces_within_try_parentheses = false 247 | ij_java_spaces_within_while_parentheses = false 248 | ij_java_special_else_if_treatment = true 249 | ij_java_subclass_name_suffix = Impl 250 | ij_java_ternary_operation_signs_on_next_line = true 251 | ij_java_ternary_operation_wrap = normal 252 | ij_java_test_name_suffix = Test 253 | ij_java_throws_keyword_wrap = normal 254 | ij_java_throws_list_wrap = normal 255 | ij_java_use_external_annotations = false 256 | ij_java_use_fq_class_names = false 257 | ij_java_use_relative_indents = false 258 | ij_java_use_single_class_imports = true 259 | ij_java_variable_annotation_wrap = normal 260 | ij_java_visibility = public 261 | ij_java_while_brace_force = if_multiline 262 | ij_java_while_on_new_line = false 263 | ij_java_wrap_comments = true 264 | ij_java_wrap_first_method_in_call_chain = false 265 | ij_java_wrap_long_lines = false 266 | 267 | [*.properties] 268 | ij_properties_align_group_field_declarations = false 269 | ij_properties_keep_blank_lines = false 270 | ij_properties_key_value_delimiter = equals 271 | ij_properties_spaces_around_key_value_delimiter = false 272 | 273 | [.editorconfig] 274 | ij_editorconfig_align_group_field_declarations = false 275 | ij_editorconfig_space_after_colon = false 276 | ij_editorconfig_space_after_comma = true 277 | ij_editorconfig_space_before_colon = false 278 | ij_editorconfig_space_before_comma = false 279 | ij_editorconfig_spaces_around_assignment_operators = true 280 | 281 | [{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.jspx,*.pom,*.rng,*.tagx,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] 282 | ij_xml_align_attributes = false 283 | ij_xml_align_text = false 284 | ij_xml_attribute_wrap = normal 285 | ij_xml_block_comment_at_first_column = true 286 | ij_xml_keep_blank_lines = 2 287 | ij_xml_keep_indents_on_empty_lines = false 288 | ij_xml_keep_line_breaks = true 289 | ij_xml_keep_line_breaks_in_text = true 290 | ij_xml_keep_whitespaces = false 291 | ij_xml_keep_whitespaces_around_cdata = preserve 292 | ij_xml_keep_whitespaces_inside_cdata = false 293 | ij_xml_line_comment_at_first_column = true 294 | ij_xml_space_after_tag_name = false 295 | ij_xml_space_around_equals_in_attribute = false 296 | ij_xml_space_inside_empty_tag = false 297 | ij_xml_text_wrap = normal 298 | ij_xml_use_custom_settings = true 299 | 300 | [{*.bash,*.sh,*.zsh}] 301 | ij_shell_binary_ops_start_line = false 302 | ij_shell_keep_column_alignment_padding = false 303 | ij_shell_minify_program = false 304 | ij_shell_redirect_followed_by_space = false 305 | ij_shell_switch_cases_indented = false 306 | 307 | [{*.gant,*.gradle,*.groovy,*.gy}] 308 | ij_groovy_align_group_field_declarations = false 309 | ij_groovy_align_multiline_array_initializer_expression = false 310 | ij_groovy_align_multiline_assignment = false 311 | ij_groovy_align_multiline_binary_operation = false 312 | ij_groovy_align_multiline_chained_methods = false 313 | ij_groovy_align_multiline_extends_list = false 314 | ij_groovy_align_multiline_for = false 315 | ij_groovy_align_multiline_list_or_map = false 316 | ij_groovy_align_multiline_method_parentheses = false 317 | ij_groovy_align_multiline_parameters = false 318 | ij_groovy_align_multiline_parameters_in_calls = false 319 | ij_groovy_align_multiline_resources = true 320 | ij_groovy_align_multiline_ternary_operation = false 321 | ij_groovy_align_multiline_throws_list = false 322 | ij_groovy_align_named_args_in_map = false 323 | ij_groovy_align_throws_keyword = false 324 | ij_groovy_array_initializer_new_line_after_left_brace = false 325 | ij_groovy_array_initializer_right_brace_on_new_line = false 326 | ij_groovy_array_initializer_wrap = off 327 | ij_groovy_assert_statement_wrap = normal 328 | ij_groovy_assignment_wrap = normal 329 | ij_groovy_binary_operation_wrap = on_every_item 330 | ij_groovy_blank_lines_after_class_header = 0 331 | ij_groovy_blank_lines_after_imports = 1 332 | ij_groovy_blank_lines_after_package = 1 333 | ij_groovy_blank_lines_around_class = 1 334 | ij_groovy_blank_lines_around_field = 0 335 | ij_groovy_blank_lines_around_field_in_interface = 0 336 | ij_groovy_blank_lines_around_method = 1 337 | ij_groovy_blank_lines_around_method_in_interface = 1 338 | ij_groovy_blank_lines_before_imports = 1 339 | ij_groovy_blank_lines_before_method_body = 0 340 | ij_groovy_blank_lines_before_package = 0 341 | ij_groovy_block_brace_style = end_of_line 342 | ij_groovy_block_comment_at_first_column = true 343 | ij_groovy_call_parameters_new_line_after_left_paren = false 344 | ij_groovy_call_parameters_right_paren_on_new_line = false 345 | ij_groovy_call_parameters_wrap = normal 346 | ij_groovy_catch_on_new_line = false 347 | ij_groovy_class_annotation_wrap = normal 348 | ij_groovy_class_brace_style = end_of_line 349 | ij_groovy_class_count_to_use_import_on_demand = 5 350 | ij_groovy_do_while_brace_force = never 351 | ij_groovy_else_on_new_line = false 352 | ij_groovy_enum_constants_wrap = off 353 | ij_groovy_extends_keyword_wrap = normal 354 | ij_groovy_extends_list_wrap = normal 355 | ij_groovy_field_annotation_wrap = normal 356 | ij_groovy_finally_on_new_line = false 357 | ij_groovy_for_brace_force = never 358 | ij_groovy_for_statement_new_line_after_left_paren = false 359 | ij_groovy_for_statement_right_paren_on_new_line = false 360 | ij_groovy_for_statement_wrap = normal 361 | ij_groovy_if_brace_force = if_multiline 362 | ij_groovy_import_annotation_wrap = 2 363 | ij_groovy_imports_layout = *,|,javax.**,java.**,|,$* 364 | ij_groovy_indent_case_from_switch = true 365 | ij_groovy_indent_label_blocks = true 366 | ij_groovy_insert_inner_class_imports = false 367 | ij_groovy_keep_blank_lines_before_right_brace = 0 368 | ij_groovy_keep_blank_lines_in_code = 1 369 | ij_groovy_keep_blank_lines_in_declarations = 1 370 | ij_groovy_keep_control_statement_in_one_line = true 371 | ij_groovy_keep_first_column_comment = false 372 | ij_groovy_keep_indents_on_empty_lines = false 373 | ij_groovy_keep_line_breaks = true 374 | ij_groovy_keep_multiple_expressions_in_one_line = false 375 | ij_groovy_keep_simple_blocks_in_one_line = false 376 | ij_groovy_keep_simple_classes_in_one_line = true 377 | ij_groovy_keep_simple_lambdas_in_one_line = true 378 | ij_groovy_keep_simple_methods_in_one_line = true 379 | ij_groovy_label_indent_absolute = false 380 | ij_groovy_label_indent_size = 0 381 | ij_groovy_lambda_brace_style = end_of_line 382 | ij_groovy_layout_static_imports_separately = true 383 | ij_groovy_line_comment_add_space = false 384 | ij_groovy_line_comment_at_first_column = true 385 | ij_groovy_method_annotation_wrap = normal 386 | ij_groovy_method_brace_style = end_of_line 387 | ij_groovy_method_call_chain_wrap = on_every_item 388 | ij_groovy_method_parameters_new_line_after_left_paren = false 389 | ij_groovy_method_parameters_right_paren_on_new_line = false 390 | ij_groovy_method_parameters_wrap = normal 391 | ij_groovy_modifier_list_wrap = false 392 | ij_groovy_names_count_to_use_import_on_demand = 3 393 | ij_groovy_parameter_annotation_wrap = normal 394 | ij_groovy_parentheses_expression_new_line_after_left_paren = false 395 | ij_groovy_parentheses_expression_right_paren_on_new_line = false 396 | ij_groovy_prefer_parameters_wrap = false 397 | ij_groovy_resource_list_new_line_after_left_paren = false 398 | ij_groovy_resource_list_right_paren_on_new_line = false 399 | ij_groovy_resource_list_wrap = off 400 | ij_groovy_space_after_assert_separator = true 401 | ij_groovy_space_after_colon = true 402 | ij_groovy_space_after_comma = true 403 | ij_groovy_space_after_comma_in_type_arguments = true 404 | ij_groovy_space_after_for_semicolon = true 405 | ij_groovy_space_after_quest = true 406 | ij_groovy_space_after_type_cast = true 407 | ij_groovy_space_before_annotation_parameter_list = false 408 | ij_groovy_space_before_array_initializer_left_brace = false 409 | ij_groovy_space_before_assert_separator = false 410 | ij_groovy_space_before_catch_keyword = true 411 | ij_groovy_space_before_catch_left_brace = true 412 | ij_groovy_space_before_catch_parentheses = true 413 | ij_groovy_space_before_class_left_brace = true 414 | ij_groovy_space_before_closure_left_brace = true 415 | ij_groovy_space_before_colon = true 416 | ij_groovy_space_before_comma = false 417 | ij_groovy_space_before_do_left_brace = true 418 | ij_groovy_space_before_else_keyword = true 419 | ij_groovy_space_before_else_left_brace = true 420 | ij_groovy_space_before_finally_keyword = true 421 | ij_groovy_space_before_finally_left_brace = true 422 | ij_groovy_space_before_for_left_brace = true 423 | ij_groovy_space_before_for_parentheses = true 424 | ij_groovy_space_before_for_semicolon = false 425 | ij_groovy_space_before_if_left_brace = true 426 | ij_groovy_space_before_if_parentheses = true 427 | ij_groovy_space_before_method_call_parentheses = false 428 | ij_groovy_space_before_method_left_brace = true 429 | ij_groovy_space_before_method_parentheses = false 430 | ij_groovy_space_before_quest = true 431 | ij_groovy_space_before_switch_left_brace = true 432 | ij_groovy_space_before_switch_parentheses = true 433 | ij_groovy_space_before_synchronized_left_brace = true 434 | ij_groovy_space_before_synchronized_parentheses = true 435 | ij_groovy_space_before_try_left_brace = true 436 | ij_groovy_space_before_try_parentheses = true 437 | ij_groovy_space_before_while_keyword = true 438 | ij_groovy_space_before_while_left_brace = true 439 | ij_groovy_space_before_while_parentheses = true 440 | ij_groovy_space_in_named_argument = true 441 | ij_groovy_space_in_named_argument_before_colon = false 442 | ij_groovy_space_within_empty_array_initializer_braces = false 443 | ij_groovy_space_within_empty_method_call_parentheses = false 444 | ij_groovy_spaces_around_additive_operators = true 445 | ij_groovy_spaces_around_assignment_operators = true 446 | ij_groovy_spaces_around_bitwise_operators = true 447 | ij_groovy_spaces_around_equality_operators = true 448 | ij_groovy_spaces_around_lambda_arrow = true 449 | ij_groovy_spaces_around_logical_operators = true 450 | ij_groovy_spaces_around_multiplicative_operators = true 451 | ij_groovy_spaces_around_regex_operators = true 452 | ij_groovy_spaces_around_relational_operators = true 453 | ij_groovy_spaces_around_shift_operators = true 454 | ij_groovy_spaces_within_annotation_parentheses = false 455 | ij_groovy_spaces_within_array_initializer_braces = false 456 | ij_groovy_spaces_within_braces = true 457 | ij_groovy_spaces_within_brackets = false 458 | ij_groovy_spaces_within_cast_parentheses = false 459 | ij_groovy_spaces_within_catch_parentheses = false 460 | ij_groovy_spaces_within_for_parentheses = false 461 | ij_groovy_spaces_within_gstring_injection_braces = false 462 | ij_groovy_spaces_within_if_parentheses = false 463 | ij_groovy_spaces_within_list_or_map = false 464 | ij_groovy_spaces_within_method_call_parentheses = false 465 | ij_groovy_spaces_within_method_parentheses = false 466 | ij_groovy_spaces_within_parentheses = false 467 | ij_groovy_spaces_within_switch_parentheses = false 468 | ij_groovy_spaces_within_synchronized_parentheses = false 469 | ij_groovy_spaces_within_try_parentheses = false 470 | ij_groovy_spaces_within_tuple_expression = false 471 | ij_groovy_spaces_within_while_parentheses = false 472 | ij_groovy_special_else_if_treatment = true 473 | ij_groovy_ternary_operation_wrap = normal 474 | ij_groovy_throws_keyword_wrap = normal 475 | ij_groovy_throws_list_wrap = normal 476 | ij_groovy_use_flying_geese_braces = false 477 | ij_groovy_use_fq_class_names = false 478 | ij_groovy_use_fq_class_names_in_javadoc = true 479 | ij_groovy_use_relative_indents = false 480 | ij_groovy_use_single_class_imports = true 481 | ij_groovy_variable_annotation_wrap = normal 482 | ij_groovy_while_brace_force = if_multiline 483 | ij_groovy_while_on_new_line = false 484 | ij_groovy_wrap_long_lines = false 485 | 486 | [{*.gradle.kts,*.kt,*.kts,*.main.kts}] 487 | ij_continuation_indent_size = 2 488 | ij_kotlin_align_in_columns_case_branch = false 489 | ij_kotlin_align_multiline_binary_operation = false 490 | ij_kotlin_align_multiline_extends_list = false 491 | ij_kotlin_align_multiline_method_parentheses = false 492 | ij_kotlin_align_multiline_parameters = false 493 | ij_kotlin_align_multiline_parameters_in_calls = false 494 | ij_kotlin_allow_trailing_comma = false 495 | ij_kotlin_allow_trailing_comma_on_call_site = false 496 | ij_kotlin_assignment_wrap = normal 497 | ij_kotlin_blank_lines_after_class_header = 0 498 | ij_kotlin_blank_lines_around_block_when_branches = 0 499 | ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 500 | ij_kotlin_block_comment_at_first_column = true 501 | ij_kotlin_call_parameters_new_line_after_left_paren = true 502 | ij_kotlin_call_parameters_right_paren_on_new_line = true 503 | ij_kotlin_call_parameters_wrap = normal 504 | ij_kotlin_catch_on_new_line = false 505 | ij_kotlin_class_annotation_wrap = split_into_lines 506 | ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL 507 | ij_kotlin_continuation_indent_for_chained_calls = true 508 | ij_kotlin_continuation_indent_for_expression_bodies = false 509 | ij_kotlin_continuation_indent_in_argument_lists = true 510 | ij_kotlin_continuation_indent_in_elvis = true 511 | ij_kotlin_continuation_indent_in_if_conditions = true 512 | ij_kotlin_continuation_indent_in_parameter_lists = false 513 | ij_kotlin_continuation_indent_in_supertype_lists = true 514 | ij_kotlin_else_on_new_line = false 515 | ij_kotlin_enum_constants_wrap = split_into_lines 516 | ij_kotlin_extends_list_wrap = on_every_item 517 | ij_kotlin_field_annotation_wrap = normal 518 | ij_kotlin_finally_on_new_line = false 519 | ij_kotlin_if_rparen_on_new_line = true 520 | ij_kotlin_import_nested_classes = true 521 | ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ 522 | ij_kotlin_insert_whitespaces_in_simple_one_line_method = true 523 | ij_kotlin_keep_blank_lines_before_right_brace = 0 524 | ij_kotlin_keep_blank_lines_in_code = 1 525 | ij_kotlin_keep_blank_lines_in_declarations = 1 526 | ij_kotlin_keep_first_column_comment = true 527 | ij_kotlin_keep_indents_on_empty_lines = false 528 | ij_kotlin_keep_line_breaks = true 529 | ij_kotlin_lbrace_on_next_line = false 530 | ij_kotlin_line_comment_add_space = true 531 | ij_kotlin_line_comment_at_first_column = false 532 | ij_kotlin_method_annotation_wrap = normal 533 | ij_kotlin_method_call_chain_wrap = on_every_item 534 | ij_kotlin_method_parameters_new_line_after_left_paren = true 535 | ij_kotlin_method_parameters_right_paren_on_new_line = true 536 | ij_kotlin_method_parameters_wrap = split_into_lines 537 | ij_kotlin_name_count_to_use_star_import = 999 538 | ij_kotlin_name_count_to_use_star_import_for_members = 999 539 | ij_kotlin_parameter_annotation_wrap = off 540 | ij_kotlin_space_after_comma = true 541 | ij_kotlin_space_after_extend_colon = true 542 | ij_kotlin_space_after_type_colon = true 543 | ij_kotlin_space_before_catch_parentheses = true 544 | ij_kotlin_space_before_comma = false 545 | ij_kotlin_space_before_extend_colon = true 546 | ij_kotlin_space_before_for_parentheses = true 547 | ij_kotlin_space_before_if_parentheses = true 548 | ij_kotlin_space_before_lambda_arrow = true 549 | ij_kotlin_space_before_type_colon = false 550 | ij_kotlin_space_before_when_parentheses = true 551 | ij_kotlin_space_before_while_parentheses = true 552 | ij_kotlin_spaces_around_additive_operators = true 553 | ij_kotlin_spaces_around_assignment_operators = true 554 | ij_kotlin_spaces_around_equality_operators = true 555 | ij_kotlin_spaces_around_function_type_arrow = true 556 | ij_kotlin_spaces_around_logical_operators = true 557 | ij_kotlin_spaces_around_multiplicative_operators = true 558 | ij_kotlin_spaces_around_range = false 559 | ij_kotlin_spaces_around_relational_operators = true 560 | ij_kotlin_spaces_around_unary_operator = false 561 | ij_kotlin_spaces_around_when_arrow = true 562 | ij_kotlin_variable_annotation_wrap = off 563 | ij_kotlin_while_on_new_line = false 564 | ij_kotlin_wrap_elvis_expressions = 1 565 | ij_kotlin_wrap_expression_body_functions = 1 566 | ij_kotlin_wrap_first_method_in_call_chain = false 567 | 568 | [{*.har,*.json}] 569 | tab_width = 4 570 | ij_continuation_indent_size = 8 571 | ij_json_keep_blank_lines_in_code = 1 572 | ij_json_keep_indents_on_empty_lines = false 573 | ij_json_keep_line_breaks = true 574 | ij_json_space_after_colon = true 575 | ij_json_space_after_comma = true 576 | ij_json_space_before_colon = true 577 | ij_json_space_before_comma = false 578 | ij_json_spaces_within_braces = false 579 | ij_json_spaces_within_brackets = false 580 | ij_json_wrap_long_lines = false 581 | 582 | [{*.htm,*.html,*.sht,*.shtm,*.shtml}] 583 | indent_size = 4 584 | tab_width = 4 585 | ij_continuation_indent_size = 8 586 | ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 587 | ij_html_align_attributes = true 588 | ij_html_align_text = false 589 | ij_html_attribute_wrap = normal 590 | ij_html_block_comment_at_first_column = true 591 | ij_html_do_not_align_children_of_min_lines = 0 592 | ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p 593 | ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot 594 | ij_html_enforce_quotes = false 595 | ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var 596 | ij_html_keep_blank_lines = 2 597 | ij_html_keep_indents_on_empty_lines = false 598 | ij_html_keep_line_breaks = true 599 | ij_html_keep_line_breaks_in_text = true 600 | ij_html_keep_whitespaces = false 601 | ij_html_keep_whitespaces_inside = span,pre,textarea 602 | ij_html_line_comment_at_first_column = true 603 | ij_html_new_line_after_last_attribute = never 604 | ij_html_new_line_before_first_attribute = never 605 | ij_html_quote_style = double 606 | ij_html_remove_new_line_before_tags = br 607 | ij_html_space_after_tag_name = false 608 | ij_html_space_around_equality_in_attribute = false 609 | ij_html_space_inside_empty_tag = false 610 | ij_html_text_wrap = normal 611 | ij_html_uniform_ident = false 612 | 613 | [{*.yaml,*.yml}] 614 | ij_yaml_keep_indents_on_empty_lines = false 615 | ij_yaml_keep_line_breaks = true 616 | ij_yaml_space_before_colon = true 617 | ij_yaml_spaces_within_braces = true 618 | ij_yaml_spaces_within_brackets = true -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo, unless a later match takes precedence. 3 | * @pyricau 4 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Open Source Code of Conduct 2 | =========================== 3 | 4 | At Square, we are committed to contributing to the open source community and simplifying the process 5 | of releasing and managing open source software. We’ve seen incredible support and enthusiasm from 6 | thousands of people who have already contributed to our projects — and we want to ensure ourcommunity 7 | continues to be truly open for everyone. 8 | 9 | This code of conduct outlines our expectations for participants, as well as steps to reporting 10 | unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and 11 | expect our code of conduct to be honored. 12 | 13 | Square’s open source community strives to: 14 | 15 | * **Be open**: We invite anyone to participate in any aspect of our projects. Our community is 16 | open, and any responsibility can be carried by a contributor who demonstrates the required 17 | capacity and competence. 18 | 19 | * **Be considerate**: People use our work, and we depend on the work of others. Consider users and 20 | colleagues before taking action. For example, changes to code, infrastructure, policy, and 21 | documentation may negatively impact others. 22 | 23 | * **Be respectful**: We expect people to work together to resolve conflict, assume good intentions, 24 | and act with empathy. Do not turn disagreements into personal attacks. 25 | 26 | * **Be collaborative**: Collaboration reduces redundancy and improves the quality of our work. We 27 | strive for transparency within our open source community, and we work closely with upstream 28 | developers and others in the free software community to coordinate our efforts. 29 | 30 | * **Be pragmatic**: Questions are encouraged and should be asked early in the process to avoid 31 | problems later. Be thoughtful and considerate when seeking out the appropriate forum for your 32 | questions. Those who are asked should be responsive and helpful. 33 | 34 | * **Step down considerately**: Members of every project come and go. When somebody leaves or 35 | disengages from the project, they should make it known and take the proper steps to ensure that 36 | others can pick up where they left off. 37 | 38 | This code is not exhaustive or complete. It serves to distill our common understanding of a 39 | collaborative, shared environment, and goals. We expect it to be followed in spirit as much as in 40 | the letter. 41 | 42 | Diversity Statement 43 | ------------------- 44 | 45 | We encourage everyone to participate and are committed to building a community for all. Although we 46 | may not be able to satisfy everyone, we all agree that everyone is equal. 47 | 48 | Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone 49 | has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do 50 | our best to right the wrong. 51 | 52 | Although this list cannot be exhaustive, we explicitly honor diversity in age, culture, ethnicity, 53 | gender identity or expression, language, national origin, political beliefs, profession, race, 54 | religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate 55 | discrimination based on any of the protected characteristics above, including participants with 56 | disabilities. 57 | 58 | Reporting Issues 59 | ---------------- 60 | 61 | If you experience or witness unacceptable behavior — or have any other concerns — please report it by 62 | emailing [codeofconduct@squareup.com][codeofconduct_at]. For more details, please see our Reporting 63 | Guidelines below. 64 | 65 | Thanks 66 | ------ 67 | 68 | Some of the ideas and wording for the statements and guidelines above were based on work by the 69 | [Twitter][twitter_coc], [Ubuntu][ubuntu_coc], [GDC][gdc_coc], and [Django][django_coc] communities. 70 | We are thankful for their work. 71 | 72 | Reporting Guide 73 | --------------- 74 | 75 | If you experience or witness unacceptable behavior — or have any other concerns — please report it by 76 | emailing [codeofconduct@squareup.com][codeofconduct_at]. All reports will be handled with 77 | discretion. 78 | 79 | In your report please include: 80 | 81 | * Your contact information. 82 | * Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional 83 | witnesses, please include them as well. 84 | * Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly 85 | available record (e.g. a mailing list archive or a public IRC logger), please include a link. 86 | * Any additional information that may be helpful. 87 | 88 | After filing a report, a representative from the Square Code of Conduct committee will contact you 89 | personally. The committee will then review the incident, follow up with any additional questions, 90 | and make a decision as to how to respond. 91 | 92 | Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual 93 | engages in unacceptable behavior, the Square Code of Conduct committee may take any action they deem 94 | appropriate, up to and including a permanent ban from all of Square spaces without warning. 95 | 96 | [codeofconduct_at]: mailto:codeofconduct@squareup.com 97 | [twitter_coc]: https://github.com/twitter/code-of-conduct/blob/master/code-of-conduct.md 98 | [ubuntu_coc]: https://ubuntu.com/community/code-of-conduct 99 | [gdc_coc]: https://www.gdconf.com/code-of-conduct 100 | [django_coc]: https://www.djangoproject.com/conduct/reporting/ 101 | 102 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Contribute code by forking the repository on GitHub and sending a pull request. When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | # Build on all pull requests, regardless of target. 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: set up JDK 17 17 | uses: actions/setup-java@v3 18 | with: 19 | distribution: 'zulu' 20 | java-version: 17 21 | - name: Build with Gradle 22 | run: ./gradlew build 23 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | publish-release: 8 | runs-on: ubuntu-latest 9 | if: github.repository == 'square/logcat' 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: set up JDK 17 13 | uses: actions/setup-java@v3 14 | with: 15 | distribution: 'zulu' 16 | java-version: 17 17 | - name: Build with Gradle 18 | run: ./gradlew build --stacktrace 19 | - name: Publish to Sonatype 20 | run: ./gradlew publish --no-daemon --no-parallel 21 | env: 22 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 23 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 24 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }} 25 | - name: Release to Maven Central 26 | run: ./gradlew closeAndReleaseRepository 27 | env: 28 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 29 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 30 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | 3 | # macOS 4 | .DS_Store 5 | 6 | # Compiled class file 7 | *.class 8 | 9 | # Log file 10 | *.log 11 | 12 | # BlueJ files 13 | *.ctxt 14 | 15 | # Mobile Tools for Java (J2ME) 16 | .mtj.tmp/ 17 | 18 | # Package Files # 19 | *.war 20 | *.nar 21 | *.ear 22 | *.zip 23 | *.tar.gz 24 | *.rar 25 | 26 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 27 | hs_err_pid* 28 | 29 | # Gradle 30 | out/ 31 | .gradle/ 32 | build/ 33 | local.properties 34 | .gradletasknamecache 35 | 36 | 37 | # Intellij 38 | *.iml 39 | .idea/ 40 | captures/ 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | Version 0.2.1 5 | ------------- 6 | 7 | _2025-05-19_ 8 | 9 | * Multiple `LogcatLogger` instances can now be added to `LoggatLogger.loggers`. 10 | * Breaking change: `LogcatLogger.install()` does not take a `LogcatLogger` instance 11 | anymore and instead takes an optional Executor to control the thread on which messages 12 | are evaluated. 13 | * Android libraries that want to use `logcat` should continue to use 14 | `AndroidLogcatLogger.installOnDebuggableApp()` which checks if already installed so as not to 15 | override any app specific set up. 16 | * Changed the `minPriority` default parameter value of 17 | `AndroidLogcatLogger.installOnDebuggableApp()` from `DEBUG` to `VERBOSE`. 18 | * Added `@JvmStatic` and removed `Kt` from class name (`LogcatKt` => `Logcat`) so that APIs are more Java friendly (this remains primarily a Kotlin focused APIs but this change helps with codebases that still have some Java code hanging around). 19 | 20 | 21 | Version 0.1 22 | ------------- 23 | 24 | _2021-09-21_ 25 | 26 | Initial alpha release. 27 | 28 | * Everything is new! 29 | 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Square Logcat 2 | 3 | ```kotlin 4 | logcat { "I CAN HAZ LOGZ?" } 5 | ``` 6 | 7 | A tiny Kotlin API for cheap logging on top of Android's normal `Log` class. 8 | 9 | ## Table of contents 10 | 11 | * [Setup](#setup) 12 | * [Usage](#usage) 13 | * [Motivations](#motivations) 14 | * [License](#license) 15 | 16 | ![logo_512.png](assets/logo_512.png) 17 | 18 | _This fantastic logo is brought to you by [@rjrjr](https://github.com/rjrjr)._ 19 | 20 | ## Setup 21 | 22 | Add the `logcat` dependency to your library or app's `build.gradle` file: 23 | 24 | ```gradle 25 | dependencies { 26 | implementation 'com.squareup.logcat:logcat:0.2.1' 27 | } 28 | ``` 29 | 30 | Install `AndroidLogcatLogger` in `Application.onCreate()`: 31 | 32 | ```kotlin 33 | import android.app.Application 34 | import logcat.AndroidLogcatLogger 35 | import logcat.LogPriority.VERBOSE 36 | 37 | class ExampleApplication : Application() { 38 | override fun onCreate() { 39 | super.onCreate() 40 | // Log all priorities in debug builds, no-op in release builds. 41 | AndroidLogcatLogger.installOnDebuggableApp(this, minPriority = VERBOSE) 42 | } 43 | } 44 | ``` 45 | 46 | ## Usage 47 | 48 | The `logcat()` function has 3 parameters: an optional priority, an optional tag, and a required 49 | string producing lambda. The lambda is only evaluated if a logger is installed and the logger deems 50 | the priority loggable. 51 | 52 | The priority defaults to `LogPriority.DEBUG`. 53 | 54 | The tag defaults to the class name of the log call site, without any extra runtime cost. This works 55 | because `logcat()` is an inlined extension function of `Any` and has access to `this` from which 56 | it can extract the class name. If logging from a standalone function which has no `this`, use the 57 | `logcat` overload which requires a tag parameter. 58 | 59 | The `logcat()` function does not take a `Throwable` parameter. Instead, the library provides 60 | a Throwable extension function: `Throwable.asLog()` which returns a loggable string. 61 | 62 | ```kotlin 63 | import logcat.LogPriority.INFO 64 | import logcat.asLog 65 | import logcat.logcat 66 | 67 | class MouseController { 68 | 69 | fun play() { 70 | val state = "CHEEZBURGER" 71 | logcat { "I CAN HAZ $state?" } 72 | // logcat output: D/MouseController: I CAN HAZ CHEEZBURGER? 73 | 74 | logcat(INFO) { "DID U ASK 4 MOAR INFO?" } 75 | // logcat output: I/MouseController: DID U ASK 4 MOAR INFO? 76 | 77 | logcat { exception.asLog() } 78 | // logcat output: D/MouseController: java.lang.RuntimeException: FYLEZ KERUPTED 79 | // at sample.MouseController.play(MouseController.kt:22) 80 | // ... 81 | 82 | logcat("Lolcat") { "OH HI" } 83 | // logcat output: D/Lolcat: OH HI 84 | } 85 | } 86 | ``` 87 | 88 | ## Motivations 89 | 90 | We built this small library to fit the specific needs of the Square 91 | [Point of Sale](https://squareup.com/us/en/point-of-sale) application. We used 92 | [Timber](https://github.com/JakeWharton/timber) heavily before that, and love the simplicity of its 93 | API and the ability of its `DebugTree` to automatically figure out from which class it's being 94 | called from and use that class name as its tag. Here are our motivations for replacing it with 95 | `logcat()` in the Square Point of Sale: 96 | 97 | - Kotlin support for string interpolation is really nice. We love to use that for logs! 98 | Unfortunately that can be costly and a waste of good CPU if logging is disabled anyway. By using 99 | an inlined string producing lambda, `logcat()` supports string interpolation without any 100 | performance cost when logging is disabled. 101 | - Timber's `DebugTree` captures the calling class name as a tag by creating a stacktrace, which can 102 | be expensive. By making `logcat()` an extension function of `Any`, we can call `this::class.java` 103 | and get the calling context without creating a stacktrace. 104 | - The current implementation uses the outer most simple class name as the tag, i.e. the string 105 | between on the last `.` and the first `$`. That might not always be what you want. Also, when 106 | logging from an abstract class the tag will be the name of the subclass at runtime. We've found 107 | these limitations to be totally fine with us so far. 108 | - Most of the time, our developers just want to "send something to logcat" without even thinking 109 | about priority. `logcat()` picks "debug" as the right default to provide more consistency across 110 | a large codebase. Making the priority a parameter also means only one method to learn, and you 111 | don't have to learn / think about priorities prior to writing a log. This becomes especially 112 | important when there are several parameters requiring overloads (e.g. in Timber (6 priorities + 1 113 | generic log method) * 3 overloads = 21 methods to choose from). 114 | - The lack of throwable parameter is also intentional. It just creates more overloads and confusion 115 | (e.g. "what's the param order?"), when really logs are about strings and all you need is an easy 116 | way to turn a throwable into a loggable string. Hence `Throwable.asLog()`. 117 | - The name `logcat()` is intentionally boring and identical to the Android command line tool. This 118 | makes it easy to remember and developers know exactly what this does, i.e. log to the local device. 119 | One could setup a custom logger that send logs remotely in release builds, however we do not 120 | recommend doing so: in our experience, remote logs should be distinct in code from local logs and 121 | clearly identified as such, because the volume and performance impact should be very distinct. 122 | - The API for installing a logger is separated out from the API to log, as these operations occur 123 | in very distinct contexts. 124 | 125 | ## License 126 | 127 |
128 | Copyright 2021 Square Inc.
129 | 
130 | Licensed under the Apache License, Version 2.0 (the "License");
131 | you may not use this file except in compliance with the License.
132 | You may obtain a copy of the License at
133 | 
134 |     http://www.apache.org/licenses/LICENSE-2.0
135 | 
136 | Unless required by applicable law or agreed to in writing, software
137 | distributed under the License is distributed on an "AS IS" BASIS,
138 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
139 | See the License for the specific language governing permissions and
140 | limitations under the License.
141 | 
142 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 4 | ## Set up GitHub CLI 5 | 6 | Install GitHub CLI 7 | 8 | ```bash 9 | brew install gh 10 | ``` 11 | 12 | ## Creating the release 13 | 14 | * Update the changelog 15 | ```bash 16 | mate CHANGELOG.md 17 | ``` 18 | 19 | * Create a local release branch from `main` and update `VERSION_NAME` in `gradle.properties` (removing `-SNAPSHOT`) and the README, then run the publish workflow and finish the release: 20 | 21 | ```bash 22 | git checkout main && \ 23 | git pull && \ 24 | git checkout -b release_{NEW_VERSION} && \ 25 | sed -i '' 's/VERSION_NAME=.*-SNAPSHOT/VERSION_NAME={NEW_VERSION}/' gradle.properties 26 | sed -i '' "s/com.squareup.logcat:logcat:.*'/com.squareup.logcat:logcat:{NEW_VERSION}'/" README.md && \ 27 | git commit -am "Prepare {NEW_VERSION} release" && \ 28 | git tag v{NEW_VERSION} && \ 29 | git push origin v{NEW_VERSION} && \ 30 | gh workflow run publish-release.yml --ref v{NEW_VERSION} && \ 31 | sleep 5 &&\ 32 | gh run list --workflow=publish-release.yml --branch v{NEW_VERSION} --json databaseId --jq ".[].databaseId" | xargs -I{} gh run watch {} --exit-status && \ 33 | git checkout main && \ 34 | git pull && \ 35 | git merge --no-ff --no-edit release_{NEW_VERSION} && \ 36 | sed -i '' 's/VERSION_NAME={NEW_VERSION}/VERSION_NAME={NEXT_VERSION}-SNAPSHOT/' gradle.properties && \ 37 | git commit -am "Prepare for next development iteration" && \ 38 | git push && \ 39 | gh release create v{NEW_VERSION} --title v{NEW_VERSION} --notes 'See [Change Log](https://github.com/square/logcat/blob/main/CHANGELOG.md)' 40 | ``` 41 | 42 | * Wait for the release to be available [on Maven Central](https://repo1.maven.org/maven2/com/squareup/logcat/logcat/). 43 | * Tell your friends, update all of your apps, and tweet the new release. As a nice extra touch, mention external contributions. -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/logcat/3a08fd5725b91a2a3920dd27920b74faab0be812/assets/logo.png -------------------------------------------------------------------------------- /assets/logo_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/logcat/3a08fd5725b91a2a3920dd27920b74faab0be812/assets/logo_512.png -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.AndroidSingleVariantLibrary 2 | import com.vanniktech.maven.publish.MavenPublishBaseExtension 3 | import com.vanniktech.maven.publish.SonatypeHost 4 | import kotlinx.validation.ApiValidationExtension 5 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 6 | import org.jlleitschuh.gradle.ktlint.KtlintExtension 7 | import org.jlleitschuh.gradle.ktlint.reporter.ReporterType 8 | 9 | println("Building with Kotlin compiler version ${Versions.KotlinCompiler}") 10 | 11 | buildscript { 12 | repositories { 13 | mavenCentral() 14 | gradlePluginPortal() 15 | google() 16 | } 17 | 18 | dependencies { 19 | classpath(Dependencies.Build.Android) 20 | classpath(Dependencies.Build.MavenPublish) 21 | classpath(Dependencies.Build.Kotlin) 22 | classpath(Dependencies.Build.Ktlint) 23 | classpath(Dependencies.Build.BinaryCompatibility) 24 | // Required for the gradle-maven-publish-plugin plugin. 25 | // See https://github.com/vanniktech/gradle-maven-publish-plugin/issues/205. 26 | classpath(Dependencies.Build.Dokka) 27 | } 28 | } 29 | 30 | // We use JetBrain's Kotlin Binary Compatibility Validator to track changes to our public binary 31 | // APIs. 32 | // When making a change that results in a public ABI change, the apiCheck task will fail. When this 33 | // happens, run ./gradlew apiDump to generate updated *.api files, and add those to your commit. 34 | // See https://github.com/Kotlin/binary-compatibility-validator 35 | apply(plugin = "binary-compatibility-validator") 36 | 37 | extensions.configure { 38 | // Ignore all sample projects, since they're not part of our API. 39 | // Only leaf project name is valid configuration, and every project must be individually ignored. 40 | // See https://github.com/Kotlin/binary-compatibility-validator/issues/3 41 | ignoredProjects = mutableSetOf( 42 | "sample" 43 | ) 44 | } 45 | 46 | // See https://stackoverflow.com/questions/25324880/detect-ide-environment-with-gradle 47 | val isRunningFromIde get() = project.properties["android.injected.invoked.from.ide"] == "true" 48 | 49 | subprojects { 50 | repositories { 51 | google() 52 | mavenCentral() 53 | } 54 | 55 | apply(plugin = "org.jlleitschuh.gradle.ktlint") 56 | 57 | plugins.withId("com.vanniktech.maven.publish.base") { 58 | configure { 59 | publishToMavenCentral(SonatypeHost.S01, automaticRelease = true) 60 | signAllPublications() 61 | pomFromGradleProperties() 62 | 63 | configure( 64 | AndroidSingleVariantLibrary(variant = "release", sourcesJar = true, publishJavadocJar = true), 65 | ) 66 | } 67 | } 68 | 69 | tasks.withType { 70 | kotlinOptions { 71 | // Allow warnings when running from IDE, makes it easier to experiment. 72 | if (!isRunningFromIde) { 73 | allWarningsAsErrors = true 74 | } 75 | 76 | jvmTarget = "1.8" 77 | } 78 | } 79 | 80 | // Configuration documentation: https://github.com/JLLeitschuh/ktlint-gradle#configuration 81 | configure { 82 | // Prints the name of failed rules. 83 | verbose.set(true) 84 | reporters { 85 | // Default "plain" reporter is actually harder to read. 86 | reporter(ReporterType.JSON) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/Dependencies.kt: -------------------------------------------------------------------------------- 1 | object Versions { 2 | /** 3 | * To change this in the IDE, use `systemProp.square.kotlinVersion=x.y.z` in your 4 | * `~/.gradle/gradle.properties` file. 5 | */ 6 | val KotlinCompiler = System.getProperty("square.kotlinVersion") ?: "1.9.10" 7 | 8 | const val AndroidXTest = "1.5.0" 9 | } 10 | 11 | object Dependencies { 12 | object Build { 13 | const val Android = "com.android.tools.build:gradle:8.1.2" 14 | const val MavenPublish = "com.vanniktech:gradle-maven-publish-plugin:0.25.3" 15 | val Kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.KotlinCompiler}" 16 | const val Ktlint = "org.jlleitschuh.gradle:ktlint-gradle:11.6.1" 17 | const val AndroidXAnnotation = "androidx.annotation:annotation:1.1.0" 18 | const val BinaryCompatibility = "org.jetbrains.kotlinx:binary-compatibility-validator:0.6.0" 19 | const val Dokka = "org.jetbrains.dokka:dokka-gradle-plugin:1.9.10" 20 | } 21 | 22 | const val AppCompat = "androidx.appcompat:appcompat:1.3.1" 23 | const val JUnit = "junit:junit:4.13" 24 | const val Mockito = "org.mockito:mockito-core:3.11.2" 25 | const val Robolectric = "org.robolectric:robolectric:4.10.3" 26 | const val Truth = "com.google.truth:truth:1.1.3" 27 | 28 | object InstrumentationTests { 29 | const val Core = "androidx.test:core:${Versions.AndroidXTest}" 30 | const val Espresso = "androidx.test.espresso:espresso-core:3.5.1" 31 | const val JUnit = "androidx.test.ext:junit:1.1.5" 32 | const val Orchestrator = "androidx.test:orchestrator:${Versions.AndroidXTest}" 33 | const val Rules = "androidx.test:rules:${Versions.AndroidXTest}" 34 | const val Runner = "androidx.test:runner:${Versions.AndroidXTest}" 35 | const val UiAutomator = "androidx.test.uiautomator:uiautomator:2.2.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs='-Dfile.encoding=UTF-8' 2 | android.useAndroidX=true 3 | 4 | # Required to publish to Nexus (see https://github.com/gradle/gradle/issues/11308) 5 | systemProp.org.gradle.internal.publish.checksums.insecure=true 6 | 7 | GROUP=com.squareup.logcat 8 | VERSION_NAME=0.3-SNAPSHOT 9 | 10 | POM_DESCRIPTION=A tiny Kotlin API for cheap logging on top of Android's normal Log class. 11 | 12 | POM_URL=https://github.com/square/logcat/ 13 | POM_SCM_URL=https://github.com/square/logcat/ 14 | POM_SCM_CONNECTION=scm:git:git://github.com/square/logcat.git 15 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/square/logcat.git 16 | 17 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 18 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 19 | POM_LICENCE_DIST=repo 20 | 21 | POM_DEVELOPER_ID=square 22 | POM_DEVELOPER_NAME=Square, Inc. 23 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/logcat/3a08fd5725b91a2a3920dd27920b74faab0be812/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /logcat/api/logcat.api: -------------------------------------------------------------------------------- 1 | public final class logcat/AndroidLogcatLogger : logcat/LogcatLogger { 2 | public static final field Companion Llogcat/AndroidLogcatLogger$Companion; 3 | public fun ()V 4 | public fun (Llogcat/LogPriority;)V 5 | public synthetic fun (Llogcat/LogPriority;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 6 | public static final fun installOnDebuggableApp (Landroid/app/Application;Llogcat/LogPriority;)V 7 | public fun isLoggable (Llogcat/LogPriority;)Z 8 | public fun log (Llogcat/LogPriority;Ljava/lang/String;Ljava/lang/String;)V 9 | } 10 | 11 | public final class logcat/AndroidLogcatLogger$Companion { 12 | public final fun installOnDebuggableApp (Landroid/app/Application;Llogcat/LogPriority;)V 13 | public static synthetic fun installOnDebuggableApp$default (Llogcat/AndroidLogcatLogger$Companion;Landroid/app/Application;Llogcat/LogPriority;ILjava/lang/Object;)V 14 | } 15 | 16 | public final class logcat/AndroidLogcatLoggerKt { 17 | } 18 | 19 | public final class logcat/LogPriority : java/lang/Enum { 20 | public static final field ASSERT Llogcat/LogPriority; 21 | public static final field DEBUG Llogcat/LogPriority; 22 | public static final field ERROR Llogcat/LogPriority; 23 | public static final field INFO Llogcat/LogPriority; 24 | public static final field VERBOSE Llogcat/LogPriority; 25 | public static final field WARN Llogcat/LogPriority; 26 | public static fun getEntries ()Lkotlin/enums/EnumEntries; 27 | public final fun getPriorityInt ()I 28 | public static fun valueOf (Ljava/lang/String;)Llogcat/LogPriority; 29 | public static fun values ()[Llogcat/LogPriority; 30 | } 31 | 32 | public final class logcat/Logcat { 33 | public static final fun logcat (Ljava/lang/Object;Llogcat/LogPriority;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V 34 | public static final fun logcat (Ljava/lang/String;Llogcat/LogPriority;Lkotlin/jvm/functions/Function0;)V 35 | public static synthetic fun logcat$default (Ljava/lang/Object;Llogcat/LogPriority;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V 36 | public static synthetic fun logcat$default (Ljava/lang/String;Llogcat/LogPriority;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V 37 | public static final fun outerClassSimpleNameInternalOnlyDoNotUseKThxBye (Ljava/lang/Object;)Ljava/lang/String; 38 | } 39 | 40 | public abstract interface class logcat/LogcatLogger { 41 | public static final field Companion Llogcat/LogcatLogger$Companion; 42 | public static fun getLoggers ()Ljava/util/List; 43 | public static fun install (Ljava/util/concurrent/Executor;)V 44 | public static fun isInstalled ()Z 45 | public abstract fun isLoggable (Llogcat/LogPriority;)Z 46 | public abstract fun log (Llogcat/LogPriority;Ljava/lang/String;Ljava/lang/String;)V 47 | public static fun uninstall ()V 48 | } 49 | 50 | public final class logcat/LogcatLogger$Companion { 51 | public final fun getLogExecutor ()Ljava/util/concurrent/Executor; 52 | public final fun getLogger ()Llogcat/LogcatLogger; 53 | public final fun getLoggers ()Ljava/util/List; 54 | public final fun install (Ljava/util/concurrent/Executor;)V 55 | public final fun install (Llogcat/LogcatLogger;)V 56 | public static synthetic fun install$default (Llogcat/LogcatLogger$Companion;Ljava/util/concurrent/Executor;ILjava/lang/Object;)V 57 | public final fun isInstalled ()Z 58 | public final fun uninstall ()V 59 | } 60 | 61 | public final class logcat/LogcatLogger$DefaultImpls { 62 | public static fun isLoggable (Llogcat/LogcatLogger;Llogcat/LogPriority;)Z 63 | } 64 | 65 | public final class logcat/PrintLogger : logcat/LogcatLogger { 66 | public static final field INSTANCE Llogcat/PrintLogger; 67 | public fun isLoggable (Llogcat/LogPriority;)Z 68 | public fun log (Llogcat/LogPriority;Ljava/lang/String;Ljava/lang/String;)V 69 | } 70 | 71 | public final class logcat/ThrowablesKt { 72 | public static final fun asLog (Ljava/lang/Throwable;)Ljava/lang/String; 73 | } 74 | 75 | -------------------------------------------------------------------------------- /logcat/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | kotlin("android") 4 | id("com.vanniktech.maven.publish.base") 5 | } 6 | 7 | android { 8 | compileSdk = 34 9 | 10 | compileOptions { 11 | sourceCompatibility = JavaVersion.VERSION_1_8 12 | targetCompatibility = JavaVersion.VERSION_1_8 13 | } 14 | 15 | defaultConfig { 16 | minSdk = 17 17 | targetSdk = 34 18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 19 | } 20 | 21 | buildFeatures { 22 | buildConfig = false 23 | } 24 | 25 | testOptions { 26 | execution = "ANDROIDX_TEST_ORCHESTRATOR" 27 | } 28 | 29 | namespace = "com.squareup.logcat" 30 | testNamespace = "com.squareup.logcat.test" 31 | } 32 | 33 | dependencies { 34 | 35 | compileOnly(Dependencies.Build.AndroidXAnnotation) 36 | 37 | testImplementation(Dependencies.JUnit) 38 | testImplementation(Dependencies.Mockito) 39 | testImplementation(Dependencies.Robolectric) 40 | testImplementation(Dependencies.Truth) 41 | 42 | androidTestImplementation(Dependencies.InstrumentationTests.Core) 43 | androidTestImplementation(Dependencies.InstrumentationTests.Espresso) 44 | androidTestImplementation(Dependencies.InstrumentationTests.JUnit) 45 | androidTestImplementation(Dependencies.InstrumentationTests.Rules) 46 | androidTestImplementation(Dependencies.InstrumentationTests.Runner) 47 | androidTestImplementation(Dependencies.InstrumentationTests.UiAutomator) 48 | androidTestImplementation(Dependencies.Truth) 49 | androidTestImplementation(Dependencies.AppCompat) 50 | 51 | androidTestUtil(Dependencies.InstrumentationTests.Orchestrator) 52 | } 53 | -------------------------------------------------------------------------------- /logcat/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=logcat 2 | POM_NAME=Logcat 3 | POM_PACKAGING=aar 4 | -------------------------------------------------------------------------------- /logcat/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /logcat/src/main/java/logcat/AndroidLogcatLogger.kt: -------------------------------------------------------------------------------- 1 | package logcat 2 | 3 | import android.app.Application 4 | import android.content.pm.ApplicationInfo 5 | import android.os.Build 6 | import android.util.Log 7 | import logcat.AndroidLogcatLogger.Companion.installOnDebuggableApp 8 | import logcat.LogPriority.DEBUG 9 | import logcat.LogPriority.VERBOSE 10 | import kotlin.math.min 11 | 12 | private const val MAX_LOG_LENGTH = 4000 13 | private const val MAX_TAG_LENGTH = 23 14 | 15 | /** 16 | * A [logcat] logger that delegates to [android.util.Log] for any log with a priority of 17 | * at least [minPriorityInt], and is otherwise a no-op. 18 | * 19 | * Handles special cases for [LogPriority.ASSERT] (which requires sending to Log.wtf) and 20 | * splitting logs to be at most 4000 characters per line (otherwise logcat just truncates). 21 | * 22 | * Call [installOnDebuggableApp] to make sure you never log in release builds. 23 | * 24 | * The implementation is based on Timber DebugTree. 25 | */ 26 | class AndroidLogcatLogger(minPriority: LogPriority = DEBUG) : LogcatLogger { 27 | 28 | private val minPriorityInt: Int = minPriority.priorityInt 29 | 30 | override fun isLoggable(priority: LogPriority): Boolean = 31 | priority.priorityInt >= minPriorityInt 32 | 33 | override fun log( 34 | priority: LogPriority, 35 | tag: String, 36 | message: String 37 | ) { 38 | // Tag length limit was removed in API 26. 39 | val trimmedTag = if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) { 40 | tag 41 | } else { 42 | tag.substring(0, MAX_TAG_LENGTH) 43 | } 44 | 45 | if (message.length < MAX_LOG_LENGTH) { 46 | logToLogcat(priority.priorityInt, trimmedTag, message) 47 | return 48 | } 49 | 50 | // Split by line, then ensure each line can fit into Log's maximum length. 51 | var i = 0 52 | val length = message.length 53 | while (i < length) { 54 | var newline = message.indexOf('\n', i) 55 | newline = if (newline != -1) newline else length 56 | do { 57 | val end = min(newline, i + MAX_LOG_LENGTH) 58 | val part = message.substring(i, end) 59 | logToLogcat(priority.priorityInt, trimmedTag, part) 60 | i = end 61 | } while (i < newline) 62 | i++ 63 | } 64 | } 65 | 66 | private fun logToLogcat( 67 | priority: Int, 68 | tag: String, 69 | part: String 70 | ) { 71 | if (priority == Log.ASSERT) { 72 | Log.wtf(tag, part) 73 | } else { 74 | Log.println(priority, tag, part) 75 | } 76 | } 77 | 78 | companion object { 79 | @JvmStatic 80 | fun installOnDebuggableApp(application: Application, minPriority: LogPriority = VERBOSE) { 81 | if (!LogcatLogger.isInstalled && application.isDebuggableApp) { 82 | LogcatLogger.install() 83 | LogcatLogger.loggers += AndroidLogcatLogger(minPriority) 84 | } 85 | } 86 | } 87 | } 88 | 89 | private val Application.isDebuggableApp: Boolean 90 | get() = (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 91 | -------------------------------------------------------------------------------- /logcat/src/main/java/logcat/LogPriority.kt: -------------------------------------------------------------------------------- 1 | package logcat 2 | 3 | /** 4 | * An enum for log priorities that map to [android.util.Log] priority constants 5 | * without a direct import. 6 | */ 7 | enum class LogPriority( 8 | val priorityInt: Int 9 | ) { 10 | VERBOSE(2), 11 | DEBUG(3), 12 | INFO(4), 13 | WARN(5), 14 | ERROR(6), 15 | ASSERT(7); 16 | } 17 | -------------------------------------------------------------------------------- /logcat/src/main/java/logcat/Logcat.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("Logcat") 2 | 3 | package logcat 4 | 5 | import logcat.LogPriority.DEBUG 6 | 7 | /** 8 | * A tiny Kotlin API for cheap logging on top of Android's normal `Log` class. 9 | * 10 | * The [logcat] function has 3 parameters: an optional [priority], an optional [tag], and a required 11 | * string producing lambda ([message]). The lambda is only evaluated if a logger is installed and 12 | * the logger deems the priority loggable. 13 | * 14 | * The priority defaults to [LogPriority.DEBUG]. 15 | * 16 | * The tag defaults to the class name of the log call site, without any extra runtime cost. This works 17 | * because [logcat] is an inlined extension function of [Any] and has access to [this] from which 18 | * it can extract the class name. If logging from a standalone function which has no [this], use the 19 | * [logcat] overload which requires a tag parameter. 20 | * 21 | * The [logcat] function does not take a [Throwable] parameter. Instead, the library provides 22 | * a Throwable extension function: [Throwable.asLog] which returns a loggable string. 23 | * 24 | * ``` 25 | * import logcat.LogPriority.INFO 26 | * import logcat.asLog 27 | * import logcat.logcat 28 | * 29 | * class MouseController { 30 | * 31 | * fun play { 32 | * var state = "CHEEZBURGER" 33 | * logcat { "I CAN HAZ $state?" } 34 | * // logcat output: D/MouseController: I CAN HAZ CHEEZBURGER? 35 | * 36 | * logcat(INFO) { "DID U ASK 4 MOAR INFO?" } 37 | * // logcat output: I/MouseController: DID U ASK 4 MOAR INFO? 38 | * 39 | * logcat { exception.asLog() } 40 | * // logcat output: D/MouseController: java.lang.RuntimeException: FYLEZ KERUPTED 41 | * // at sample.MouseController.play(MouseController.kt:22) 42 | * // ... 43 | * 44 | * logcat("Lolcat") { "OH HI" } 45 | * // logcat output: D/Lolcat: OH HI 46 | * } 47 | * } 48 | * ``` 49 | * 50 | * To install a logger, see [LogcatLogger]. 51 | * 52 | * @param tag If provided, the log will use this [tag] instead of the simple class name of [this] at 53 | * the call site. 54 | */ 55 | inline fun Any.logcat( 56 | priority: LogPriority = DEBUG, 57 | tag: String? = null, 58 | crossinline message: () -> String 59 | ) { 60 | val logExecutor = LogcatLogger.logExecutor ?: return 61 | val loggers = LogcatLogger.loggers.filter { it.isLoggable(priority) } 62 | if (loggers.isNotEmpty()) { 63 | val tagOrCaller = tag ?: outerClassSimpleNameInternalOnlyDoNotUseKThxBye() 64 | logExecutor.execute { 65 | val evaluatedMessage = message() 66 | for (logger in loggers) { 67 | logger.log(priority, tagOrCaller, evaluatedMessage) 68 | } 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * An overload for logging that does not capture the calling code as tag. This should only 75 | * be used in standalone functions where there is no `this`. 76 | * @see logcat above 77 | */ 78 | inline fun logcat( 79 | tag: String, 80 | priority: LogPriority = DEBUG, 81 | crossinline message: () -> String 82 | ) { 83 | val logExecutor = LogcatLogger.logExecutor ?: return 84 | val loggers = LogcatLogger.loggers.filter { it.isLoggable(priority) } 85 | if (loggers.isNotEmpty()) { 86 | logExecutor.execute { 87 | val evaluatedMessage = message() 88 | for (logger in loggers) { 89 | logger.log(priority, tag, evaluatedMessage) 90 | } 91 | } 92 | } 93 | } 94 | 95 | @PublishedApi 96 | internal fun Any.outerClassSimpleNameInternalOnlyDoNotUseKThxBye(): String { 97 | val javaClass = this::class.java 98 | val fullClassName = javaClass.name 99 | val outerClassName = fullClassName.substringBefore('$') 100 | val simplerOuterClassName = outerClassName.substringAfterLast('.') 101 | return if (simplerOuterClassName.isEmpty()) { 102 | fullClassName 103 | } else { 104 | simplerOuterClassName.removeSuffix("Kt") 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /logcat/src/main/java/logcat/LogcatLogger.kt: -------------------------------------------------------------------------------- 1 | package logcat 2 | 3 | import logcat.LogcatLogger.Companion.install 4 | import logcat.LogcatLogger.Companion.uninstall 5 | import java.util.concurrent.CopyOnWriteArrayList 6 | import java.util.concurrent.Executor 7 | 8 | /** 9 | * Logger that [logcat] delegates to. Call [install] to install a new logger, the default is a 10 | * no-op logger. Calling [uninstall] falls back to the default no-op logger. 11 | * 12 | * You should install [AndroidLogcatLogger] on Android and [PrintLogger] on a JVM. 13 | */ 14 | interface LogcatLogger { 15 | 16 | /** 17 | * Whether a log with the provided priority should be logged and the corresponding message 18 | * providing lambda evaluated. Called by [logcat]. 19 | */ 20 | fun isLoggable(priority: LogPriority) = true 21 | 22 | /** 23 | * Write a log to its destination. Called by [logcat]. 24 | */ 25 | fun log( 26 | priority: LogPriority, 27 | tag: String, 28 | message: String 29 | ) 30 | 31 | companion object { 32 | @JvmStatic 33 | val loggers: MutableList = CopyOnWriteArrayList() 34 | 35 | @Volatile 36 | @PublishedApi 37 | internal var logExecutor: Executor? = null 38 | private set 39 | 40 | @Volatile 41 | private var installedThrowable: Throwable? = null 42 | 43 | private val installLock = Any() 44 | 45 | @JvmStatic 46 | val isInstalled: Boolean 47 | get() = installedThrowable != null 48 | 49 | /** 50 | * Installs the Logcat library, enabling logging. Logs will not actually be evaluated 51 | * until at least one logger is added to [loggers]. 52 | * 53 | * Pass in an optional [logExecutor] to evaluate log messages on a different thread. 54 | * 55 | * Libraries should check [isInstalled] before calling this, to avoid overriding any app 56 | * set [logExecutor]. 57 | * 58 | * It is an error to call [install] more than once without calling [uninstall] in between, 59 | * however doing this won't throw, it'll log an error to the newly provided logger. 60 | */ 61 | @JvmStatic 62 | fun install(logExecutor: Executor = Executor { it.run() }) { 63 | synchronized(installLock) { 64 | if (isInstalled) { 65 | println( 66 | "Installing LogcatLogger even though it was previously installed here: " + 67 | installedThrowable!!.asLog() 68 | ) 69 | } 70 | installedThrowable = RuntimeException("LogcatLogger previously installed here") 71 | Companion.logExecutor = logExecutor 72 | } 73 | } 74 | 75 | /** 76 | * Disables logging. 77 | */ 78 | @JvmStatic 79 | fun uninstall() { 80 | synchronized(installLock) { 81 | installedThrowable = null 82 | logExecutor = null 83 | } 84 | } 85 | 86 | @Deprecated( 87 | "Maintains backward binary compat for libraries that depend on v0.1 with " + 88 | "inline logcat {} calls" 89 | ) 90 | @PublishedApi 91 | internal val logger: LogcatLogger 92 | get() { 93 | // New instance per call so that "local" fields aren't retained. 94 | return object : LogcatLogger { 95 | 96 | private lateinit var localLoggers: List 97 | private var localLogExecutor: Executor? = null 98 | 99 | override fun isLoggable(priority: LogPriority): Boolean { 100 | localLogExecutor = logExecutor ?: return false 101 | val filteredLoggers = loggers.filter { it.isLoggable(priority) } 102 | localLoggers = filteredLoggers 103 | return filteredLoggers.isNotEmpty() 104 | } 105 | 106 | override fun log( 107 | priority: LogPriority, 108 | tag: String, 109 | message: String 110 | ) { 111 | localLogExecutor!!.execute { 112 | for (logger in localLoggers) { 113 | logger.log(priority, tag, message) 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | @Deprecated( 121 | "LogcatLogger.install() does not take a LogcatLogger instance anymore", 122 | ReplaceWith("install()") 123 | ) 124 | fun install(logger: LogcatLogger) { 125 | install() 126 | loggers += logger 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /logcat/src/main/java/logcat/PrintLogger.kt: -------------------------------------------------------------------------------- 1 | package logcat 2 | 3 | /** 4 | * A [LogcatLogger] that always logs and delegates to [println] concatenating 5 | * the tag and message, separated by a space. Alternative to [AndroidLogcatLogger] 6 | * when running on a JVM. 7 | */ 8 | object PrintLogger : LogcatLogger { 9 | 10 | override fun log(priority: LogPriority, tag: String, message: String) { 11 | println("$tag $message") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /logcat/src/main/java/logcat/Throwables.kt: -------------------------------------------------------------------------------- 1 | package logcat 2 | 3 | import java.io.PrintWriter 4 | import java.io.StringWriter 5 | 6 | /** 7 | * Utility to turn a [Throwable] into a loggable string. 8 | * 9 | * The implementation is based on Timber.getStackTraceString(). It's different 10 | * from [android.util.Log.getStackTraceString] in the following ways: 11 | * - No silent swallowing of UnknownHostException. 12 | * - The buffer size is 256 bytes instead of the default 16 bytes. 13 | */ 14 | fun Throwable.asLog(): String { 15 | val stringWriter = StringWriter(256) 16 | val printWriter = PrintWriter(stringWriter, false) 17 | printStackTrace(printWriter) 18 | printWriter.flush() 19 | return stringWriter.toString() 20 | } 21 | -------------------------------------------------------------------------------- /logcat/src/test/java/logcat/LogcatTest.kt: -------------------------------------------------------------------------------- 1 | package logcat 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import logcat.LogPriority.INFO 5 | import logcat.LogcatLogger.Companion.loggers 6 | import org.junit.After 7 | import org.junit.Test 8 | 9 | class LogcatTest { 10 | 11 | private val logger = TestLogcatLogger().apply { 12 | loggers += this 13 | } 14 | 15 | @After 16 | fun tearDown() { 17 | LogcatLogger.uninstall() 18 | loggers -= logger 19 | } 20 | 21 | @Test fun `when no logger set, calling logcat() does not crash`() { 22 | logcat { "Yo" } 23 | } 24 | 25 | @Test fun `when no logger set, the message lambda isn't invoked`() { 26 | var count = 0 27 | 28 | logcat { "Yo${++count}" } 29 | 30 | assertThat(count).isEqualTo(0) 31 | } 32 | 33 | @Test fun `logcat() logs message from lambda`() { 34 | LogcatLogger.install() 35 | 36 | logcat { "Hi" } 37 | 38 | assertThat(logger.latestLog!!.message).isEqualTo("Hi") 39 | } 40 | 41 | @Test fun `logcat() captures tag from outer context class name`() { 42 | LogcatLogger.install() 43 | 44 | logcat { "Hi" } 45 | 46 | assertThat(logger.latestLog!!.tag).isEqualTo(LogcatTest::class.java.simpleName) 47 | } 48 | 49 | @Test fun `logcat() tag overriding passes tag to logger`() { 50 | LogcatLogger.install() 51 | 52 | logcat(tag = "Bonjour") { "Hi" } 53 | 54 | assertThat(logger.latestLog!!.tag).isEqualTo("Bonjour") 55 | } 56 | 57 | @Test fun `logcat() passes priority to logger`() { 58 | LogcatLogger.install() 59 | 60 | logcat(INFO) { "Hi" } 61 | 62 | assertThat(logger.latestLog!!.priority).isEqualTo(INFO) 63 | } 64 | 65 | @Test fun `logcat() passes priority to isLoggable check`() { 66 | LogcatLogger.install() 67 | 68 | logcat(INFO) { "Hi" } 69 | 70 | assertThat(logger.latestPriority).isEqualTo(INFO) 71 | } 72 | 73 | @Test fun `when not loggable, the message lambda isn't invoked`() { 74 | logger.shouldLog = false 75 | var count = 0 76 | 77 | logcat { "Yo${++count}" } 78 | 79 | assertThat(count).isEqualTo(0) 80 | } 81 | 82 | @Test fun `Throwable asLogMessage() has stacktrace logged`() { 83 | LogcatLogger.install() 84 | val exception = RuntimeException("damn") 85 | 86 | logcat { exception.asLog() } 87 | 88 | assertThat(logger.latestLog!!.message).contains( 89 | """ 90 | |java.lang.RuntimeException: damn 91 | | at logcat.LogcatTest.Throwable asLogMessage() has stacktrace logged(LogcatTest.kt: 92 | """.trimMargin() 93 | ) 94 | } 95 | 96 | @Test fun `standalone function can log with tag`() { 97 | LogcatLogger.install() 98 | 99 | standaloneFunctionLog(tag = "Bonjour", message = { "Hi" }) 100 | 101 | with(logger.latestLog!!) { 102 | assertThat(tag).isEqualTo("Bonjour") 103 | assertThat(message).isEqualTo("Hi") 104 | } 105 | } 106 | 107 | @Test fun `logcat() captures outer this tag from lambda`() { 108 | LogcatLogger.install() 109 | 110 | val lambda = { 111 | logcat { "Hi" } 112 | } 113 | lambda() 114 | 115 | assertThat(logger.latestLog!!.tag).isEqualTo(LogcatTest::class.java.simpleName) 116 | } 117 | 118 | @Test fun `logcat() captures outer this tag from nested lambda`() { 119 | LogcatLogger.install() 120 | 121 | val lambda = { 122 | val lambda = { 123 | logcat { "Hi" } 124 | } 125 | lambda() 126 | } 127 | lambda() 128 | 129 | assertThat(logger.latestLog!!.tag).isEqualTo(LogcatTest::class.java.simpleName) 130 | } 131 | 132 | @Test fun `logcat() captures outer this tag from anonymous object`() { 133 | LogcatLogger.install() 134 | 135 | val anonymousRunnable = object : Runnable { 136 | override fun run() { 137 | logcat { "Hi" } 138 | } 139 | } 140 | anonymousRunnable.run() 141 | 142 | assertThat(logger.latestLog!!.tag).isEqualTo(LogcatTest::class.java.simpleName) 143 | } 144 | 145 | @Test fun `logcat() captures tag from companion function`() { 146 | LogcatLogger.install() 147 | 148 | companionFunctionLog { "Hi" } 149 | 150 | assertThat(logger.latestLog!!.tag).isEqualTo(LogcatTest::class.java.simpleName) 151 | } 152 | 153 | companion object { 154 | fun companionFunctionLog( 155 | message: () -> String 156 | ) { 157 | logcat(message = message) 158 | } 159 | } 160 | } 161 | 162 | fun standaloneFunctionLog( 163 | tag: String, 164 | message: () -> String 165 | ) { 166 | logcat(tag, message = message) 167 | } 168 | -------------------------------------------------------------------------------- /logcat/src/test/java/logcat/TestLogcatLogger.kt: -------------------------------------------------------------------------------- 1 | package logcat 2 | 3 | class TestLogcatLogger : LogcatLogger { 4 | override fun isLoggable(priority: LogPriority): Boolean { 5 | latestPriority = priority 6 | return shouldLog 7 | } 8 | 9 | data class Log( 10 | val priority: LogPriority, 11 | val tag: String, 12 | val message: String 13 | ) 14 | 15 | var latestLog: Log? = null 16 | var latestPriority: LogPriority? = null 17 | var shouldLog = true 18 | 19 | override fun log( 20 | priority: LogPriority, 21 | tag: String, 22 | message: String 23 | ) { 24 | latestLog = Log(priority, tag, message) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | kotlin("android") 4 | } 5 | 6 | android { 7 | compileSdk = 34 8 | 9 | compileOptions { 10 | sourceCompatibility = JavaVersion.VERSION_1_8 11 | targetCompatibility = JavaVersion.VERSION_1_8 12 | } 13 | 14 | defaultConfig { 15 | minSdk = 21 16 | targetSdk = 34 17 | applicationId = "com.squareup.logcat.sample" 18 | } 19 | 20 | namespace = "com.squareup.logcat.sample" 21 | testNamespace = "com.squareup.logcat.sample.test" 22 | } 23 | 24 | dependencies { 25 | implementation(project(":logcat")) 26 | implementation(Dependencies.AppCompat) 27 | } 28 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /sample/src/main/java/com/squareup/logcat/sample/ExampleApplication.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.logcat.sample 2 | 3 | import android.app.Application 4 | import logcat.AndroidLogcatLogger 5 | import logcat.LogPriority.VERBOSE 6 | 7 | class ExampleApplication : Application() { 8 | override fun onCreate() { 9 | super.onCreate() 10 | // Log all priorities in debug builds, no-op in release builds. 11 | AndroidLogcatLogger.installOnDebuggableApp(this, minPriority = VERBOSE) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sample/src/main/java/com/squareup/logcat/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.logcat.sample 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.appcompat.app.AppCompatActivity 6 | import logcat.LogPriority.INFO 7 | import logcat.asLog 8 | import logcat.logcat 9 | import java.lang.RuntimeException 10 | 11 | class MainActivity : AppCompatActivity() { 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | 15 | setContentView(R.layout.main) 16 | 17 | logcat { "Created" } 18 | 19 | logcat(INFO) { "I like turtles" } 20 | 21 | findViewById(R.id.log_stacktrace).setOnClickListener { 22 | logcat { RuntimeException("Logged Stacktrace").asLog() } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 |