diff --git a/api/.classpath b/api/.classpath new file mode 100644 index 0000000000..52ae72c838 --- /dev/null +++ b/api/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 0000000000..b5c234f8ed --- /dev/null +++ b/api/.gitignore @@ -0,0 +1,8 @@ +*~ +.metadata +*.class +org.eclipse.ltk.core.refactoring.prefs +gen +local.properties +ecbuild + diff --git a/api/.project b/api/.project new file mode 100644 index 0000000000..3ce3a857b6 --- /dev/null +++ b/api/.project @@ -0,0 +1,33 @@ + + + astridApi + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/api/.settings/org.eclipse.jdt.core.prefs b/api/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000000..070dbff7de --- /dev/null +++ b/api/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,332 @@ +#Wed Jul 07 18:43:41 PDT 2010 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=ignore +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=warning +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=error +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=warning +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=ignore +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=warning +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=warning +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.6 +org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_assignment=0 +org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=0 +org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_after_package=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +org.eclipse.jdt.core.formatter.blank_lines_before_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 +org.eclipse.jdt.core.formatter.blank_lines_before_package=0 +org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 +org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false +org.eclipse.jdt.core.formatter.comment.format_block_comments=true +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.comment.format_html=true +org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true +org.eclipse.jdt.core.formatter.comment.format_line_comments=true +org.eclipse.jdt.core.formatter.comment.format_source_code=true +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true +org.eclipse.jdt.core.formatter.comment.indent_root_tags=true +org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert +org.eclipse.jdt.core.formatter.comment.line_length=80 +org.eclipse.jdt.core.formatter.compact_else_if=true +org.eclipse.jdt.core.formatter.continuation_indentation=2 +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true +org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_empty_lines=false +org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false +org.eclipse.jdt.core.formatter.indentation.size=4 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert +org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.join_lines_in_comments=true +org.eclipse.jdt.core.formatter.join_wrapped_lines=true +org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.lineSplit=80 +org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false +org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.tabulation.char=space +org.eclipse.jdt.core.formatter.tabulation.size=4 +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false +org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true diff --git a/api/.settings/org.eclipse.jdt.ui.prefs b/api/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 0000000000..ace644cbc6 --- /dev/null +++ b/api/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,109 @@ +#Tue Jun 29 14:53:46 PDT 2010 +cleanup.add_default_serial_version_id=true +cleanup.add_generated_serial_version_id=false +cleanup.add_missing_annotations=true +cleanup.add_missing_deprecated_annotations=true +cleanup.add_missing_methods=false +cleanup.add_missing_nls_tags=false +cleanup.add_missing_override_annotations=true +cleanup.add_serial_version_id=true +cleanup.always_use_blocks=true +cleanup.always_use_parentheses_in_expressions=false +cleanup.always_use_this_for_non_static_field_access=false +cleanup.always_use_this_for_non_static_method_access=false +cleanup.convert_to_enhanced_for_loop=false +cleanup.correct_indentation=false +cleanup.format_source_code=false +cleanup.format_source_code_changes_only=false +cleanup.make_local_variable_final=true +cleanup.make_parameters_final=false +cleanup.make_private_fields_final=true +cleanup.make_type_abstract_if_missing_method=false +cleanup.make_variable_declarations_final=false +cleanup.never_use_blocks=false +cleanup.never_use_parentheses_in_expressions=true +cleanup.organize_imports=true +cleanup.qualify_static_field_accesses_with_declaring_class=false +cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +cleanup.qualify_static_member_accesses_with_declaring_class=true +cleanup.qualify_static_method_accesses_with_declaring_class=false +cleanup.remove_private_constructors=true +cleanup.remove_trailing_whitespaces=true +cleanup.remove_trailing_whitespaces_all=true +cleanup.remove_trailing_whitespaces_ignore_empty=false +cleanup.remove_unnecessary_casts=true +cleanup.remove_unnecessary_nls_tags=true +cleanup.remove_unused_imports=true +cleanup.remove_unused_local_variables=false +cleanup.remove_unused_private_fields=true +cleanup.remove_unused_private_members=false +cleanup.remove_unused_private_methods=true +cleanup.remove_unused_private_types=true +cleanup.sort_members=false +cleanup.sort_members_all=false +cleanup.use_blocks=false +cleanup.use_blocks_only_for_return_and_throw=false +cleanup.use_parentheses_in_expressions=false +cleanup.use_this_for_non_static_field_access=false +cleanup.use_this_for_non_static_field_access_only_if_necessary=true +cleanup.use_this_for_non_static_method_access=false +cleanup.use_this_for_non_static_method_access_only_if_necessary=true +cleanup_profile=_Astrid +cleanup_settings_version=2 +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +formatter_profile=_Astrid +formatter_settings_version=11 +org.eclipse.jdt.ui.text.custom_code_templates= +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=true +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.correct_indentation=false +sp_cleanup.format_source_code=false +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.make_local_variable_final=false +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=true +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.organize_imports=true +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_trailing_whitespaces=true +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_casts=false +sp_cleanup.remove_unnecessary_nls_tags=true +sp_cleanup.remove_unused_imports=true +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.use_blocks=false +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/api/AndroidManifest.xml b/api/AndroidManifest.xml new file mode 100644 index 0000000000..2fd098d1a2 --- /dev/null +++ b/api/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/api/LICENSE b/api/LICENSE new file mode 100644 index 0000000000..c0c5ea9335 --- /dev/null +++ b/api/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2010, Todoroo, Inc +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000000..a732c92679 --- /dev/null +++ b/api/README.md @@ -0,0 +1,35 @@ +Astrid API Library - libraries for writing add-ons to [Astrid](http://www.weloveastrid.com/) - +================================ + +This code is licensed under the New BSD License (see LICENSE) + +Note that this is a beta release of the API - things may (and probably will) change from now until the official release. Documentation is also being written. + +If you are planning on using this project, make sure to watch it for changes. Your feedback is also appreciated. + +Getting Started With Development +--------------- + +1. Install the following: + • *[git](http://git.or.cz/)* + • *[Eclipse](http://eclipse.org)* (preferred: Eclipse IDE for Java Developers) + • *[Android SDK](http://developer.android.com/sdk/index.html)* - version 0.9.7 of Eclipse ADT is required + +2. Use **git** to clone this repository (see Github's instructions if you need help). + +2b. mkdir libs (in case your Android SDK is [not up to date](http://comments.gmane.org/gmane.comp.handhelds.android.devel/101722)) + +3. Open up **eclipse** and import the *astridApi* project. + +4. If you are creating a new add-on for Astrid, create a new project in **eclipse** + • in the Android tab of the project, indicate astridApi as a library reference + +5. Check out the [wiki](http://wiki.github.com/todoroo/astridApi) and [javadoc](http://todoroo.github.com/astridApi) + +Contact +------- +For support requests, use the Astrid issue tracker. For development questions, contact [timsu](http://github.com/timsu) via e-mail. + +Astrid also has an IRC channel, irc.freenode.net #astrid + + diff --git a/api/build.xml b/api/build.xml new file mode 100644 index 0000000000..e471fa5027 --- /dev/null +++ b/api/build.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/default.properties b/api/default.properties new file mode 100644 index 0000000000..2262a388ea --- /dev/null +++ b/api/default.properties @@ -0,0 +1,12 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "build.properties", and override values to adapt the script to your +# project structure. + +android.library=true +# Project target. +target=android-3 diff --git a/api/res/.gitignore b/api/res/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/res/layout/status_preference.xml b/api/res/layout/status_preference.xml new file mode 100644 index 0000000000..65bf255aab --- /dev/null +++ b/api/res/layout/status_preference.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + diff --git a/api/res/values-ca/strings.xml b/api/res/values-ca/strings.xml new file mode 100644 index 0000000000..627fbec465 --- /dev/null +++ b/api/res/values-ca/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1 Any + + %d Anys + + + + 1 Mes + + %d Mesos + + + + 1 Setmana + + %d Setmanes + + + + 1 dia + + %d Dies + + + + 1 Hora + + %d Hores + + + + 1 Minut + + %d Minuts + + + + 1 Segon + + %d Segons + + + + 1 Hr + + %d Hrs + + + + 1 Min + + %d Min + + + + 1 Seg + + %d Seg + + + + 1 tasca + + %d tasques + + + + + + Confirmar? + + + Pregunta: + + + Informació + + + Error! + + + + + + No + + + Tancar + + + Fet + + + ¡Ui, sembla que hi ha hagut un problema! Això es el que ha passat:\n\n%s + + + ¡Ui, sembla que hi ha hagut un problema! + + + Si us plau, espera... + + + + + Sincronitzant les seves tasques... + + + Sincronitzant... + + + Sincronització + + + Error de conexió! Verifiqui la conexió d\'internet. + + + + + Estat + + + No conectat! + + Sincronització en curs... + + Última sincronització: %s + + Fallida el: %s + + Última sincronització correcte: %s + + Mai sincronitzat! + + + Opcions + + + Sincronitzar en segon pla + + Desactivada la sincronització en segon pla + + Actualment configurat en: %s + + + Configuració Wifi + + La sincronització en segon pla només funciona amb el Wifi activat. + + Sempre es produirà la sincronització en segon pla + + + Accions + + + Sincronitzar Ara! + + Ingressar & Sincronitzar! + + + Surt + + Esborra tota la informació de sincronització + + + Tancar sessió / esborra la informació de sincronització? + + + + desactivat + cada quince minuts + cada trenta minuts + cada hora + cada tres hores + cada sis hores + cada dotze hores + diàriament + cada tres dies + setmanalment + + + + diff --git a/api/res/values-cs/strings.xml b/api/res/values-cs/strings.xml new file mode 100644 index 0000000000..6e0f9ad473 --- /dev/null +++ b/api/res/values-cs/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1 rok + + %d Roky + + + + 1 měsíc + + %d Měsíce + + + + 1 týden + + %d Týdny + + + + 1 den + + %d Dnů + + + + 1 hodina + + %d hodin + + + + 1 minuta + + %d minut + + + + 1 vteřina + + %d vteřin + + + + 1 hod. + + %d hod. + + + + 1 min. + + %d min. + + + + 1 s + + %d s + + + + 1 úkol + + %d úkolů + + + + + + Potvrdit? + + + Otázka: + + + Informace + + + Error! + + + Ano + + + Ne + + + Zavřít + + + Hotovo + + + Oops, looks like an error occurred! Here\'s what happened:\n\n%s + + + Oops, looks like an error occurred! + + + Prosím čekejte... + + + + + Probíhá synchronizace Vašich úkolů... + + + Sychronizuji... + + + Synchronizace + + + Connection Error! Check your Internet connection. + + + + + Stav + + + Not Logged In! + + Probíhá synchronizace... + + Poslední synchronizace: %s + + Selhalo: %s + + Poslední úspěšná synchronizace: %s + + Nikdo nesynchronizováno! + + + Možnosti + + + Synchronizace na pozadí + + Synchronizace na pozadí je zakázána + + Současně nastaveno na: %s + + + Nastavení jen pro Wifi + + Synchronizovat na pozadí se bude pouze při zapnuté Wifi + + Synchronizovat na pozadí se bude vždy + + + Činnosti + + + Synchronizuj teď! + + Přihlásit se & Synchronizovat! + + + Odhlásit se + + Clears all synchronization data + + + Odhlásit se / vymazat synchronizační data? + + + + zakázat + každých patnáct minut + každých třicet minut + každou hodinu + každé tři hodiny + každých šest hodin + každých dvanáct hodin + každý den + každé tři dny + každý týden + + + + diff --git a/api/res/values-de/strings.xml b/api/res/values-de/strings.xml new file mode 100644 index 0000000000..a765d70fbd --- /dev/null +++ b/api/res/values-de/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + Ein Jahr + + %d Jahre + + + + Ein Monat + + %d Monate + + + + Eine Woche + + %d Wochen + + + + 1 Tag + + %d Tage + + + + 1 Stunde + + %d Stunden + + + + 1 Minute + + %d Minuten + + + + 1 Sekunde + + %d Sekunden + + + + 1 Std + + %d Std + + + + 1 Min + + %d Min + + + + 1 Sek + + %d Sek + + + + 1 Aufgabe + + %d Aufgaben + + + + + + Bestätigen + + + Frage: + + + Information + + + Fehler! + + + Ja + + + Nein + + + Schließen + + + Erledigt + + + Ups, sieht aus, als ob ein Fehler passiert ist! Hier, was passiert ist:\n\n%s + + + Ups, sieht aus, als ob ein Fehler passiert ist! + + + Bitte warten... + + + + + Synchronisiere deine Aufgaben + + + Synchronisiere… + + + Abgleich + + + Verbindungsfehler! Überprüfen Sie Ihre Internetverbindung. + + + + + Status + + + Nicht angemeldet! + + Synchronisierung läuft... + + Letzte Synchronisierung: %s + + Fehlgeschlagen am: %s + + Letzte erfolgreiche Synchronisierung: %s + + Noch nie synchronisiert! + + + Einstellungen + + + Hintergrund-Synchronisierung + + Hintergrund-Synchronisierung ist deaktiviert + + Gesetzt auf: %s + + + WLAN Einstellungen + + Hintergrund-Synchronisierung nur bei WLAN-Verbindung + + Hintergrund-Synchronisierung findet immer statt + + + Aktionen + + + Jetzt abgleichen! + + Einloggen & Synchroniseren! + + + Abmelden + + Alle Synchronisationsdaten löschen + + + Ausloggen / synchronisierte Daten löschen? + + + + deaktivieren + alle 15 Minuten + alle 30 Minuten + stündlich + alle 3 Stunden + alle 6 Stunden + alle 12 Stunden + täglich + jeden dritten Tag + wöchentlich + + + + diff --git a/api/res/values-es/strings.xml b/api/res/values-es/strings.xml new file mode 100644 index 0000000000..ee8985b981 --- /dev/null +++ b/api/res/values-es/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1 año + + %d años + + + + 1 Mes + + %d meses + + + + 1 semana + + %d semanas + + + + 1 día + + %d días + + + + 1 hora + + %d horas + + + + 1 minuto + + %d minutos + + + + 1 segundo + + %d segundos + + + + 1 hora + + %d horas + + + + 1 minuto + + %d minutos + + + + 1 segundo + + %d segundos + + + + 1 tarea + + %d tareas + + + + + + ¿Confirmar? + + + Pregunta: + + + Información + + + ¡Error! + + + + + + No + + + Cerrar + + + Listo + + + ¡Uy, al parecer hay algún problema! Esto es lo que pasó:\n\n%s + + + ¡Uy, al parecer hay algún problema! + + + Espere por favor... + + + + + Sincronizando sus tareas... + + + Sincronizando... + + + Sincronización + + + Error de conexión! Verifique su conexión a internet. + + + + + Estado + + + No conectado! + + Sincronización en curso... + + Última sincronización: %s + + Falló el: %s + + Última sincronización exitosa: %s + + ¡Jamás se sincronizó! + + + Opciones + + + Sincronizar en segundo plano + + Sincronización en segundo plano desactivada + + Actualmente configurado para: %s + + + Sólo Configuración Wifi + + La sincronización en segundo plano sólo funciona con el Wifi activado + + La sincronización en segundo plano funciona siempre + + + Acciones + + + ¡Sincronizar ahora! + + Iniciar sesión y sincronizar! + + + Cerrar sesión + + Borra todos los datos de sincronización + + + Cierre de sesión / cancelar la sincronización de datos? + + + + desactivar + cada quince minutos + cada treinta minutos + cada hora + cada tres horas + cada seis horas + cada doce horas + cada día + cada tres días + cada semana + + + + diff --git a/api/res/values-fr/strings.xml b/api/res/values-fr/strings.xml new file mode 100644 index 0000000000..9541fb4665 --- /dev/null +++ b/api/res/values-fr/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1 année + + %d années + + + + 1 mois + + %d mois + + + + 1 semaine + + %d semaines + + + + 1 jour + + %d jours + + + + 1 heure + + %d heures + + + + 1 minute + + %d minutes + + + + 1 seconde + + %d secondes + + + + 1 h + + %d h + + + + 1 min + + %d min + + + + 1 s + + %d s + + + + 1 tâche + + %d tâches + + + + + + Confirmer ? + + + Question : + + + Information + + + Erreur ! + + + Oui + + + Non + + + Fermer + + + Terminé + + + Oops, looks like an error occurred! Here\'s what happened:\n\n%s + + + Oops, looks like an error occurred! + + + Veuillez patienter... + + + + + Synchronisation de vos tâches... + + + Synchronisation... + + + Synchronisation + + + Connection Error! Check your Internet connection. + + + + + Statut + + + Not Logged In! + + Synchronisation en cours... + + Dernière synchro. : %s + + Échec sur : %s + + Dernière synchro. réussie : %s + + Jamais synchronisé ! + + + Options + + + Synchro. en arrière-plan + + Synchronisation en arrière-plan désactivée + + Actuellement configuré sur : %s + + + Paramètre Wifi seul + + La synchronisation en arrière-plan ne s\'effectue uniquement sous Wifi + + La synchronisation en arrière-plan s\'effectuera toujours + + + Actions + + + Synchroniser maintenant ! + + Se connecter et synchroniser ! + + + Se déconnecter + + Clears all synchronization data + + + Se déconnecter/purger les données de synchronisation ? + + + + désactiver + toutes les quinze minutes + toutes les trente minutes + toutes les heures + toutes les trois heures + toutes les six heures + toutes les douze heures + tous les jours + tous les trois jours + toutes les semaines + + + + diff --git a/api/res/values-id/strings.xml b/api/res/values-id/strings.xml new file mode 100644 index 0000000000..e689e1fbf4 --- /dev/null +++ b/api/res/values-id/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1 Year + + %d Years + + + + 1 Month + + %d Months + + + + 1 Week + + %d Weeks + + + + 1 Hari + + %d Hari + + + + 1 Jam + + %d Jam + + + + 1 Menit + + %d Menit + + + + 1 Detik + + %d Detik + + + + 1 Jam + + %d Jam + + + + 1 Mnt + + %d Mnt + + + + 1 Dtk + + %d Dtk + + + + 1 task + + %d tasks + + + + + + Confirm? + + + Question: + + + Informasi + + + Error! + + + Yes + + + No + + + Close + + + Selesai + + + Oops, looks like an error occurred! Here\'s what happened:\n\n%s + + + Oops, looks like an error occurred! + + + Please wait... + + + + + Synchronizing your tasks... + + + Synchronizing... + + + Sinkronisasi + + + Connection Error! Check your Internet connection. + + + + + Status + + + Not Logged In! + + Sync Ongoing... + + Last Sync: %s + + Failed On: %s + + Last Successful Sync: %s + + Never Synchronized! + + + Pilihan + + + Background Sync + + Background synchronization is disabled + + Currently set to: %s + + + Wifi Only Setting + + Background synchronization only happens when on Wifi + + Background synchronization will always occur + + + Aksi + + + Sinkronkan Sekarang! + + Log In & Synchronize! + + + Log Out + + Clears all synchronization data + + + Log out / clear synchronization data? + + + + tidak difungsikan + every fifteen minutes + every thirty minutes + every hour + every three hours + every six hours + every twelve hours + every day + every three days + every week + + + + diff --git a/api/res/values-it/strings.xml b/api/res/values-it/strings.xml new file mode 100644 index 0000000000..018d2bc041 --- /dev/null +++ b/api/res/values-it/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1 Anno + + %d Anni + + + + 1 Mese + + %d Mesi + + + + 1 Settimana + + %d Settimane + + + + 1 giorno + + %d Giorni + + + + 1 ora + + %d ore + + + + 1 minuto + + %d minuti + + + + 1 secondo + + %d secondi + + + + 1 ora + + %d ore + + + + 1 min + + %d min + + + + 1 sec + + %d sec + + + + 1 Attività + + %d attività + + + + + + Conferma? + + + Domanda: + + + Informazioni + + + Error! + + + + + + No + + + Chiudi + + + Completata + + + Oops, looks like an error occurred! Here\'s what happened:\n\n%s + + + Oops, looks like an error occurred! + + + Attendere per favore... + + + + + Sincronizzando le tue attività... + + + Sincronizzando... + + + Sincronizzazione + + + Connection Error! Check your Internet connection. + + + + + Stato + + + Not Logged In! + + Sincronizzazione in corso ... + + Ultima Sincronizzazione: %s + + Fallita Su: %s + + Ultima sincronizzazione eseguita con successo in data: %s + + Mai sincronizzato! + + + Opzioni + + + Sincronizzazione eseguita in background + + La sincronizzazione in background è disattivata + + Attualmente impostata su: %s + + + Unica Impostazione Wifi + + la sincronizzazione in background avviene solo quando la rete Wifi è abilitata + + La sincronizzazione in background avviene sempre + + + Azioni + + + Sincronizza Ora! + + Esegui l\'accesso & Sincronizza! + + + Esci + + Clears all synchronization data + + + Esci / cancella i file di sincronizzazione? + + + + disabilita + ogni quindici minuti + ogni trenta minuti + ogni ora + ogni tre ore + ogni sei ore + ogni dodici ore + ogni giorno + ogni tre giorni + Ogni settimana + + + + diff --git a/api/res/values-ja/strings.xml b/api/res/values-ja/strings.xml new file mode 100644 index 0000000000..70cba6c77c --- /dev/null +++ b/api/res/values-ja/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1年 + + %d 年 + + + + 1か月 + + %d か月 + + + + 1週間 + + %d 週間 + + + + 1 日 + + %d 日 + + + + 1 時間 + + %d 時間 + + + + 1 分 + + %d 分 + + + + 1 秒 + + %d 秒 + + + + 1 時間 + + %d 時間 + + + + 1 分 + + %d 分 + + + + 1 秒 + + %d 秒 + + + + タスク 1 件 + + タスク %d 件 + + + + + + 確認 + + + 確認 + + + インフォメーション + + + エラー + + + はい + + + いいえ + + + 閉じる + + + 完了 + + + Oops, looks like an error occurred! Here\'s what happened:\n\n%s + + + Oops, looks like an error occurred! + + + お待ちください + + + + + タスクの同期中... + + + 同期中... + + + 同期 + + + Connection Error! Check your Internet connection. + + + + + 状況 + + + ログインしていません + + 同期中 + + 前回の同期: %s + + 失敗: %s + + 最後の同期: %s + + 同期していません + + + オプション + + + バックグラウンド同期 + + バックグラウンド同期は無効になっています + + 現在の設定: %s + + + Wi-Fi のみ + + Wi-Fi が有効なときだけバックグラウンドで同期する + + Background synchronization will always occur + + + アクション + + + すぐに同期! + + ログインと同期 + + + ログアウト + + すべての同期データを消去します + + + ログアウトと同期データを消去しますか? + + + + 無効 + 15分毎 + 30分毎 + 1時間毎 + 3時間毎 + 6時間毎 + 12時間毎 + 毎日 + 3日に一度 + 毎週 + + + + diff --git a/api/res/values-ko/strings.xml b/api/res/values-ko/strings.xml new file mode 100644 index 0000000000..a540627639 --- /dev/null +++ b/api/res/values-ko/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1년 + + %d 년 + + + + 1개월 + + %d 개월 + + + + 1주 + + %d 주 + + + + 1일 + + %d 일 + + + + 1시간 + + %d 시간 + + + + 1 분 + + %d 분 + + + + 1 초 + + %d 초 + + + + 1 시간 + + %d 시간 + + + + 1 분 + + %d 분 + + + + 1 초 + + %d 초 + + + + 1 작업 + + %d 작업 + + + + + + 확인? + + + 질문: + + + 정보 + + + 오류! + + + + + + 아니오 + + + 닫기 + + + 마침 + + + 오류가 발생한 것 같습니다! 원인은 다음과 같습니다:\n\n%s + + + 오류가 발생한 것 같습니다! + + + 잠시만 기다려주세요... + + + + + 작업 동기화 중입니다... + + + 동기화하는 중... + + + 동기화 + + + 연결 오류! 인터넷 연결을 확인하세요. + + + + + 상태 + + + 로그인 되지 않았습니다! + + 동기화 진행중... + + 마지막 동기화: %s + + 실패: %s + + 마지막 동기화 성공시간: %s + + 한번도 동기화 되지 않았습니다! + + + 옵션 + + + 백그라운드 동기화 + + 백그라운드 동기화가 설정되지 않았습니다. + + 현재 설정: %s + + + WiFi 일때만 설정 + + 백그라운드 동기화는 WiFi 지역에서만 작동합니다. + + 백그라운드 동기화는 항상 작동합니다. + + + 작업 + + + 동기화 시작! + + 로그인 & 동기화! + + + 로그아웃 + + 모든 동기화 데이터 삭제 + + + 로그아웃 / 모든 동기화 데이터 삭제? + + + + 사용안함 + 매 15분 마다 + 매 30분마다 + 매 시간 + 매 3시간마다 + 매 6시간마다 + 매 12시간마다 + 매일 + 매 3일마다 + 매주 + + + + diff --git a/api/res/values-nb/strings.xml b/api/res/values-nb/strings.xml new file mode 100644 index 0000000000..a9378784d4 --- /dev/null +++ b/api/res/values-nb/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1 år + + %d år + + + + 1 måned + + %d måneder + + + + 1 uke + + %d uker + + + + 1 dag + + %d dager + + + + 1 time + + %d timer + + + + 1 minutt + + %d minutter + + + + 1 sekund + + %d sekunder + + + + 1 t + + %d t + + + + 1 min + + %d min + + + + 1 s + + %d s + + + + 1 oppgave + + %d oppgaver + + + + + + Bekreft? + + + Spørsmål: + + + Informasjon + + + Feil! + + + Ja + + + Nei + + + Lukk + + + Utført + + + Oi sann! Det ser ut for at en feil oppstod. Her er hva som skjedde:\n\n%s + + + Oi sann! Det ser ut for at en feil oppstod + + + Vennligst vent... + + + + + Synkroniserer oppgavene dine... + + + Synkroniserer... + + + Synkronisering + + + Tilkoblingsfeil! Kontroller at du er koblet til Internett + + + + + Status + + + Not Logged In! + + Synkronisering pågår... + + Siste synkronisering: %s + + Feilet: %s + + Siste vellykkede synkronisering: %s + + Aldri synkronisert! + + + Alternativer + + + Bakgrunnssynkronisering + + Bakgrunnssynkronisering er deaktivert + + Foreløpig satt til %s + + + Bare Wifi Innstilling + + Synkronisering i bakgrunnen skal kun utføres med WiFi-tilkobling + + Synkronisering i bakgrunnen skal alltid utføres + + + Handlinger + + + Synkroniser nå! + + Logg Inn & Synkroniser! + + + Logg av + + Clears all synchronization data + + + Logge ut / slette synkroniserings data? + + + + deaktiver + hvert kvarter + hver halvtime + hver time + hver tredje time + hver sjette time + hver tolvte time + daglig + hver tredje dag + hver uke + + + + diff --git a/api/res/values-nl/strings.xml b/api/res/values-nl/strings.xml new file mode 100644 index 0000000000..01d34df456 --- /dev/null +++ b/api/res/values-nl/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1 jaar + + %d jaren + + + + 1 maand + + %d maanden + + + + 1 week + + %d weken + + + + 1 dag + + %d Dagen + + + + 1 Uur + + %d Uren + + + + 1 Minuut + + %d Minuten + + + + 1 Seconde + + %d Seconden + + + + 1 U + + %d Uur + + + + 1 Min + + %d Min + + + + 1 Sec + + %d Sec + + + + 1 task + + %d tasks + + + + + + Confirm? + + + Vraag: + + + Informatie + + + Error! + + + Ja + + + Nee + + + Sluiten + + + Voltooid + + + Oops, looks like an error occurred! Here\'s what happened:\n\n%s + + + Oops, looks like an error occurred! + + + Even geduld alstublieft... + + + + + Synchronizing your tasks... + + + Bezig met synchroniseren... + + + Synchronisatie + + + Connection Error! Check your Internet connection. + + + + + Status + + + Not Logged In! + + Sync Ongoing... + + Last Sync: %s + + Failed On: %s + + Last Successful Sync: %s + + Never Synchronized! + + + Opties + + + Background Sync + + Background synchronization is disabled + + Currently set to: %s + + + Wifi Only Setting + + Background synchronization only happens when on Wifi + + Background synchronization will always occur + + + Acties + + + Synchroniseer nu! + + Log In & Synchronize! + + + Afmelden + + Clears all synchronization data + + + Log out / clear synchronization data? + + + + uit + elke 15 minuten + elke 30 minuten + elk uur + elke 3 uur + elke 6 uur + elke 12 uur + elke dag + elke 3 dagen + elke week + + + + diff --git a/api/res/values-pl/strings.xml b/api/res/values-pl/strings.xml new file mode 100644 index 0000000000..1d8362a665 --- /dev/null +++ b/api/res/values-pl/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1 rok + + %d lat + + + + 1 miesiąc + + %d miesięcy + + + + 1 tydzień + + %d tygodni + + + + 1 dzień + + %d dni + + + + 1 godzina + + %d godzin + + + + 1 minuta + + %d minut + + + + 1 sekunda + + %d sekund + + + + 1 godz. + + %d godz. + + + + 1 min. + + %d min. + + + + 1 sek. + + %d sek. + + + + 1 zadanie + + %d zadań + + + + + + Potwierdzić? + + + Pytanie: + + + Informacja + + + Błąd! + + + Tak + + + Nie + + + Zamknij + + + Gotowe + + + Ups! Wygląda na to, że wystąpił jakiś błąd! Oto, co się stało:\n\n%s + + + Ups! Wygląda na to, że wystąpił jakiś błąd! + + + Proszę czekać... + + + + + Synchronizowanie Twoich zadań... + + + Synchronizacja... + + + Synchronizacja + + + Błąd połączenia! Sprawdź swoje połączenie z Internetem! + + + + + Stan + + + Nie zalogowano! + + Synchronizacja trwa... + + Ostatnia synchronizacja: %s + + Nieudana: %s + + Ostatnia udana synchronizacja: %s + + Nigdy nie synchronizowano! + + + Ustawienia + + + Synchronizacja w tle + + Synchronizacja w tle wyłączona + + Aktualnie ustawione na: %s + + + Tylko połączenie Wi-Fi + + Synchronizacja w tle przebiega tylko poprzez Wi-Fi + + Synchronizowanie w tle zawsze, niezależnie od rodzaju połączenia + + + Działania + + + Synchronizuj teraz! + + Zaloguj & Synchronizuj! + + + Wyloguj + + Czyści wszystkie dane synchronizacji + + + Wyloguj / wyczyść dane synchronizacji? + + + + Wyłączone + co 15 minut + co 30 minut + co godzinę + co 3 godziny + co 6 godzin + co 12 godzin + raz dziennie + co 3 dni + co tydzień + + + + diff --git a/api/res/values-pt/strings.xml b/api/res/values-pt/strings.xml new file mode 100644 index 0000000000..f6e9ce04b7 --- /dev/null +++ b/api/res/values-pt/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1 Ano + + %d Anos + + + + 1 Mês + + %d Meses + + + + 1 Semana + + %d Semanas + + + + 1 Dia + + %d Dias + + + + 1 Hora + + %d Horas + + + + 1 Minuto + + %d Minutos + + + + 1 Segundo + + %d Segundos + + + + 1 h + + %d h + + + + 1 min + + %d min + + + + 1 s + + %d s + + + + 1 tarefa + + %d tarefas + + + + + + Confirma? + + + Pergunta: + + + Informação + + + Error! + + + Sim + + + Não + + + Fechar + + + Concluído + + + Oops, looks like an error occurred! Here\'s what happened:\n\n%s + + + Oops, looks like an error occurred! + + + Por favor aguarde... + + + + + Synchronizing your tasks... + + + A Sincronizar... + + + Sincronização + + + Connection Error! Check your Internet connection. + + + + + Estado + + + Not Logged In! + + Sync Ongoing... + + Last Sync: %s + + Failed On: %s + + Last Successful Sync: %s + + Never Synchronized! + + + Opções + + + Background Sync + + Background synchronization is disabled + + Currently set to: %s + + + Wifi Only Setting + + Background synchronization only happens when on Wifi + + Background synchronization will always occur + + + Acções + + + Sincronizar Agora! + + Log In & Synchronize! + + + Terminar sessão + + Clears all synchronization data + + + Log out / clear synchronization data? + + + + desactivar + every fifteen minutes + every thirty minutes + every hour + every three hours + every six hours + every twelve hours + every day + every three days + every week + + + + diff --git a/api/res/values-ru/strings.xml b/api/res/values-ru/strings.xml new file mode 100644 index 0000000000..34b75bba86 --- /dev/null +++ b/api/res/values-ru/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1 год + + %d года/лет + + + + 1 месяц + + %d месяца/месяцев + + + + 1 неделя + + %d недели/недель + + + + 1 день + + %d для/дней + + + + 1 час + + %d часа/часов + + + + 1 минута + + %d минуты/минут + + + + 1 секунда + + %d секунды/секунд + + + + 1 час + + %d ч + + + + 1 мин + + %d мин + + + + 1 с + + %d с + + + + 1 задача + + %d задач(а/и) + + + + + + Подтвердить? + + + Вопрос: + + + Информация + + + Ошибка! + + + Да + + + Нет + + + Закрыть + + + Готово + + + Ой, похоже произошла ошибка! Подробности ниже:\n\n%s + + + Ой, похоже произошла ошибка! + + + Пожалуйста, подождите… + + + + + Синхронизация задач… + + + Синхронизация… + + + Синхронизация + + + Ошибка соединения! Проверьте подключение к интернету. + + + + + Состояние + + + Вы не вошли в систему! + + Процесс синхронизации… + + Последняя синхронизация: %s + + Ошибка: %s + + Последняя успешная синхронизация: %s + + Синхронизаций не выполнялось! + + + Параметры + + + Фоновая синхронизация + + Фоновая синхронизация отключена + + Сейчас установлено: %s + + + Только через Wifi + + Фоновая синхронизация происходит только через Wifi + + Фоновая синхронизация происходит всегда + + + Действия + + + Синхронизировать! + + Войти и синхронизировать! + + + Выход + + Очищает все данные синхронизации + + + Выйти / очистить данные синхронизации? + + + + отключить + каждые 15 минут + каждые 30 минут + каждый час + каждые 3 часа + каждые 6 часов + каждые 12 часов + каждый день + каждые 3 дня + каждую неделю + + + + diff --git a/api/res/values-sv/strings.xml b/api/res/values-sv/strings.xml new file mode 100644 index 0000000000..4468a27a40 --- /dev/null +++ b/api/res/values-sv/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1 år + + %d år + + + + 1 månad + + %d månader + + + + 1 vecka + + %d veckor + + + + 1 dag + + %d dagar + + + + 1 timme + + %d timmar + + + + 1 minut + + %d minuter + + + + 1 sekund + + %d sekunder + + + + 1 tim + + %d tim + + + + 1 min + + %d min + + + + 1 sek + + %d sek + + + + 1 uppgift + + %d uppgifter + + + + + + Bekräfta? + + + Fråga: + + + Information + + + Fel! + + + Ja + + + Nej + + + Stäng + + + Klar + + + Oj, det uppstod ett fel! Detta hände:\n\n%s + + + Oj, det uppstod ett fel! + + + Var god vänta... + + + + + Synkroniserar dina uppgifter... + + + Synkroniserar... + + + Synkronisering + + + Tillkopplingsfel! Kontrollera din tillkoppling till internet. + + + + + Status + + + Ej inloggad! + + Synkronisering pågår... + + Synkroniserades senast: %s + + Misslyckades: %s + + Synkronisering lyckades senast: %s + + Aldrig synkroniserad! + + + Alternativ + + + Bakgrundssynkronisering + + Bakgrundssynkronisering är inaktiverad + + Aktuell inställning: %s + + + Endast Wi-Fi Inställning + + Bakgrundssynkronisering sker endast när du är ansluten till Wi-Fi + + Bakgrundssynkronisering sker alltid + + + Åtgärder + + + Synkronisera nu! + + Logga in & synkronisera! + + + Logga ut + + Rensar alla synkroniseringsdata + + + Logga ut / rensa synkroniseringsdata? + + + + inaktivera + varje kvartstimme + varje halvtimme + varje timme + var tredje timme + var sjätte timme + var tolfte timme + varje dag + var tredje dag + varje vecka + + + + diff --git a/api/res/values-tr/strings.xml b/api/res/values-tr/strings.xml new file mode 100644 index 0000000000..1fd6997d10 --- /dev/null +++ b/api/res/values-tr/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1 Year + + %d Years + + + + 1 Month + + %d Months + + + + 1 Week + + %d Weeks + + + + 1 Gün + + %d gün + + + + 1 Saat + + %d saat + + + + 1 Dakika + + %d dakika + + + + 1 Saniye + + %d saniye + + + + 1 saat + + %d saat + + + + 1 dakika + + %d dakika + + + + 1 saniye + + %d saniye + + + + 1 task + + %d tasks + + + + + + Confirm? + + + Question: + + + Bilgi + + + Error! + + + Yes + + + No + + + Close + + + Tamamlandı + + + Oops, looks like an error occurred! Here\'s what happened:\n\n%s + + + Oops, looks like an error occurred! + + + Please wait... + + + + + Synchronizing your tasks... + + + Synchronizing... + + + Senkronizasyon + + + Connection Error! Check your Internet connection. + + + + + Status + + + Not Logged In! + + Sync Ongoing... + + Last Sync: %s + + Failed On: %s + + Last Successful Sync: %s + + Never Synchronized! + + + Ayarlar + + + Background Sync + + Background synchronization is disabled + + Currently set to: %s + + + Wifi Only Setting + + Background synchronization only happens when on Wifi + + Background synchronization will always occur + + + Eylemler + + + Senkronize et + + Log In & Synchronize! + + + Log Out + + Clears all synchronization data + + + Log out / clear synchronization data? + + + + devre dışı bırak + every fifteen minutes + every thirty minutes + every hour + every three hours + every six hours + every twelve hours + every day + every three days + every week + + + + diff --git a/api/res/values-zh-rCN/strings.xml b/api/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000..715a029de0 --- /dev/null +++ b/api/res/values-zh-rCN/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1年 + + %d年 + + + + 1个月 + + %d个月 + + + + 1周 + + %d周 + + + + 1天 + + %d天 + + + + 1小时 + + %d小时 + + + + 1分钟 + + %d分钟 + + + + 1秒 + + %d秒 + + + + 1小时 + + %d小时 + + + + 1分钟 + + %d分钟 + + + + 1秒 + + %d秒 + + + + 1任务 + + %d任务 + + + + + + 确认? + + + 问题: + + + 信息 + + + 错误! + + + + + + + + + 关闭 + + + 完成 + + + 哇,看来出错了!情况如下:\n\n%s + + + 哇,看来出错了! + + + 请稍候... + + + + + 正在同步任务... + + + 正在同步... + + + 同步 + + + 连接错误!查看您的网络连接。 + + + + + 状态 + + + 未登录! + + 正在同步... + + 上一次同步:%s + + 同步失败:%s + + 上一次成功同步:%s + + 从未同步! + + + 选项 + + + 后台同步 + + 后台同步已禁用 + + 当前设置为:%s + + + 仅使用 Wifi + + 仅当 Wifi 打开时使用后台同步 + + 总是使用后台同步 + + + 行动 + + + 现在同步! + + 登录&同步 + + + 注销 + + 清除所有同步数据 + + + 注销并清除同步数据? + + + + 禁用 + 每十五分钟 + 每半小时 + 每小时 + 每三小时 + 每六小时 + 每十二小时 + 每天 + 每三天 + 每周 + + + + diff --git a/api/res/values-zh-rTW/strings.xml b/api/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..accf33169f --- /dev/null +++ b/api/res/values-zh-rTW/strings.xml @@ -0,0 +1,189 @@ + + + + + + + + + 1 年 + + %d 年 + + + + 1 個月 + + %d 月 + + + + 1 週 + + %d 週 + + + + 1 天 + + %d 天 + + + + 1 小時 + + %d 小時 + + + + 1 分鐘 + + %d 分鐘 + + + + 1 秒 + + %d 秒 + + + + 1 小時 + + %d 小時 + + + + 1 分鐘 + + %d 分鐘 + + + + 1 秒 + + %d 秒 + + + + 1 個工作 + + %d 個工作 + + + + + + 確認? + + + 問題: + + + 資訊 + + + Error! + + + 確定 + + + 取消 + + + 關閉 + + + 完成 + + + Oops, looks like an error occurred! Here\'s what happened:\n\n%s + + + Oops, looks like an error occurred! + + + 請稍候... + + + + + 同步工作中... + + + 正在同步中... + + + 同步 + + + 連結錯誤! 檢查您的網際網路連線. + + + + + 狀態 + + + 未登入! + + 同步中... + + 上次同步: %s + + 失敗: %s + + 上次成功同步: %s + + 未同步過! + + + 選項 + + + 背景同步 + + 背景同步關閉 + + 目前同步設定: %s + + + Wifi 才可使用之設定 + + 使用Wifi才啟動背景同步 + + 總是使用背景同步 + + + 動作 + + + 現在同步! + + 登入並同步! + + + 登出 + + 清除所有同步資料 + + + 登出 / 清除同步資料? + + + + 停用 + 每15分 + 每30分 + 每小時 + 每3小時 + 每6小時 + 每12小時 + 每天 + 每3天 + 每週 + + + + diff --git a/api/res/values/colors.xml b/api/res/values/colors.xml new file mode 100644 index 0000000000..cfa0c5dd7a --- /dev/null +++ b/api/res/values/colors.xml @@ -0,0 +1,12 @@ + + + + #ffff5555 + #fffea400 + #ff33a5e8 + #ff808080 + #ff505050 + #ff202020 + + + diff --git a/api/res/values/keys.xml b/api/res/values/keys.xml new file mode 100644 index 0000000000..ed660ae6b5 --- /dev/null +++ b/api/res/values/keys.xml @@ -0,0 +1,33 @@ + + + + + + + + + 0 + 900 + 1800 + 3600 + 10800 + 21600 + 43200 + 86400 + 259200 + 604800 + + + + sync_status + + sync_bgwifi + + sync_sync + + sync_forget + + diff --git a/api/res/values/strings.xml b/api/res/values/strings.xml new file mode 100644 index 0000000000..63decbc80c --- /dev/null +++ b/api/res/values/strings.xml @@ -0,0 +1,194 @@ + + + + + + + + + 1 Year + + %d Years + + + + 1 Month + + %d Months + + + + 1 Week + + %d Weeks + + + + 1 Day + + %d Days + + + + 1 Weekday + + %d Weekdays + + + + 1 Hour + + %d Hours + + + + 1 Minute + + %d Minutes + + + + 1 Second + + %d Seconds + + + + 1 Hr + + %d Hrs + + + + 1 Min + + %d Min + + + + 1 Sec + + %d Sec + + + + 1 task + + %d tasks + + + + + + Confirm? + + + Question: + + + Information + + + Error! + + + Yes + + + No + + + Close + + + Done + + + Oops, looks like an error occurred! Here\'s what happened:\n\n%s + + + Oops, looks like an error occurred! + + + Please wait... + + + + + Synchronizing your tasks... + + + Synchronizing... + + + Synchronization + + + Connection Error! Check your Internet connection. + + + + + Status + + + Not Logged In! + + Sync Ongoing... + + Last Sync: %s + + Failed On: %s + + Last Successful Sync: %s + + Never Synchronized! + + + Options + + + Background Sync + + Background synchronization is disabled + + Currently set to: %s + + + Wifi Only Setting + + Background synchronization only happens when on Wifi + + Background synchronization will always occur + + + Actions + + + Synchronize Now! + + Log In & Synchronize! + + + Log Out + + Clears all synchronization data + + + Log out / clear synchronization data? + + + + disable + every fifteen minutes + every thirty minutes + every hour + every three hours + every six hours + every twelve hours + every day + every three days + every week + + + diff --git a/api/src/com/todoroo/andlib/data/AbstractDatabase.java b/api/src/com/todoroo/andlib/data/AbstractDatabase.java new file mode 100644 index 0000000000..05937031ba --- /dev/null +++ b/api/src/com/todoroo/andlib/data/AbstractDatabase.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2009, Todoroo Inc + * All Rights Reserved + * http://www.todoroo.com + */ +package com.todoroo.andlib.data; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteDatabase.CursorFactory; +import android.util.Log; + +import com.todoroo.andlib.data.Property.PropertyVisitor; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.andlib.service.ExceptionService; +import com.todoroo.andlib.utility.AndroidUtilities; + +/** + * AbstractDatabase is a database abstraction which wraps a SQLite database. + *

+ * Users of this class are in charge of the database's lifecycle - ensuring that + * the database is open when needed and closed when usage is finished. Within an + * activity, this is typically accomplished through the onResume and onPause + * methods, though if the database is not needed for the activity's entire + * lifecycle, it can be closed earlier. + *

+ * Direct querying is not recommended for type safety reasons. Instead, use one + * of the service classes to issue the request and return a {@link TodorooCursor}. + * + * @author Tim Su + * + */ +@SuppressWarnings("nls") +abstract public class AbstractDatabase { + + // --- abstract methods + + /** + * @return database name + */ + protected abstract String getName(); + + /** + * @return all tables in this database + */ + protected abstract Table[] getTables(); + + /** + * @return database version + */ + protected abstract int getVersion(); + + /** + * Called after database and tables are created. Use this method to + * create indices and perform other database maintenance + */ + protected abstract void onCreateTables(); + + /** + * Upgrades an open database from one version to the next + * @param oldVersion + * @param newVersion + * @return true if upgrade was handled, false otherwise + */ + protected abstract boolean onUpgrade(int oldVersion, int newVersion); + + // --- protected variables + + /** + * SQLiteOpenHelper that takes care of database operations + */ + protected SQLiteOpenHelper helper = null; + + /** + * Internal pointer to open database. Hides the fact that there is a + * database and a wrapper by making a single monolithic interface + */ + protected SQLiteDatabase database = null; + + // --- internal implementation + + @Autowired + private ExceptionService exceptionService; + + public AbstractDatabase() { + DependencyInjectionService.getInstance().inject(this); + } + + /** + * Return the name of the table containing these models + * @param modelType + * @return + */ + public final Table getTable(Class modelType) { + for(Table table : getTables()) { + if(table.modelClass.equals(modelType)) + return table; + } + throw new UnsupportedOperationException("Unknown model class " + modelType); //$NON-NLS-1$ + } + + protected synchronized final void initializeHelper() { + if(helper == null) { + if(ContextManager.getContext() == null) + throw new NullPointerException("Null context creating database helper"); + helper = new DatabaseHelper(ContextManager.getContext(), + getName(), null, getVersion()); + } + } + + /** + * Open the database for writing. Must be closed afterwards. If user is + * out of disk space, database may be opened for reading instead + */ + public synchronized final void openForWriting() { + initializeHelper(); + + if(database != null && !database.isReadOnly() && database.isOpen()) + return; + + try { + database = helper.getWritableDatabase(); + } catch (NullPointerException e) { + // don't know why this happens + throw new IllegalStateException(e); + } catch (final RuntimeException original) { + Log.e("database-" + getName(), "Error opening db", + original); + try { + // provide read-only database + openForReading(); + } catch (Exception readException) { + exceptionService.reportError("database-open-" + getName(), original); + + // throw original write exception + throw original; + } + } + } + + /** + * Open the database for reading. Must be closed afterwards + */ + public synchronized final void openForReading() { + initializeHelper(); + if(database != null && database.isOpen()) + return; + database = helper.getReadableDatabase(); + } + + /** + * Close the database if it has been opened previously + */ + public synchronized final void close() { + if(database != null) { + database.close(); + } + database = null; + } + + /** + * Clear all data in database. Warning: this does what it says. Any open + * database resources will be abruptly closed. + */ + public synchronized final void clear() { + close(); + ContextManager.getContext().deleteDatabase(getName()); + } + + /** + * @return sql database. opens database if not yet open + */ + public synchronized final SQLiteDatabase getDatabase() { + if(database == null) { + AndroidUtilities.sleepDeep(300L); + openForWriting(); + } + return database; + } + + /** + * @return human-readable database name for debugging + */ + @Override + public String toString() { + return "DB:" + getName(); + } + + // --- database wrapper + + /* + * @see android.database.sqlite.SQLiteDatabase#rawQuery(String sql, String[] selectionArgs) + */ + public synchronized Cursor rawQuery(String sql, String[] selectionArgs) { + return getDatabase().rawQuery(sql, selectionArgs); + } + + /* + * @see android.database.sqlite.SQLiteDatabase#insert(String table, String nullColumnHack, ContentValues values) + */ + public synchronized long insert(String table, String nullColumnHack, ContentValues values) { + return getDatabase().insert(table, nullColumnHack, values); + } + + /* + * @see android.database.sqlite.SQLiteDatabase#delete(String table, String whereClause, String[] whereArgs) + */ + public synchronized int delete(String table, String whereClause, String[] whereArgs) { + return getDatabase().delete(table, whereClause, whereArgs); + } + + /* + * @see android.database.sqlite.SQLiteDatabase#update(String table, ContentValues values, String whereClause, String[] whereArgs) + */ + public synchronized int update(String table, ContentValues values, String whereClause, String[] whereArgs) { + return getDatabase().update(table, values, whereClause, whereArgs); + } + + // --- helper classes + + /** + * Default implementation of Astrid database helper + */ + private class DatabaseHelper extends SQLiteOpenHelper { + + public DatabaseHelper(Context context, String name, + CursorFactory factory, int version) { + super(context, name, factory, version); + } + + /** + * Called to create the database tables + */ + @Override + public synchronized void onCreate(SQLiteDatabase db) { + StringBuilder sql = new StringBuilder(); + SqlConstructorVisitor sqlVisitor = new SqlConstructorVisitor(); + + // create tables + for(Table table : getTables()) { + sql.append("CREATE TABLE IF NOT EXISTS ").append(table.name).append('('). + append(AbstractModel.ID_PROPERTY).append(" INTEGER PRIMARY KEY AUTOINCREMENT"); + for(Property property : table.getProperties()) { + if(AbstractModel.ID_PROPERTY.name.equals(property.name)) + continue; + sql.append(',').append(property.accept(sqlVisitor, null)); + } + sql.append(')'); + db.execSQL(sql.toString()); + sql.setLength(0); + } + + // post-table-creation + database = db; + onCreateTables(); + } + + /** + * Called to upgrade the database to a new version + */ + @Override + public synchronized void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.w("database-" + getName(), String.format("Upgrading database from version %d to %d.", + oldVersion, newVersion)); + + database = db; + try { + if(!AbstractDatabase.this.onUpgrade(oldVersion, newVersion)) { + // We don't know how to handle this case because someone forgot to + // implement the upgrade. We can't drop tables, we can only + // throw a nasty exception at this time + + throw new IllegalStateException("Missing database migration " + + "from " + oldVersion + " to " + newVersion); + } + } catch (Exception e) { + exceptionService.reportError(String.format("database-upgrade-%s-%d-%d", + getName(), oldVersion, newVersion), e); + } + } + } + + /** + * Visitor that returns SQL constructor for this property + * + * @author Tim Su + * + */ + public static class SqlConstructorVisitor implements PropertyVisitor { + + public String visitDouble(Property property, Void data) { + return String.format("%s REAL", property.name); + } + + public String visitInteger(Property property, Void data) { + return String.format("%s INTEGER", property.name); + } + + public String visitLong(Property property, Void data) { + return String.format("%s INTEGER", property.name); + } + + public String visitString(Property property, Void data) { + return String.format("%s TEXT", property.name); + } + } +} + diff --git a/api/src/com/todoroo/andlib/data/AbstractModel.java b/api/src/com/todoroo/andlib/data/AbstractModel.java new file mode 100644 index 0000000000..a17aa8b23e --- /dev/null +++ b/api/src/com/todoroo/andlib/data/AbstractModel.java @@ -0,0 +1,431 @@ +/* + * Copyright (c) 2009, Todoroo Inc + * All Rights Reserved + * http://www.todoroo.com + */ +package com.todoroo.andlib.data; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; + +import android.content.ContentValues; +import android.os.Parcel; +import android.os.Parcelable; + +import com.todoroo.andlib.data.Property.DoubleProperty; +import com.todoroo.andlib.data.Property.IntegerProperty; +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.data.Property.PropertyVisitor; + +/** + * AbstractModel represents a row in a database. + *

+ * A single database can be represented by multiple AbstractModels + * corresponding to different queries that return a different set of columns. + * Each model exposes a set of properties that it contains. + * + * @author Tim Su + * + */ +public abstract class AbstractModel implements Parcelable { + + // --- static variables + + private static final ContentValuesSavingVisitor saver = new ContentValuesSavingVisitor(); + + // --- constants + + /** id property common to all models */ + protected static final String ID_PROPERTY_NAME = "_id"; //$NON-NLS-1$ + + /** id field common to all models */ + public static final IntegerProperty ID_PROPERTY = new IntegerProperty(null, ID_PROPERTY_NAME); + + /** sentinel for objects without an id */ + public static final long NO_ID = 0; + + // --- abstract methods + + /** Get the default values for this object */ + abstract public ContentValues getDefaultValues(); + + // --- data store variables and management + + /* Data Source Ordering: + * + * In order to return the best data, we want to check first what the user + * has explicitly set (setValues), then the values we have read out of + * the database (values), then defaults (getDefaultValues) + */ + + /** User set values */ + protected ContentValues setValues = null; + + /** Values from database */ + protected ContentValues values = null; + + /** Get database-read values for this object */ + public ContentValues getDatabaseValues() { + return values; + } + + /** Get the user-set values for this object */ + public ContentValues getSetValues() { + return setValues; + } + + /** Get a list of all field/value pairs merged across data sources */ + public ContentValues getMergedValues() { + ContentValues mergedValues = new ContentValues(); + + ContentValues defaultValues = getDefaultValues(); + if(defaultValues != null) + mergedValues.putAll(defaultValues); + if(values != null) + mergedValues.putAll(values); + if(setValues != null) + mergedValues.putAll(setValues); + + return mergedValues; + } + + /** + * Clear all data on this model + */ + public void clear() { + values = null; + setValues = null; + } + + /** + * Transfers all set values into values. This occurs when a task is + * saved - future saves will not need to write all the data as before. + */ + public void markSaved() { + if(values == null) + values = setValues; + else if(setValues != null) + values.putAll(setValues); + setValues = null; + } + + /** + * Use merged values to compare two models to each other. Must be of + * exactly the same class. + */ + @Override + public boolean equals(Object other) { + if(other == null || other.getClass() != getClass()) + return false; + + return getMergedValues().equals(((AbstractModel)other).getMergedValues()); + } + + @Override + public int hashCode() { + return getMergedValues().hashCode() ^ getClass().hashCode(); + } + + // --- data retrieval + + /** + * Reads all properties from the supplied cursor and store + */ + protected synchronized void readPropertiesFromCursor(TodorooCursor cursor) { + if (values == null) + values = new ContentValues(); + + // clears user-set values + setValues = null; + + for (Property property : cursor.getProperties()) { + saver.save(property, values, cursor.get(property)); + } + } + + /** + * Reads the given property. Make sure this model has this property! + */ + public synchronized TYPE getValue(Property property) { + Object value; + if(setValues != null && setValues.containsKey(property.name)) + value = setValues.get(property.name); + + else if(values != null && values.containsKey(property.name)) + value = values.get(property.name); + + else if(getDefaultValues().containsKey(property.name)) + value = getDefaultValues().get(property.name); + + else + throw new UnsupportedOperationException( + "Model Error: Did not read property " + property.name); //$NON-NLS-1$ + + // resolve properties that were retrieved with a different type than accessed + if(value instanceof String && property instanceof LongProperty) + return (TYPE) Long.valueOf((String)value); + else if(value instanceof String && property instanceof IntegerProperty) + return (TYPE) Integer.valueOf((String)value); + else if(value instanceof String && property instanceof DoubleProperty) + return (TYPE) Double.valueOf((String)value); + else if(value instanceof Integer && property instanceof LongProperty) + return (TYPE) Long.valueOf(((Number)value).longValue()); + return (TYPE) value; + } + + /** + * Utility method to get the identifier of the model, if it exists. + * + * @return {@value #NO_ID} if this model was not added to the database + */ + abstract public long getId(); + + protected long getIdHelper(LongProperty id) { + if(setValues != null && setValues.containsKey(id.name)) + return setValues.getAsLong(id.name); + else if(values != null && values.containsKey(id.name)) + return values.getAsLong(id.name); + else + return NO_ID; + } + + public void setId(long id) { + if (setValues == null) + setValues = new ContentValues(); + + if(id == NO_ID) + setValues.remove(ID_PROPERTY_NAME); + else + setValues.put(ID_PROPERTY_NAME, id); + } + + /** + * @return true if this model has found Jesus (i.e. the database) + */ + public boolean isSaved() { + return getId() != NO_ID; + } + + /** + * @param property + * @return true if setValues or values contains this property + */ + public boolean containsValue(Property property) { + if(setValues != null && setValues.containsKey(property.name)) + return true; + if(values != null && values.containsKey(property.name)) + return true; + return false; + } + + /** + * @param property + * @return true if setValues or values contains this property, and the value + * stored is not null + */ + public boolean containsNonNullValue(Property property) { + if(setValues != null && setValues.containsKey(property.name)) + return setValues.get(property.name) != null; + if(values != null && values.containsKey(property.name)) + return values.get(property.name) != null; + return false; + } + + // --- data storage + + /** + * Check whether the user has changed this property value and it should be + * stored for saving in the database + */ + protected synchronized boolean shouldSaveValue( + Property property, TYPE newValue) { + + // we've already decided to save it, so overwrite old value + if (setValues.containsKey(property.name)) + return true; + + // values contains this key, we should check it out + if(values != null && values.containsKey(property.name)) { + TYPE value = getValue(property); + if (value == null) { + if (newValue == null) + return false; + } else if (value.equals(newValue)) + return false; + } + + // otherwise, good to save + return true; + } + + /** + * Sets the given property. Make sure this model has this property! + */ + public synchronized void setValue(Property property, + TYPE value) { + if (setValues == null) + setValues = new ContentValues(); + if (!shouldSaveValue(property, value)) + return; + + saver.save(property, setValues, value); + } + + /** + * Merges content values with those coming from another source + */ + public synchronized void mergeWith(ContentValues other) { + if (setValues == null) + setValues = new ContentValues(); + setValues.putAll(other); + } + + /** + * Clear the key for the given property + * @param property + */ + public synchronized void clearValue(Property property) { + if(setValues != null && setValues.containsKey(property.name)) + setValues.remove(property.name); + else if(values != null && values.containsKey(property.name)) + values.remove(property.name); + else if(getDefaultValues().containsKey(property.name)) + throw new IllegalArgumentException("Property has a default value"); //$NON-NLS-1$ + } + + // --- property management + + /** + * Looks inside the given class and finds all declared properties + */ + protected static Property[] generateProperties(Class cls) { + ArrayList> properties = new ArrayList>(); + if(cls.getSuperclass() != AbstractModel.class) + properties.addAll(Arrays.asList(generateProperties( + (Class) cls.getSuperclass()))); + + // a property is public, static & extends Property + for(Field field : cls.getFields()) { + if((field.getModifiers() & Modifier.STATIC) == 0) + continue; + if(!Property.class.isAssignableFrom(field.getType())) + continue; + try { + properties.add((Property) field.get(null)); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + return properties.toArray(new Property[properties.size()]); + } + + /** + * Visitor that saves a value into a content values store + * + * @author Tim Su + * + */ + public static class ContentValuesSavingVisitor implements PropertyVisitor { + + private ContentValues store; + + public synchronized void save(Property property, ContentValues newStore, Object value) { + this.store = newStore; + + // we don't allow null values, as they indicate unset properties + // when the database was written + + if(value != null) + property.accept(this, value); + } + + public Void visitDouble(Property property, Object value) { + store.put(property.name, (Double) value); + return null; + } + + public Void visitInteger(Property property, Object value) { + store.put(property.name, (Integer) value); + return null; + } + + public Void visitLong(Property property, Object value) { + store.put(property.name, (Long) value); + return null; + } + + public Void visitString(Property property, Object value) { + store.put(property.name, (String) value); + return null; + } + } + + // --- parcelable helpers + + /** + * {@inheritDoc} + */ + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(setValues, 0); + dest.writeParcelable(values, 0); + } + + /** + * In addition to overriding this class, model classes should create + * a static final variable named "CREATOR" in order to satisfy the + * requirements of the Parcelable interface. + */ + abstract protected Parcelable.Creator getCreator(); + + /** + * Parcelable creator helper + */ + protected static final class ModelCreator + implements Parcelable.Creator { + + private final Class cls; + + public ModelCreator(Class cls) { + super(); + this.cls = cls; + } + + /** + * {@inheritDoc} + */ + public TYPE createFromParcel(Parcel source) { + TYPE model; + try { + model = cls.newInstance(); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } + model.setValues = source.readParcelable(ContentValues.class.getClassLoader()); + model.values = source.readParcelable(ContentValues.class.getClassLoader()); + return model; + } + + /** + * {@inheritDoc} + */ + public TYPE[] newArray(int size) { + return (TYPE[]) Array.newInstance(cls, size); + }; + }; + +} diff --git a/api/src/com/todoroo/andlib/data/ContentResolverDao.java b/api/src/com/todoroo/andlib/data/ContentResolverDao.java new file mode 100644 index 0000000000..2cf61e0594 --- /dev/null +++ b/api/src/com/todoroo/andlib/data/ContentResolverDao.java @@ -0,0 +1,148 @@ +package com.todoroo.andlib.data; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.andlib.sql.Criterion; +import com.todoroo.andlib.sql.Query; + + +/** + * DAO for reading and writing values from an Android ContentResolver + * + * @author Tim Su + * + * @param model type + */ +public class ContentResolverDao { + + /** class of model */ + private final Class modelClass; + + /** base content uri */ + private final Uri baseUri; + + /** content resolver */ + private final ContentResolver cr; + + @Autowired + protected Boolean debug; + + public ContentResolverDao(Class modelClass, Context context, Uri baseUri) { + DependencyInjectionService.getInstance().inject(this); + this.modelClass = modelClass; + if(debug == null) + debug = false; + this.baseUri = baseUri; + + cr = context.getContentResolver(); + } + + /** + * Returns a URI for a single id + * @param id + * @return + */ + private Uri uriWithId(long id) { + return Uri.withAppendedPath(baseUri, Long.toString(id)); + } + + /** + * Delete specific item from the given table + * @param id + * @return number of rows affected + */ + public int delete(long id) { + return cr.delete(uriWithId(id), null, null); + } + + /** + * Delete by criteria + * @param where + * @return number of rows affected + */ + public int deleteWhere(Criterion where) { + return cr.delete(baseUri, where.toString(), null); + } + + /** + * Query content provider + * @param query + * @return + */ + public TodorooCursor query(Query query) { + if(debug) + Log.i("SQL-" + modelClass.getSimpleName(), query.toString()); //$NON-NLS-1$ + Cursor cursor = query.queryContentResolver(cr, baseUri); + return new TodorooCursor(cursor, query.getFields()); + } + + /** + * Create new or save existing model + * @param model + * @return true if data was written to the db, false otherwise + */ + public boolean save(TYPE model) { + if(model.isSaved()) { + if(model.getSetValues() == null) + return false; + if(cr.update(uriWithId(model.getId()), model.getSetValues(), null, null) != 0) + return true; + } + Uri uri = cr.insert(baseUri, model.getMergedValues()); + long id = Long.parseLong(uri.getLastPathSegment()); + model.setId(id); + model.markSaved(); + return true; + } + + /** + * Returns object corresponding to the given identifier + * + * @param database + * @param table + * name of table + * @param properties + * properties to read + * @param id + * id of item + * @return null if no item found + */ + public TYPE fetch(long id, Property... properties) { + TodorooCursor cursor = query( + Query.select(properties).where(AbstractModel.ID_PROPERTY.eq(id))); + try { + if (cursor.getCount() == 0) + return null; + cursor.moveToFirst(); + Constructor constructor = modelClass.getConstructor(TodorooCursor.class); + return constructor.newInstance(cursor); + } catch (SecurityException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } finally { + try { + cursor.close(); + } catch (NullPointerException e) { + // cursor was not open + } + } + } +} diff --git a/api/src/com/todoroo/andlib/data/DatabaseDao.java b/api/src/com/todoroo/andlib/data/DatabaseDao.java new file mode 100644 index 0000000000..ac8fc3d25a --- /dev/null +++ b/api/src/com/todoroo/andlib/data/DatabaseDao.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2009, Todoroo Inc + * All Rights Reserved + * http://www.todoroo.com + */ +package com.todoroo.andlib.data; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import android.content.ContentValues; +import android.database.Cursor; +import android.util.Log; + +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.andlib.sql.Criterion; +import com.todoroo.andlib.sql.Query; + + + +/** + * DAO for reading data from an instance of {@link AbstractDatabase}. If you + * are writing an add-on for Astrid, you probably want to be using a subclass + * of {@link ContentResolverDao} instead. + * + * @author Tim Su + * + */ +public class DatabaseDao { + + private final Class modelClass; + + private Table table; + + private AbstractDatabase database; + + @Autowired + protected Boolean debug; + + public DatabaseDao(Class modelClass) { + DependencyInjectionService.getInstance().inject(this); + this.modelClass = modelClass; + if(debug == null) + debug = false; + } + + public DatabaseDao(Class modelClass, AbstractDatabase database) { + this(modelClass); + setDatabase(database); + } + + /** Gets table associated with this DAO */ + public Table getTable() { + return table; + } + + /** + * Sets database accessed by this DAO. Used for dependency-injected + * initialization by child classes and unit tests + * + * @param database + */ + public void setDatabase(AbstractDatabase database) { + if(database == this.database) + return; + this.database = database; + table = database.getTable(modelClass); + } + + // --- dao methods + + /** + * Construct a query with SQL DSL objects + * + * @param query + * @return + */ + public TodorooCursor query(Query query) { + query.from(table); + if(debug) + Log.i("SQL-" + modelClass.getSimpleName(), query.toString()); //$NON-NLS-1$ + Cursor cursor = database.rawQuery(query.toString(), null); + return new TodorooCursor(cursor, query.getFields()); + } + + /** + * Construct a query with raw SQL + * + * @param properties + * @param selection + * @param selectionArgs + * @return + */ + public TodorooCursor rawQuery(String selection, String[] selectionArgs, Property... properties) { + String[] fields = new String[properties.length]; + for(int i = 0; i < properties.length; i++) + fields[i] = properties[i].name; + return new TodorooCursor(database.getDatabase().query(table.name, + fields, selection, selectionArgs, null, null, null), + properties); + } + + /** + * Returns object corresponding to the given identifier + * + * @param database + * @param table + * name of table + * @param properties + * properties to read + * @param id + * id of item + * @return null if no item found + */ + public TYPE fetch(long id, Property... properties) { + TodorooCursor cursor = fetchItem(id, properties); + try { + if (cursor.getCount() == 0) + return null; + Constructor constructor = modelClass.getConstructor(TodorooCursor.class); + return constructor.newInstance(cursor); + } catch (SecurityException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } finally { + cursor.close(); + } + } + + /** + * Delete the given id + * + * @param database + * @param id + * @return true if delete was successful + */ + public boolean delete(long id) { + return database.delete(table.name, + AbstractModel.ID_PROPERTY.eq(id).toString(), null) > 0; + } + + /** + * Delete all matching a clause + * @param where predicate for deletion + * @return # of deleted items + */ + public int deleteWhere(Criterion where) { + return database.delete(table.name, + where.toString(), null); + } + + /** + * Update all matching a clause to have the values set on template object. + *

+ * Example (updates "joe" => "bob" in metadata value1): + * {code} + * Metadata item = new Metadata(); + * item.setValue(Metadata.VALUE1, "bob"); + * update(item, Metadata.VALUE1.eq("joe")); + * {code} + * @param where sql criteria + * @param template set fields on this object in order to set them in the db. + * @return # of updated items + */ + public int update(Criterion where, TYPE template) { + return database.update(table.name, template.getSetValues(), + where.toString(), null); + } + + /** + * Save the given object to the database. Creates a new object if + * model id property has not been set + * + * @return true on success. + */ + public boolean persist(TYPE item) { + if (item.getId() == AbstractModel.NO_ID) { + return createNew(item); + } else { + ContentValues values = item.getSetValues(); + + if (values.size() == 0) // nothing changed + return true; + + return saveExisting(item); + } + } + + /** + * Creates the given item. + * + * @param database + * @param table + * table name + * @param item + * item model + * @return returns true on success. + */ + public boolean createNew(TYPE item) { + long newRow = database.insert(table.name, + AbstractModel.ID_PROPERTY.name, item.getMergedValues()); + boolean result = newRow >= 0; + if(result) { + item.markSaved(); + item.setId(newRow); + } + return result; + } + + /** + * Saves the given item. Will not create a new item! + * + * @param database + * @param table + * table name + * @param item + * item model + * @return returns true on success. + */ + public boolean saveExisting(TYPE item) { + ContentValues values = item.getSetValues(); + if(values == null || values.size() == 0) // nothing changed + return true; + boolean result = database.update(table.name, values, + AbstractModel.ID_PROPERTY.eq(item.getId()).toString(), null) > 0; + if(result) + item.markSaved(); + return result; + } + + /** + * Updates multiple rows of the database based on model set values + * + * @param item + * item model + * @param criterion + * @return returns true on success. + */ + public int updateMultiple(ContentValues values, Criterion criterion) { + if(values.size() == 0) // nothing changed + return 0; + return database.update(table.name, values, criterion.toString(), null); + } + + // --- helper methods + + + /** + * Returns cursor to object corresponding to the given identifier + * + * @param database + * @param table + * name of table + * @param properties + * properties to read + * @param id + * id of item + * @return + */ + protected TodorooCursor fetchItem(long id, Property... properties) { + TodorooCursor cursor = query( + Query.select(properties).where(AbstractModel.ID_PROPERTY.eq(id))); + cursor.moveToFirst(); + return new TodorooCursor(cursor, properties); + } +} diff --git a/api/src/com/todoroo/andlib/data/Property.java b/api/src/com/todoroo/andlib/data/Property.java new file mode 100644 index 0000000000..9a4665bf2f --- /dev/null +++ b/api/src/com/todoroo/andlib/data/Property.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2009, Todoroo Inc + * All Rights Reserved + * http://www.todoroo.com + */ +package com.todoroo.andlib.data; + +import com.todoroo.andlib.sql.Field; + +/** + * Property represents a typed column in a database. + * + * Within a given database row, the parameter may not exist, in which case the + * value is null, it may be of an incorrect type, in which case an exception is + * thrown, or the correct type, in which case the value is returned. + * + * @author Tim Su + * + * @param + * a database supported type, such as String or Integer + */ +@SuppressWarnings("nls") +public abstract class Property extends Field implements Cloneable { + + // --- implementation + + /** The database table name this property */ + public final Table table; + + /** The database column name for this property */ + public final String name; + + /** + * Create a property by table and column name. Uses the default property + * expression which is derived from default table name + */ + protected Property(Table table, String columnName) { + this(table, columnName, (table == null) ? (columnName) : (table.name + "." + columnName)); + } + + /** + * Create a property by table and column name, manually specifying an + * expression to use in SQL + */ + protected Property(Table table, String columnName, String expression) { + super(expression); + this.table = table; + this.name = columnName; + } + + /** + * Accept a visitor + */ + abstract public RETURN accept( + PropertyVisitor visitor, PARAMETER data); + + /** + * Return a clone of this property + */ + @Override + public Property clone() { + try { + return (Property) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } + + // --- helper classes and interfaces + + /** + * Visitor interface for property classes + * + * @author Tim Su + * + */ + public interface PropertyVisitor { + public RETURN visitInteger(Property property, PARAMETER data); + + public RETURN visitLong(Property property, PARAMETER data); + + public RETURN visitDouble(Property property, PARAMETER data); + + public RETURN visitString(Property property, PARAMETER data); + } + + // --- children + + /** + * Integer property type. See {@link Property} + * + * @author Tim Su + * + */ + public static class IntegerProperty extends Property { + + public IntegerProperty(Table table, String name) { + super(table, name); + } + + protected IntegerProperty(Table table, String name, String expression) { + super(table, name, expression); + } + + @Override + public RETURN accept( + PropertyVisitor visitor, PARAMETER data) { + return visitor.visitInteger(this, data); + } + } + + /** + * String property type. See {@link Property} + * + * @author Tim Su + * + */ + public static class StringProperty extends Property { + + public StringProperty(Table table, String name) { + super(table, name); + } + + protected StringProperty(Table table, String name, String expression) { + super(table, name, expression); + } + + @Override + public RETURN accept( + PropertyVisitor visitor, PARAMETER data) { + return visitor.visitString(this, data); + } + } + + /** + * Double property type. See {@link Property} + * + * @author Tim Su + * + */ + public static class DoubleProperty extends Property { + + public DoubleProperty(Table table, String name) { + super(table, name); + } + + protected DoubleProperty(Table table, String name, String expression) { + super(table, name, expression); + } + + + @Override + public RETURN accept( + PropertyVisitor visitor, PARAMETER data) { + return visitor.visitDouble(this, data); + } + } + + /** + * Long property type. See {@link Property} + * + * @author Tim Su + * + */ + public static class LongProperty extends Property { + + public LongProperty(Table table, String name) { + super(table, name); + } + + protected LongProperty(Table table, String name, String expression) { + super(table, name, expression); + } + + @Override + public RETURN accept( + PropertyVisitor visitor, PARAMETER data) { + return visitor.visitLong(this, data); + } + } + + // --- pseudo-properties + + /** Runs a SQL function and returns the result as a string */ + public static class StringFunctionProperty extends StringProperty { + public StringFunctionProperty(String function, String columnName) { + super(null, columnName, function); + alias = columnName; + } + } + + /** Runs a SQL function and returns the result as a string */ + public static class IntegerFunctionProperty extends IntegerProperty { + public IntegerFunctionProperty(String function, String columnName) { + super(null, columnName, function); + alias = columnName; + } + } + + /** Counting in aggregated tables. Returns the result of COUNT(1) */ + public static final class CountProperty extends IntegerFunctionProperty { + public CountProperty() { + super("COUNT(1)", "count"); + } + } + +} diff --git a/api/src/com/todoroo/andlib/data/Table.java b/api/src/com/todoroo/andlib/data/Table.java new file mode 100644 index 0000000000..3d2cb88808 --- /dev/null +++ b/api/src/com/todoroo/andlib/data/Table.java @@ -0,0 +1,68 @@ +package com.todoroo.andlib.data; + +import com.todoroo.andlib.sql.Field; +import com.todoroo.andlib.sql.SqlTable; + +/** + * Table class. Most fields are final, so methods such as as will + * clone the table when it returns. + * + * @author Tim Su + * + */ +public final class Table extends SqlTable { + public final String name; + public final Class modelClass; + + public Table(String name, Class modelClass) { + this(name, modelClass, null); + } + + public Table(String name, Class modelClass, String alias) { + super(name); + this.name = name; + this.alias = alias; + this.modelClass = modelClass; + } + + /** + * Reads a list of properties from model class by reflection + * @return property array + */ + @SuppressWarnings("nls") + public Property[] getProperties() { + try { + return (Property[])modelClass.getField("PROPERTIES").get(null); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } catch (SecurityException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + // --- for sql-dsl + + /** + * Create a new join table based on this table, but with an alias + */ + @Override + public Table as(String newAlias) { + return new Table(name, modelClass, newAlias); + } + + /** + * Create a field object based on the given property + * @param property + * @return + */ + @SuppressWarnings("nls") + public Field field(Property property) { + if(alias != null) + return Field.field(alias + "." + property.name); + return Field.field(name + "." + property.name); + } +} \ No newline at end of file diff --git a/api/src/com/todoroo/andlib/data/TodorooCursor.java b/api/src/com/todoroo/andlib/data/TodorooCursor.java new file mode 100644 index 0000000000..4ef2c8ccc9 --- /dev/null +++ b/api/src/com/todoroo/andlib/data/TodorooCursor.java @@ -0,0 +1,109 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.andlib.data; + +import java.util.WeakHashMap; + +import android.database.Cursor; +import android.database.CursorWrapper; + +import com.todoroo.andlib.data.Property.PropertyVisitor; + +/** + * AstridCursor wraps a cursor and allows users to query for individual + * {@link Property} types or read an entire {@link AbstractModel} from + * a database row. + * + * @author Tim Su + * + * @param a model type that is returned by this cursor + */ +public class TodorooCursor extends CursorWrapper { + + /** Properties read by this cursor */ + private final Property[] properties; + + /** Weakly cache field name to column id references for this cursor. + * Because it's a weak hash map, entire keys can be discarded by GC */ + private final WeakHashMap columnIndexCache; + + /** Property reading visitor */ + private static final CursorReadingVisitor reader = new CursorReadingVisitor(); + + /** + * Create an AstridCursor from the supplied {@link Cursor} + * object. + * + * @param cursor + * @param properties properties read from this cursor + */ + public TodorooCursor(Cursor cursor, Property[] properties) { + super(cursor); + + this.properties = properties; + columnIndexCache = new WeakHashMap(); + } + + /** + * Get the value for the given property on the underlying {@link Cursor} + * + * @param type to return + * @param property to retrieve + * @return + */ + public PROPERTY_TYPE get(Property property) { + return (PROPERTY_TYPE)property.accept(reader, this); + } + + /** + * Gets entire property list + * @return + */ + public Property[] getProperties() { + return properties; + } + + /** + * Use cache to get the column index for the given field name + */ + public synchronized int getColumnIndexFromCache(String field) { + Integer index = columnIndexCache.get(field); + if(index == null) { + index = getColumnIndexOrThrow(field); + columnIndexCache.put(field, index); + } + + return index; + } + + /** + * Visitor that reads the given property from a cursor + * + * @author Tim Su + * + */ + public static class CursorReadingVisitor implements PropertyVisitor> { + + public Object visitDouble(Property property, + TodorooCursor cursor) { + return cursor.getDouble(cursor.getColumnIndexFromCache(property.name)); + } + + public Object visitInteger(Property property, + TodorooCursor cursor) { + return cursor.getInt(cursor.getColumnIndexFromCache(property.name)); + } + + public Object visitLong(Property property, TodorooCursor cursor) { + return cursor.getLong(cursor.getColumnIndexFromCache(property.name)); + } + + public Object visitString(Property property, + TodorooCursor cursor) { + return cursor.getString(cursor.getColumnIndexFromCache(property.name)); + } + + } + +} diff --git a/api/src/com/todoroo/andlib/data/package-info.java b/api/src/com/todoroo/andlib/data/package-info.java new file mode 100644 index 0000000000..3be4c791dc --- /dev/null +++ b/api/src/com/todoroo/andlib/data/package-info.java @@ -0,0 +1,5 @@ +/** + * Todoroo Library classes for the data layer: accessing data from a database or + * {@link ContentResolver} + */ +package com.todoroo.andlib.data; \ No newline at end of file diff --git a/api/src/com/todoroo/andlib/service/AbstractDependencyInjector.java b/api/src/com/todoroo/andlib/service/AbstractDependencyInjector.java new file mode 100644 index 0000000000..673cc35f14 --- /dev/null +++ b/api/src/com/todoroo/andlib/service/AbstractDependencyInjector.java @@ -0,0 +1,100 @@ +package com.todoroo.andlib.service; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.util.HashMap; + +import com.todoroo.andlib.service.ExceptionService.ErrorReporter; + +/** + * A Dependency Injector knows how to inject certain dependencies based + * on the field that is passed in. You will need to write your own initialization + * code to insert this dependency injector into the DI chain. + * + * @author Tim Su + * + */ +abstract public class AbstractDependencyInjector { + + /** + * Initialize list of injection variables. Special care must used when + * instantiating classes that themselves depend on dependency injection + * (i.e. {@link ErrorReporter}. + */ + protected void addInjectables() { + // your injectables here + } + + // --- + + /** + * Constructor + */ + protected AbstractDependencyInjector() { + addInjectables(); + } + + /** + * Dependencies this class knows how to handle + */ + protected final HashMap injectables = new HashMap(); + + /** + * Cache of classes that were instantiated by the injector + */ + protected final HashMap, WeakReference> createdObjects = + new HashMap, WeakReference>(); + + /** + * Gets the injected object for this field. If implementing class does not + * know how to handle this dependency, it should return null + * + * @param object + * object to perform dependency injection on + * @param field + * field tagged with {link Autowired} annotation + * @return object to assign to this field, or null + */ + public Object getInjection(Object object, Field field) { + if(injectables.containsKey(field.getName())) { + Object injection = injectables.get(field.getName()); + + // if it's a class, instantiate the class + if(injection instanceof Class) { + if(createdObjects.containsKey(injection) && + createdObjects.get(injection).get() != null) { + injection = createdObjects.get(injection).get(); + } else { + Class cls = (Class)injection; + try { + injection = cls.newInstance(); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } + + createdObjects.put(cls, + new WeakReference(injection)); + } + } + + return injection; + } + + return null; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + /** + * Flush dependency injection cache. Useful for unit tests. + */ + protected void flushCreated() { + createdObjects.clear(); + } + +} diff --git a/api/src/com/todoroo/andlib/service/Autowired.java b/api/src/com/todoroo/andlib/service/Autowired.java new file mode 100644 index 0000000000..3b0b1c7654 --- /dev/null +++ b/api/src/com/todoroo/andlib/service/Autowired.java @@ -0,0 +1,19 @@ +package com.todoroo.andlib.service; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Autowired is an annotation that tells the dependency injector to + * set up the class as appropriate. + * + * @author Tim Su + * + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Autowired { + // +} diff --git a/api/src/com/todoroo/andlib/service/ContextManager.java b/api/src/com/todoroo/andlib/service/ContextManager.java new file mode 100644 index 0000000000..b4f647fd1a --- /dev/null +++ b/api/src/com/todoroo/andlib/service/ContextManager.java @@ -0,0 +1,63 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.andlib.service; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; + +/** + * Singleton class to manage current application context + * b + * @author Tim Su + * + */ +public final class ContextManager { + + /** + * Global application context + */ + private static Context context = null; + + /** + * Sets the global context + * + * @param context + */ + public static void setContext(Context context) { + if(context == null || context.getApplicationContext() == null) + return; + if(ContextManager.context != null && !(context instanceof Activity)) + return; + ContextManager.context = context; + } + + /** + * Gets the global context + */ + public static Context getContext() { + return context; + } + + /** + * Convenience method to read a string from the resources + * + * @param resId resource + * @param parameters % arguments + * @return resource string + */ + public static String getString(int resId, Object... formatArgs) { + return context.getString(resId, formatArgs); + } + + /** + * Convenience method to read resources + * + * @return resources object + */ + public static Resources getResources() { + return context.getResources(); + } + +} diff --git a/api/src/com/todoroo/andlib/service/DependencyInjectionService.java b/api/src/com/todoroo/andlib/service/DependencyInjectionService.java new file mode 100644 index 0000000000..c73858ccea --- /dev/null +++ b/api/src/com/todoroo/andlib/service/DependencyInjectionService.java @@ -0,0 +1,165 @@ +package com.todoroo.andlib.service; + +import java.lang.reflect.Field; +import java.util.LinkedList; + + + +/** + * Simple Dependency Injection Service for Android. + *

+ * Add dependency injectors to the injector chain, then invoke this method + * against classes you wish to perform dependency injection for. + *

+ * All errors encountered are handled as warnings, so if dependency injection + * seems to be failing, check the logs for more information. + * + * @author Tim Su + * + */ +public class DependencyInjectionService { + + /** + * Dependency injectors. Use getters and setters to modify this list + */ + private final LinkedList injectors = new LinkedList(); + + /** + * Perform dependency injection in the caller object + * + * @param caller + * object to perform DI on + */ + @SuppressWarnings("nls") + public void inject(Object caller) { + // Traverse through class and all parent classes, looking for + // fields declared with the @Autowired annotation and using + // dependency injection to set them as appropriate + + Class cls = caller.getClass(); + while(cls != null) { + String packageName = cls.getPackage().getName(); + if(!isQualifiedPackage(packageName)) + break; + + for(Field field : cls.getDeclaredFields()) { + if(field.getAnnotation(Autowired.class) != null) { + field.setAccessible(true); + try { + handleField(caller, field); + } catch (IllegalStateException e) { + throw new RuntimeException(String.format("Unable to set field '%s' of type '%s'", + field.getName(), field.getType()), e); + } catch (IllegalArgumentException e) { + throw new RuntimeException(String.format("Unable to set field '%s' of type '%s'", + field.getName(), field.getType()), e); + } catch (IllegalAccessException e) { + throw new RuntimeException(String.format("Unable to set field '%s' of type '%s'", + field.getName(), field.getType()), e); + } + } + } + + cls = cls.getSuperclass(); + } + } + + @SuppressWarnings("nls") + private boolean isQualifiedPackage(String packageName) { + if(packageName.startsWith("com.todoroo")) + return true; + if(packageName.startsWith("com.timsu")) + return true; + if(packageName.startsWith("org.weloveastrid")) + return true; + return false; + } + + /** + * This method returns the appropriate dependency object based on the type + * that this autowired field accepts + * + * @param caller + * calling object + * @param field + * field to inject + */ + @SuppressWarnings("nls") + private synchronized void handleField(Object caller, Field field) + throws IllegalStateException, IllegalArgumentException, + IllegalAccessException { + + if(field.getType().isPrimitive()) + throw new IllegalStateException(String.format( + "Tried to dependency-inject primative field '%s' of type '%s'", + field.getName(), field.getType())); + + // field has already been processed, ignore + if (field.get(caller) != null) { + return; + } + + for (AbstractDependencyInjector injector : injectors) { + Object injection = injector.getInjection(caller, field); + if (injection != null) { + field.set(caller, injection); + return; + } + } + + throw new IllegalStateException( + String.format("No dependency injector found for autowired " + + "field '%s' in class '%s'. Injectors: %s", + field.getName(), caller.getClass().getName(), + injectors)); + } + + // --- default dependency injector + + private class DefaultDependencyInjector extends AbstractDependencyInjector { + @SuppressWarnings("nls") + @Override + protected void addInjectables() { + injectables.put("debug", false); + injectables.put("exceptionService", ExceptionService.class); + } + } + + // --- static methods + + private static DependencyInjectionService instance = null; + + DependencyInjectionService() { + // prevent instantiation + injectors.add(new DefaultDependencyInjector()); + } + + /** + * Gets the singleton instance of the dependency injection service. + * @return + */ + public synchronized static DependencyInjectionService getInstance() { + if(instance == null) + instance = new DependencyInjectionService(); + return instance; + } + + /** + * Removes the supplied injector + * @return + */ + public synchronized void removeInjector(AbstractDependencyInjector injector) { + injectors.remove(injector); + } + + /** + * Adds a Dependency Injector to the front of the list + * @param injectors + */ + public synchronized void addInjector(AbstractDependencyInjector injector) { + removeInjector(injector); + + this.injectors.addFirst(injector); + } + +} diff --git a/api/src/com/todoroo/andlib/service/ExceptionService.java b/api/src/com/todoroo/andlib/service/ExceptionService.java new file mode 100644 index 0000000000..1372288700 --- /dev/null +++ b/api/src/com/todoroo/andlib/service/ExceptionService.java @@ -0,0 +1,162 @@ +package com.todoroo.andlib.service; + +import java.lang.Thread.UncaughtExceptionHandler; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.util.Log; + +import com.todoroo.astrid.api.R; + +/** + * Exception handling utility class - reports and logs errors + * + * @author Tim Su + * + */ +public class ExceptionService { + + @Autowired + public ErrorReporter[] errorReporters; + + public ExceptionService() { + DependencyInjectionService.getInstance().inject(this); + if(errorReporters == null) { + errorReporters = new ErrorReporter[] { + new AndroidLogReporter() + }; + } + } + + /** + * Report the error via registered error handlers + * + * @param name Internal error name. Not displayed to user + * @param error Exception encountered. Message will be displayed to user + */ + public void reportError(String name, Throwable error) { + if(errorReporters == null) + return; + + for(ErrorReporter reporter : errorReporters) { + try { + reporter.handleError(name, error); + } catch (Exception e) { + Log.e("astrid-exception-service", "Exception handling error", e); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + } + + /** + * Display error dialog if context is activity and report error + * + * @param context Application Context + * @param name Internal error name. Not displayed to user + * @param error Exception encountered. Message will be displayed to user + */ + public void displayAndReportError(final Context context, String name, Throwable error) { + if(context instanceof Activity) { + final String messageToDisplay; + + // pretty up the message when displaying to user + if(error == null) + messageToDisplay = context.getString(R.string.DLG_error_generic); + else + messageToDisplay = context.getString(R.string.DLG_error, error); + + ((Activity)context).runOnUiThread(new Runnable() { + public void run() { + try { + new AlertDialog.Builder(context) + .setTitle(R.string.DLG_error_title) + .setMessage(messageToDisplay) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(android.R.string.ok, null) + .show(); + } catch (Exception e) { + // suppress errors during dialog creation + } + } + }); + } + + reportError(name, error); + } + + /** + * Error reporter interface + * + * @author Tim Su + * + */ + public interface ErrorReporter { + public void handleError(String name, Throwable error); + } + + /** + * AndroidLogReporter reports errors to LogCat + * + * @author Tim Su + * + */ + public static class AndroidLogReporter implements ErrorReporter { + + /** + * Report the error to the logs + * + * @param name + * @param error + */ + public void handleError(String name, Throwable error) { + String tag = null; + if(ContextManager.getContext() != null) { + PackageManager pm = ContextManager.getContext().getPackageManager(); + try { + String appName = pm.getApplicationInfo(ContextManager.getContext(). + getPackageName(), 0).loadLabel(pm).toString(); + tag = appName + "-" + name; //$NON-NLS-1$ + } catch (NameNotFoundException e) { + // give up + } + } + + if(tag == null) + tag = "unknown-" + name; //$NON-NLS-1$ + + if(error == null) + Log.e(tag, "Exception: " + name); //$NON-NLS-1$ + else + Log.e(tag, error.toString(), error); + } + } + + /** + * Uncaught exception handler uses the exception utilities class to + * report errors + * + * @author Tim Su + * + */ + public static class TodorooUncaughtExceptionHandler implements UncaughtExceptionHandler { + private final UncaughtExceptionHandler defaultUEH; + + @Autowired + protected ExceptionService exceptionService; + + public TodorooUncaughtExceptionHandler() { + defaultUEH = Thread.getDefaultUncaughtExceptionHandler(); + DependencyInjectionService.getInstance().inject(this); + } + + public void uncaughtException(Thread thread, Throwable ex) { + if(exceptionService != null) + exceptionService.reportError("uncaught", ex); //$NON-NLS-1$ + defaultUEH.uncaughtException(thread, ex); + } + } + +} + diff --git a/api/src/com/todoroo/andlib/service/HttpErrorException.java b/api/src/com/todoroo/andlib/service/HttpErrorException.java new file mode 100644 index 0000000000..c2c46fc036 --- /dev/null +++ b/api/src/com/todoroo/andlib/service/HttpErrorException.java @@ -0,0 +1,13 @@ +package com.todoroo.andlib.service; + +import java.io.IOException; + +public class HttpErrorException extends IOException { + + private static final long serialVersionUID = 5373340422464657279L; + + public HttpErrorException(int code, String message) { + super(String.format("%d %s", code, message)); //$NON-NLS-1$ + } + +} diff --git a/api/src/com/todoroo/andlib/service/HttpRestClient.java b/api/src/com/todoroo/andlib/service/HttpRestClient.java new file mode 100644 index 0000000000..15aedf6b70 --- /dev/null +++ b/api/src/com/todoroo/andlib/service/HttpRestClient.java @@ -0,0 +1,164 @@ +package com.todoroo.andlib.service; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.ref.WeakReference; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; + +import android.util.Log; + +/** + * RestClient allows Android to consume web requests. + *

+ * Portions by Praeda: + * http://senior.ceng.metu.edu.tr/2009/praeda/2009/01/11/a-simple + * -restful-client-at-android/ + * + * @author Tim Su + * + */ +public class HttpRestClient implements RestClient { + + private static final int HTTP_UNAVAILABLE_END = 599; + private static final int HTTP_UNAVAILABLE_START = 500; + private static final int HTTP_OK = 200; + + private static final int TIMEOUT_MILLIS = 30000; + + private static WeakReference httpClient = null; + + protected boolean debug = false; + + public HttpRestClient() { + DependencyInjectionService.getInstance().inject(this); + } + + private static String convertStreamToString(InputStream is) { + /* + * To convert the InputStream to String we use the + * BufferedReader.readLine() method. We iterate until the BufferedReader + * return null which means there's no more data to read. Each line will + * appended to a StringBuilder and returned as String. + */ + BufferedReader reader = new BufferedReader(new InputStreamReader(is), 16384); + StringBuilder sb = new StringBuilder(); + + String line = null; + try { + while ((line = reader.readLine()) != null) { + sb.append(line + "\n"); //$NON-NLS-1$ + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + return sb.toString(); + } + + private synchronized static HttpClient getClient() { + if (httpClient == null || httpClient.get() == null) { + HttpParams params = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(params, TIMEOUT_MILLIS); + HttpConnectionParams.setSoTimeout(params, TIMEOUT_MILLIS); + HttpClient client = new DefaultHttpClient(params); + httpClient = new WeakReference(client); + return client; + } else { + return httpClient.get(); + } + } + + private String processHttpResponse(HttpResponse response) throws IOException { + int statusCode = response.getStatusLine().getStatusCode(); + if(statusCode >= HTTP_UNAVAILABLE_START && statusCode <= HTTP_UNAVAILABLE_END) { + throw new HttpUnavailableException(); + } else if(statusCode != HTTP_OK) { + throw new HttpErrorException(response.getStatusLine().getStatusCode(), + response.getStatusLine().getReasonPhrase()); + } + + HttpEntity entity = response.getEntity(); + + if (entity != null) { + InputStream contentStream = entity.getContent(); + try { + return convertStreamToString(contentStream); + } finally { + contentStream.close(); + } + } + + return null; + } + + /** + * Issue an HTTP GET for the given URL, return the response + * + * @param url url with url-encoded params + * @return response, or null if there was no response + * @throws IOException + */ + public synchronized String get(String url) throws IOException { + if(debug) + Log.d("http-rest-client-get", url); //$NON-NLS-1$ + + try { + HttpGet httpGet = new HttpGet(url); + HttpResponse response = getClient().execute(httpGet); + + return processHttpResponse(response); + } catch (IOException e) { + throw e; + } catch (Exception e) { + IOException ioException = new IOException(e.getMessage()); + ioException.initCause(e); + throw ioException; + } + } + + /** + * Issue an HTTP POST for the given URL, return the response + * + * @param url + * @param data + * url-encoded data + * @throws IOException + */ + public synchronized String post(String url, String data) throws IOException { + if(debug) + Log.d("http-rest-client-post", url + " | " + data); //$NON-NLS-1$ //$NON-NLS-2$ + + try { + HttpPost httpPost = new HttpPost(url); + httpPost.setEntity(new StringEntity(data)); + HttpResponse response = getClient().execute(httpPost); + + return processHttpResponse(response); + } catch (IOException e) { + throw e; + } catch (Exception e) { + IOException ioException = new IOException(e.getMessage()); + ioException.initCause(e); + throw ioException; + } + } + +} diff --git a/api/src/com/todoroo/andlib/service/HttpUnavailableException.java b/api/src/com/todoroo/andlib/service/HttpUnavailableException.java new file mode 100644 index 0000000000..ac6e8c8606 --- /dev/null +++ b/api/src/com/todoroo/andlib/service/HttpUnavailableException.java @@ -0,0 +1,25 @@ +package com.todoroo.andlib.service; + +import java.io.IOException; + +/** + * Exception displayed when a 500 error is received on an HTTP invocation + * + * @author Tim Su + * + */ +public class HttpUnavailableException extends IOException { + + private static final long serialVersionUID = 5373340422464657279L; + + public HttpUnavailableException() { + super(); + DependencyInjectionService.getInstance().inject(this); + } + + @Override + public String getMessage() { + return "Sorry, our servers are experiencing some issues. Please try again later!"; //$NON-NLS-1$ // FIXME + } + +} diff --git a/api/src/com/todoroo/andlib/service/NotificationManager.java b/api/src/com/todoroo/andlib/service/NotificationManager.java new file mode 100644 index 0000000000..201d449038 --- /dev/null +++ b/api/src/com/todoroo/andlib/service/NotificationManager.java @@ -0,0 +1,49 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.andlib.service; + +import android.app.Notification; +import android.content.Context; + +/** + * Notification Manager stub + * + * @author Tim Su + * + */ +public interface NotificationManager { + + public void cancel(int id); + + public void cancelAll(); + + public void notify(int id, Notification notification); + + /** + * Instantiation of notification manager that passes through to + * Android's notification manager + * + * @author Tim Su + * + */ + public static class AndroidNotificationManager implements NotificationManager { + private final android.app.NotificationManager nm; + public AndroidNotificationManager(Context context) { + nm = (android.app.NotificationManager) + context.getSystemService(Context.NOTIFICATION_SERVICE); + } + + public void cancel(int id) { + nm.cancel(id); + } + + public void cancelAll() { + nm.cancelAll(); + } + + public void notify(int id, Notification notification) { + nm.notify(id, notification); + } + } +} diff --git a/api/src/com/todoroo/andlib/service/RestClient.java b/api/src/com/todoroo/andlib/service/RestClient.java new file mode 100644 index 0000000000..0bf35e4619 --- /dev/null +++ b/api/src/com/todoroo/andlib/service/RestClient.java @@ -0,0 +1,14 @@ +package com.todoroo.andlib.service; + +import java.io.IOException; + +/** + * RestClient stub invokes the HTML requests as desired + * + * @author Tim Su + * + */ +public interface RestClient { + public String get(String url) throws IOException; + public String post(String url, String data) throws IOException; +} \ No newline at end of file diff --git a/api/src/com/todoroo/andlib/service/package-info.java b/api/src/com/todoroo/andlib/service/package-info.java new file mode 100644 index 0000000000..eab2637324 --- /dev/null +++ b/api/src/com/todoroo/andlib/service/package-info.java @@ -0,0 +1,5 @@ +/** + * Todoroo Library classes for the service layer: dependency injection, + * exceptions, HTTP requests, etc. + */ +package com.todoroo.andlib.service; \ No newline at end of file diff --git a/api/src/com/todoroo/andlib/sql/Criterion.java b/api/src/com/todoroo/andlib/sql/Criterion.java new file mode 100644 index 0000000000..5acdc65356 --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/Criterion.java @@ -0,0 +1,89 @@ +package com.todoroo.andlib.sql; + +import static com.todoroo.andlib.sql.SqlConstants.AND; +import static com.todoroo.andlib.sql.SqlConstants.EXISTS; +import static com.todoroo.andlib.sql.SqlConstants.LEFT_PARENTHESIS; +import static com.todoroo.andlib.sql.SqlConstants.NOT; +import static com.todoroo.andlib.sql.SqlConstants.OR; +import static com.todoroo.andlib.sql.SqlConstants.RIGHT_PARENTHESIS; +import static com.todoroo.andlib.sql.SqlConstants.SPACE; + +public abstract class Criterion { + protected final Operator operator; + + Criterion(Operator operator) { + this.operator = operator; + } + + public static Criterion all = new Criterion(Operator.exists) { + @Override + protected void populate(StringBuilder sb) { + sb.append(1); + } + }; + + public static Criterion none = new Criterion(Operator.exists) { + @Override + protected void populate(StringBuilder sb) { + sb.append(0); + } + }; + + public static Criterion and(final Criterion criterion, final Criterion... criterions) { + return new Criterion(Operator.and) { + + @Override + protected void populate(StringBuilder sb) { + sb.append(criterion); + for (Criterion c : criterions) { + sb.append(SPACE).append(AND).append(SPACE).append(c); + } + } + }; + } + + public static Criterion or(final Criterion criterion, final Criterion... criterions) { + return new Criterion(Operator.or) { + + @Override + protected void populate(StringBuilder sb) { + sb.append(criterion); + for (Criterion c : criterions) { + sb.append(SPACE).append(OR).append(SPACE).append(c.toString()); + } + } + }; + } + + public static Criterion exists(final Query query) { + return new Criterion(Operator.exists) { + + @Override + protected void populate(StringBuilder sb) { + sb.append(EXISTS).append(SPACE).append(LEFT_PARENTHESIS).append(query).append(RIGHT_PARENTHESIS); + } + }; + } + + public static Criterion not(final Criterion criterion) { + return new Criterion(Operator.not) { + + @Override + protected void populate(StringBuilder sb) { + sb.append(NOT).append(SPACE); + criterion.populate(sb); + } + }; + } + + protected abstract void populate(StringBuilder sb); + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(LEFT_PARENTHESIS); + populate(builder); + builder.append(RIGHT_PARENTHESIS); + return builder.toString(); + } + +} diff --git a/api/src/com/todoroo/andlib/sql/DBObject.java b/api/src/com/todoroo/andlib/sql/DBObject.java new file mode 100644 index 0000000000..b29c6550b4 --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/DBObject.java @@ -0,0 +1,67 @@ +package com.todoroo.andlib.sql; + +import static com.todoroo.andlib.sql.SqlConstants.AS; +import static com.todoroo.andlib.sql.SqlConstants.SPACE; + +public abstract class DBObject> implements Cloneable { + protected String alias; + protected final String expression; + + protected DBObject(String expression){ + this.expression = expression; + } + + public T as(String newAlias) { + try { + T clone = (T) clone(); + clone.alias = newAlias; + return clone; + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } + + public boolean hasAlias() { + return alias != null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DBObject dbObject = (DBObject) o; + + if (alias != null ? !alias.equals(dbObject.alias) : dbObject.alias != null) return false; + if (expression != null ? !expression.equals(dbObject.expression) : dbObject.expression != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = alias != null ? alias.hashCode() : 0; + result = 31 * result + (expression != null ? expression.hashCode() : 0); + return result; + } + + @Override + public final String toString() { + if (hasAlias()) { + return alias; + } + return expression; + } + + public final String toStringInSelect() { + StringBuilder sb = new StringBuilder(expression); + if (hasAlias()) { + sb.append(SPACE).append(AS).append(SPACE).append(alias); + } else { + int pos = expression.indexOf('.'); + if(pos > 0) + sb.append(SPACE).append(AS).append(SPACE).append(expression.substring(pos + 1)); + } + return sb.toString(); + } +} diff --git a/api/src/com/todoroo/andlib/sql/EqCriterion.java b/api/src/com/todoroo/andlib/sql/EqCriterion.java new file mode 100644 index 0000000000..8873069cb0 --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/EqCriterion.java @@ -0,0 +1,7 @@ +package com.todoroo.andlib.sql; + +public class EqCriterion extends UnaryCriterion { + EqCriterion(Field field, Object value) { + super(field, Operator.eq, value); + } +} diff --git a/api/src/com/todoroo/andlib/sql/Field.java b/api/src/com/todoroo/andlib/sql/Field.java new file mode 100644 index 0000000000..d0aaf2fe3c --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/Field.java @@ -0,0 +1,112 @@ +package com.todoroo.andlib.sql; + +import static com.todoroo.andlib.sql.SqlConstants.AND; +import static com.todoroo.andlib.sql.SqlConstants.BETWEEN; +import static com.todoroo.andlib.sql.SqlConstants.COMMA; +import static com.todoroo.andlib.sql.SqlConstants.LEFT_PARENTHESIS; +import static com.todoroo.andlib.sql.SqlConstants.RIGHT_PARENTHESIS; +import static com.todoroo.andlib.sql.SqlConstants.SPACE; + +public class Field extends DBObject { + + protected Field(String expression) { + super(expression); + } + + public static Field field(String expression) { + return new Field(expression); + } + + public Criterion eq(Object value) { + if(value == null) + return UnaryCriterion.isNull(this); + return UnaryCriterion.eq(this, value); + } + + /** + * Adds the criterion that the field must be equal to the given string, ignoring case. + * + * Thanks to a sqlite bug, this will only work for ASCII values. + * + * @param value string which field must equal + * @return the criterion + */ + @SuppressWarnings("nls") + public Criterion eqCaseInsensitive(String value) { + String escaped = value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_"); + return UnaryCriterion.like(this, escaped, "\\"); + } + + public Criterion neq(Object value) { + if(value == null) + return UnaryCriterion.isNotNull(this); + return UnaryCriterion.neq(this, value); + } + + public Criterion gt(Object value) { + return UnaryCriterion.gt(this, value); + } + + public Criterion lt(final Object value) { + return UnaryCriterion.lt(this, value); + } + + public Criterion lte(final Object value) { + return UnaryCriterion.lte(this, value); + } + + public Criterion isNull() { + return UnaryCriterion.isNull(this); + } + + public Criterion isNotNull() { + return UnaryCriterion.isNotNull(this); + } + + public Criterion between(final Object lower, final Object upper) { + final Field field = this; + return new Criterion(null) { + + @Override + protected void populate(StringBuilder sb) { + sb.append(field).append(SPACE).append(BETWEEN).append(SPACE).append(lower).append(SPACE).append(AND) + .append(SPACE).append(upper); + } + }; + } + + public Criterion like(final String value) { + return UnaryCriterion.like(this, value); + } + + public Criterion like(String value, String escape) { + return UnaryCriterion.like(this, value, escape); + } + + public Criterion in(final T... value) { + final Field field = this; + return new Criterion(Operator.in) { + + @Override + protected void populate(StringBuilder sb) { + sb.append(field).append(SPACE).append(Operator.in).append(SPACE).append(LEFT_PARENTHESIS).append(SPACE); + for (T t : value) { + sb.append(t.toString()).append(COMMA); + } + sb.deleteCharAt(sb.length() - 1).append(RIGHT_PARENTHESIS); + } + }; + } + + public Criterion in(final Query query) { + final Field field = this; + return new Criterion(Operator.in) { + + @Override + protected void populate(StringBuilder sb) { + sb.append(field).append(SPACE).append(Operator.in).append(SPACE).append(LEFT_PARENTHESIS).append(query) + .append(RIGHT_PARENTHESIS); + } + }; + } +} diff --git a/api/src/com/todoroo/andlib/sql/Functions.java b/api/src/com/todoroo/andlib/sql/Functions.java new file mode 100644 index 0000000000..803e9426f9 --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/Functions.java @@ -0,0 +1,39 @@ +package com.todoroo.andlib.sql; + + + + +@SuppressWarnings("nls") +public final class Functions { + + public static String caseStatement(Criterion when, Object ifTrue, Object ifFalse) { + return new StringBuilder("(CASE WHEN "). + append(when.toString()).append(" THEN ").append(value(ifTrue)). + append(" ELSE ").append(value(ifFalse)).append(" END)").toString(); + } + + private static String value(Object value) { + return value.toString(); + } + + public static Field upper(Field title) { + return new Field("UPPER(" + title.toString() + ")"); + } + + /** + * @return SQL now (in milliseconds) + */ + public static Field now() { + return new Field("(strftime('%s','now')*1000)"); + } + + public static Field cast(Field field, String newType) { + return new Field("CAST(" + field.toString() + " AS " + + newType + ")"); + } + + public static Field max(Field field) { + return new Field("MAX(" + field.toString() + ")"); + } + +} diff --git a/api/src/com/todoroo/andlib/sql/GroupBy.java b/api/src/com/todoroo/andlib/sql/GroupBy.java new file mode 100644 index 0000000000..df8c19df91 --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/GroupBy.java @@ -0,0 +1,14 @@ +package com.todoroo.andlib.sql; + +import java.util.ArrayList; +import java.util.List; + +public class GroupBy { + private List fields = new ArrayList(); + + public static GroupBy groupBy(Field field) { + GroupBy groupBy = new GroupBy(); + groupBy.fields.add(field); + return groupBy; + } +} diff --git a/api/src/com/todoroo/andlib/sql/Join.java b/api/src/com/todoroo/andlib/sql/Join.java new file mode 100644 index 0000000000..ac8a479f2f --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/Join.java @@ -0,0 +1,43 @@ +package com.todoroo.andlib.sql; + +import static com.todoroo.andlib.sql.SqlConstants.JOIN; +import static com.todoroo.andlib.sql.SqlConstants.ON; +import static com.todoroo.andlib.sql.SqlConstants.SPACE; + +public class Join { + private final SqlTable joinTable; + private final JoinType joinType; + private final Criterion[] criterions; + + private Join(SqlTable table, JoinType joinType, Criterion... criterions) { + joinTable = table; + this.joinType = joinType; + this.criterions = criterions; + } + + public static Join inner(SqlTable expression, Criterion... criterions) { + return new Join(expression, JoinType.INNER, criterions); + } + + public static Join left(SqlTable table, Criterion... criterions) { + return new Join(table, JoinType.LEFT, criterions); + } + + public static Join right(SqlTable table, Criterion... criterions) { + return new Join(table, JoinType.RIGHT, criterions); + } + + public static Join out(SqlTable table, Criterion... criterions) { + return new Join(table, JoinType.OUT, criterions); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(joinType).append(SPACE).append(JOIN).append(SPACE).append(joinTable).append(SPACE).append(ON); + for (Criterion criterion : criterions) { + sb.append(SPACE).append(criterion); + } + return sb.toString(); + } +} diff --git a/api/src/com/todoroo/andlib/sql/JoinType.java b/api/src/com/todoroo/andlib/sql/JoinType.java new file mode 100644 index 0000000000..84e632b533 --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/JoinType.java @@ -0,0 +1,5 @@ +package com.todoroo.andlib.sql; + +public enum JoinType { + INNER, LEFT, RIGHT, OUT +} diff --git a/api/src/com/todoroo/andlib/sql/Operator.java b/api/src/com/todoroo/andlib/sql/Operator.java new file mode 100644 index 0000000000..5581987d43 --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/Operator.java @@ -0,0 +1,57 @@ +package com.todoroo.andlib.sql; + +import static com.todoroo.andlib.sql.SqlConstants.SPACE; + +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("nls") +public final class Operator { + + private final String operator; + public static final Operator eq = new Operator("="); + public static final Operator neq = new Operator("<>"); + public static final Operator isNull = new Operator("IS NULL"); + public static final Operator isNotNull = new Operator("IS NOT NULL"); + public static final Operator gt = new Operator(">"); + public static final Operator lt = new Operator("<"); + public static final Operator gte = new Operator(">="); + public static final Operator lte = new Operator("<="); + public static final Operator and = new Operator("AND"); + public static final Operator or = new Operator("OR"); + public static final Operator not = new Operator("NOT"); + public static final Operator exists = new Operator("EXISTS"); + public static final Operator like = new Operator("LIKE"); + public static final Operator in = new Operator("IN"); + + private static final Map contraryRegistry = new HashMap(); + + static { + contraryRegistry.put(eq, neq); + contraryRegistry.put(neq, eq); + contraryRegistry.put(isNull, isNotNull); + contraryRegistry.put(isNotNull, isNull); + contraryRegistry.put(gt, lte); + contraryRegistry.put(lte, gt); + contraryRegistry.put(lt, gte); + contraryRegistry.put(gte, lt); + } + + private Operator(String operator) { + this.operator = operator; + } + + public Operator getContrary() { + if(!contraryRegistry.containsKey(this)){ + Operator opposite = new Operator(not.toString() + SPACE + this.toString()); + contraryRegistry.put(this, opposite); + contraryRegistry.put(opposite, this); + } + return contraryRegistry.get(this); + } + + @Override + public String toString() { + return this.operator.toString(); + } +} diff --git a/api/src/com/todoroo/andlib/sql/Order.java b/api/src/com/todoroo/andlib/sql/Order.java new file mode 100644 index 0000000000..0cb91a670f --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/Order.java @@ -0,0 +1,37 @@ +package com.todoroo.andlib.sql; + +import static com.todoroo.andlib.sql.SqlConstants.SPACE; + +public class Order { + private final Object expression; + private final OrderType orderType; + + private Order(Object expression) { + this(expression, OrderType.ASC); + } + + private Order(Object expression, OrderType orderType) { + this.expression = expression; + this.orderType = orderType; + } + + public static Order asc(Object expression) { + return new Order(expression); + } + + public static Order desc(Object expression) { + return new Order(expression, OrderType.DESC); + } + + @Override + public String toString() { + return expression + SPACE + orderType; + } + + public Order reverse() { + if(orderType == OrderType.ASC) + return new Order(expression, OrderType.DESC); + else + return new Order(expression, OrderType.ASC); + } +} diff --git a/api/src/com/todoroo/andlib/sql/OrderType.java b/api/src/com/todoroo/andlib/sql/OrderType.java new file mode 100644 index 0000000000..d84742e28a --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/OrderType.java @@ -0,0 +1,5 @@ +package com.todoroo.andlib.sql; + +public enum OrderType { + DESC, ASC +} diff --git a/api/src/com/todoroo/andlib/sql/Query.java b/api/src/com/todoroo/andlib/sql/Query.java new file mode 100644 index 0000000000..d87613b6de --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/Query.java @@ -0,0 +1,296 @@ +package com.todoroo.andlib.sql; + +import static com.todoroo.andlib.sql.SqlConstants.ALL; +import static com.todoroo.andlib.sql.SqlConstants.COMMA; +import static com.todoroo.andlib.sql.SqlConstants.DISTINCT; +import static com.todoroo.andlib.sql.SqlConstants.FROM; +import static com.todoroo.andlib.sql.SqlConstants.GROUP_BY; +import static com.todoroo.andlib.sql.SqlConstants.LEFT_PARENTHESIS; +import static com.todoroo.andlib.sql.SqlConstants.LIMIT; +import static com.todoroo.andlib.sql.SqlConstants.ORDER_BY; +import static com.todoroo.andlib.sql.SqlConstants.RIGHT_PARENTHESIS; +import static com.todoroo.andlib.sql.SqlConstants.SELECT; +import static com.todoroo.andlib.sql.SqlConstants.SPACE; +import static com.todoroo.andlib.sql.SqlConstants.WHERE; +import static com.todoroo.andlib.sql.SqlTable.table; +import static java.util.Arrays.asList; + +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; + +import com.todoroo.andlib.data.Property; +import com.todoroo.astrid.api.AstridApiConstants; + +public final class Query { + + private SqlTable table; + private String queryTemplate = null; + private final ArrayList criterions = new ArrayList(); + private final ArrayList fields = new ArrayList(); + private final ArrayList joins = new ArrayList(); + private final ArrayList groupBies = new ArrayList(); + private final ArrayList orders = new ArrayList(); + private final ArrayList havings = new ArrayList(); + private int limits = -1; + private boolean distinct = false; + + private Query(Field... fields) { + this.fields.addAll(asList(fields)); + } + + public static Query select(Field... fields) { + return new Query(fields); + } + + public static Query selectDistinct(Field... fields) { + Query query = new Query(fields); + query.distinct = true; + return query; + } + + public Query from(SqlTable fromTable) { + this.table = fromTable; + return this; + } + + public Query join(Join... join) { + joins.addAll(asList(join)); + return this; + } + + public Query where(Criterion criterion) { + criterions.add(criterion); + return this; + } + + public Query groupBy(Field... groupBy) { + groupBies.addAll(asList(groupBy)); + return this; + } + + public Query orderBy(Order... order) { + orders.addAll(asList(order)); + return this; + } + + public Query limit(int limit) { + limits = limit; + return this; + } + + public Query appendSelectFields(Property... selectFields) { + this.fields.addAll(asList(selectFields)); + return this; + } + + @Override + public boolean equals(Object o) { + return this == o || !(o == null || getClass() != o.getClass()) && this.toString().equals(o.toString()); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + visitSelectClause(sql); + visitFromClause(sql); + + visitJoinClause(sql); + if(queryTemplate == null) { + visitWhereClause(sql); + visitGroupByClause(sql); + visitOrderByClause(sql); + visitLimitClause(sql); + } else { + if(groupBies.size() > 0 || orders.size() > 0 || + havings.size() > 0) + throw new IllegalStateException("Can't have extras AND query template"); //$NON-NLS-1$ + sql.append(queryTemplate); + } + + return sql.toString(); + } + + private void visitOrderByClause(StringBuilder sql) { + if (orders.isEmpty()) { + return; + } + sql.append(ORDER_BY); + for (Order order : orders) { + sql.append(SPACE).append(order).append(COMMA); + } + sql.deleteCharAt(sql.length() - 1).append(SPACE); + } + + @SuppressWarnings("nls") + private void visitGroupByClause(StringBuilder sql) { + if (groupBies.isEmpty()) { + return; + } + sql.append(GROUP_BY); + for (Field groupBy : groupBies) { + sql.append(SPACE).append(groupBy).append(COMMA); + } + sql.deleteCharAt(sql.length() - 1).append(SPACE); + if (havings.isEmpty()) { + return; + } + sql.append("HAVING"); + for (Criterion havingCriterion : havings) { + sql.append(SPACE).append(havingCriterion).append(COMMA); + } + sql.deleteCharAt(sql.length() - 1).append(SPACE); + } + + private void visitWhereClause(StringBuilder sql) { + if (criterions.isEmpty()) { + return; + } + sql.append(WHERE); + for (Criterion criterion : criterions) { + sql.append(SPACE).append(criterion).append(SPACE); + } + } + + private void visitJoinClause(StringBuilder sql) { + for (Join join : joins) { + sql.append(join).append(SPACE); + } + } + + private void visitFromClause(StringBuilder sql) { + if (table == null) { + return; + } + sql.append(FROM).append(SPACE).append(table).append(SPACE); + } + + private void visitSelectClause(StringBuilder sql) { + sql.append(SELECT).append(SPACE); + if(distinct) + sql.append(DISTINCT).append(SPACE); + if (fields.isEmpty()) { + sql.append(ALL).append(SPACE); + return; + } + for (Field field : fields) { + sql.append(field.toStringInSelect()).append(COMMA); + } + sql.deleteCharAt(sql.length() - 1).append(SPACE); + } + + private void visitLimitClause(StringBuilder sql) { + if(limits > -1) + sql.append(LIMIT).append(SPACE).append(limits).append(SPACE); + } + + public SqlTable as(String alias) { + return table(LEFT_PARENTHESIS + this.toString() + RIGHT_PARENTHESIS).as(alias); + } + + public Query having(Criterion criterion) { + this.havings.add(criterion); + return this; + } + + /** + * Gets a list of fields returned by this query + * @return + */ + public Property[] getFields() { + return fields.toArray(new Property[fields.size()]); + } + + /** + * Add the SQL query template (comes after the "from") + * @param template + * @return query + */ + public Query withQueryTemplate(String template) { + queryTemplate = template; + return this; + } + + /** + * Parse out properties and run query + * @param cr + * @param baseUri + * @return + */ + public Cursor queryContentResolver(ContentResolver cr, Uri baseUri) { + Uri uri = baseUri; + + if(joins.size() != 0) + throw new UnsupportedOperationException("can't perform join in content resolver query"); //$NON-NLS-1$ + + String[] projection = new String[fields.size()]; + for(int i = 0; i < projection.length; i++) + projection[i] = fields.get(i).toString(); + + StringBuilder groupByClause = new StringBuilder(); + StringBuilder selectionClause = new StringBuilder(); + StringBuilder orderClause = new StringBuilder(); + if(queryTemplate != null) { + QueryTemplateHelper.queryForContentResolver(queryTemplate, + selectionClause, orderClause, groupByClause); + } else { + if(groupBies.size() > 0) { + for (Field groupBy : groupBies) + groupByClause.append(SPACE).append(groupBy).append(COMMA); + if(groupByClause.length() > 0) + groupByClause.deleteCharAt(groupByClause.length() - 1); + } + + for (Criterion criterion : criterions) + selectionClause.append(criterion).append(SPACE); + + for (Order order : orders) + orderClause.append(SPACE).append(order).append(COMMA); + if(orderClause.length() > 0) + orderClause.deleteCharAt(orderClause.length() - 1); + } + + if(groupByClause.length() > 0) + uri = Uri.withAppendedPath(baseUri, AstridApiConstants.GROUP_BY_URI + + groupByClause.toString().trim()); + return cr.query(uri, projection, selectionClause.toString(), null, + orderClause.toString()); + } + + /** query template helper */ + public static class QueryTemplateHelper { + + /** build a content resolver query */ + @SuppressWarnings("nls") + public static void queryForContentResolver(String queryTemplate, + StringBuilder selectionClause, StringBuilder orderClause, + StringBuilder groupByClause) { + + Pattern where = Pattern.compile("WHERE (.*?)(LIMIT|HAVING|GROUP|ORDER|\\Z)"); + Matcher whereMatcher = where.matcher(queryTemplate); + if(whereMatcher.find()) + selectionClause.append(whereMatcher.group(1).trim()); + + Pattern group = Pattern.compile("GROUP BY (.*?)(LIMIT|HAVING|ORDER|\\Z)"); + Matcher groupMatcher = group.matcher(queryTemplate); + if(groupMatcher.find()) + groupByClause.append(groupMatcher.group(1).trim()); + + Pattern order = Pattern.compile("ORDER BY (.*?)(LIMIT|HAVING|\\Z)"); + Matcher orderMatcher = order.matcher(queryTemplate); + if(orderMatcher.find()) + orderClause.append(orderMatcher.group(1).trim()); + } + + } + +} diff --git a/api/src/com/todoroo/andlib/sql/QueryTemplate.java b/api/src/com/todoroo/andlib/sql/QueryTemplate.java new file mode 100644 index 0000000000..abd506f183 --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/QueryTemplate.java @@ -0,0 +1,117 @@ +package com.todoroo.andlib.sql; + +import static com.todoroo.andlib.sql.SqlConstants.COMMA; +import static com.todoroo.andlib.sql.SqlConstants.GROUP_BY; +import static com.todoroo.andlib.sql.SqlConstants.LIMIT; +import static com.todoroo.andlib.sql.SqlConstants.ORDER_BY; +import static com.todoroo.andlib.sql.SqlConstants.SPACE; +import static com.todoroo.andlib.sql.SqlConstants.WHERE; +import static java.util.Arrays.asList; + +import java.util.ArrayList; + +/** + * Query Template returns a bunch of criteria that allows a query to be + * constructed + * + * @author Tim Su + * + */ +public final class QueryTemplate { + + private final ArrayList criterions = new ArrayList(); + private final ArrayList joins = new ArrayList(); + private final ArrayList groupBies = new ArrayList(); + private final ArrayList orders = new ArrayList(); + private final ArrayList havings = new ArrayList(); + private Integer limit = null; + + public QueryTemplate join(Join... join) { + joins.addAll(asList(join)); + return this; + } + + public QueryTemplate where(Criterion criterion) { + criterions.add(criterion); + return this; + } + + public QueryTemplate groupBy(Field... groupBy) { + groupBies.addAll(asList(groupBy)); + return this; + } + + public QueryTemplate orderBy(Order... order) { + orders.addAll(asList(order)); + return this; + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + visitJoinClause(sql); + visitWhereClause(sql); + visitGroupByClause(sql); + visitOrderByClause(sql); + if(limit != null) + sql.append(LIMIT).append(SPACE).append(limit); + return sql.toString(); + } + + private void visitOrderByClause(StringBuilder sql) { + if (orders.isEmpty()) { + return; + } + sql.append(ORDER_BY); + for (Order order : orders) { + sql.append(SPACE).append(order).append(COMMA); + } + sql.deleteCharAt(sql.length() - 1).append(SPACE); + } + + @SuppressWarnings("nls") + private void visitGroupByClause(StringBuilder sql) { + if (groupBies.isEmpty()) { + return; + } + sql.append(GROUP_BY); + for (Field groupBy : groupBies) { + sql.append(SPACE).append(groupBy).append(COMMA); + } + sql.deleteCharAt(sql.length() - 1).append(SPACE); + if (havings.isEmpty()) { + return; + } + sql.append("HAVING"); + for (Criterion havingCriterion : havings) { + sql.append(SPACE).append(havingCriterion).append(COMMA); + } + sql.deleteCharAt(sql.length() - 1).append(SPACE); + } + + private void visitWhereClause(StringBuilder sql) { + if (criterions.isEmpty()) { + return; + } + sql.append(WHERE); + for (Criterion criterion : criterions) { + sql.append(SPACE).append(criterion).append(SPACE); + } + } + + private void visitJoinClause(StringBuilder sql) { + for (Join join : joins) { + sql.append(join).append(SPACE); + } + } + + public QueryTemplate having(Criterion criterion) { + this.havings.add(criterion); + return this; + } + + public QueryTemplate limit(int limitValue) { + this.limit = limitValue; + return this; + } +} diff --git a/api/src/com/todoroo/andlib/sql/SqlConstants.java b/api/src/com/todoroo/andlib/sql/SqlConstants.java new file mode 100644 index 0000000000..fa9d647fc7 --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/SqlConstants.java @@ -0,0 +1,26 @@ +package com.todoroo.andlib.sql; + +@SuppressWarnings("nls") +public final class SqlConstants { + static final String SELECT = "SELECT"; + static final String DISTINCT = "DISTINCT"; + static final String SPACE = " "; + static final String AS = "AS"; + static final String COMMA = ","; + static final String FROM = "FROM"; + static final String ON = "ON"; + static final String JOIN = "JOIN"; + static final String ALL = "*"; + static final String LEFT_PARENTHESIS = "("; + static final String RIGHT_PARENTHESIS = ")"; + static final String AND = "AND"; + static final String BETWEEN = "BETWEEN"; + static final String LIKE = "LIKE"; + static final String OR = "OR"; + static final String ORDER_BY = "ORDER BY"; + static final String GROUP_BY = "GROUP BY"; + static final String WHERE = "WHERE"; + public static final String EXISTS = "EXISTS"; + public static final String NOT = "NOT"; + public static final String LIMIT = "LIMIT"; +} diff --git a/api/src/com/todoroo/andlib/sql/SqlTable.java b/api/src/com/todoroo/andlib/sql/SqlTable.java new file mode 100644 index 0000000000..4205ce6171 --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/SqlTable.java @@ -0,0 +1,20 @@ +package com.todoroo.andlib.sql; + +public class SqlTable extends DBObject { + + protected SqlTable(String expression) { + super(expression); + } + + public static SqlTable table(String table) { + return new SqlTable(table); + } + + @SuppressWarnings("nls") + protected String fieldExpression(String fieldName) { + if (hasAlias()) { + return alias + "." + fieldName; + } + return expression+"."+fieldName; + } +} diff --git a/api/src/com/todoroo/andlib/sql/UnaryCriterion.java b/api/src/com/todoroo/andlib/sql/UnaryCriterion.java new file mode 100644 index 0000000000..0dcff2bd57 --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/UnaryCriterion.java @@ -0,0 +1,111 @@ +package com.todoroo.andlib.sql; + +import static com.todoroo.andlib.sql.SqlConstants.SPACE; + +public class UnaryCriterion extends Criterion { + protected final Field expression; + protected final Object value; + + UnaryCriterion(Field expression, Operator operator, Object value) { + super(operator); + this.expression = expression; + this.value = value; + } + + @Override + protected void populate(StringBuilder sb) { + beforePopulateOperator(sb); + populateOperator(sb); + afterPopulateOperator(sb); + } + + public static Criterion eq(Field expression, Object value) { + return new UnaryCriterion(expression, Operator.eq, value); + } + + protected void beforePopulateOperator(StringBuilder sb) { + sb.append(expression); + } + + protected void populateOperator(StringBuilder sb) { + sb.append(operator); + } + + @SuppressWarnings("nls") + protected void afterPopulateOperator(StringBuilder sb) { + if(value == null) + return; + else if(value instanceof String) + sb.append("'").append(sanitize((String) value)).append("'"); + else + sb.append(value); + } + + /** + * Sanitize the given input for SQL + * @param input + * @return + */ + @SuppressWarnings("nls") + public static String sanitize(String input) { + return input.replace("'", "''"); + } + + public static Criterion neq(Field field, Object value) { + return new UnaryCriterion(field, Operator.neq, value); + } + + public static Criterion gt(Field field, Object value) { + return new UnaryCriterion(field, Operator.gt, value); + } + + public static Criterion lt(Field field, Object value) { + return new UnaryCriterion(field, Operator.lt, value); + } + + public static Criterion lte(Field field, Object value) { + return new UnaryCriterion(field, Operator.lte, value); + } + + public static Criterion isNull(Field field) { + return new UnaryCriterion(field, Operator.isNull, null) { + @Override + protected void populateOperator(StringBuilder sb) { + sb.append(SPACE).append(operator); + } + }; + } + + public static Criterion isNotNull(Field field) { + return new UnaryCriterion(field, Operator.isNotNull, null) { + @Override + protected void populateOperator(StringBuilder sb) { + sb.append(SPACE).append(operator); + } + }; + } + + public static Criterion like(Field field, String value) { + return new UnaryCriterion(field, Operator.like, value) { + @Override + protected void populateOperator(StringBuilder sb) { + sb.append(SPACE).append(operator).append(SPACE); + } + }; + } + + public static Criterion like(Field field, String value, final String escape) { + return new UnaryCriterion(field, Operator.like, value) { + @Override + protected void populateOperator(StringBuilder sb) { + sb.append(SPACE).append(operator).append(SPACE); + } + @SuppressWarnings("nls") + @Override + protected void afterPopulateOperator(StringBuilder sb) { + super.afterPopulateOperator(sb); + sb.append(SPACE).append("ESCAPE").append(" '").append(sanitize(escape)).append("'"); + } + }; + } +} diff --git a/api/src/com/todoroo/andlib/sql/package-info.java b/api/src/com/todoroo/andlib/sql/package-info.java new file mode 100644 index 0000000000..e8d170b15d --- /dev/null +++ b/api/src/com/todoroo/andlib/sql/package-info.java @@ -0,0 +1,4 @@ +/** + * Fork of sql-dsl + */ +package com.todoroo.andlib.sql; \ No newline at end of file diff --git a/api/src/com/todoroo/andlib/utility/AndroidUtilities.java b/api/src/com/todoroo/andlib/utility/AndroidUtilities.java new file mode 100644 index 0000000000..e0a6df633f --- /dev/null +++ b/api/src/com/todoroo/andlib/utility/AndroidUtilities.java @@ -0,0 +1,547 @@ +package com.todoroo.andlib.utility; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.math.BigInteger; +import java.net.URL; +import java.net.URLConnection; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Map; +import java.util.Map.Entry; + +import android.app.Activity; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.NetworkInfo.State; +import android.text.InputType; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnTouchListener; +import android.widget.TextView; + +import com.todoroo.andlib.data.Property; +import com.todoroo.andlib.service.ExceptionService; + +/** + * Android Utility Classes + * + * @author Tim Su + * + */ +public class AndroidUtilities { + + public static final String SEPARATOR_ESCAPE = "!PIPE!"; //$NON-NLS-1$ + public static final String SERIALIZATION_SEPARATOR = "|"; //$NON-NLS-1$ + + // --- utility methods + + /** Suppress virtual keyboard until user's first tap */ + public static void suppressVirtualKeyboard(final TextView editor) { + final int inputType = editor.getInputType(); + editor.setInputType(InputType.TYPE_NULL); + editor.setOnTouchListener(new OnTouchListener() { + public boolean onTouch(View v, MotionEvent event) { + editor.setInputType(inputType); + editor.setOnTouchListener(null); + return false; + } + }); + } + + /** + * @return true if we're connected to the internet + */ + public static boolean isConnected(Context context) { + ConnectivityManager manager = (ConnectivityManager) + context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo info = manager.getActiveNetworkInfo(); + if (info == null) + return false; + if (info.getState() != State.CONNECTED) + return false; + return true; + } + + /** Fetch the image specified by the given url */ + public static Bitmap fetchImage(URL url) throws IOException { + InputStream is = null; + try { + URLConnection conn = url.openConnection(); + conn.connect(); + is = conn.getInputStream(); + BufferedInputStream bis = new BufferedInputStream(is, 16384); + try { + Bitmap bitmap = BitmapFactory.decodeStream(bis); + return bitmap; + } finally { + bis.close(); + } + } finally { + if(is != null) + is.close(); + } + } + + /** + * Start the given intent, handling security exceptions if they arise + * + * @param context + * @param intent + * @param request request code. if negative, no request. + */ + public static void startExternalIntent(Context context, Intent intent, int request) { + try { + if(request > -1 && context instanceof Activity) + ((Activity)context).startActivityForResult(intent, request); + else + context.startActivity(intent); + } catch (Exception e) { + getExceptionService().displayAndReportError(context, + "start-external-intent-" + intent.toString(), //$NON-NLS-1$ + e); + } + } + + /** + * Start the given intent, handling security exceptions if they arise + * + * @param activity + * @param intent + * @param requestCode + */ + public static void startExternalIntentForResult( + Activity activity, Intent intent, int requestCode) { + try { + activity.startActivityForResult(intent, requestCode); + } catch (SecurityException e) { + getExceptionService().displayAndReportError(activity, + "start-external-intent-" + intent.toString(), //$NON-NLS-1$ + e); + } + } + + /** + * Put an arbitrary object into a {@link ContentValues} + * @param target + * @param key + * @param value + */ + public static void putInto(ContentValues target, String key, Object value) { + if(value instanceof String) + target.put(key, (String) value); + else if(value instanceof Long) + target.put(key, (Long) value); + else if(value instanceof Integer) + target.put(key, (Integer) value); + else if(value instanceof Double) + target.put(key, (Double) value); + else + throw new UnsupportedOperationException("Could not handle type " + //$NON-NLS-1$ + value.getClass()); + } + + /** + * Rips apart a content value into two string arrays, keys and value + */ + public static String[][] contentValuesToStringArrays(ContentValues source) { + String[][] result = new String[2][source.size()]; + int i = 0; + for(Entry entry : source.valueSet()) { + result[0][i] = entry.getKey(); + result[1][i++] = entry.getValue().toString(); + } + return result; + } + + /** + * Return index of value in array + * @param array array to search + * @param value value to look for + * @return + */ + public static int indexOf(TYPE[] array, TYPE value) { + for(int i = 0; i < array.length; i++) + if(array[i].equals(value)) + return i; + return -1; + } + + /** + * Serializes a content value into a string + */ + public static String contentValuesToSerializedString(ContentValues source) { + StringBuilder result = new StringBuilder(); + for(Entry entry : source.valueSet()) { + result.append(entry.getKey().replace(SERIALIZATION_SEPARATOR, SEPARATOR_ESCAPE)).append( + SERIALIZATION_SEPARATOR); + Object value = entry.getValue(); + if(value instanceof Integer) + result.append('i').append(value); + else if(value instanceof Double) + result.append('d').append(value); + else if(value instanceof Long) + result.append('l').append(value); + else if(value instanceof String) + result.append('s').append(value.toString()); + else + throw new UnsupportedOperationException(value.getClass().toString()); + result.append(SERIALIZATION_SEPARATOR); + } + return result.toString(); + } + + /** + * Turn ContentValues into a string + * @param string + * @return + */ + public static ContentValues contentValuesFromSerializedString(String string) { + if(string == null) + return new ContentValues(); + + String[] pairs = string.split("\\" + SERIALIZATION_SEPARATOR); //$NON-NLS-1$ + ContentValues result = new ContentValues(); + for(int i = 0; i < pairs.length; i += 2) { + String key = pairs[i].replaceAll(SEPARATOR_ESCAPE, SERIALIZATION_SEPARATOR); + String value = pairs[i+1].substring(1); + try { + switch(pairs[i+1].charAt(0)) { + case 'i': + result.put(key, Integer.parseInt(value)); + break; + case 'd': + result.put(key, Double.parseDouble(value)); + break; + case 'l': + result.put(key, Long.parseLong(value)); + break; + case 's': + result.put(key, value.replace(SEPARATOR_ESCAPE, SERIALIZATION_SEPARATOR)); + break; + } + } catch (NumberFormatException e) { + // failed parse to number, try to put a string + result.put(key, value); + } + } + return result; + } + + /** + * Turn ContentValues into a string + * @param string + * @return + */ + @SuppressWarnings("nls") + public static ContentValues contentValuesFromString(String string) { + if(string == null) + return null; + + String[] pairs = string.split("="); + ContentValues result = new ContentValues(); + String key = null; + for(int i = 0; i < pairs.length; i++) { + String newKey = null; + int lastSpace = pairs[i].lastIndexOf(' '); + if(lastSpace != -1) { + newKey = pairs[i].substring(lastSpace + 1); + pairs[i] = pairs[i].substring(0, lastSpace); + } else { + newKey = pairs[i]; + } + if(key != null) + result.put(key.trim(), pairs[i].trim()); + key = newKey; + } + return result; + } + + /** + * Returns true if a and b or null or a.equals(b) + * @param a + * @param b + * @return + */ + public static boolean equals(Object a, Object b) { + if(a == null && b == null) + return true; + if(a == null) + return false; + return a.equals(b); + } + + /** + * Copy a file from one place to another + * + * @param in + * @param out + * @throws Exception + */ + public static void copyFile(File in, File out) throws Exception { + FileInputStream fis = new FileInputStream(in); + FileOutputStream fos = new FileOutputStream(out); + try { + byte[] buf = new byte[1024]; + int i = 0; + while ((i = fis.read(buf)) != -1) { + fos.write(buf, 0, i); + } + } catch (Exception e) { + throw e; + } finally { + fis.close(); + fos.close(); + } + } + + /** + * Find a child view of a certain type + * @param view + * @param type + * @return first view (by DFS) if found, or null if none + */ + public static TYPE findViewByType(View view, Class type) { + if(view == null) + return null; + if(type.isInstance(view)) + return (TYPE) view; + if(view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + for(int i = 0; i < group.getChildCount(); i++) { + TYPE v = findViewByType(group.getChildAt(i), type); + if(v != null) + return v; + } + } + return null; + } + + /** + * @return Android SDK version as an integer. Works on all versions + */ + public static int getSdkVersion() { + return Integer.parseInt(android.os.Build.VERSION.SDK); + } + + /** + * Copy databases to a given folder. Useful for debugging + * @param folder + */ + public static void copyDatabases(Context context, String folder) { + File folderFile = new File(folder); + if(!folderFile.exists()) + folderFile.mkdir(); + for(String db : context.databaseList()) { + File dbFile = context.getDatabasePath(db); + try { + copyFile(dbFile, new File(folderFile.getAbsolutePath() + + File.separator + db)); + } catch (Exception e) { + Log.e("ERROR", "ERROR COPYING DB " + db, e); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + } + + /** + * Sort files by date so the newest file is on top + * @param files + */ + public static void sortFilesByDateDesc(File[] files) { + Arrays.sort(files, new Comparator() { + public int compare(File o1, File o2) { + return Long.valueOf(o2.lastModified()).compareTo(Long.valueOf(o1.lastModified())); + } + }); + } + + /** + * Search for the given value in the map, returning key if found + * @param map + * @param value + * @return null if not found, otherwise key + */ + public static KEY findKeyInMap(Map map, VALUE value){ + for (Entry entry: map.entrySet()) { + if(entry.getValue().equals(value)) + return entry.getKey(); + } + return null; + } + + /** + * Sleep, ignoring interruption. Before using this method, think carefully + * about why you are ignoring interruptions. + * + * @param l + */ + public static void sleepDeep(long l) { + try { + Thread.sleep(l); + } catch (InterruptedException e) { + // ignore + } + } + + /** + * Call a method via reflection if API level is at least minSdk + * @param minSdk minimum sdk number (i.e. 8) + * @param receiver object to call method on + * @param methodName method name to call + * @param params method parameter types + * @param args arguments + * @return method return value, or null if nothing was called or exception + */ + @SuppressWarnings("nls") + public static Object callApiMethod(int minSdk, Object receiver, + String methodName, Class[] params, Object... args) { + if(getSdkVersion() < minSdk) + return null; + + Method method; + try { + method = receiver.getClass().getMethod(methodName, params); + return method.invoke(receiver, args); + } catch (SecurityException e) { + getExceptionService().reportError("call-method", e); + } catch (NoSuchMethodException e) { + getExceptionService().reportError("call-method", e); + } catch (IllegalArgumentException e) { + getExceptionService().reportError("call-method", e); + } catch (IllegalAccessException e) { + getExceptionService().reportError("call-method", e); + } catch (InvocationTargetException e) { + getExceptionService().reportError("call-method", e); + } + + return null; + } + + /** + * From Android MyTracks project (http://mytracks.googlecode.com/). + * Licensed under the Apache Public License v2 + * @param activity + * @param id + * @return + */ + public static CharSequence readFile(Context activity, int id) { + BufferedReader in = null; + try { + in = new BufferedReader(new InputStreamReader( + activity.getResources().openRawResource(id))); + String line; + StringBuilder buffer = new StringBuilder(); + while ((line = in.readLine()) != null) { + buffer.append(line).append('\n'); + } + return buffer; + } catch (IOException e) { + return ""; //$NON-NLS-1$ + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + // Ignore + } + } + } + } + + /** + * Performs an md5 hash on the input string + * @param input + * @return + */ + @SuppressWarnings("nls") + public static String md5(String input) { + try { + byte[] bytesOfMessage = input.getBytes("UTF-8"); + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(bytesOfMessage); + BigInteger bigInt = new BigInteger(1,digest); + String hashtext = bigInt.toString(16); + while(hashtext.length() < 32 ){ + hashtext = "0" + hashtext; + } + return hashtext; + } catch (Exception e) { + return ""; + } + } + + /** + * Create an intent to a remote activity + * @param appPackage + * @param activityClass + * @return + */ + public static Intent remoteIntent(String appPackage, String activityClass) { + Intent intent = new Intent(); + intent.setClassName(appPackage, activityClass); + return intent; + } + + /** + * Gets application signature + * @return application signature, or null if an error was encountered + */ + public static String getSignature(Context context, String packageName) { + try { + PackageInfo packageInfo = context.getPackageManager().getPackageInfo(packageName, + PackageManager.GET_SIGNATURES); + return packageInfo.signatures[0].toCharsString(); + } catch (Exception e) { + return null; + } + } + + /** + * Join items to a list + * @param + * @param list + * @param newList + * @param newItems + * @return + */ + public static Property[] addToArray(Property[] list, Property... newItems) { + Property[] newList = new Property[list.length + newItems.length]; + for(int i = 0; i < list.length; i++) + newList[i] = list[i]; + for(int i = 0; i < newItems.length; i++) + newList[list.length + i] = newItems[i]; + return newList; + } + + // --- internal + + private static ExceptionService exceptionService = null; + + private static ExceptionService getExceptionService() { + if(exceptionService == null) + synchronized(AndroidUtilities.class) { + if(exceptionService == null) + exceptionService = new ExceptionService(); + } + return exceptionService; + } + +} diff --git a/api/src/com/todoroo/andlib/utility/DateUtilities.java b/api/src/com/todoroo/andlib/utility/DateUtilities.java new file mode 100644 index 0000000000..9147ad4de6 --- /dev/null +++ b/api/src/com/todoroo/andlib/utility/DateUtilities.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2009, Todoroo Inc + * All Rights Reserved + * http://www.todoroo.com + */ +package com.todoroo.andlib.utility; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +import android.content.Context; +import android.text.format.DateUtils; + + +public class DateUtilities { + + /* ====================================================================== + * ============================================================ long time + * ====================================================================== */ + + /** Convert unixtime into date */ + public static final Date unixtimeToDate(long millis) { + if(millis == 0) + return null; + return new Date(millis); + } + + /** Convert date into unixtime */ + public static final long dateToUnixtime(Date date) { + if(date == null) + return 0; + return date.getTime(); + } + + /** Returns unixtime for current time */ + public static final long now() { + return System.currentTimeMillis(); + } + + /** Returns unixtime one month from now */ + public static final long oneMonthFromNow() { + Date date = new Date(); + date.setMonth(date.getMonth() + 1); + return date.getTime(); + } + + /** Represents a single hour */ + public static long ONE_HOUR = 3600000L; + + /** Represents a single day */ + public static long ONE_DAY = 24 * ONE_HOUR; + + /** Represents a single week */ + public static long ONE_WEEK = 7 * ONE_DAY; + + /* ====================================================================== + * =========================================================== formatters + * ====================================================================== */ + + static Boolean is24HourOverride = null; + + @SuppressWarnings("nls") + public static boolean is24HourFormat(Context context) { + if(is24HourOverride != null) + return is24HourOverride; + + String value = android.provider.Settings.System.getString(context.getContentResolver(), + android.provider.Settings.System.TIME_12_24); + boolean b24 = !(value == null || value.equals("12")); + return b24; + } + + /** + * @param context android context + * @param date time to format + * @return time, with hours and minutes + */ + @SuppressWarnings("nls") + public static String getTimeString(Context context, Date date) { + String value; + if (is24HourFormat(context)) { + value = "H:mm"; + } else { + value = "h:mm a"; + } + return new SimpleDateFormat(value).format(date); + } + + /** + * @param context android context + * @param date date to format + * @return date, with month, day, and year + */ + @SuppressWarnings("nls") + public static String getDateString(Context context, Date date) { + String month = DateUtils.getMonthString(date.getMonth() + + Calendar.JANUARY, DateUtils.LENGTH_MEDIUM); + String value; + // united states, you are special + if (Locale.US.equals(Locale.getDefault()) + || Locale.CANADA.equals(Locale.getDefault())) + value = "'#' d yyyy"; + else + value = "d '#' yyyy"; + return new SimpleDateFormat(value).format(date).replace("#", month); + } + + /** + * @return date format as getDateFormat with weekday + */ + @SuppressWarnings("nls") + public static String getDateStringWithWeekday(Context context, Date date) { + String weekday = DateUtils.getDayOfWeekString(date.getDay() + Calendar.SUNDAY, + DateUtils.LENGTH_LONG); + return weekday + ", " + getDateString(context, date); + } + + /** + * @return date format as getDateFormat with weekday + */ + @SuppressWarnings("nls") + public static String getDateStringWithTimeAndWeekday(Context context, Date date) { + return getDateStringWithWeekday(context, date) + " " + getTimeString(context, date); + } + + /** + * @return date with time at the end + */ + @SuppressWarnings("nls") + public static String getDateStringWithTime(Context context, Date date) { + return getDateString(context, date) + " " + getTimeString(context, date); + } + +} diff --git a/api/src/com/todoroo/andlib/utility/DialogUtilities.java b/api/src/com/todoroo/andlib/utility/DialogUtilities.java new file mode 100644 index 0000000000..995296b789 --- /dev/null +++ b/api/src/com/todoroo/andlib/utility/DialogUtilities.java @@ -0,0 +1,217 @@ +package com.todoroo.andlib.utility; + + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.view.View; +import android.webkit.WebView; + +import com.todoroo.astrid.api.R; + +public class DialogUtilities { + + /** + * Displays a dialog box with a EditText and an ok / cancel + * + * @param activity + * @param text + * @param okListener + */ + public static void viewDialog(final Activity activity, final String text, + final View view, final DialogInterface.OnClickListener okListener, + final DialogInterface.OnClickListener cancelListener) { + if(activity.isFinishing()) + return; + + tryOnUiThread(activity, new Runnable() { + public void run() { + new AlertDialog.Builder(activity) + .setTitle(R.string.DLG_question_title) + .setMessage(text) + .setView(view) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(android.R.string.ok, okListener) + .setNegativeButton(android.R.string.cancel, cancelListener) + .show().setOwnerActivity(activity); + } + }); + } + + /** + * Display an OK dialog with HTML content + * + * @param context + * @param html + * @param title + */ + public static void htmlDialog(Context context, String html, int title) { + WebView webView = new WebView(context); + webView.loadData(html, "text/html", "utf-8"); //$NON-NLS-1$ //$NON-NLS-2$ + webView.setBackgroundColor(0); + + new AlertDialog.Builder(context) + .setTitle(title) + .setView(webView) + .setIcon(android.R.drawable.ic_dialog_info) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + + /** + * Displays a dialog box with an OK button + * + * @param activity + * @param text + * @param okListener + */ + public static void okDialog(final Activity activity, final String text, + final DialogInterface.OnClickListener okListener) { + if(activity.isFinishing()) + return; + + tryOnUiThread(activity, new Runnable() { + public void run() { + new AlertDialog.Builder(activity) + .setTitle(R.string.DLG_information_title) + .setMessage(text) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(android.R.string.ok, okListener) + .show().setOwnerActivity(activity); + } + }); + } + + /** + * Displays a dialog box with an OK button + * + * @param activity + * @param text + * @param okListener + */ + public static void okDialog(final Activity activity, final String title, + final int icon, final CharSequence text, + final DialogInterface.OnClickListener okListener) { + if(activity.isFinishing()) + return; + + tryOnUiThread(activity, new Runnable() { + public void run() { + new AlertDialog.Builder(activity) + .setTitle(title) + .setMessage(text) + .setIcon(icon) + .setPositiveButton(android.R.string.ok, okListener) + .show().setOwnerActivity(activity); + } + }); + } + + /** + * Displays a dialog box with OK and Cancel buttons and custom title + * + * @param activity + * @param title + * @param text + * @param okListener + * @param cancelListener + */ + public static void okCancelDialog(final Activity activity, final String title, + final String text, final DialogInterface.OnClickListener okListener, + final DialogInterface.OnClickListener cancelListener) { + if(activity.isFinishing()) + return; + + tryOnUiThread(activity, new Runnable() { + public void run() { + new AlertDialog.Builder(activity) + .setTitle(title) + .setMessage(text) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(android.R.string.ok, okListener) + .setNegativeButton(android.R.string.cancel, cancelListener) + .show().setOwnerActivity(activity); + } + }); + } + + /** + * Displays a dialog box with OK and Cancel buttons + * + * @param activity + * @param text + * @param okListener + * @param cancelListener + */ + public static void okCancelDialog(final Activity activity, final String text, + final DialogInterface.OnClickListener okListener, + final DialogInterface.OnClickListener cancelListener) { + if(activity.isFinishing()) + return; + + tryOnUiThread(activity, new Runnable() { + public void run() { + new AlertDialog.Builder(activity) + .setTitle(R.string.DLG_confirm_title) + .setMessage(text) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(android.R.string.ok, okListener) + .setNegativeButton(android.R.string.cancel, cancelListener) + .show().setOwnerActivity(activity); + } + }); + } + + /** + * Displays a progress dialog. Must be run on the UI thread + * @param context + * @param text + * @return + */ + public static ProgressDialog progressDialog(Context context, String text) { + ProgressDialog dialog = new ProgressDialog(context); + dialog.setIndeterminate(true); + dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + dialog.setMessage(text); + dialog.show(); + dialog.setOwnerActivity((Activity)context); + return dialog; + } + + /** + * Dismiss a dialog off the UI thread + * + * @param activity + * @param dialog + */ + public static void dismissDialog(Activity activity, final Dialog dialog) { + if(dialog == null) + return; + tryOnUiThread(activity, new Runnable() { + public void run() { + try { + dialog.dismiss(); + } catch (Exception e) { + // could have killed activity + } + } + }); + } + + + private static void tryOnUiThread(Activity activity, final Runnable runnable) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + try { + runnable.run(); + } catch (Exception e) { + // probably window was closed + } + } + }); + } +} diff --git a/api/src/com/todoroo/andlib/utility/Pair.java b/api/src/com/todoroo/andlib/utility/Pair.java new file mode 100644 index 0000000000..d3001a5688 --- /dev/null +++ b/api/src/com/todoroo/andlib/utility/Pair.java @@ -0,0 +1,56 @@ +package com.todoroo.andlib.utility; + +/** + * Pair utility class + * + * @author Tim Su + * + * @param + * @param + */ +public class Pair { + + private final L left; + private final R right; + + public R getRight() { + return right; + } + + public L getLeft() { + return left; + } + + public Pair(final L left, final R right) { + this.left = left; + this.right = right; + } + + public static Pair create(A left, B right) { + return new Pair(left, right); + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof Pair)) + return false; + + final Pair other = (Pair) o; + return equal(getLeft(), other.getLeft()) && equal(getRight(), other.getRight()); + } + + public static final boolean equal(Object o1, Object o2) { + if (o1 == null) { + return o2 == null; + } + return o1.equals(o2); + } + + @Override + public int hashCode() { + int hLeft = getLeft() == null ? 0 : getLeft().hashCode(); + int hRight = getRight() == null ? 0 : getRight().hashCode(); + + return hLeft + (57 * hRight); + } +} diff --git a/api/src/com/todoroo/andlib/utility/Preferences.java b/api/src/com/todoroo/andlib/utility/Preferences.java new file mode 100644 index 0000000000..590ea57cef --- /dev/null +++ b/api/src/com/todoroo/andlib/utility/Preferences.java @@ -0,0 +1,255 @@ +package com.todoroo.andlib.utility; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.res.Resources; +import android.preference.PreferenceManager; + +import com.todoroo.andlib.service.ContextManager; + +/** + * Helper class for reading and writing SharedPreferences + * + * @author Tim Su + * + */ +public class Preferences { + + /** + * Helper to write to editor if key specified is null. Writes a String + * property with the given integer + * + * @param prefs + * @param editor + * @param r + * @param keyResource + * @param value + */ + public static void setIfUnset(SharedPreferences prefs, Editor editor, Resources r, int keyResource, int value) { + String key = r.getString(keyResource); + if(!prefs.contains(key)) + editor.putString(key, Integer.toString(value)); + } + + /** + * Helper to write to editor if key specified is null + * @param prefs + * @param editor + * @param r + * @param keyResource + * @param value + */ + public static void setIfUnset(SharedPreferences prefs, Editor editor, Resources r, int keyResource, boolean value) { + String key = r.getString(keyResource); + if(!prefs.contains(key) || !(prefs.getAll().get(key) instanceof Boolean)) + editor.putBoolean(key, value); + } + + /* ====================================================================== + * ======================================================= helper methods + * ====================================================================== */ + + /** Get preferences object from the context */ + public static SharedPreferences getPrefs(Context context) { + context = context.getApplicationContext(); + return PreferenceManager.getDefaultSharedPreferences(context); + } + + /** @return true if given preference is set */ + public static boolean isSet(String key) { + Context context = ContextManager.getContext(); + return getPrefs(context).contains(key); + } + + // --- preference fetching (string) + + /** Gets an string value from a string preference. Returns null + * if the value is not set + * + * @param context + * @param key + * @return integer value, or null on error + */ + public static String getStringValue(String key) { + Context context = ContextManager.getContext(); + return getPrefs(context).getString(key, null); + } + + /** Gets an string value from a string preference. Returns null + * if the value is not set + * + * @param context + * @param key + * @return integer value, or null on error + */ + public static String getStringValue(int keyResource) { + Context context = ContextManager.getContext(); + return getPrefs(context).getString(context.getResources().getString(keyResource), null); + } + + /** Gets an integer value from a string preference. Returns null + * if the value is not set or not an integer. + * + * @param keyResource resource from string.xml + * @return integer value, or null on error + */ + public static int getIntegerFromString(int keyResource, int defaultValue) { + Context context = ContextManager.getContext(); + Resources r = context.getResources(); + String value = getPrefs(context).getString(r.getString(keyResource), null); + if(value == null) + return defaultValue; + + try { + return Integer.parseInt(value); + } catch (Exception e) { + return defaultValue; + } + } + + /** Gets an float value from a string preference. Returns null + * if the value is not set or not an flat. + * + * @param keyResource resource from string.xml + * @return + */ + public static Float getFloatFromString(int keyResource) { + Context context = ContextManager.getContext(); + Resources r = context.getResources(); + String value = getPrefs(context).getString(r.getString(keyResource), ""); //$NON-NLS-1$ + + try { + return Float.parseFloat(value); + } catch (Exception e) { + return null; + } + } + + /** + * Sets string preference + */ + public static void setString(int keyResource, String newValue) { + Context context = ContextManager.getContext(); + setString(context.getString(keyResource), newValue); + } + + /** + * Sets string preference + */ + public static void setString(String key, String newValue) { + Context context = ContextManager.getContext(); + Editor editor = getPrefs(context).edit(); + editor.putString(key, newValue); + editor.commit(); + } + + /** + * Sets string preference from integer value + */ + public static void setStringFromInteger(int keyResource, int newValue) { + Context context = ContextManager.getContext(); + Editor editor = getPrefs(context).edit(); + editor.putString(context.getString(keyResource), Integer.toString(newValue)); + editor.commit(); + } + + // --- preference fetching (boolean) + + /** Gets a boolean preference (e.g. a CheckBoxPreference setting) + * + * @param key + * @param defValue + * @return default if value is unset otherwise the value + */ + public static boolean getBoolean(String key, boolean defValue) { + Context context = ContextManager.getContext(); + try { + return getPrefs(context).getBoolean(key, defValue); + } catch (ClassCastException e) { + return defValue; + } + } + + /** Gets a boolean preference (e.g. a CheckBoxPreference setting) + * + * @param keyResource + * @param defValue + * @return default if value is unset otherwise the value + */ + public static boolean getBoolean(int keyResources, boolean defValue) { + return getBoolean(ContextManager.getString(keyResources), defValue); + } + + /** + * Sets boolean preference + * @param key + * @param value + */ + public static void setBoolean(int keyResource, boolean value) { + setBoolean(ContextManager.getString(keyResource), value); + } + + /** + * Sets boolean preference + * @param key + * @param value + */ + public static void setBoolean(String key, boolean value) { + Context context = ContextManager.getContext(); + Editor editor = getPrefs(context).edit(); + editor.putBoolean(key, value); + editor.commit(); + } + + // --- preference fetching (int) + + /** Gets a int preference + * + * @param key + * @param defValue + * @return default if value is unset otherwise the value + */ + public static int getInt(String key, int defValue) { + Context context = ContextManager.getContext(); + return getPrefs(context).getInt(key, defValue); + } + + /** + * Sets int preference + * @param key + * @param value + */ + public static void setInt(String key, int value) { + Context context = ContextManager.getContext(); + Editor editor = getPrefs(context).edit(); + editor.putInt(key, value); + editor.commit(); + } + + // --- preference fetching (long) + + /** Gets a long preference + * + * @param key + * @param defValue + * @return default if value is unset otherwise the value + */ + public static long getLong(String key, long defValue) { + Context context = ContextManager.getContext(); + return getPrefs(context).getLong(key, defValue); + } + + /** + * Sets long preference + * @param key + * @param value + */ + public static void setLong(String key, long value) { + Context context = ContextManager.getContext(); + Editor editor = getPrefs(context).edit(); + editor.putLong(key, value); + editor.commit(); + } + +} diff --git a/api/src/com/todoroo/andlib/utility/TodorooPreferenceActivity.java b/api/src/com/todoroo/andlib/utility/TodorooPreferenceActivity.java new file mode 100644 index 0000000000..6d02887cc7 --- /dev/null +++ b/api/src/com/todoroo/andlib/utility/TodorooPreferenceActivity.java @@ -0,0 +1,85 @@ +package com.todoroo.andlib.utility; +/** + * See the file "LICENSE" for the full license governing this code. + */ + + +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.PreferenceActivity; +import android.preference.PreferenceGroup; +import android.preference.RingtonePreference; + +import com.todoroo.andlib.service.ContextManager; + +/** + * Displays a preference screen for users to edit their preferences. Override + * updatePreferences to update the summary with preference values. + * + * @author Tim Su + * + */ +abstract public class TodorooPreferenceActivity extends PreferenceActivity { + + // --- abstract methods + + public abstract int getPreferenceResource(); + + /** + * Update preferences for the given preference + * @param preference + * @param value setting. may be null. + */ + public abstract void updatePreferences(Preference preference, Object value); + + // --- implementation + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + ContextManager.setContext(this); + addPreferencesFromResource(getPreferenceResource()); + } + + protected void initializePreference(Preference preference) { + if(preference instanceof PreferenceGroup) { + PreferenceGroup group = (PreferenceGroup)preference; + for(int i = 0; i < group.getPreferenceCount(); i++) { + initializePreference(group.getPreference(i)); + } + } else { + Object value = null; + if(preference instanceof ListPreference) + value = ((ListPreference)preference).getValue(); + else if(preference instanceof CheckBoxPreference) + value = ((CheckBoxPreference)preference).isChecked(); + else if(preference instanceof EditTextPreference) + value = ((EditTextPreference)preference).getText(); + else if(preference instanceof RingtonePreference) + value = getPreferenceManager().getSharedPreferences().getString(preference.getKey(), null); + + updatePreferences(preference, value); + + preference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference myPreference, Object newValue) { + updatePreferences(myPreference, newValue); + return true; + } + }); + } + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if(hasFocus) { + initializePreference(getPreferenceScreen()); + } + } + +} \ No newline at end of file diff --git a/api/src/com/todoroo/andlib/utility/package-info.java b/api/src/com/todoroo/andlib/utility/package-info.java new file mode 100644 index 0000000000..d8457ff4e9 --- /dev/null +++ b/api/src/com/todoroo/andlib/utility/package-info.java @@ -0,0 +1,4 @@ +/** + * Todoroo Library utility classes + */ +package com.todoroo.andlib.utility; \ No newline at end of file diff --git a/api/src/com/todoroo/astrid/api/Addon.java b/api/src/com/todoroo/astrid/api/Addon.java new file mode 100644 index 0000000000..98c98e7e50 --- /dev/null +++ b/api/src/com/todoroo/astrid/api/Addon.java @@ -0,0 +1,93 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.api; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Represents an add-onn for Astrid. Users can enable or disable add-ons, + * which affect all other extension points that share the same identifier. + * + * @author Tim Su + * + */ +public class Addon implements Parcelable { + + /** + * Add-on Identifier + */ + public String addon = null; + + /** + * Plug-in Title + */ + public String title = null; + + /** + * Plug-in Author + */ + public String author = null; + + /** + * Plug-in Description + */ + public String description = null; + + /** + * Convenience constructor to generate a plug-in object + * + * @param addon + * @param title + * @param author + * @param description + */ + public Addon(String addon, String title, String author, String description) { + this.addon = addon; + this.title = title; + this.author = author; + this.description = description; + } + + // --- parcelable helpers + + + /** + * {@inheritDoc} + */ + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(addon); + dest.writeString(title); + dest.writeString(author); + dest.writeString(description); + } + + /** + * Parcelable creator + */ + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + /** + * {@inheritDoc} + */ + public Addon createFromParcel(Parcel source) { + return new Addon(source.readString(), source.readString(), + source.readString(), source.readString()); + } + + /** + * {@inheritDoc} + */ + public Addon[] newArray(int size) { + return new Addon[size]; + }; + }; + +} diff --git a/api/src/com/todoroo/astrid/api/AstridApiConstants.java b/api/src/com/todoroo/astrid/api/AstridApiConstants.java new file mode 100644 index 0000000000..b46de409a9 --- /dev/null +++ b/api/src/com/todoroo/astrid/api/AstridApiConstants.java @@ -0,0 +1,270 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.api; + +import android.content.Intent; +import android.widget.RemoteViews; + +/** + * Constants for interfacing with Astrid. + * + * @author Tim Su + */ +@SuppressWarnings("nls") +public class AstridApiConstants { + + // --- General Constants + + /** + * Astrid broadcast base package name + */ + public static final String PACKAGE = "com.todoroo.astrid"; + + /** + * Permission for reading tasks and receiving to GET_FILTERS intent + */ + public static final String PERMISSION_READ = PACKAGE + ".READ"; + + /** + * Permission for writing and creating tasks + */ + public static final String PERMISSION_WRITE = PACKAGE + ".WRITE"; + + /** + * Name of Astrid's publicly readable preference store + */ + public static final String PUBLIC_PREFS = "public"; + + // --- Content Provider + + /** + * URI to append to base content URI for making group-by queries + */ + public static final String GROUP_BY_URI = "/groupby/"; + + // --- Broadcast Extras + + /** + * Extras name for task id + */ + public static final String EXTRAS_TASK_ID = "task"; + + /** + * Extras name for a response item broadcast to astrid + */ + public static final String EXTRAS_RESPONSE = "response"; + + /** + * Extras name for plug-in identifier + */ + public static final String EXTRAS_ADDON = "addon"; + + /** + * Extras name for whether task detail request is extended + */ + public static final String EXTRAS_EXTENDED = "extended"; + + /** + * Extras name for old task due date + */ + public static final String EXTRAS_OLD_DUE_DATE= "oldDueDate"; + + /** + * Extras name for new task due date + */ + public static final String EXTRAS_NEW_DUE_DATE = "newDueDate"; + + /** + * Extras name for sync provider name + */ + public static final String EXTRAS_NAME = "name"; + + // --- Add-ons API + + /** + * Action name for broadcast intent requesting add-ons + */ + public static final String BROADCAST_REQUEST_ADDONS = PACKAGE + ".REQUEST_ADDONS"; + + /** + * Action name for broadcast intent sending add-ons back to Astrid + *

  • EXTRAS_RESPONSE an {@link Addon} object + */ + public static final String BROADCAST_SEND_ADDONS = PACKAGE + ".SEND_ADDONS"; + + // --- Filters API + + /** + * Action name for broadcast intent requesting filters + */ + public static final String BROADCAST_REQUEST_FILTERS = PACKAGE + ".REQUEST_FILTERS"; + + /** + * Action name for broadcast intent sending filters back to Astrid + *
  • EXTRAS_ADDON your add-on identifier
  • + *
  • EXTRAS_RESPONSE an array of {@link FilterListItem}s
  • + */ + public static final String BROADCAST_SEND_FILTERS = PACKAGE + ".SEND_FILTERS"; + + // -- Custom criteria API + + /** + * Action name for a broadcast intent requesting custom filter criteria (e.g. "Due by, Tagged, Tag contains", etc.) + */ + public static final String BROADCAST_REQUEST_CUSTOM_FILTER_CRITERIA = PACKAGE + ".REQUEST_CUSTOM_FILTER_CRITERIA"; + + + /** + * Action name for broadcast intent sending custom filter criteria back to Astrid + *
  • EXTRAS_ADDON you add-on identifier + *
  • EXTRAS_RESPONSE an array of {@link CustomFilterCriterion}
  • + */ + public static final String BROADCAST_SEND_CUSTOM_FILTER_CRITERIA = PACKAGE + ".SEND_CUSTOM_FILTER_CRITERIA"; + + // --- Edit Controls API + + /** + * Action name for broadcast intent requesting task edit controls + *
  • EXTRAS_TASK_ID id of the task user is editing + */ + public static final String BROADCAST_REQUEST_EDIT_CONTROLS = PACKAGE + ".REQUEST_EDIT_CONTROLS"; + + /** + * Action name for broadcast intent sending task edit controls back to Astrid + *
  • EXTRAS_ADDON your add-on identifier + *
  • EXTRAS_RESPONSE a {@link RemoteViews} with your edit controls + */ + public static final String BROADCAST_SEND_EDIT_CONTROLS = PACKAGE + ".SEND_EDIT_CONTROLS"; + + // --- Task Details API + + /** + * Action name for broadcast intent requesting details for a task. + * Extended details are displayed when a user presses on a task. + * + *
  • EXTRAS_TASK_ID id of the task + *
  • EXTRAS_EXTENDED whether request is for standard or extended details + */ + public static final String BROADCAST_REQUEST_DETAILS = PACKAGE + ".REQUEST_DETAILS"; + + /** + * Action name for broadcast intent sending details back to Astrid + *
  • EXTRAS_ADDON your add-on identifier + *
  • EXTRAS_TASK_ID id of the task + *
  • EXTRAS_EXTENDED whether request is for standard or extended details + *
  • EXTRAS_RESPONSE a String + */ + public static final String BROADCAST_SEND_DETAILS = PACKAGE + ".SEND_DETAILS"; + + // --- Sync Action API + + /** + * Action name for broadcast intent requesting a listing of active + * sync actions users can activate from the menu + */ + public static final String BROADCAST_REQUEST_SYNC_ACTIONS = PACKAGE + ".REQUEST_SYNC_ACTIONS"; + + /** + * Action name for broadcast intent sending sync provider information back to Astrid + *
  • EXTRAS_ADDON your add-on identifier + *
  • EXTRAS_RESPONSE a {@link SyncAction} to invoke synchronization + */ + public static final String BROADCAST_SEND_SYNC_ACTIONS = PACKAGE + ".SEND_SYNC_ACTIONS"; + + // --- Task Actions API + + /** + * Action name for broadcast intent requesting actions for a task + *
  • EXTRAS_TASK_ID id of the task + */ + public static final String BROADCAST_REQUEST_ACTIONS = PACKAGE + ".REQUEST_ACTIONS"; + + /** + * Action name for broadcast intent sending actions back to Astrid + *
  • EXTRAS_ADDON your add-on identifier + *
  • EXTRAS_TASK_ID id of the task + *
  • EXTRAS_RESPONSE a String + */ + public static final String BROADCAST_SEND_ACTIONS = PACKAGE + ".SEND_ACTIONS"; + + // --- Task Decorations API + + /** + * Action name for broadcast intent requesting task list decorations for a task + *
  • EXTRAS_TASK_ID id of the task + */ + public static final String BROADCAST_REQUEST_DECORATIONS = PACKAGE + ".REQUEST_DECORATIONS"; + + /** + * Action name for broadcast intent sending decorations back to Astrid + *
  • EXTRAS_ADDON your add-on identifier + *
  • EXTRAS_TASK_ID id of the task + *
  • EXTRAS_RESPONSE a {@link TaskDecoration} + */ + public static final String BROADCAST_SEND_DECORATIONS = PACKAGE + ".SEND_DECORATIONS"; + + // --- Actions API + + /** + * Action name for intents to be displayed on task context menu + *
  • EXTRAS_TASK_ID id of the task + */ + public static final String ACTION_TASK_CONTEXT_MENU = PACKAGE + ".CONTEXT_MENU"; + + /** + * Action name for intents to be displayed on Astrid's task list menu + *
  • EXTRAS_ADDON your add-on identifier + *
  • EXTRAS_RESPONSE an array of {@link Intent}s + */ + public static final String ACTION_TASK_LIST_MENU = PACKAGE + ".TASK_LIST_MENU"; + + /** + * Action name for intents to be displayed in Astrid's settings. By default, + * your application will be put into the category named by your application, + * but you can add a string meta-data with name "category" to override this. + */ + public static final String ACTION_SETTINGS = PACKAGE + ".SETTINGS"; + + // --- Events API + + /** + * Action name for broadcast intent notifying add-ons that Astrid started up + */ + public static final String BROADCAST_EVENT_STARTUP = PACKAGE + ".STARTUP"; + + /** + * Action name for broadcast intent notifying Astrid task list to refresh + */ + public static final String BROADCAST_EVENT_REFRESH = PACKAGE + ".REFRESH"; + + /** + * Action name for broadcast intent notifying Astrid to clear detail cache + * because an event occurred that potentially affects all tasks (e.g. + * logging out of a sync provider). Use this call carefully, as loading + * details can degrade the performance of Astrid. + */ + public static final String BROADCAST_EVENT_FLUSH_DETAILS = PACKAGE + ".FLUSH_DETAILS"; + + /** + * Action name for broadcast intent notifying that task was created or + * title was changed + *
  • EXTRAS_TASK_ID id of the task + */ + public static final String BROADCAST_EVENT_TASK_LIST_UPDATED = PACKAGE + ".TASK_LIST_UPDATED"; + + /** + * Action name for broadcast intent notifying that task was completed + *
  • EXTRAS_TASK_ID id of the task + */ + public static final String BROADCAST_EVENT_TASK_COMPLETED = PACKAGE + ".TASK_COMPLETED"; + + /** + * Action name for broadcast intent notifying that task was created from repeating template + *
  • EXTRAS_TASK_ID id of the task + *
  • EXTRAS_OLD_DUE_DATE task old due date (could be 0) + *
  • EXTRAS_NEW_DUE_DATE task new due date (will not be 0) + */ + public static final String BROADCAST_EVENT_TASK_REPEATED = PACKAGE + ".TASK_REPEATED"; + +} diff --git a/api/src/com/todoroo/astrid/api/CustomFilterCriterion.java b/api/src/com/todoroo/astrid/api/CustomFilterCriterion.java new file mode 100644 index 0000000000..8b67750588 --- /dev/null +++ b/api/src/com/todoroo/astrid/api/CustomFilterCriterion.java @@ -0,0 +1,91 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.api; + +import android.content.ContentValues; +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * CustomFilterCriteria allow users to build a custom filter by chaining + * together criteria + * + * @author Tim Su + * + */ +abstract public class CustomFilterCriterion implements Parcelable { + + /** + * Criteria Identifier. This identifier allows saved filters to be reloaded. + *

    + * e.g "duedate" + */ + public String identifier; + + /** + * Criteria Title. If the title contains ?, this is replaced by the entry + * label string selected. + *

    + * e.g "Due: ?" + */ + public String text; + + /** + * Criterion SQL. This query should return task id's. If this contains + * ?, it will be replaced by the entry value + *

    + * Examples: + *

      + *
    • SELECT _id FROM tasks WHERE dueDate <= ? + *
    • SELECT task FROM metadata WHERE value = '?' + *
    + */ + public String sql; + + /** + * Values to apply to a task when quick-adding a task from a filter + * created from this criterion. ? will be replaced with the entry value. + * For example, when a user views tasks tagged 'ABC', the + * tasks they create should also be tagged 'ABC'. If set to null, no + * additional values will be stored for a task. + */ + public ContentValues valuesForNewTasks = null; + + /** + * Icon for this criteria. Can be null for no bitmap + */ + public Bitmap icon; + + /** + * Criteria name. This is displayed when users are selecting a criteria + */ + public String name; + + // --- parcelable utilities + + /** + * Utility method to write to parcel + */ + public void writeToParcel(Parcel dest) { + dest.writeString(identifier); + dest.writeString(text); + dest.writeString(sql); + dest.writeParcelable(valuesForNewTasks, 0); + dest.writeParcelable(icon, 0); + dest.writeString(name); + } + + /** + * Utility method to read from parcel + */ + public void readFromParcel(Parcel source) { + identifier = source.readString(); + text = source.readString(); + sql = source.readString(); + valuesForNewTasks = (ContentValues)source.readParcelable(ContentValues.class.getClassLoader()); + icon = (Bitmap)source.readParcelable(Bitmap.class.getClassLoader()); + name = source.readString(); + } +} diff --git a/api/src/com/todoroo/astrid/api/Filter.java b/api/src/com/todoroo/astrid/api/Filter.java new file mode 100644 index 0000000000..9205642a59 --- /dev/null +++ b/api/src/com/todoroo/astrid/api/Filter.java @@ -0,0 +1,164 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.api; + +import android.content.ContentValues; +import android.os.Parcel; +import android.os.Parcelable; + +import com.todoroo.andlib.sql.QueryTemplate; + +/** + * A FilterListFilter allows users to display tasks that have + * something in common. + *

    + * A plug-in can expose new FilterListFilters to the system by + * responding to the com.todoroo.astrid.GET_FILTERS broadcast + * intent. + * + * @author Tim Su + * + */ +public class Filter extends FilterListItem { + + // --- constants + + /** Constant for valuesForNewTasks to indicate the value should be replaced + * with the current time as long */ + public static final long VALUE_NOW = Long.MIN_VALUE + 1; + + // --- instance variables + + /** + * Expanded title of this filter. This is displayed at the top + * of the screen when user is viewing this filter. + *

    + * e.g "Tasks With Notes" + */ + public String title; + + /** + * {@link PermaSql} query for this filter. The query will be appended to the select + * statement after "SELECT fields FROM table %s". It is + * recommended that you use a {@link QueryTemplate} to construct your + * query. + *

    + * Examples: + *

      + *
    • "WHERE completionDate = 0" + *
    • "INNER JOIN " + + * Constants.TABLE_METADATA + " ON metadata.task = tasks.id WHERE + * metadata.namespace = " + NAMESPACE + " AND metadata.key = 'a' AND + * metadata.value = 'b' GROUP BY tasks.id ORDER BY tasks.title" + *
    + */ + public String sqlQuery; + + /** + * Values to apply to a task when quick-adding a task from this filter. + * For example, when a user views tasks tagged 'ABC', the + * tasks they create should also be tagged 'ABC'. If set to null, no + * additional values will be stored for a task. Can use {@link PermaSql} + */ + public ContentValues valuesForNewTasks = null; + + /** + * Utility constructor for creating a Filter object + * @param listingTitle + * Title of this item as displayed on the lists page, e.g. Inbox + * @param title + * Expanded title of this filter when user is viewing this + * filter, e.g. Inbox (20 tasks) + * @param sqlQuery + * SQL query for this list (see {@link #sqlQuery} for examples). + * @param valuesForNewTasks + * see {@link #sqlForNewTasks} + */ + public Filter(String listingTitle, String title, + QueryTemplate sqlQuery, ContentValues valuesForNewTasks) { + this(listingTitle, title, sqlQuery == null ? null : sqlQuery.toString(), + valuesForNewTasks); + } + + /** + * Utility constructor for creating a Filter object + * @param listingTitle + * Title of this item as displayed on the lists page, e.g. Inbox + * @param title + * Expanded title of this filter when user is viewing this + * filter, e.g. Inbox (20 tasks) + * @param sqlQuery + * SQL query for this list (see {@link #sqlQuery} for examples). + * @param valuesForNewTasks + * see {@link #sqlForNewTasks} + */ + public Filter(String listingTitle, String title, + String sqlQuery, ContentValues valuesForNewTasks) { + this.listingTitle = listingTitle; + this.title = title; + this.sqlQuery = sqlQuery; + this.valuesForNewTasks = valuesForNewTasks; + } + + /** + * Utility constructor + * + * @param plugin + * {@link Addon} identifier that encompasses object + */ + protected Filter() { + // do nothing + } + + // --- parcelable + + /** + * {@inheritDoc} + */ + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(title); + dest.writeString(sqlQuery); + dest.writeParcelable(valuesForNewTasks, 0); + } + + @Override + public void readFromParcel(Parcel source) { + super.readFromParcel(source); + title = source.readString(); + sqlQuery = source.readString(); + valuesForNewTasks = source.readParcelable(ContentValues.class.getClassLoader()); + } + + /** + * Parcelable Creator Object + */ + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + + /** + * {@inheritDoc} + */ + public Filter createFromParcel(Parcel source) { + Filter item = new Filter(); + item.readFromParcel(source); + return item; + } + + /** + * {@inheritDoc} + */ + public Filter[] newArray(int size) { + return new Filter[size]; + } + + }; +} diff --git a/api/src/com/todoroo/astrid/api/FilterCategory.java b/api/src/com/todoroo/astrid/api/FilterCategory.java new file mode 100644 index 0000000000..3fe51d2698 --- /dev/null +++ b/api/src/com/todoroo/astrid/api/FilterCategory.java @@ -0,0 +1,96 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.api; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A FilterCategory groups common {@link Filter}s and allows + * a user to show/hide all of its children. + * + * @author Tim Su + * + */ +public class FilterCategory extends FilterListItem { + + /** + * {@link Filter}s contained by this category + */ + public Filter[] children; + + /** + * Constructor for creating a new FilterCategory + * @param listingTitle + * Title of this item as displayed on the lists page, e.g. Inbox + * @param children + * filters belonging to this category + */ + public FilterCategory(String listingTitle, Filter[] children) { + this.listingTitle = listingTitle; + this.children = children; + } + + /** + * Constructor for creating a new FilterCategory + * + * @param plugin + * {@link Addon} identifier that encompasses object + */ + protected FilterCategory() { + // + } + + // --- parcelable + + /** + * {@inheritDoc} + */ + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeParcelableArray(children, 0); + } + + /** + * Parcelable creator + */ + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + + /** + * {@inheritDoc} + */ + public FilterCategory createFromParcel(Parcel source) { + FilterCategory item = new FilterCategory(); + item.readFromParcel(source); + + Parcelable[] parcelableChildren = source.readParcelableArray( + FilterCategory.class.getClassLoader()); + item.children = new Filter[parcelableChildren.length]; + for(int i = 0; i < item.children.length; i++) { + if(parcelableChildren[i] instanceof FilterListItem) + item.children[i] = (Filter) parcelableChildren[i]; + else + item.children[i] = null; + } + + return item; + } + + /** + * {@inheritDoc} + */ + public FilterCategory[] newArray(int size) { + return new FilterCategory[size]; + } + + }; +} diff --git a/api/src/com/todoroo/astrid/api/FilterListHeader.java b/api/src/com/todoroo/astrid/api/FilterListHeader.java new file mode 100644 index 0000000000..47b8d751f6 --- /dev/null +++ b/api/src/com/todoroo/astrid/api/FilterListHeader.java @@ -0,0 +1,61 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.api; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Section Header for Filter List + * + * @author Tim Su + * + */ +public class FilterListHeader extends FilterListItem { + + /** + * Constructor for creating a new FilterListHeader + * @param listingTitle + * @param listingIconResource + * @param priority + */ + public FilterListHeader(String listingTitle) { + this.listingTitle = listingTitle; + } + + /** + * Constructor for creating a new FilterListHeader + * + * @param plugin + * {@link Addon} identifier that encompasses object + */ + protected FilterListHeader() { + // + } + + // --- parcelable + + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + + public FilterListHeader createFromParcel(Parcel source) { + FilterListHeader item = new FilterListHeader(); + item.readFromParcel(source); + return item; + } + + public FilterListHeader[] newArray(int size) { + return new FilterListHeader[size]; + } + + }; +} diff --git a/api/src/com/todoroo/astrid/api/FilterListItem.java b/api/src/com/todoroo/astrid/api/FilterListItem.java new file mode 100644 index 0000000000..b8a2c57b99 --- /dev/null +++ b/api/src/com/todoroo/astrid/api/FilterListItem.java @@ -0,0 +1,75 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.api; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Represents an item displayed by Astrid's FilterListActivity + * + * @author Tim Su + * + */ +abstract public class FilterListItem implements Parcelable { + + /** + * Title of this item displayed on the Filters page + */ + public String listingTitle = null; + + /** + * Bitmap for icon used on listing page. null => no icon + */ + public Bitmap listingIcon = null; + + /** + * Text Color. 0 => default color + */ + public int color = 0; + + /** + * Context Menu labels. The context menu will be displayed when users + * long-press on this filter list item. + */ + public String contextMenuLabels[] = new String[0]; + + /** + * Context menu intents. This intent will be started when the corresponding + * content menu label is invoked. This array must be the same size as + * the contextMenuLabels array. + */ + public Intent contextMenuIntents[] = new Intent[0]; + + // --- parcelable helpers + + /** + * {@inheritDoc} + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(listingTitle); + dest.writeParcelable(listingIcon, 0); + dest.writeInt(color); + + // write array lengths before arrays + dest.writeStringArray(contextMenuLabels); + dest.writeTypedArray(contextMenuIntents, 0); + } + + /** + * Utility method to read FilterListItem properties from a parcel. + * + * @param source + */ + public void readFromParcel(Parcel source) { + listingTitle = source.readString(); + listingIcon = source.readParcelable(Bitmap.class.getClassLoader()); + color = source.readInt(); + + contextMenuLabels = source.createStringArray(); + contextMenuIntents = source.createTypedArray(Intent.CREATOR); + } +} diff --git a/api/src/com/todoroo/astrid/api/FilterWithCustomIntent.java b/api/src/com/todoroo/astrid/api/FilterWithCustomIntent.java new file mode 100644 index 0000000000..70da0dc289 --- /dev/null +++ b/api/src/com/todoroo/astrid/api/FilterWithCustomIntent.java @@ -0,0 +1,83 @@ +package com.todoroo.astrid.api; + + +import android.content.ComponentName; +import android.content.ContentValues; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import com.todoroo.andlib.sql.QueryTemplate; + +public class FilterWithCustomIntent extends Filter { + + public ComponentName customTaskList = null; + public Bundle customExtras = null; + + protected FilterWithCustomIntent() { + super(); + } + + public FilterWithCustomIntent(String listingTitle, String title, + QueryTemplate sqlQuery, ContentValues valuesForNewTasks) { + super(listingTitle, title, sqlQuery, valuesForNewTasks); + } + + public FilterWithCustomIntent(String listingTitle, String title, + String sqlQuery, ContentValues valuesForNewTasks) { + super(listingTitle, title, sqlQuery, valuesForNewTasks); + } + + + // --- parcelable + + /** + * {@inheritDoc} + */ + @Override + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeParcelable(customTaskList, 0); + dest.writeParcelable(customExtras, 0); + } + + @Override + public void readFromParcel(Parcel source) { + super.readFromParcel(source); + customTaskList = source.readParcelable(ComponentName.class.getClassLoader()); + customExtras = source.readParcelable(Bundle.class.getClassLoader()); + } + + /** + * Parcelable Creator Object + */ + @SuppressWarnings("hiding") + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + + /** + * {@inheritDoc} + */ + public FilterWithCustomIntent createFromParcel(Parcel source) { + FilterWithCustomIntent item = new FilterWithCustomIntent(); + item.readFromParcel(source); + return item; + } + + /** + * {@inheritDoc} + */ + public FilterWithCustomIntent[] newArray(int size) { + return new FilterWithCustomIntent[size]; + } + + }; + +} diff --git a/api/src/com/todoroo/astrid/api/IntentFilter.java b/api/src/com/todoroo/astrid/api/IntentFilter.java new file mode 100644 index 0000000000..e372c52826 --- /dev/null +++ b/api/src/com/todoroo/astrid/api/IntentFilter.java @@ -0,0 +1,83 @@ +package com.todoroo.astrid.api; + +import android.app.PendingIntent; +import android.os.Parcel; +import android.os.Parcelable; + + +/** + * Special filter that launches a PendingIntent when accessed. + * + * @author Tim Su + * + */ +public final class IntentFilter extends FilterListItem implements Parcelable { + + /** + * PendingIntent to trigger when pressed + */ + public PendingIntent intent; + + /** + * Constructor for creating a new IntentFilter + * + * @param listingTitle + * Title of this item as displayed on the lists page, e.g. Inbox + * @param intent + * intent to load + */ + public IntentFilter(String listingTitle, PendingIntent intent) { + this.listingTitle = listingTitle; + this.intent = intent; + } + + /** + * Constructor for creating a new IntentFilter used internally + */ + protected IntentFilter(PendingIntent intent) { + this.intent = intent; + } + + // --- parcelable + + /** + * {@inheritDoc} + */ + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(intent, 0); + super.writeToParcel(dest, flags); + } + + /** + * Parcelable creator + */ + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + + /** + * {@inheritDoc} + */ + public IntentFilter createFromParcel(Parcel source) { + IntentFilter item = new IntentFilter((PendingIntent) source.readParcelable( + PendingIntent.class.getClassLoader())); + item.readFromParcel(source); + return item; + } + + /** + * {@inheritDoc} + */ + public IntentFilter[] newArray(int size) { + return new IntentFilter[size]; + } + + }; + +} diff --git a/api/src/com/todoroo/astrid/api/MultipleSelectCriterion.java b/api/src/com/todoroo/astrid/api/MultipleSelectCriterion.java new file mode 100644 index 0000000000..95e7481382 --- /dev/null +++ b/api/src/com/todoroo/astrid/api/MultipleSelectCriterion.java @@ -0,0 +1,102 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.api; + +import android.content.ContentValues; +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * CustomFilterCriteria allow users to build a custom filter by chaining + * together criteria + * + * @author Tim Su + * + */ +public class MultipleSelectCriterion extends CustomFilterCriterion implements Parcelable { + + /** + * Array of entries for user to select from + */ + public String[] entryTitles; + + /** + * Array of entry values corresponding to entries + */ + public String[] entryValues; + + + /** + * Create a new CustomFilterCriteria object + * + * @param title + * @param sql + * @param valuesForNewTasks + * @param entryTitles + * @param entryValues + * @param icon + * @param name + */ + public MultipleSelectCriterion(String identifier, String title, String sql, + ContentValues valuesForNewTasks, String[] entryTitles, + String[] entryValues, Bitmap icon, String name) { + this.identifier = identifier; + this.text = title; + this.sql = sql; + this.valuesForNewTasks = valuesForNewTasks; + this.entryTitles = entryTitles; + this.entryValues = entryValues; + this.icon = icon; + this.name = name; + } + + protected MultipleSelectCriterion() { + // constructor for inflating from parceling + } + + // --- parcelable + + /** + * {@inheritDoc} + */ + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeStringArray(entryTitles); + dest.writeStringArray(entryValues); + super.writeToParcel(dest); + } + + /** + * Parcelable Creator Object + */ + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + + /** + * {@inheritDoc} + */ + public MultipleSelectCriterion createFromParcel(Parcel source) { + MultipleSelectCriterion item = new MultipleSelectCriterion(); + item.entryTitles = source.createStringArray(); + item.entryValues = source.createStringArray(); + item.readFromParcel(source); + return item; + } + + /** + * {@inheritDoc} + */ + public MultipleSelectCriterion[] newArray(int size) { + return new MultipleSelectCriterion[size]; + } + + }; + +} diff --git a/api/src/com/todoroo/astrid/api/PermaSql.java b/api/src/com/todoroo/astrid/api/PermaSql.java new file mode 100644 index 0000000000..b8b2386c19 --- /dev/null +++ b/api/src/com/todoroo/astrid/api/PermaSql.java @@ -0,0 +1,65 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.api; + +import java.util.Date; + +import com.todoroo.andlib.utility.DateUtilities; + +/** + * PermaSql allows for creating SQL statements that can be saved and used + * later without dates getting stale. It also allows these values to be + * used in + * + * @author Tim Su + * + */ +public final class PermaSql { + + // --- placeholder strings + + /** value to be replaced with the current time as long */ + public static final String VALUE_NOW = "NOW()"; //$NON-NLS-1$ + + /** value to be replaced by end of day as long */ + public static final String VALUE_EOD = "EOD()"; //$NON-NLS-1$ + + /** value to be replaced by end of day yesterday as long */ + public static final String VALUE_EOD_YESTERDAY = "EODY()"; //$NON-NLS-1$ + + /** value to be replaced by end of day tomorrow as long */ + public static final String VALUE_EOD_TOMORROW = "EODT()"; //$NON-NLS-1$ + + /** value to be replaced by end of day day after tomorrow as long */ + public static final String VALUE_EOD_DAY_AFTER = "EODTT()"; //$NON-NLS-1$ + + /** value to be replaced by end of day next week as long */ + public static final String VALUE_EOD_NEXT_WEEK = "EODW()"; //$NON-NLS-1$ + + /** value to be replaced by approximate end of day next month as long */ + public static final String VALUE_EOD_NEXT_MONTH = "EODM()"; //$NON-NLS-1$ + + /** Replace placeholder strings with actual */ + public static String replacePlaceholders(String value) { + if(value.contains(VALUE_NOW)) + value = value.replace(VALUE_NOW, Long.toString(DateUtilities.now())); + if(value.contains(VALUE_EOD) || value.contains(VALUE_EOD_DAY_AFTER) || + value.contains(VALUE_EOD_NEXT_WEEK) || value.contains(VALUE_EOD_TOMORROW) || + value.contains(VALUE_EOD_YESTERDAY) || value.contains(VALUE_EOD_NEXT_MONTH)) { + Date date = new Date(); + date.setHours(23); + date.setMinutes(59); + date.setSeconds(59); + long time = date.getTime() / 1000l * 1000l; // chop milliseconds off + value = value.replace(VALUE_EOD_YESTERDAY, Long.toString(time - DateUtilities.ONE_DAY)); + value = value.replace(VALUE_EOD, Long.toString(time)); + value = value.replace(VALUE_EOD_TOMORROW, Long.toString(time + DateUtilities.ONE_DAY)); + value = value.replace(VALUE_EOD_DAY_AFTER, Long.toString(time + 2 * DateUtilities.ONE_DAY)); + value = value.replace(VALUE_EOD_NEXT_WEEK, Long.toString(time + 7 * DateUtilities.ONE_DAY)); + value = value.replace(VALUE_EOD_NEXT_MONTH, Long.toString(time + 30 * DateUtilities.ONE_DAY)); + } + return value; + } + +} diff --git a/api/src/com/todoroo/astrid/api/SyncAction.java b/api/src/com/todoroo/astrid/api/SyncAction.java new file mode 100644 index 0000000000..4ecf997c1f --- /dev/null +++ b/api/src/com/todoroo/astrid/api/SyncAction.java @@ -0,0 +1,105 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.api; + +import android.app.PendingIntent; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Represents an intent that can be called to perform synchronization + * + * @author Tim Su + * + */ +public class SyncAction implements Parcelable { + + /** + * Label + */ + public String label = null; + + /** + * Intent to call when invoking this operation + */ + public PendingIntent intent = null; + + /** + * Create an EditOperation object + * + * @param label + * label to display + * @param intent + * intent to invoke + */ + public SyncAction(String label, PendingIntent intent) { + super(); + this.label = label; + this.intent = intent; + } + + /** + * Returns the label of this action + */ + @Override + public String toString() { + return label; + } + + @Override + public int hashCode() { + return label.hashCode() ^ intent.getTargetPackage().hashCode(); + } + + /** + * We consider two sync actions equal if target package is identical + * and the labels are the same. This prevents duplicate pendingIntents + * from creating multiple SyncAction objects. + */ + @Override + public boolean equals(Object o) { + if(!(o instanceof SyncAction)) + return false; + SyncAction other = (SyncAction) o; + return label.equals(other.label) && intent.getTargetPackage().equals(other.intent.getTargetPackage()); + } + + // --- parcelable helpers + + /** + * {@inheritDoc} + */ + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(label); + dest.writeParcelable(intent, 0); + } + + /** + * Parcelable creator + */ + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + /** + * {@inheritDoc} + */ + public SyncAction createFromParcel(Parcel source) { + return new SyncAction(source.readString(), (PendingIntent)source.readParcelable( + PendingIntent.class.getClassLoader())); + } + + /** + * {@inheritDoc} + */ + public SyncAction[] newArray(int size) { + return new SyncAction[size]; + }; + }; + +} diff --git a/api/src/com/todoroo/astrid/api/TaskAction.java b/api/src/com/todoroo/astrid/api/TaskAction.java new file mode 100644 index 0000000000..eeb807ecc5 --- /dev/null +++ b/api/src/com/todoroo/astrid/api/TaskAction.java @@ -0,0 +1,88 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.api; + +import android.app.PendingIntent; +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Represents an intent that can be called on a task + * + * @author Tim Su + * + */ +public class TaskAction implements Parcelable { + + /** + * Label + */ + public String text = null; + + /** + * Intent to call when invoking this operation + */ + public PendingIntent intent = null; + + /** + * Quick action icon + */ + public Bitmap icon = null; + + /** + * Create an EditOperation object + * + * @param text + * label to display + * @param intent + * intent to invoke. {@link #EXTRAS_TASK_ID} will be passed + */ + public TaskAction(String text, PendingIntent intent, Bitmap icon) { + super(); + this.text = text; + this.intent = intent; + this.icon = icon; + } + + // --- parcelable helpers + + /** + * {@inheritDoc} + */ + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(text); + dest.writeParcelable(intent, 0); + dest.writeParcelable(icon, 0); + } + + /** + * Parcelable creator + */ + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + /** + * {@inheritDoc} + */ + public TaskAction createFromParcel(Parcel source) { + return new TaskAction(source.readString(), + (PendingIntent)source.readParcelable(PendingIntent.class.getClassLoader()), + (Bitmap)source.readParcelable(Bitmap.class.getClassLoader())); + } + + /** + * {@inheritDoc} + */ + public TaskAction[] newArray(int size) { + return new TaskAction[size]; + }; + }; + +} diff --git a/api/src/com/todoroo/astrid/api/TaskDecoration.java b/api/src/com/todoroo/astrid/api/TaskDecoration.java new file mode 100644 index 0000000000..e1cf4115c1 --- /dev/null +++ b/api/src/com/todoroo/astrid/api/TaskDecoration.java @@ -0,0 +1,96 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.api; + +import android.os.Parcel; +import android.os.Parcelable; +import android.widget.RemoteViews; +import android.widget.RemoteViews.RemoteView; + +/** + * Represents a line of text displayed in the Task List + * + * @author Tim Su + * + */ +public final class TaskDecoration implements Parcelable { + + /** + * Place decoration between completion box and task title + */ + public static final int POSITION_LEFT = 0; + + /** + * Place decoration between task title and importance bar + */ + public static final int POSITION_RIGHT = 1; + + /** + * {@link RemoteView} decoration + */ + public RemoteViews decoration = null; + + /** + * Decoration position + */ + public int position = POSITION_LEFT; + + /** + * Decorated task background color. 0 is default + */ + public int color = 0; + + /** + * Creates a TaskDetail object + * @param text + * text to display + * @param color + * color to use for text. Use 0 for default color + */ + public TaskDecoration(RemoteViews decoration, int position, int color) { + this.decoration = decoration; + this.position = position; + this.color = color; + } + + // --- parcelable helpers + + /** + * {@inheritDoc} + */ + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(decoration, 0); + dest.writeInt(position); + dest.writeInt(color); + } + + /** + * Parcelable creator + */ + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + /** + * {@inheritDoc} + */ + public TaskDecoration createFromParcel(Parcel source) { + return new TaskDecoration((RemoteViews)source.readParcelable( + RemoteViews.class.getClassLoader()), + source.readInt(), source.readInt()); + } + + /** + * {@inheritDoc} + */ + public TaskDecoration[] newArray(int size) { + return new TaskDecoration[size]; + }; + }; + +} diff --git a/api/src/com/todoroo/astrid/api/TextInputCriterion.java b/api/src/com/todoroo/astrid/api/TextInputCriterion.java new file mode 100644 index 0000000000..f92687dc93 --- /dev/null +++ b/api/src/com/todoroo/astrid/api/TextInputCriterion.java @@ -0,0 +1,103 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.api; + +import android.content.ContentValues; +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * CustomFilterCriteria allow users to build a custom filter by chaining + * together criteria + * + * @author Tim Su + * + */ +public class TextInputCriterion extends CustomFilterCriterion implements Parcelable { + + /** + * Text area prompt + */ + public String prompt; + + /** + * Text area hint + */ + public String hint; + + + /** + * Create a new CustomFilterCriteria object + * + * @param identifier + * @param title + * @param sql + * @param valuesForNewTasks + * @param prompt + * @param hint + * @param icon + * @param name + */ + public TextInputCriterion(String identifier, String title, String sql, + ContentValues valuesForNewTasks, String prompt, String hint, + Bitmap icon, String name) { + this.identifier = identifier; + this.text = title; + this.sql = sql; + this.valuesForNewTasks = valuesForNewTasks; + this.prompt = prompt; + this.hint = hint; + this.icon = icon; + this.name = name; + } + + protected TextInputCriterion() { + // constructor for inflating from parceling + } + + // --- parcelable + + /** + * {@inheritDoc} + */ + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(prompt); + dest.writeString(hint); + super.writeToParcel(dest); + } + + /** + * Parcelable Creator Object + */ + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + + /** + * {@inheritDoc} + */ + public TextInputCriterion createFromParcel(Parcel source) { + TextInputCriterion item = new TextInputCriterion(); + item.prompt = source.readString(); + item.hint = source.readString(); + item.readFromParcel(source); + return item; + } + + /** + * {@inheritDoc} + */ + public TextInputCriterion[] newArray(int size) { + return new TextInputCriterion[size]; + } + + }; + +} diff --git a/api/src/com/todoroo/astrid/api/package-info.java b/api/src/com/todoroo/astrid/api/package-info.java new file mode 100644 index 0000000000..c1d700d1e6 --- /dev/null +++ b/api/src/com/todoroo/astrid/api/package-info.java @@ -0,0 +1,4 @@ +/** + * Astrid API constants and container packages + */ +package com.todoroo.astrid.api; \ No newline at end of file diff --git a/api/src/com/todoroo/astrid/core/SearchFilter.java b/api/src/com/todoroo/astrid/core/SearchFilter.java new file mode 100644 index 0000000000..9b3440edbd --- /dev/null +++ b/api/src/com/todoroo/astrid/core/SearchFilter.java @@ -0,0 +1,73 @@ +package com.todoroo.astrid.core; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.todoroo.astrid.api.FilterListItem; + +/** + * Special filter that triggers the search functionality when accessed. + * + * @author Tim Su + * + */ +public class SearchFilter extends FilterListItem { + + /** + * Constructor for creating a new SearchFilter + * + * @param listingTitle + * Title of this item as displayed on the lists page, e.g. Inbox + */ + public SearchFilter(String listingTitle) { + this.listingTitle = listingTitle; + } + + /** + * Constructor for creating a new SearchFilter + */ + protected SearchFilter() { + // + } + + // --- parcelable + + /** + * {@inheritDoc} + */ + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + } + + /** + * Parcelable creator + */ + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + + /** + * {@inheritDoc} + */ + public SearchFilter createFromParcel(Parcel source) { + SearchFilter item = new SearchFilter(); + item.readFromParcel(source); + return item; + } + + /** + * {@inheritDoc} + */ + public SearchFilter[] newArray(int size) { + return new SearchFilter[size]; + } + + }; + +} diff --git a/api/src/com/todoroo/astrid/core/SortHelper.java b/api/src/com/todoroo/astrid/core/SortHelper.java new file mode 100644 index 0000000000..ebf1b4a2a3 --- /dev/null +++ b/api/src/com/todoroo/astrid/core/SortHelper.java @@ -0,0 +1,103 @@ +package com.todoroo.astrid.core; + +import com.todoroo.andlib.sql.Criterion; +import com.todoroo.andlib.sql.Functions; +import com.todoroo.andlib.sql.Order; +import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.data.TaskApiDao.TaskCriteria; + +/** + * Helpers for sorting a list of tasks + * + * @author Tim Su + * + */ +public class SortHelper { + + public static final int FLAG_REVERSE_SORT = 1 << 0; + public static final int FLAG_SHOW_COMPLETED = 1 << 1; + public static final int FLAG_SHOW_HIDDEN = 1 << 2; + public static final int FLAG_SHOW_DELETED = 1 << 3; + + public static final int SORT_AUTO = 0; + public static final int SORT_ALPHA = 1; + public static final int SORT_DUE = 2; + public static final int SORT_IMPORTANCE = 3; + public static final int SORT_MODIFIED = 4; + + /** preference key for sort flags. stored in public prefs */ + public static final String PREF_SORT_FLAGS = "sort_flags"; //$NON-NLS-1$ + + /** preference key for sort sort. stored in public prefs */ + public static final String PREF_SORT_SORT = "sort_sort"; //$NON-NLS-1$ + + /** + * Takes a SQL query, and if there isn't already an order, creates an order. + * @param originalSql + * @param flags + * @param sort + * @return + */ + @SuppressWarnings("nls") + public static String adjustQueryForFlagsAndSort(String originalSql, int flags, int sort) { + // sort + if(originalSql == null) + originalSql = ""; + if(!originalSql.toUpperCase().contains("ORDER BY")) { + Order order; + switch(sort) { + case SORT_ALPHA: + order = Order.asc(Functions.upper(Task.TITLE)); + break; + case SORT_DUE: + order = Order.asc(Functions.caseStatement(Task.DUE_DATE.eq(0), + DateUtilities.now()*2, Task.DUE_DATE) + "+" + Task.IMPORTANCE + + "+3*" + Task.COMPLETION_DATE); + break; + case SORT_IMPORTANCE: + order = Order.asc(Task.IMPORTANCE + "*" + (2*DateUtilities.now()) + //$NON-NLS-1$ + "+" + Functions.caseStatement(Task.DUE_DATE.eq(0), //$NON-NLS-1$ + Functions.now() + "+" + DateUtilities.ONE_WEEK, //$NON-NLS-1$ + Task.DUE_DATE) + "+8*" + Task.COMPLETION_DATE); + break; + case SORT_MODIFIED: + order = Order.desc(Task.MODIFICATION_DATE); + break; + default: + order = defaultTaskOrder(); + } + + if((flags & FLAG_REVERSE_SORT) > 0) + order = order.reverse(); + originalSql += " ORDER BY " + order; + } + + // flags + if((flags & FLAG_SHOW_COMPLETED) > 0) + originalSql = originalSql.replace(Task.COMPLETION_DATE.eq(0).toString(), + Criterion.all.toString()); + if((flags & FLAG_SHOW_HIDDEN) > 0) + originalSql = originalSql.replace(TaskCriteria.isVisible().toString(), + Criterion.all.toString()); + if((flags & FLAG_SHOW_DELETED) > 0) + originalSql = originalSql.replace(Task.DELETION_DATE.eq(0).toString(), + Criterion.all.toString()); + + return originalSql; + } + + + /** + * Returns SQL task ordering that is astrid's default algorithm + * @return + */ + @SuppressWarnings("nls") + public static Order defaultTaskOrder() { + return Order.asc(Functions.caseStatement(Task.DUE_DATE.eq(0), + DateUtilities.now() + DateUtilities.ONE_WEEK, + Task.DUE_DATE) + " + 200000000 * " + + Task.IMPORTANCE + " + 2*" + Task.COMPLETION_DATE); + } + +} diff --git a/api/src/com/todoroo/astrid/data/Metadata.java b/api/src/com/todoroo/astrid/data/Metadata.java new file mode 100644 index 0000000000..38896369f0 --- /dev/null +++ b/api/src/com/todoroo/astrid/data/Metadata.java @@ -0,0 +1,112 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.data; + + +import android.content.ContentValues; +import android.net.Uri; + +import com.todoroo.andlib.data.AbstractModel; +import com.todoroo.andlib.data.Property; +import com.todoroo.andlib.data.Table; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.data.Property.StringProperty; +import com.todoroo.astrid.api.AstridApiConstants; + +/** + * Data Model which represents a piece of metadata associated with a task + * + * @author Tim Su + * + */ +@SuppressWarnings("nls") +public class Metadata extends AbstractModel { + + // --- table + + /** table for this model */ + public static final Table TABLE = new Table("metadata", Metadata.class); + + /** content uri for this model */ + public static final Uri CONTENT_URI = Uri.parse("content://" + AstridApiConstants.PACKAGE + "/" + + TABLE.name); + + // --- properties + + /** ID */ + public static final LongProperty ID = new LongProperty( + TABLE, ID_PROPERTY_NAME); + + /** Associated Task */ + public static final LongProperty TASK = new LongProperty( + TABLE, "task"); + + /** Metadata Key */ + public static final StringProperty KEY = new StringProperty( + TABLE, "key"); + + /** Metadata Text Value Column 1 */ + public static final StringProperty VALUE1 = new StringProperty( + TABLE, "value"); + + /** Metadata Text Value Column 2 */ + public static final StringProperty VALUE2 = new StringProperty( + TABLE, "value2"); + + /** Metadata Text Value Column 3 */ + public static final StringProperty VALUE3 = new StringProperty( + TABLE, "value3"); + + /** Metadata Text Value Column 4 */ + public static final StringProperty VALUE4 = new StringProperty( + TABLE, "value4"); + + /** Metadata Text Value Column 5 */ + public static final StringProperty VALUE5 = new StringProperty( + TABLE, "value5"); + + /** List of all properties for this model */ + public static final Property[] PROPERTIES = generateProperties(Metadata.class); + + // --- defaults + + /** Default values container */ + private static final ContentValues defaultValues = new ContentValues(); + + @Override + public ContentValues getDefaultValues() { + return defaultValues; + } + + // --- data access boilerplate + + public Metadata() { + super(); + } + + public Metadata(TodorooCursor cursor) { + this(); + readPropertiesFromCursor(cursor); + } + + public void readFromCursor(TodorooCursor cursor) { + super.readPropertiesFromCursor(cursor); + } + + @Override + public long getId() { + return getIdHelper(ID); + }; + + // --- parcelable helpers + + private static final Creator CREATOR = new ModelCreator(Metadata.class); + + @Override + protected Creator getCreator() { + return CREATOR; + } + +} diff --git a/api/src/com/todoroo/astrid/data/MetadataApiDao.java b/api/src/com/todoroo/astrid/data/MetadataApiDao.java new file mode 100644 index 0000000000..d519aaf16b --- /dev/null +++ b/api/src/com/todoroo/astrid/data/MetadataApiDao.java @@ -0,0 +1,101 @@ +package com.todoroo.astrid.data; + +import java.util.ArrayList; +import java.util.HashSet; + +import android.content.ContentValues; +import android.content.Context; + +import com.todoroo.andlib.data.ContentResolverDao; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.sql.Criterion; +import com.todoroo.andlib.sql.Query; + +/** + * Data access object for accessing Astrid's {@link Metadata} table. A + * piece of Metadata is information about a task, for example a tag or a + * note. It operates in a one-to-many relation with tasks. + * + * @author Tim Su + * + */ +public class MetadataApiDao extends ContentResolverDao { + + public MetadataApiDao(Context context) { + super(Metadata.class, context, Metadata.CONTENT_URI); + } + + /** + * Generates SQL clauses + */ + public static class MetadataCriteria { + + /** Returns all metadata associated with a given task */ + public static Criterion byTask(long taskId) { + return Metadata.TASK.eq(taskId); + } + + /** Returns all metadata associated with a given key */ + public static Criterion withKey(String key) { + return Metadata.KEY.eq(key); + } + + /** Returns all metadata associated with a given key */ + public static Criterion byTaskAndwithKey(long taskId, String key) { + return Criterion.and(withKey(key), byTask(taskId)); + } + + } + + /** + * Synchronize metadata for given task id. Deletes rows in database that + * are not identical to those in the metadata list, creates rows that + * have no match. + * + * @param taskId id of task to perform synchronization on + * @param metadata list of new metadata items to save + * @param metadataCriteria criteria to load data for comparison from metadata + */ + public void synchronizeMetadata(long taskId, ArrayList metadata, + Criterion metadataCriteria) { + HashSet newMetadataValues = new HashSet(); + for(Metadata metadatum : metadata) { + metadatum.setValue(Metadata.TASK, taskId); + metadatum.clearValue(Metadata.ID); + newMetadataValues.add(metadatum.getMergedValues()); + } + + Metadata item = new Metadata(); + TodorooCursor cursor = query(Query.select(Metadata.PROPERTIES).where(Criterion.and(MetadataCriteria.byTask(taskId), + metadataCriteria))); + try { + // try to find matches within our metadata list + for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + item.readFromCursor(cursor); + long id = item.getId(); + + // clear item id when matching with incoming values + item.clearValue(Metadata.ID); + ContentValues itemMergedValues = item.getMergedValues(); + if(newMetadataValues.contains(itemMergedValues)) { + newMetadataValues.remove(itemMergedValues); + continue; + } + + // not matched. cut it + delete(id); + } + } finally { + cursor.close(); + } + + // everything that remains shall be written + for(ContentValues values : newMetadataValues) { + item.clear(); + item.mergeWith(values); + save(item); + } + } + + +} diff --git a/api/src/com/todoroo/astrid/data/StoreObject.java b/api/src/com/todoroo/astrid/data/StoreObject.java new file mode 100644 index 0000000000..b80d6da381 --- /dev/null +++ b/api/src/com/todoroo/astrid/data/StoreObject.java @@ -0,0 +1,112 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.data; + + +import android.content.ContentValues; +import android.net.Uri; + +import com.todoroo.andlib.data.AbstractModel; +import com.todoroo.andlib.data.Property; +import com.todoroo.andlib.data.Table; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.data.Property.StringProperty; +import com.todoroo.astrid.api.AstridApiConstants; + +/** + * Data Model which represents a piece of data unrelated to a task + * + * @author Tim Su + * + */ +@SuppressWarnings("nls") +public class StoreObject extends AbstractModel { + + // --- table + + /** table for this model */ + public static final Table TABLE = new Table("store", StoreObject.class); + + /** content uri for this model */ + public static final Uri CONTENT_URI = Uri.parse("content://" + AstridApiConstants.PACKAGE + "/" + + TABLE.name); + + // --- properties + + /** ID */ + public static final LongProperty ID = new LongProperty( + TABLE, ID_PROPERTY_NAME); + + /** Store Type Key */ + public static final StringProperty TYPE = new StringProperty( + TABLE, "type"); + + /** Store Item Key */ + public static final StringProperty ITEM= new StringProperty( + TABLE, "item"); + + /** Store Value Column 1 */ + public static final StringProperty VALUE1 = new StringProperty( + TABLE, "value"); + + /** Store Value Column 2 */ + public static final StringProperty VALUE2 = new StringProperty( + TABLE, "value2"); + + /** Store Value Column 3 */ + public static final StringProperty VALUE3 = new StringProperty( + TABLE, "value3"); + + /** Store Value Column 4 */ + public static final StringProperty VALUE4 = new StringProperty( + TABLE, "value4"); + + /** Store Value Column 5 */ + public static final StringProperty VALUE5 = new StringProperty( + TABLE, "value5"); + + /** List of all properties for this model */ + public static final Property[] PROPERTIES = generateProperties(StoreObject.class); + + // --- defaults + + /** Default values container */ + private static final ContentValues defaultValues = new ContentValues(); + + @Override + public ContentValues getDefaultValues() { + return defaultValues; + } + + // --- data access boilerplate + + public StoreObject() { + super(); + } + + public StoreObject(TodorooCursor cursor) { + this(); + readPropertiesFromCursor(cursor); + } + + public void readFromCursor(TodorooCursor cursor) { + super.readPropertiesFromCursor(cursor); + } + + @Override + public long getId() { + return getIdHelper(ID); + }; + + // --- parcelable helpers + + private static final Creator CREATOR = new ModelCreator(StoreObject.class); + + @Override + protected Creator getCreator() { + return CREATOR; + } + +} diff --git a/api/src/com/todoroo/astrid/data/StoreObjectApiDao.java b/api/src/com/todoroo/astrid/data/StoreObjectApiDao.java new file mode 100644 index 0000000000..dbc7bda4d1 --- /dev/null +++ b/api/src/com/todoroo/astrid/data/StoreObjectApiDao.java @@ -0,0 +1,40 @@ +package com.todoroo.astrid.data; + +import android.content.Context; + +import com.todoroo.andlib.data.ContentResolverDao; +import com.todoroo.andlib.sql.Criterion; + +/** + * Data access object for accessing Astrid's {@link StoreObject} table. A + * StoreObject is an arbitrary piece of data stored inside of Astrid. + * + * @author Tim Su + * + */ +public class StoreObjectApiDao extends ContentResolverDao { + + public StoreObjectApiDao(Context context) { + super(StoreObject.class, context, StoreObject.CONTENT_URI); + } + + // --- SQL clause generators + + /** + * Generates SQL clauses + */ + public static class StoreObjectCriteria { + + /** Returns all store objects with given type */ + public static Criterion byType(String type) { + return StoreObject.TYPE.eq(type); + } + + /** Returns store object with type and key */ + public static Criterion byTypeAndItem(String type, String item) { + return Criterion.and(byType(type), StoreObject.ITEM.eq(item)); + } + + } + +} diff --git a/api/src/com/todoroo/astrid/data/Task.java b/api/src/com/todoroo/astrid/data/Task.java new file mode 100644 index 0000000000..4540080781 --- /dev/null +++ b/api/src/com/todoroo/astrid/data/Task.java @@ -0,0 +1,433 @@ +/* + * Copyright (c) 2009, Todoroo Inc + * All Rights Reserved + * http://www.todoroo.com + */ +package com.todoroo.astrid.data; + + +import java.util.Date; + +import android.content.ContentValues; +import android.content.res.Resources; +import android.net.Uri; + +import com.todoroo.andlib.data.AbstractModel; +import com.todoroo.andlib.data.Property; +import com.todoroo.andlib.data.Table; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.data.Property.IntegerProperty; +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.data.Property.StringProperty; +import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.api.R; + +/** + * Data Model which represents a task users need to accomplish. + * + * @author Tim Su + * + */ +@SuppressWarnings("nls") +public final class Task extends AbstractModel { + + // --- table and uri + + /** table for this model */ + public static final Table TABLE = new Table("tasks", Task.class); + + /** content uri for this model */ + public static final Uri CONTENT_URI = Uri.parse("content://" + AstridApiConstants.PACKAGE + "/" + + TABLE.name); + + // --- properties + + /** ID */ + public static final LongProperty ID = new LongProperty( + TABLE, ID_PROPERTY_NAME); + + /** Name of Task */ + public static final StringProperty TITLE = new StringProperty( + TABLE, "title"); + + /** Importance of Task (see importance flags) */ + public static final IntegerProperty IMPORTANCE = new IntegerProperty( + TABLE, "importance"); + + /** Unixtime Task is due, 0 if not set */ + public static final LongProperty DUE_DATE = new LongProperty( + TABLE, "dueDate"); + + /** Unixtime Task should be hidden until, 0 if not set */ + public static final LongProperty HIDE_UNTIL = new LongProperty( + TABLE, "hideUntil"); + + /** Unixtime Task was created */ + public static final LongProperty CREATION_DATE = new LongProperty( + TABLE, "created"); + + /** Unixtime Task was last touched */ + public static final LongProperty MODIFICATION_DATE = new LongProperty( + TABLE, "modified"); + + /** Unixtime Task was completed. 0 means active */ + public static final LongProperty COMPLETION_DATE = new LongProperty( + TABLE, "completed"); + + /** Unixtime Task was deleted. 0 means not deleted */ + public static final LongProperty DELETION_DATE = new LongProperty( + TABLE, "deleted"); + + /** Cached Details Column - built from add-on detail exposers. A null + * value means there is no value in the cache and it needs to be + * refreshed */ + public static final StringProperty DETAILS = new StringProperty( + TABLE, "details"); + + /** Date details were last updated */ + public static final LongProperty DETAILS_DATE = new LongProperty( + TABLE, "detailsDate"); + + // --- for migration purposes from astrid 2 (eventually we may want to + // move these into the metadata table and treat them as plug-ins + + public static final StringProperty NOTES = new StringProperty( + TABLE, "notes"); + + public static final IntegerProperty ESTIMATED_SECONDS = new IntegerProperty( + TABLE, "estimatedSeconds"); + + public static final IntegerProperty ELAPSED_SECONDS = new IntegerProperty( + TABLE, "elapsedSeconds"); + + public static final LongProperty TIMER_START = new LongProperty( + TABLE, "timerStart"); + + public static final IntegerProperty POSTPONE_COUNT = new IntegerProperty( + TABLE, "postponeCount"); + + /** Flags for when to send reminders */ + public static final IntegerProperty REMINDER_FLAGS = new IntegerProperty( + TABLE, "notificationFlags"); + + /** Reminder period, in milliseconds. 0 means disabled */ + public static final LongProperty REMINDER_PERIOD = new LongProperty( + TABLE, "notifications"); + + /** Unixtime the last reminder was triggered */ + public static final LongProperty REMINDER_LAST = new LongProperty( + TABLE, "lastNotified"); + + /** Unixtime snooze is set (0 -> no snooze) */ + public static final LongProperty REMINDER_SNOOZE = new LongProperty( + TABLE, "snoozeTime"); + + public static final StringProperty RECURRENCE = new StringProperty( + TABLE, "recurrence"); + + public static final IntegerProperty FLAGS = new IntegerProperty( + TABLE, "flags"); + + public static final StringProperty CALENDAR_URI = new StringProperty( + TABLE, "calendarUri"); + + /** List of all properties for this model */ + public static final Property[] PROPERTIES = generateProperties(Task.class); + + // --- flags + + /** whether repeat occurs relative to completion date instead of due date */ + public static final int FLAG_REPEAT_AFTER_COMPLETION = 1 << 1; + + // --- notification flags + + /** whether to send a reminder at deadline */ + public static final int NOTIFY_AT_DEADLINE = 1 << 1; + + /** whether to send reminders while task is overdue */ + public static final int NOTIFY_AFTER_DEADLINE = 1 << 2; + + /** reminder mode non-stop */ + public static final int NOTIFY_NONSTOP = 1 << 3; + + // --- importance settings (note: importance > 3 are supported via plugin) + + public static final int IMPORTANCE_DO_OR_DIE = 0; + public static final int IMPORTANCE_MUST_DO = 1; + public static final int IMPORTANCE_SHOULD_DO = 2; + public static final int IMPORTANCE_NONE = 3; + + /** + * @return colors that correspond to importance values + */ + public static int[] getImportanceColors(Resources r) { + return new int[] { + r.getColor(R.color.importance_1), + r.getColor(R.color.importance_2), + r.getColor(R.color.importance_3), + r.getColor(R.color.importance_4), + r.getColor(R.color.importance_5), + r.getColor(R.color.importance_6), + }; + } + + public static final int IMPORTANCE_MOST = IMPORTANCE_DO_OR_DIE; + public static final int IMPORTANCE_LEAST = IMPORTANCE_NONE; + + // --- defaults + + /** Default values container */ + private static final ContentValues defaultValues = new ContentValues(); + + static { + defaultValues.put(TITLE.name, ""); + defaultValues.put(DUE_DATE.name, 0); + defaultValues.put(HIDE_UNTIL.name, 0); + defaultValues.put(COMPLETION_DATE.name, 0); + defaultValues.put(DELETION_DATE.name, 0); + defaultValues.put(IMPORTANCE.name, IMPORTANCE_NONE); + + defaultValues.put(CALENDAR_URI.name, ""); + defaultValues.put(RECURRENCE.name, ""); + defaultValues.put(REMINDER_PERIOD.name, 0); + defaultValues.put(REMINDER_FLAGS.name, 0); + defaultValues.put(REMINDER_LAST.name, 0); + defaultValues.put(REMINDER_SNOOZE.name, 0); + defaultValues.put(ESTIMATED_SECONDS.name, 0); + defaultValues.put(ELAPSED_SECONDS.name, 0); + defaultValues.put(POSTPONE_COUNT.name, 0); + defaultValues.put(NOTES.name, ""); + defaultValues.put(FLAGS.name, 0); + defaultValues.put(TIMER_START.name, 0); + defaultValues.put(DETAILS.name, (String)null); + defaultValues.put(DETAILS_DATE.name, 0); + } + + @Override + public ContentValues getDefaultValues() { + return defaultValues; + } + + // --- data access boilerplate + + public Task() { + super(); + } + + public Task(TodorooCursor cursor) { + this(); + readPropertiesFromCursor(cursor); + } + + public void readFromCursor(TodorooCursor cursor) { + super.readPropertiesFromCursor(cursor); + } + + @Override + public long getId() { + return getIdHelper(ID); + } + + // --- parcelable helpers + + public static final Creator CREATOR = new ModelCreator(Task.class); + + @Override + protected Creator getCreator() { + return CREATOR; + } + + // --- data access methods + + /** Checks whether task is done. Requires COMPLETION_DATE */ + public boolean isCompleted() { + return getValue(COMPLETION_DATE) > 0; + } + + /** Checks whether task is deleted. Will return false if DELETION_DATE not read */ + public boolean isDeleted() { + // assume false if we didn't load deletion date + if(!containsValue(DELETION_DATE)) + return false; + else + return getValue(DELETION_DATE) > 0; + } + + /** Checks whether task is hidden. Requires HIDDEN_UNTIL */ + public boolean isHidden() { + return getValue(HIDE_UNTIL) > DateUtilities.now(); + } + + /** Checks whether task is done. Requires DUE_DATE */ + public boolean hasDueDate() { + return getValue(DUE_DATE) > 0; + } + + /** + * Returns the set state of the given flag on the given property + * @param property + * @param flag + * @return + */ + public boolean getFlag(IntegerProperty property, int flag) { + return (getValue(property) & flag) > 0; + } + + /** + * Sets the state of the given flag on the given property + * @param property + * @param flag + * @param value + */ + public void setFlag(IntegerProperty property, int flag, boolean value) { + if(value) + setValue(property, getValue(property) | flag); + else + setValue(property, getValue(property) & ~flag); + } + + // --- due and hide until date management + + /** urgency array index -> significance */ + public static final int URGENCY_NONE = 0; + public static final int URGENCY_TODAY = 1; + public static final int URGENCY_TOMORROW = 2; + public static final int URGENCY_DAY_AFTER = 3; + public static final int URGENCY_NEXT_WEEK = 4; + public static final int URGENCY_NEXT_MONTH = 5; + public static final int URGENCY_SPECIFIC_DAY = 6; + public static final int URGENCY_SPECIFIC_DAY_TIME = 7; + + /** hide until array index -> significance */ + public static final int HIDE_UNTIL_NONE = 0; + public static final int HIDE_UNTIL_DUE = 1; + public static final int HIDE_UNTIL_DAY_BEFORE = 2; + public static final int HIDE_UNTIL_WEEK_BEFORE = 3; + public static final int HIDE_UNTIL_SPECIFIC_DAY = 4; + public static final int HIDE_UNTIL_SPECIFIC_DAY_TIME = 5; + + /** + * Creates due date for this task. If this due date has no time associated, + * we move it to the last millisecond of the day. + * + * @param setting + * one of the URGENCY_* constants + * @param customDate + * if specific day or day & time is set, this value + */ + public long createDueDate(int setting, long customDate) { + long date; + + switch(setting) { + case URGENCY_NONE: + date = 0; + break; + case URGENCY_TODAY: + date = DateUtilities.now(); + break; + case URGENCY_TOMORROW: + date = DateUtilities.now() + DateUtilities.ONE_DAY; + break; + case URGENCY_DAY_AFTER: + date = DateUtilities.now() + 2 * DateUtilities.ONE_DAY; + break; + case URGENCY_NEXT_WEEK: + date = DateUtilities.now() + DateUtilities.ONE_WEEK; + break; + case URGENCY_NEXT_MONTH: + date = DateUtilities.oneMonthFromNow(); + break; + case URGENCY_SPECIFIC_DAY: + case URGENCY_SPECIFIC_DAY_TIME: + date = customDate; + break; + default: + throw new IllegalArgumentException("Unknown setting " + setting); + } + + if(date <= 0) + return date; + + Date dueDate = new Date(date / 1000L * 1000L); // get rid of millis + if(setting != URGENCY_SPECIFIC_DAY_TIME) { + dueDate.setHours(23); + dueDate.setMinutes(59); + dueDate.setSeconds(59); + } else if(isEndOfDay(dueDate)) { + dueDate.setSeconds(58); + } + return dueDate.getTime(); + } + + /** + * Create hide until for this task. + * + * @param setting + * one of the HIDE_UNTIL_* constants + * @param customDate + * if specific day is set, this value + * @return + */ + public long createHideUntil(int setting, long customDate) { + long date; + + switch(setting) { + case HIDE_UNTIL_NONE: + return 0; + case HIDE_UNTIL_DUE: + date = getValue(DUE_DATE); + break; + case HIDE_UNTIL_DAY_BEFORE: + date = getValue(DUE_DATE) - DateUtilities.ONE_DAY; + break; + case HIDE_UNTIL_WEEK_BEFORE: + date = getValue(DUE_DATE) - DateUtilities.ONE_WEEK; + break; + case HIDE_UNTIL_SPECIFIC_DAY: + case HIDE_UNTIL_SPECIFIC_DAY_TIME: + date = customDate; + break; + default: + throw new IllegalArgumentException("Unknown setting " + setting); + } + + if(date <= 0) + return date; + + Date hideUntil = new Date(date / 1000L * 1000L); // get rid of millis + if(setting != HIDE_UNTIL_SPECIFIC_DAY_TIME) { + hideUntil.setHours(0); + hideUntil.setMinutes(0); + hideUntil.setSeconds(0); + } + return hideUntil.getTime(); + } + + /** + * @return true if hours, minutes, and seconds indicate end of day + */ + private static boolean isEndOfDay(Date date) { + int hours = date.getHours(); + int minutes = date.getMinutes(); + int seconds = date.getSeconds(); + return hours == 23 && minutes == 59 && seconds == 59; + } + + /** + * Checks whether this due date has a due time or only a date + */ + public boolean hasDueTime() { + if(!hasDueDate()) + return false; + return hasDueTime(getValue(Task.DUE_DATE)); + } + + /** + * Checks whether provided due date has a due time or only a date + */ + public static boolean hasDueTime(long dueDate) { + return !isEndOfDay(new Date(dueDate)); + } + +} diff --git a/api/src/com/todoroo/astrid/data/TaskApiDao.java b/api/src/com/todoroo/astrid/data/TaskApiDao.java new file mode 100644 index 0000000000..0885b72a9b --- /dev/null +++ b/api/src/com/todoroo/astrid/data/TaskApiDao.java @@ -0,0 +1,185 @@ +package com.todoroo.astrid.data; + +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; + +import com.todoroo.andlib.data.ContentResolverDao; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.andlib.sql.Criterion; +import com.todoroo.andlib.sql.Functions; +import com.todoroo.andlib.sql.Query; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.api.PermaSql; + +/** + * Data access object for accessing Astrid's {@link Task} table. If you + * are looking to store extended information about a Task, you probably + * want to use the {@link MetadataApiDao} object. + * + * @author Tim Su + * + */ +public class TaskApiDao extends ContentResolverDao { + + public TaskApiDao(Context context) { + super(Task.class, context, Task.CONTENT_URI); + } + + /** + * Generates SQL clauses + */ + public static class TaskCriteria { + + /** @return tasks by id */ + public static Criterion byId(long id) { + return Task.ID.eq(id); + } + + /** @return tasks that were deleted */ + public static Criterion isDeleted() { + return Task.DELETION_DATE.neq(0); + } + + /** @return tasks that were not deleted */ + public static Criterion notDeleted() { + return Task.DELETION_DATE.eq(0); + } + + /** @return tasks that have not yet been completed or deleted */ + public static Criterion activeAndVisible() { + return Criterion.and(Task.COMPLETION_DATE.eq(0), + Task.DELETION_DATE.eq(0), + Task.HIDE_UNTIL.lt(Functions.now())); + } + + /** @return tasks that have not yet been completed or deleted */ + public static Criterion isActive() { + return Criterion.and(Task.COMPLETION_DATE.eq(0), + Task.DELETION_DATE.eq(0)); + } + + /** @return tasks that are not hidden at current time */ + public static Criterion isVisible() { + return Task.HIDE_UNTIL.lt(Functions.now()); + } + + /** @return tasks that have a due date */ + public static Criterion hasDeadlines() { + return Task.DUE_DATE.neq(0); + } + + /** @return tasks that are due before a certain unixtime */ + public static Criterion dueBeforeNow() { + return Criterion.and(Task.DUE_DATE.gt(0), Task.DUE_DATE.lt(Functions.now())); + } + + /** @return tasks that are due after a certain unixtime */ + public static Criterion dueAfterNow() { + return Task.DUE_DATE.gt(Functions.now()); + } + + /** @return tasks completed before a given unixtime */ + public static Criterion completed() { + return Criterion.and(Task.COMPLETION_DATE.gt(0), Task.COMPLETION_DATE.lt(Functions.now())); + } + + /** @return tasks that have a blank or null title */ + @SuppressWarnings("nls") + public static Criterion hasNoTitle() { + return Criterion.or(Task.TITLE.isNull(), Task.TITLE.eq("")); + } + + } + + /** + * Count tasks matching criterion + * @param criterion + * @return # of tasks matching + */ + public int countTasks(Criterion criterion) { + TodorooCursor cursor = query(Query.select(Task.ID).where(criterion)); + try { + return cursor.getCount(); + } finally { + cursor.close(); + } + } + + /** + * Count tasks matching query tepmlate + * @param queryTemplate + * @return # of tasks matching + */ + public int countTasks(String queryTemplate) { + queryTemplate = PermaSql.replacePlaceholders(queryTemplate); + TodorooCursor cursor = query(Query.select(Task.ID).withQueryTemplate(queryTemplate)); + try { + return cursor.getCount(); + } finally { + cursor.close(); + } + } + + @Override + public boolean save(Task model) { + ContentValues setValues = model.getSetValues(); + if(super.save(model)) { + afterSave(model, setValues); + return true; + } + return false; + } + + /** @return true if task change shouldn't be broadcast */ + public static boolean insignificantChange(ContentValues values) { + if(values == null || values.size() == 0) + return true; + + if(values.containsKey(Task.DETAILS_DATE.name) && + values.containsKey(Task.DETAILS.name) && + values.size() == 2) + return true; + + if(values.containsKey(Task.REMINDER_LAST.name) && + values.size() == 1) + return true; + + return false; + } + + /** + * Send broadcasts on task change (triggers things like task repeats) + * @param task task that was saved + * @param values values that were updated + */ + public static void afterSave(Task task, ContentValues values) { + if(insignificantChange(values)) + return; + + if(values.containsKey(Task.COMPLETION_DATE.name) && task.isCompleted()) { + Context context = ContextManager.getContext(); + if(context != null) { + Intent broadcastIntent; + broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_TASK_COMPLETED); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, task.getId()); + context.sendOrderedBroadcast(broadcastIntent, null); + } + } + + afterTaskListChanged(); + } + + /** + * Send broadcast when task list changes. Widgets should update. + */ + public static void afterTaskListChanged() { + Context context = ContextManager.getContext(); + if(context != null) { + Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_TASK_LIST_UPDATED); + context.sendOrderedBroadcast(broadcastIntent, null); + } + } + +} diff --git a/api/src/com/todoroo/astrid/data/package-info.java b/api/src/com/todoroo/astrid/data/package-info.java new file mode 100644 index 0000000000..ad7c7e063c --- /dev/null +++ b/api/src/com/todoroo/astrid/data/package-info.java @@ -0,0 +1,4 @@ +/** + * Astrid data model classes and ContentResolver data access objects + */ +package com.todoroo.astrid.data; \ No newline at end of file diff --git a/api/src/com/todoroo/astrid/sync/SyncBackgroundService.java b/api/src/com/todoroo/astrid/sync/SyncBackgroundService.java new file mode 100644 index 0000000000..4d92d02554 --- /dev/null +++ b/api/src/com/todoroo/astrid/sync/SyncBackgroundService.java @@ -0,0 +1,151 @@ +package com.todoroo.astrid.sync; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; + +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.andlib.service.ExceptionService; +import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.andlib.utility.Preferences; + +/** + * Performs synchronization service logic in background service to avoid + * ANR (application not responding) messages. + *

    + * Starting this service + * schedules a repeating alarm which handles + * synchronization with your serv + * + * @author Tim Su + * + */ +abstract public class SyncBackgroundService extends Service { + + /** Minimum time before an auto-sync */ + private static final long AUTO_SYNC_MIN_OFFSET = 5*60*1000L; + + @Autowired private ExceptionService exceptionService; + + // --- abstract methods + + abstract protected SyncProvider getSyncProvider(); + + abstract protected SyncProviderUtilities getSyncUtilities(); + + // --- implementation + + public SyncBackgroundService() { + DependencyInjectionService.getInstance().inject(this); + } + + /** Receive the alarm - start the synchronize service! */ + @Override + public void onStart(Intent intent, int startId) { + try { + if(intent != null) + startSynchronization(this); + } catch (Exception e) { + exceptionService.reportError(getSyncUtilities().getIdentifier() + "-bg-sync", e); //$NON-NLS-1$ + } + } + + /** Start the actual synchronization */ + private void startSynchronization(Context context) { + if(context == null || context.getResources() == null) + return; + + ContextManager.setContext(context); + + if(!getSyncUtilities().isLoggedIn()) + return; + + getSyncProvider().synchronize(context); + + stopSelf(); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + // --- alarm management + + /** + * Schedules repeating alarm for auto-synchronization + */ + public void scheduleService() { + int syncFrequencySeconds = 0; + try { + syncFrequencySeconds = Preferences.getIntegerFromString( + getSyncUtilities().getSyncIntervalKey(), -1); + } catch(ClassCastException e) { + Preferences.setStringFromInteger(getSyncUtilities().getSyncIntervalKey(), 0); + } + Context context = ContextManager.getContext(); + if(syncFrequencySeconds <= 0) { + unscheduleService(context); + return; + } + + // figure out synchronization frequency + long interval = 1000L * syncFrequencySeconds; + long offset = computeNextSyncOffset(interval); + + // give a little padding + offset = Math.max(offset, AUTO_SYNC_MIN_OFFSET); + + AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + PendingIntent pendingIntent = PendingIntent.getService(context, getSyncUtilities().getSyncIntervalKey(), + createAlarmIntent(context), PendingIntent.FLAG_UPDATE_CURRENT); + + Log.i("Astrid", "Autosync set for " + offset / 1000 //$NON-NLS-1$ //$NON-NLS-2$ + + " seconds repeating every " + syncFrequencySeconds); //$NON-NLS-1$ + + // cancel all existing + am.cancel(pendingIntent); + + // schedule new + am.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + offset, + interval, pendingIntent); + } + + + /** + * Removes repeating alarm for auto-synchronization + */ + private void unscheduleService(Context context) { + AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); + PendingIntent pendingIntent = PendingIntent.getService(context, 0, + createAlarmIntent(context), PendingIntent.FLAG_UPDATE_CURRENT); + am.cancel(pendingIntent); + } + + /** Create the alarm intent */ + private Intent createAlarmIntent(Context context) { + Intent intent = new Intent(context, getClass()); + return intent; + } + + // --- utility methods + + private long computeNextSyncOffset(long interval) { + // figure out last synchronize time + long lastSyncDate = getSyncUtilities().getLastSyncDate(); + + // if user never synchronized, give them a full offset period before bg sync + if(lastSyncDate != 0) + return Math.max(0, lastSyncDate + interval - DateUtilities.now()); + else + return interval; + } + + +} diff --git a/api/src/com/todoroo/astrid/sync/SyncContainer.java b/api/src/com/todoroo/astrid/sync/SyncContainer.java new file mode 100644 index 0000000000..c2d692f99e --- /dev/null +++ b/api/src/com/todoroo/astrid/sync/SyncContainer.java @@ -0,0 +1,40 @@ +package com.todoroo.astrid.sync; + +import java.util.ArrayList; + +import com.todoroo.andlib.utility.AndroidUtilities; +import com.todoroo.astrid.data.Metadata; +import com.todoroo.astrid.data.Task; + +/** + * Container class for transmitting tasks and including local and remote + * metadata. Synchronization Providers can subclass this class if desired. + * + * @see SyncProvider + * @author Tim Su + * + */ +public class SyncContainer { + public Task task; + public ArrayList metadata; + + /** + * Check if the metadata contains anything with the given key + * @param key + * @return first match. or null + */ + public Metadata findMetadata(String key) { + for(Metadata item : metadata) { + if(AndroidUtilities.equals(key, item.getValue(Metadata.KEY))) + return item; + } + return null; + } + + /** + * Method called when sync container is about to be saved into the database. + */ + public void prepareForSaving() { + // override me necessary + } +} \ No newline at end of file diff --git a/api/src/com/todoroo/astrid/sync/SyncMetadataService.java b/api/src/com/todoroo/astrid/sync/SyncMetadataService.java new file mode 100644 index 0000000000..108025aecb --- /dev/null +++ b/api/src/com/todoroo/astrid/sync/SyncMetadataService.java @@ -0,0 +1,232 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.sync; + +import java.util.ArrayList; + +import android.content.Context; + +import com.todoroo.andlib.data.Property; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.sql.Criterion; +import com.todoroo.andlib.sql.Order; +import com.todoroo.andlib.sql.Query; +import com.todoroo.astrid.data.Metadata; +import com.todoroo.astrid.data.MetadataApiDao; +import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.data.TaskApiDao; +import com.todoroo.astrid.data.MetadataApiDao.MetadataCriteria; +import com.todoroo.astrid.data.TaskApiDao.TaskCriteria; + +abstract public class SyncMetadataService { + + /** metadata key of tag add-on */ + public static final String TAG_KEY = "tags-tag"; //$NON-NLS-1$ + + // --- instance variables + + protected final TaskApiDao taskDao; + protected final MetadataApiDao metadataDao; + + // --- abstract methods + + /** @return metadata key identifying this sync provider's metadata */ + abstract public String getMetadataKey(); + + /** @return sync provider utilities */ + abstract public SyncProviderUtilities getUtilities(); + + /** create a task container based on the given data */ + abstract public TYPE createContainerFromLocalTask(Task task, ArrayList metadata); + + /** @return criterion for matching all metadata keys that your provider synchronizes */ + abstract public Criterion getMetadataCriteria(); + + /** @return criterion for finding local matches of sync container in task database */ + abstract public Criterion getLocalMatchCriteria(TYPE remoteTask); + + /** @return criterion for matching metadata that indicate remote task exists */ + abstract public Criterion getMetadataWithRemoteId(); + + // --- implementation + + public SyncMetadataService(Context context) { + taskDao = new TaskApiDao(context); + metadataDao = new MetadataApiDao(context); + } + + /** + * Clears metadata information. Used when user logs out of sync provider + */ + public void clearMetadata() { + metadataDao.deleteWhere(Metadata.KEY.eq(getMetadataKey())); + } + + /** + * Gets cursor across all task metadata for joining + * + * @return cursor + */ + private TodorooCursor getRemoteTaskMetadata() { + return metadataDao.query(Query.select(Metadata.TASK).where( + Criterion.and(MetadataCriteria.withKey(getMetadataKey()), + getMetadataWithRemoteId())).orderBy(Order.asc(Metadata.TASK))); + } + + /** + * Gets tasks that were created since last sync + * @param properties + * @return + */ + public TodorooCursor getLocallyCreated(Property... properties) { + TodorooCursor tasks = taskDao.query(Query.select(Task.ID).where( + TaskCriteria.isActive()).orderBy(Order.asc(Task.ID))); + + return joinWithMetadata(tasks, false, properties); + } + + /** + * Gets tasks that were modified since last sync + * @param properties + * @return null if never sync'd + */ + public TodorooCursor getLocallyUpdated(Property... properties) { + TodorooCursor tasks; + long lastSyncDate = getUtilities().getLastSyncDate(); + if(lastSyncDate == 0) + tasks = taskDao.query(Query.select(Task.ID).orderBy(Order.asc(Task.ID))); + else + tasks = taskDao.query(Query.select(Task.ID).where(Task.MODIFICATION_DATE. + gt(lastSyncDate)).orderBy(Order.asc(Task.ID))); + + return joinWithMetadata(tasks, true, properties); + } + + private TodorooCursor joinWithMetadata(TodorooCursor tasks, + boolean both, Property... properties) { + try { + TodorooCursor metadata = getRemoteTaskMetadata(); + try { + ArrayList matchingRows = new ArrayList(); + joinRows(tasks, metadata, matchingRows, both); + + return + taskDao.query(Query.select(properties).where(Task.ID.in( + matchingRows.toArray(new Long[matchingRows.size()])))); + } finally { + metadata.close(); + } + } finally { + tasks.close(); + } + } + + /** + * Join rows from two cursors on the first column, assuming its an id column + * @param left + * @param right + * @param matchingRows + * @param both - if false, returns left join, if true, returns both join + */ + private static void joinRows(TodorooCursor left, + TodorooCursor right, ArrayList matchingRows, + boolean both) { + + left.moveToPosition(-1); + right.moveToFirst(); + + while(true) { + left.moveToNext(); + if(left.isAfterLast()) + break; + long leftValue = left.getLong(0); + + // advance right until it is equal or bigger + while(!right.isAfterLast() && right.getLong(0) < leftValue) { + right.moveToNext(); + } + + if(right.isAfterLast()) { + if(!both) + matchingRows.add(leftValue); + continue; + } + + if((right.getLong(0) == leftValue) == both) + matchingRows.add(leftValue); + } + } + + /** + * Searches for a local task with same remote id, updates this task's id + * @param remoteTask + */ + public void findLocalMatch(TYPE remoteTask) { + if(remoteTask.task.getId() != Task.NO_ID) + return; + TodorooCursor cursor = metadataDao.query(Query.select(Metadata.TASK). + where(Criterion.and(MetadataCriteria.withKey(getMetadataKey()), + getLocalMatchCriteria(remoteTask)))); + try { + if(cursor.getCount() == 0) + return; + cursor.moveToFirst(); + remoteTask.task.setId(cursor.get(Metadata.TASK)); + } finally { + cursor.close(); + } + } + + /** + * Saves a task and its metadata + * @param task + */ + public void saveTaskAndMetadata(TYPE task) { + task.prepareForSaving(); + taskDao.save(task.task); + metadataDao.synchronizeMetadata(task.task.getId(), task.metadata, + getMetadataCriteria()); + } + + /** + * Reads a task and its metadata + * @param task + * @return + */ + public TYPE readTaskAndMetadata(TodorooCursor taskCursor) { + Task task = new Task(taskCursor); + + ArrayList metadata = new ArrayList(); + TodorooCursor metadataCursor = metadataDao.query(Query.select(Metadata.PROPERTIES). + where(Criterion.and(MetadataCriteria.byTask(task.getId()), + getMetadataCriteria()))); + try { + for(metadataCursor.moveToFirst(); !metadataCursor.isAfterLast(); metadataCursor.moveToNext()) { + metadata.add(new Metadata(metadataCursor)); + } + } finally { + metadataCursor.close(); + } + + return createContainerFromLocalTask(task, metadata); + } + + /** + * Reads metadata out of a task + * @return null if no metadata found + */ + public Metadata getTaskMetadata(long taskId) { + TodorooCursor cursor = metadataDao.query(Query.select(Metadata.PROPERTIES).where( + MetadataCriteria.byTaskAndwithKey(taskId, getMetadataKey()))); + try { + if(cursor.getCount() == 0) + return null; + cursor.moveToFirst(); + return new Metadata(cursor); + } finally { + cursor.close(); + } + } + +} diff --git a/api/src/com/todoroo/astrid/sync/SyncProvider.java b/api/src/com/todoroo/astrid/sync/SyncProvider.java new file mode 100644 index 0000000000..78d0c15dff --- /dev/null +++ b/api/src/com/todoroo/astrid/sync/SyncProvider.java @@ -0,0 +1,350 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.sync; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; + +import android.app.Activity; +import android.app.Notification; +import android.app.Service; +import android.content.Context; +import android.widget.Toast; + +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.service.NotificationManager; +import com.todoroo.astrid.api.R; +import com.todoroo.astrid.data.Task; + +/** + * A helper class for writing synchronization services for Astrid. This class + * contains logic for merging incoming changes and writing outgoing changes. + *

    + * Use {@link #initiateManual} as the entry point for your synchronization + * service, which should check if a user is logged in. If not, you should + * handle that in the UI, otherwise, you should launch your background + * service to perform synchronization in the background. + *

    + * Your background service should {@link #synchronize}, which in turn + * invokes {@link #initiateBackground} to initiate synchronization. + * + * @author Tim Su + * + */ +public abstract class SyncProvider { + + // --- abstract methods - your services should implement these + + /** + * Perform log in (launching activity if necessary) and sync. This is + * invoked when users manually request synchronization + * + * @param activity + * context + */ + abstract protected void initiateManual(Activity activity); + + /** + * Perform synchronize. Since this can be called from background services, + * you should not open up new activities. Instead, if the user is not signed + * in, your service should do nothing. + */ + abstract protected void initiateBackground(); + + /** + * Updates the text of a notification and the intent to open when tapped + * @param context + * @param notification + * @return notification id (in Android, there is at most one notification + * in the tray for a given id) + */ + abstract protected int updateNotification(Context context, Notification n); + + /** + * Deal with an exception that occurs during synchronization + * + * @param tag + * short string description of where error occurred + * @param e + * exception + * @param displayError + * whether to display error to the user + */ + abstract protected void handleException(String tag, Exception e, boolean displayError); + + /** + * Create a task on the remote server. + * + * @param task + * task to create + * @return task created on remote server + */ + abstract protected TYPE create(TYPE task) throws IOException; + + /** + * Push variables from given task to the remote server. + * + * @param task + * task proxy to push + * @param remoteTask + * remote task that we merged with. may be null + */ + abstract protected void push(TYPE task, TYPE remote) throws IOException; + + /** + * Fetch remote task. Used to re-read merged tasks + * + * @param task + * task with id's to re-read + * @return new Task + */ + abstract protected TYPE pull(TYPE task) throws IOException; + + /** + * Reads a task container from a task in the database + * + * @param task + */ + abstract protected TYPE read(TodorooCursor task) throws IOException; + + /** + * Save task. Used to save local tasks that have been updated and remote + * tasks that need to be created locally + * + * @param task + */ + abstract protected void write(TYPE task) throws IOException; + + /** + * Finds a task in the list with the same remote identifier(s) as + * the task passed in + * + * @return task from list if matches, null otherwise + */ + abstract protected int matchTask(ArrayList tasks, TYPE target); + + /** + * Transfer remote identifier(s) from one task to another + */ + abstract protected void transferIdentifiers(TYPE source, + TYPE destination); + + // --- implementation + + private final Notification notification; + + public SyncProvider() { + // initialize notification + int icon = android.R.drawable.stat_notify_sync; + long when = System.currentTimeMillis(); + notification = new Notification(icon, null, when); + notification.flags |= Notification.FLAG_ONGOING_EVENT; + } + + public void synchronize(final Context context) { + // display toast + if(context instanceof Activity) { + ((Activity) context).runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(context, R.string.SyP_progress_toast, + Toast.LENGTH_LONG).show(); + } + }); + initiateManual((Activity)context); + } else if(context instanceof Service) { + // display notification + final int notificationId = updateNotification(context, notification); + final NotificationManager nm = new NotificationManager.AndroidNotificationManager(context); + nm.notify(notificationId, notification); + + // start next step in background thread + new Thread(new Runnable() { + public void run() { + try { + initiateBackground(); + } finally { + nm.cancel(notificationId); + } + } + }).start(); + } else { + // unit test + initiateBackground(); + } + } + + // --- synchronization logic + + /** + * Helper to synchronize remote tasks with our local database. + * + * This initiates the following process: 1. local changes are read 2. remote + * changes are read 3. local tasks are merged with remote changes and pushed + * across 4. remote changes are then read in + * + * @param data synchronization data structure + */ + protected void synchronizeTasks(SyncData data) throws IOException { + int length; + + // create internal data structures + HashMap remoteNewTaskNameMap = new HashMap(); + length = data.remoteUpdated.size(); + for(int i = 0; i < length; i++) { + TYPE remote = data.remoteUpdated.get(i); + if(remote.task.getId() != Task.NO_ID) + continue; + remoteNewTaskNameMap.put(remote.task.getValue(Task.TITLE), i); + } + + // 1. CREATE: grab newly created tasks and create them remotely + sendLocallyCreated(data, remoteNewTaskNameMap); + + // 2. UPDATE: for each updated local task + sendLocallyUpdated(data); + + // 3. REMOTE: load remote information + readRemotelyUpdated(data); + } + + protected void readRemotelyUpdated(SyncData data) throws IOException { + int length; + // Rearrange remoteTasks so completed tasks get synchronized first. + // This prevents bugs where a repeated task has two copies come down + // the wire, the new version and the completed old version. The new + // version would get merged, then completed, if done in the wrong order. + + Collections.sort(data.remoteUpdated, new Comparator() { + private static final int SENTINEL = -2; + private final int check(TYPE o1, TYPE o2, LongProperty property) { + long o1Property = o1.task.getValue(property); + long o2Property = o2.task.getValue(property); + if(o1Property != 0 && o2Property != 0) + return 0; + else if(o1Property != 0) + return -1; + else if(o2Property != 0) + return 1; + return SENTINEL; + } + public int compare(TYPE o1, TYPE o2) { + int comparison = check(o1, o2, Task.DELETION_DATE); + if(comparison != SENTINEL) + return comparison; + comparison = check(o1, o2, Task.COMPLETION_DATE); + if(comparison != SENTINEL) + return comparison; + return 0; + } + }); + + length = data.remoteUpdated.size(); + for(int i = 0; i < length; i++) { + TYPE remote = data.remoteUpdated.get(i); + + // don't synchronize new & deleted tasks + if(!remote.task.isSaved() && (remote.task.isDeleted())) + continue; + + try { + write(remote); + } catch (Exception e) { + handleException("sync-remote-updated", e, false); //$NON-NLS-1$ + } + } + } + + protected void sendLocallyUpdated(SyncData data) throws IOException { + int length; + length = data.localUpdated.getCount(); + for(int i = 0; i < length; i++) { + data.localUpdated.moveToNext(); + TYPE local = read(data.localUpdated); + try { + if(local.task == null) + continue; + + // if there is a conflict, merge + int remoteIndex = matchTask((ArrayList)data.remoteUpdated, local); + if(remoteIndex != -1) { + TYPE remote = data.remoteUpdated.get(remoteIndex); + push(local, remote); + + // re-read remote task after merge + remote = pull(remote); + remote.task.setId(local.task.getId()); + data.remoteUpdated.set(remoteIndex, remote); + } else { + push(local, null); + } + } catch (Exception e) { + handleException("sync-local-updated", e, false); //$NON-NLS-1$ + } + write(local); + } + } + + protected void sendLocallyCreated(SyncData data, + HashMap remoteNewTaskNameMap) throws IOException { + int length; + length = data.localCreated.getCount(); + for(int i = 0; i < length; i++) { + data.localCreated.moveToNext(); + TYPE local = read(data.localCreated); + try { + + String taskTitle = local.task.getValue(Task.TITLE); + + /* If there exists an incoming remote task with the same name and no + * mapping, we don't want to create this on the remote server, + * because user could have synchronized like this before. Instead, + * we create a mapping and do an update. + */ + if (remoteNewTaskNameMap.containsKey(taskTitle)) { + int remoteIndex = remoteNewTaskNameMap.remove(taskTitle); + TYPE remote = data.remoteUpdated.get(remoteIndex); + + transferIdentifiers(remote, local); + push(local, remote); + + // re-read remote task after merge, update remote task list + remote = pull(remote); + remote.task.setId(local.task.getId()); + data.remoteUpdated.set(remoteIndex, remote); + + } else { + create(local); + } + } catch (Exception e) { + handleException("sync-local-created", e, false); //$NON-NLS-1$ + } + write(local); + } + } + + // --- helper classes + + /** data structure builder */ + protected static class SyncData { + public ArrayList remoteUpdated; + + public TodorooCursor localCreated; + public TodorooCursor localUpdated; + + public SyncData(ArrayList remoteUpdated, + TodorooCursor localCreated, + TodorooCursor localUpdated) { + super(); + this.remoteUpdated = remoteUpdated; + this.localCreated = localCreated; + this.localUpdated = localUpdated; + } + + } +} diff --git a/api/src/com/todoroo/astrid/sync/SyncProviderPreferences.java b/api/src/com/todoroo/astrid/sync/SyncProviderPreferences.java new file mode 100644 index 0000000000..389b169a24 --- /dev/null +++ b/api/src/com/todoroo/astrid/sync/SyncProviderPreferences.java @@ -0,0 +1,199 @@ +package com.todoroo.astrid.sync; + +import java.util.Date; + +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.res.Resources; +import android.graphics.Color; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.view.View; +import android.view.ViewGroup.OnHierarchyChangeListener; + +import com.todoroo.andlib.utility.AndroidUtilities; +import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.andlib.utility.DialogUtilities; +import com.todoroo.andlib.utility.TodorooPreferenceActivity; +import com.todoroo.astrid.api.R; + +/** + * Utility class for common synchronization action: displaying synchronization + * preferences and an action panel so users can initiate actions from the menu. + * + * @author Tim Su sync + if(!loggedIn) { + status = r.getString(R.string.sync_status_loggedout); + statusColor = Color.RED; + preference.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference p) { + startSync(); + return true; + } + }); + } + // sync is occurring + else if(getUtilities().isOngoing()) { + status = r.getString(R.string.sync_status_ongoing); + statusColor = Color.rgb(0, 0, 100); + } + // last sync was error + else if(getUtilities().getLastAttemptedSyncDate() != 0) { + status = r.getString(R.string.sync_status_failed, + DateUtilities.getDateStringWithTime(SyncProviderPreferences.this, + new Date(getUtilities().getLastAttemptedSyncDate()))); + if(getUtilities().getLastSyncDate() > 0) { + subtitle = r.getString(R.string.sync_status_failed_subtitle, + DateUtilities.getDateStringWithTime(SyncProviderPreferences.this, + new Date(getUtilities().getLastSyncDate()))); + } + statusColor = Color.rgb(100, 0, 0); + preference.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference p) { + String error = getUtilities().getLastError(); + if(error != null) + DialogUtilities.okDialog(SyncProviderPreferences.this, error, null); + return true; + } + }); + } else if(getUtilities().getLastSyncDate() > 0) { + status = r.getString(R.string.sync_status_success, + DateUtilities.getDateStringWithTime(SyncProviderPreferences.this, + new Date(getUtilities().getLastSyncDate()))); + statusColor = Color.rgb(0, 100, 0); + } else { + status = r.getString(R.string.sync_status_never); + statusColor = Color.rgb(0, 0, 100); + preference.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference p) { + startSync(); + return true; + } + }); + } + preference.setTitle(status); + preference.setSummary(subtitle); + + View view = findViewById(R.id.status); + if(view != null) + view.setBackgroundColor(statusColor); + } + + // sync button + else if (r.getString(R.string.sync_SPr_sync_key).equals(preference.getKey())) { + boolean loggedIn = getUtilities().isLoggedIn(); + preference.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference p) { + startSync(); + return true; + } + }); + if(!loggedIn) + preference.setTitle(R.string.sync_SPr_sync_log_in); + } + + // log out button + else if (r.getString(R.string.sync_SPr_forget_key).equals(preference.getKey())) { + boolean loggedIn = getUtilities().isLoggedIn(); + preference.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference p) { + DialogUtilities.okCancelDialog(SyncProviderPreferences.this, + r.getString(R.string.sync_forget_confirm), new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int which) { + logOut(); + initializePreference(getPreferenceScreen()); + } + }, null); + return true; + } + }); + if(!loggedIn) + preference.setEnabled(false); + } + } + +} \ No newline at end of file diff --git a/api/src/com/todoroo/astrid/sync/SyncProviderUtilities.java b/api/src/com/todoroo/astrid/sync/SyncProviderUtilities.java new file mode 100644 index 0000000000..0f43d9d2ba --- /dev/null +++ b/api/src/com/todoroo/astrid/sync/SyncProviderUtilities.java @@ -0,0 +1,137 @@ +package com.todoroo.astrid.sync; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.preference.PreferenceManager; + +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.andlib.utility.DateUtilities; + +/** + * Sync Provider Utility class for accessing preferences + */ +abstract public class SyncProviderUtilities { + + /** + * @return your plugin identifier + */ + abstract public String getIdentifier(); + + /** + * @return key for sync interval + */ + abstract public int getSyncIntervalKey(); + + // --- implementation + + protected static final String PREF_TOKEN = "_token"; //$NON-NLS-1$ + + protected static final String PREF_LAST_SYNC = "_last_sync"; //$NON-NLS-1$ + + protected static final String PREF_LAST_ATTEMPTED_SYNC = "_last_attempted"; //$NON-NLS-1$ + + protected static final String PREF_LAST_ERROR = "_last_error"; //$NON-NLS-1$ + + protected static final String PREF_ONGOING = "_ongoing"; //$NON-NLS-1$ + + /** Get preferences object from the context */ + protected static SharedPreferences getPrefs() { + return PreferenceManager.getDefaultSharedPreferences(ContextManager.getContext()); + } + + /** + * @return true if we have a token for this user, false otherwise + */ + public boolean isLoggedIn() { + return getPrefs().getString(getIdentifier() + PREF_TOKEN, null) != null; + } + + /** authentication token, or null if doesn't exist */ + public String getToken() { + return getPrefs().getString(getIdentifier() + PREF_TOKEN, null); + } + + /** Sets the authentication token. Set to null to clear. */ + public void setToken(String setting) { + Editor editor = getPrefs().edit(); + editor.putString(getIdentifier() + PREF_TOKEN, setting); + editor.commit(); + } + + /** @return Last Successful Sync Date, or 0 */ + public long getLastSyncDate() { + return getPrefs().getLong(getIdentifier() + PREF_LAST_SYNC, 0); + } + + /** @return Last Attempted Sync Date, or 0 if it was successful */ + public long getLastAttemptedSyncDate() { + return getPrefs().getLong(getIdentifier() + PREF_LAST_ATTEMPTED_SYNC, 0); + } + + /** @return Last Error, or null if no last error */ + public String getLastError() { + return getPrefs().getString(PREF_LAST_ERROR, null); + } + + /** @return Last Error, or null if no last error */ + public boolean isOngoing() { + return getPrefs().getBoolean(getIdentifier() + PREF_ONGOING, false); + } + + /** Deletes Last Successful Sync Date */ + public void clearLastSyncDate() { + Editor editor = getPrefs().edit(); + editor.remove(getIdentifier() + PREF_LAST_SYNC); + editor.commit(); + } + + /** Set Last Successful Sync Date */ + public void setLastError(String error) { + Editor editor = getPrefs().edit(); + editor.putString(getIdentifier() + PREF_LAST_ERROR, error); + editor.commit(); + } + + /** Set Ongoing */ + public void stopOngoing() { + Editor editor = getPrefs().edit(); + editor.putBoolean(getIdentifier() + PREF_ONGOING, false); + editor.commit(); + } + + /** Set Last Successful Sync Date */ + public void recordSuccessfulSync() { + Editor editor = getPrefs().edit(); + editor.putLong(getIdentifier() + PREF_LAST_SYNC, DateUtilities.now()); + editor.putLong(getIdentifier() + PREF_LAST_ATTEMPTED_SYNC, 0); + editor.commit(); + } + + /** Set Last Attempted Sync Date */ + public void recordSyncStart() { + Editor editor = getPrefs().edit(); + editor.putLong(getIdentifier() + PREF_LAST_ATTEMPTED_SYNC, + DateUtilities.now()); + editor.putString(getIdentifier() + PREF_LAST_ERROR, null); + editor.putBoolean(getIdentifier() + PREF_ONGOING, true); + editor.commit(); + } + + /** + * Reads the frequency, in seconds, auto-sync should occur. + * + * @return seconds duration, or 0 if not desired + */ + public int getSyncAutoSyncFrequency() { + String value = getPrefs().getString( + ContextManager.getContext().getString( + getSyncIntervalKey()), null); + if (value == null) + return 0; + try { + return Integer.parseInt(value); + } catch (Exception e) { + return 0; + } + } +} diff --git a/api/src/com/todoroo/astrid/sync/package-info.java b/api/src/com/todoroo/astrid/sync/package-info.java new file mode 100644 index 0000000000..cb6c32013d --- /dev/null +++ b/api/src/com/todoroo/astrid/sync/package-info.java @@ -0,0 +1,4 @@ +/** + * Astrid synchronization helpers + */ +package com.todoroo.astrid.sync; \ No newline at end of file