From cd64b2e8eadf98aa703dcd4eee61da1657b1bb87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 5 Nov 2025 15:45:14 +0400 Subject: [PATCH 01/35] feat: add directories to each platform --- contactos_android/.gitkeep | 0 contactos_ios/.gitkeep | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 contactos_android/.gitkeep create mode 100644 contactos_ios/.gitkeep diff --git a/contactos_android/.gitkeep b/contactos_android/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/contactos_ios/.gitkeep b/contactos_ios/.gitkeep new file mode 100644 index 0000000..e69de29 From 310a47dbab43787715c485ad56f51b76d4429ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Tue, 25 Nov 2025 16:29:31 +0400 Subject: [PATCH 02/35] feat(1.0.1): added `LICENCE` --- contactos_platform_interface/CHANGELOG.md | 3 ++ contactos_platform_interface/LICENSE | 14 +++---- .../lib/contactos_platform_interface.dart | 4 ++ .../lib/src/contactos_platform_interface.dart | 4 ++ .../lib/src/types.dart | 4 ++ contactos_platform_interface/pubspec.lock | 38 +++++++++---------- contactos_platform_interface/pubspec.yaml | 24 +++++++++--- 7 files changed, 58 insertions(+), 33 deletions(-) diff --git a/contactos_platform_interface/CHANGELOG.md b/contactos_platform_interface/CHANGELOG.md index 0ea42ac..3a90c72 100644 --- a/contactos_platform_interface/CHANGELOG.md +++ b/contactos_platform_interface/CHANGELOG.md @@ -1,2 +1,5 @@ +## 1.0.1 +- **ADDED**: `LICENSE` file to the package + ## 1.0.0 - **ADDED**: Initial release \ No newline at end of file diff --git a/contactos_platform_interface/LICENSE b/contactos_platform_interface/LICENSE index 789dcc0..4de1fa1 100644 --- a/contactos_platform_interface/LICENSE +++ b/contactos_platform_interface/LICENSE @@ -1,18 +1,16 @@ BSD 3-Clause License -Copyright (c) 2025, Anton Ustinoff +Copyright (c) 2025, Anton Ustinoff All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. 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. - +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. 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. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/contactos_platform_interface/lib/contactos_platform_interface.dart b/contactos_platform_interface/lib/contactos_platform_interface.dart index f6e2177..921c728 100644 --- a/contactos_platform_interface/lib/contactos_platform_interface.dart +++ b/contactos_platform_interface/lib/contactos_platform_interface.dart @@ -1,3 +1,7 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + export 'src/contactos_platform_interface.dart'; export 'src/method_channel_contactos.dart'; export 'src/types.dart' hide JSON; diff --git a/contactos_platform_interface/lib/src/contactos_platform_interface.dart b/contactos_platform_interface/lib/src/contactos_platform_interface.dart index 93b4c17..14b3bce 100644 --- a/contactos_platform_interface/lib/src/contactos_platform_interface.dart +++ b/contactos_platform_interface/lib/src/contactos_platform_interface.dart @@ -1,3 +1,7 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:typed_data'; import 'package:contactos_platform_interface/contactos_platform_interface.dart'; diff --git a/contactos_platform_interface/lib/src/types.dart b/contactos_platform_interface/lib/src/types.dart index d3dc628..f9a7ed1 100644 --- a/contactos_platform_interface/lib/src/types.dart +++ b/contactos_platform_interface/lib/src/types.dart @@ -1,3 +1,7 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:typed_data'; import 'package:collection/collection.dart'; diff --git a/contactos_platform_interface/pubspec.lock b/contactos_platform_interface/pubspec.lock index 15b5e8e..83e7d69 100644 --- a/contactos_platform_interface/pubspec.lock +++ b/contactos_platform_interface/pubspec.lock @@ -58,10 +58,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -71,42 +71,42 @@ packages: dependency: "direct dev" description: name: l - sha256: "4f4c5a88028cd052e0840e7420f57fbf2a32fb945c45355db7bacb70bfa94a9f" + sha256: "48db3024c2f74e1bc84b753b17d6754a066969c246de59505d6306f5154cbc86" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.0.1" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.0.0" matcher: dependency: transitive description: @@ -127,10 +127,10 @@ packages: dependency: "direct main" description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" path: dependency: transitive description: @@ -196,18 +196,18 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.7" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -217,5 +217,5 @@ packages: source: hosted version: "15.0.0" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.8.0 <4.0.0" flutter: ">=3.29.3" diff --git a/contactos_platform_interface/pubspec.yaml b/contactos_platform_interface/pubspec.yaml index c53f0d3..1bfb754 100644 --- a/contactos_platform_interface/pubspec.yaml +++ b/contactos_platform_interface/pubspec.yaml @@ -1,13 +1,27 @@ name: contactos_platform_interface description: A common platform interface for the contactos plugin. +version: 1.0.1 + +homepage: https://github.com/ziqq/contactos/contactos_platform_interface + repository: https://github.com/ziqq/contactos/contactos_platform_interface + issue_tracker: https://github.com/ziqq/contactos/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+contactos%22 -version: 1.0.0 + +funding: + - https://www.buymeacoffee.com/ziqq + - https://www.patreon.com/ziqq + - https://boosty.to/ziqq + +topics: + - contacts + environment: sdk: '>=3.6.0 <4.0.0' flutter: ">=3.29.3" + dependencies: flutter: sdk: flutter @@ -17,15 +31,13 @@ dependencies: collection: '>=1.19.0 <2.0.0' meta: '>=1.15.0 <2.0.0' + dev_dependencies: flutter_test: sdk: flutter # Linting - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 # Logging - l: ^5.0.0 - -topics: - - contacts + l: ^5.0.1 From 25a362a5ecf023d5b69415242880baf8c9c67055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Tue, 25 Nov 2025 16:31:05 +0400 Subject: [PATCH 03/35] chore: update workflow --- .github/workflows/checkout.yml | 266 +++++++++++++++------------------ 1 file changed, 117 insertions(+), 149 deletions(-) diff --git a/.github/workflows/checkout.yml b/.github/workflows/checkout.yml index 51bb841..af3d328 100644 --- a/.github/workflows/checkout.yml +++ b/.github/workflows/checkout.yml @@ -1,177 +1,145 @@ -name: "Checkout" - +name: CI on: - workflow_call: - + push: + branches: [ main ] + paths: + - ".github/workflows/**" + - "contactos/**" + - "contactos_android/**" + - "contactos_foundation/**" + - "contactos_platform_interface/**" + pull_request: + branches: [ main ] + paths: + - ".github/workflows/**" + - "contactos/**" + - "contactos_android/**" + - "contactos_foundation/**" + - "contactos_platform_interface/**" + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true jobs: - checkout: + detect-changes: + name: Detect changed packages runs-on: ubuntu-latest - defaults: - run: - working-directory: ./contactos/ - env: - pub-cache-name: pub - threshold: 50 - timeout-minutes: 15 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - - name: πŸš‚ Get latest code - id: checkout - uses: actions/checkout@v4 - with: - sparse-checkout: | - .github - pubspec.yaml - lib - test - analysis_options.yaml - CHANGELOG.md - - - name: πŸ‘· Install flutter - uses: subosito/flutter-action@v2 - id: install-flutter + - uses: actions/checkout@v4 with: - channel: 'stable' - - - name: πŸ”Ž Check flutter version - id: check-flutter-version - run: flutter --version - - - name: πŸ“€ Restore Pub modules - id: cache-pub-restore - uses: actions/cache/restore@v4 - with: - path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} - - - name: πŸ—„οΈ Export Pub cache directory - id: export-pub-cache - timeout-minutes: 1 - run: | - export PUB_CACHE=$PWD/.pub_cache/ - export PATH="$PATH":"$HOME/.pub-cache/bin" - echo "${HOME}/.pub-cache/bin" >> $GITHUB_PATH - - - name: πŸ‘· Install Dependencies - id: install-dependencies - timeout-minutes: 2 + fetch-depth: 0 + - id: set-matrix run: | - apt-get update && apt-get install -y lcov - flutter pub get --no-example - - - name: πŸ“₯ Save Pub modules - id: cache-pub-save - uses: actions/cache/save@v4 - with: - path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} - - - name: πŸ”Ž Check content - id: check_files - uses: andstor/file-existence-action@v1 - with: - files: "README.md, CHANGELOG.md, LICENSE, example" - - - name: ❔ File exists - if: steps.check_files.outputs.files_exists == 'true' - run: echo Content is ok! - - - name: πŸ”Ž Check description - run: echo | grep -q Description README.md ; echo $? - - - name: πŸ”Ž Check example - run: echo | grep -q Example README.md ; echo $? + MATRIX_JSON='{"include":[{"name":"plugin","path":"contactos"},{"name":"android","path":"contactos_android"},{"name":"foundation","path":"contactos_foundation"},{"name":"interface","path":"contactos_platform_interface"}]}' + echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT - - name: πŸ”Ž Check installation - run: echo | grep -q Installation README.md ; echo $? - - - name: πŸ‘· Install dependencies - run: flutter pub get - - - name: πŸ§ͺ Run dependency validator - run: | - dart pub global activate dependency_validator - dart pub global run dependency_validator:dependency_validator - - - name: πŸ”Ž Check format - id: check_format - run: dart format --set-exit-if-changed -l 80 -o none lib/ - - - name: πŸ“ˆ Check analyzer - id: check_analyzer - run: dart analyze --fatal-infos --fatal-warnings lib/ - - testing: + package-ci: + name: Package (${{ matrix.name }}) + needs: detect-changes runs-on: ubuntu-latest - defaults: - run: - working-directory: ./contactos/ - env: - pub-cache-name: pub - threshold: 50 - timeout-minutes: 15 + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.detect-changes.outputs.matrix) }} steps: - - name: πŸš‚ Get latest code - id: checkout + - name: Checkout uses: actions/checkout@v4 - with: - sparse-checkout: | - .github - pubspec.yaml - lib - test - analysis_options.yaml - CHANGELOG.md - - name: πŸ‘· Install flutter + - name: Install Flutter uses: subosito/flutter-action@v2 - id: install-flutter with: - channel: 'stable' + channel: stable - - name: πŸ”Ž Check flutter version - id: check-flutter-version + - name: Flutter version run: flutter --version - - name: πŸ“€ Restore Pub modules - id: cache-pub-restore - uses: actions/cache/restore@v4 + - name: Cache pub + uses: actions/cache@v4 with: path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} + ~/.pub-cache + key: ${{ runner.os }}-pub-${{ matrix.name }}-${{ hashFiles(format('{0}/pubspec.lock', matrix.path)) }} + restore-keys: | + ${{ runner.os }}-pub-${{ matrix.name }}- + ${{ runner.os }}-pub- + + - name: Pub get + working-directory: ${{ matrix.path }} + run: flutter pub get - - name: πŸ—„οΈ Export Pub cache directory - id: export-pub-cache - timeout-minutes: 1 - run: | - export PUB_CACHE=$PWD/.pub_cache/ - export PATH="$PATH":"$HOME/.pub-cache/bin" - echo "${HOME}/.pub-cache/bin" >> $GITHUB_PATH + - name: Format check + working-directory: ${{ matrix.path }} + run: dart format --set-exit-if-changed -l 80 . - - name: πŸ‘· Install Dependencies - id: install-dependencies - timeout-minutes: 2 + - name: Analyze + working-directory: ${{ matrix.path }} + run: dart analyze --fatal-infos --fatal-warnings lib/ + + - name: Test (with coverage if Dart/Flutter tests exist) + working-directory: ${{ matrix.path }} run: | - apt-get update && apt-get install -y lcov - flutter pub get --no-example + if ls test/*.dart >/dev/null 2>&1; then + flutter test --coverage + else + echo "No tests" + fi - - name: πŸ“₯ Save Pub modules - id: cache-pub-save - uses: actions/cache/save@v4 + - name: Upload partial coverage artifact + if: success() + uses: actions/upload-artifact@v4 with: - path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} + name: coverage-${{ matrix.name }} + path: ${{ matrix.path }}/coverage/lcov.info + if-no-files-found: ignore - - name: πŸ§ͺ Run tests - timeout-minutes: 2 + coverage-merge: + name: Merge coverage & Codecov + needs: package-ci + runs-on: ubuntu-latest + if: always() + steps: + - uses: actions/checkout@v4 + - name: Download coverage artifacts + uses: actions/download-artifact@v4 + with: + path: coverage_parts + pattern: coverage-* + merge-multiple: true + - name: Merge lcov + run: | + sudo apt-get update && sudo apt-get install -y lcov + mkdir merged || true + FILES=$(find coverage_parts -name "lcov.info") + if [ -z "$FILES" ]; then + echo "No coverage files"; exit 0 + fi + # ΠΠ°Ρ‡Π°Π»ΡŒΠ½Ρ‹ΠΉ Ρ„Π°ΠΉΠ» + FIRST=true + for f in $FILES; do + if $FIRST; then + cp "$f" merged/lcov.info + FIRST=false + else + lcov -a merged/lcov.info -a "$f" -o merged/lcov.tmp + mv merged/lcov.tmp merged/lcov.info + fi + done + lcov --remove merged/lcov.info '*_test.dart' -o merged/lcov.info || true + echo "Merged coverage:" + lcov --summary merged/lcov.info || true + - name: Upload merged artifact + uses: actions/upload-artifact@v4 + with: + name: coverage-merged + path: merged/lcov.info + if-no-files-found: ignore + - name: Upload to Codecov + if: env.CODECOV_TOKEN != '' env: - CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} - if: success() + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: | - flutter test --coverage - bash <(curl -s https://codecov.io/bash) -f coverage/lcov.info \ No newline at end of file + bash <(curl -s https://codecov.io/bash) -f merged/lcov.info || echo "Codecov upload skipped" From ac074986552283dcaba87fba329f0d92d55ae9cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Tue, 25 Nov 2025 16:31:15 +0400 Subject: [PATCH 04/35] chore: update vscode settings --- .vscode/launch.json | 21 ++++++++++++++++++--- .vscode/settings.json | 2 +- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 8471a77..ae2d8aa 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,7 +2,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Run Android or iOS Example", + "name": "Run Example", "type": "dart", "request": "launch", "cwd": "${workspaceFolder}/contactos/example", @@ -10,10 +10,25 @@ "env": {} }, { - "name": "Flutter Test", + "name": "Run Android", + "type": "dart", + "request": "launch", + "cwd": "${workspaceFolder}/contactos_android/example", + "args": [], + "env": {} + }, + { + "name": "Run IOS", "type": "dart", "request": "launch", - "cwd": "${workspaceFolder}/contactos", + "cwd": "${workspaceFolder}/contactos_foundation/example", + "args": [], + "env": {} + }, + { + "name": "Flutter Test", + "request": "launch", + "type": "dart", "program": "test/contacts_test.dart", "env": { "ENVIRONMENT": "test" diff --git a/.vscode/settings.json b/.vscode/settings.json index 914d444..589159f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -63,7 +63,7 @@ "explorer.fileNesting.expand": false, "explorer.fileNesting.patterns": { "pubspec.yaml": ".flutter-plugins, .packages, .dart_tool, .flutter-plugins-dependencies, .metadata, .packages, pubspec.lock, build.yaml, analysis_options.yaml, all_lint_rules.yaml, dart*.yaml, flutter*.yaml, icons_launcher.yaml, devtools_options.yaml, l10n.yaml", - ".gitignore": ".gitattributes, .gitmodules, .gitmessage, .mailmap, .git-blame*", + ".gitignore": ".gitattributes, .gitmodules, .gitmessage, .mailmap, .git-blame*, .pubignore", "readme*": "authors, backers.md, changelog*, citation*, code_of_conduct.md, codeowners, contributing.md, contributors, copying, credits, governance.md, history.md, license*, maintainers, readme*, security.md, sponsors.md", "*.dart": "$(capture).g.dart, $(capture).gr.dart, $(capture).freezed.dart, $(capture).config.dart, $(capture).mocks.dart, $(capture).router.dart, $(capture).locator.dart, $(capture)_skeleton.dart" }, From 45ad091aa465afd9588be97c7f3de56261e55009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Tue, 25 Nov 2025 16:36:31 +0400 Subject: [PATCH 05/35] feat(0.0.1): add `foundation` implementation of the contactos plugin --- contactos_foundation/.gitignore | 82 +++ contactos_foundation/.pubignore | 5 + contactos_foundation/CHANGELOG.md | 2 + contactos_foundation/LICENSE | 27 + contactos_foundation/Makefile | 75 ++ contactos_foundation/README.md | 15 + contactos_foundation/analysis_options.yaml | 235 +++++++ .../dart_dependency_validator.yaml | 8 + .../darwin/contactos_foundation.podspec | 38 ++ .../darwin/contactos_foundation/Package.swift | 28 + .../ContactosPlugin.swift | 641 ++++++++++++++++++ .../Resources/PrivacyInfo.xcprivacy | 25 + contactos_foundation/example/.gitignore | 32 + contactos_foundation/example/.metadata | 8 + contactos_foundation/example/README.md | 8 + contactos_foundation/example/ios/.gitignore | 43 ++ .../ios/Flutter/AppFrameworkInfo.plist | 30 + .../example/ios/Flutter/Debug.xcconfig | 2 + .../example/ios/Flutter/Release.xcconfig | 2 + contactos_foundation/example/ios/Podfile | 49 ++ contactos_foundation/example/ios/Podfile.lock | 35 + .../ios/Runner.xcodeproj/project.pbxproj | 525 ++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 96 +++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 121 ++++ .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 564 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 1588 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 1025 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 1716 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 1920 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 1895 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 3831 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 1888 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 3294 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 3612 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + .../example/ios/Runner/Info.plist | 55 ++ .../ios/Runner/Runner-Bridging-Header.h | 1 + contactos_foundation/example/lib/main.dart | 99 +++ .../example/lib/src/contacts_list_screen.dart | 588 ++++++++++++++++ .../src/navite_contacts_picker_screen.dart | 59 ++ contactos_foundation/example/pubspec.lock | 343 ++++++++++ contactos_foundation/example/pubspec.yaml | 34 + .../example/test/widget_test.dart | 33 + .../lib/contactos_foundation.dart | 7 + .../lib/src/contactos_foundation.dart | 161 +++++ contactos_foundation/pubspec.lock | 419 ++++++++++++ contactos_foundation/pubspec.yaml | 64 ++ contactos_ios/.gitkeep | 0 63 files changed, 4132 insertions(+) create mode 100644 contactos_foundation/.gitignore create mode 100644 contactos_foundation/.pubignore create mode 100644 contactos_foundation/CHANGELOG.md create mode 100644 contactos_foundation/LICENSE create mode 100644 contactos_foundation/Makefile create mode 100644 contactos_foundation/README.md create mode 100644 contactos_foundation/analysis_options.yaml create mode 100644 contactos_foundation/dart_dependency_validator.yaml create mode 100644 contactos_foundation/darwin/contactos_foundation.podspec create mode 100644 contactos_foundation/darwin/contactos_foundation/Package.swift create mode 100644 contactos_foundation/darwin/contactos_foundation/Sources/contactos_foundation/ContactosPlugin.swift create mode 100644 contactos_foundation/darwin/contactos_foundation/Sources/contactos_foundation/Resources/PrivacyInfo.xcprivacy create mode 100644 contactos_foundation/example/.gitignore create mode 100644 contactos_foundation/example/.metadata create mode 100644 contactos_foundation/example/README.md create mode 100644 contactos_foundation/example/ios/.gitignore create mode 100644 contactos_foundation/example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 contactos_foundation/example/ios/Flutter/Debug.xcconfig create mode 100644 contactos_foundation/example/ios/Flutter/Release.xcconfig create mode 100644 contactos_foundation/example/ios/Podfile create mode 100644 contactos_foundation/example/ios/Podfile.lock create mode 100644 contactos_foundation/example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 contactos_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 contactos_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 contactos_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 contactos_foundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 contactos_foundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 contactos_foundation/example/ios/Runner/AppDelegate.swift create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 contactos_foundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 contactos_foundation/example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 contactos_foundation/example/ios/Runner/Info.plist create mode 100644 contactos_foundation/example/ios/Runner/Runner-Bridging-Header.h create mode 100644 contactos_foundation/example/lib/main.dart create mode 100644 contactos_foundation/example/lib/src/contacts_list_screen.dart create mode 100644 contactos_foundation/example/lib/src/navite_contacts_picker_screen.dart create mode 100644 contactos_foundation/example/pubspec.lock create mode 100644 contactos_foundation/example/pubspec.yaml create mode 100644 contactos_foundation/example/test/widget_test.dart create mode 100644 contactos_foundation/lib/contactos_foundation.dart create mode 100644 contactos_foundation/lib/src/contactos_foundation.dart create mode 100644 contactos_foundation/pubspec.lock create mode 100644 contactos_foundation/pubspec.yaml delete mode 100644 contactos_ios/.gitkeep diff --git a/contactos_foundation/.gitignore b/contactos_foundation/.gitignore new file mode 100644 index 0000000..9cfb36e --- /dev/null +++ b/contactos_foundation/.gitignore @@ -0,0 +1,82 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +**/build +**/doc/api/ +**/Flutter/ephemeral/ +**/ios/Flutter/.last_build_id +**/Pods/ +.dart_tool/ +.flutter-plugins-dependencies +.flutter-plugins +.pub-cache/ +.pub/ +.fvm/ + +# Platform-specific generated files +**/android/**/GeneratedPluginRegistrant.* +**/android/**/local.properties +**/android/**/.cxx/ +**/android/.kotlin +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +**/android/**/key.properties +**/*.keystore +**/*.jks +**/android/**/captures/ +**/android/**/.gradle +**/android/**/gradlew +**/android/**/gradle-wrapper.jar +**/android/**/gradlew.bat +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/flutter_export_environment.sh +**/ios/Runner/GeneratedPluginRegistrant.* +**/macos/Flutter/GeneratedPluginRegistrant.* +**/windows/flutter/generated_* +**/linux/flutter/generated_* + +# Xcode-related +**/dgph +**/xcuserdata/ + +# Pana +log.pana.json + +# Test +.coverage/ +coverage/ +reports/ +/test/**/*.json +/test/.test_coverage.dart +!/test/**/fixtures/*.json +integration_test/screenshots + +# Temp +.temp/ +temp/ +.tmp/ +tmp/ \ No newline at end of file diff --git a/contactos_foundation/.pubignore b/contactos_foundation/.pubignore new file mode 100644 index 0000000..7b5ee92 --- /dev/null +++ b/contactos_foundation/.pubignore @@ -0,0 +1,5 @@ +pubspec.lock +reports/ +coverage/ +build/ +example/ \ No newline at end of file diff --git a/contactos_foundation/CHANGELOG.md b/contactos_foundation/CHANGELOG.md new file mode 100644 index 0000000..1e64072 --- /dev/null +++ b/contactos_foundation/CHANGELOG.md @@ -0,0 +1,2 @@ +## 0.0.1 +- **ADDED**: Initial release \ No newline at end of file diff --git a/contactos_foundation/LICENSE b/contactos_foundation/LICENSE new file mode 100644 index 0000000..4de1fa1 --- /dev/null +++ b/contactos_foundation/LICENSE @@ -0,0 +1,27 @@ +BSD 3-Clause License + +Copyright (c) 2025, Anton Ustinoff +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. 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. +3. Neither the name of the copyright holder 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 THE COPYRIGHT HOLDER OR CONTRIBUTORS 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/contactos_foundation/Makefile b/contactos_foundation/Makefile new file mode 100644 index 0000000..a9ec8a7 --- /dev/null +++ b/contactos_foundation/Makefile @@ -0,0 +1,75 @@ +SHELL :=/bin/bash -e -o pipefail +PWD :=$(shell pwd) + +.DEFAULT_GOAL := all +.PHONY: all +all: ## build pipeline +all: get format test-unit publish-check + +.PHONY: ci +ci: ## CI build pipeline +ci: all + +.PHONY: precommit +precommit: ## validate the branch before commit +precommit: all + +.PHONY: help +help: ## Help dialog + @echo 'Usage: make ' + @echo '' + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: doctor +doctor: ## Check flutter doctor + @fvm flutter doctor + +.PHONY: version +version: ## Check flutter version + @fvm flutter --version + +.PHONY: format +format: ## Format code + @fvm dart format -l 80 lib test || (echo "Β―\_(ツ)_/Β― Format code error"; exit 1) + +.PHONY: fix +fix: format ## Fix code + @fvm dart fix --apply lib + +.PHONY: clean +clean: ## Clean flutter + @fvm flutter clean + +.PHONY: get +get: ## Get dependencies + @fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos_platform_interface dependencies error"; exit 1) + +.PHONY: analyze +analyze: get format ## Analyze code + @fvm flutter analyze --fatal-warnings --no-fatal-infos lib/ test/ || (echo "Β―\_(ツ)_/Β― Analyze code error"; exit 1) + +.PHONY: check +check: analyze ## Check the code + @fvm dart pub global activate pana + @pana --json --no-warning --line-length 80 > log.pana.json + +.PHONY: publish-check +publish-check: ## Check the code before publish + @fvm dart pub publish --dry-run + +.PHONY: publish +publish: ## Publish package + @fvm dart pub publish --server=https://pub.dartlang.org || (echo "Β―\_(ツ)_/Β― Publish error"; exit 1) + +.PHONY: coverage +coverage: ## Runs get coverage + @lcov --summary coverage/lcov.info + +.PHONY: run-genhtml +run-genhtml: ## Runs generage coverage html + @genhtml coverage/lcov.info -o coverage/html + +.PHONY: test-unit +test-unit: ## Runs unit tests + @fvm flutter test --coverage || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) + @genhtml coverage/lcov.info --output=coverage -o coverage/html || (echo "Β―\_(ツ)_/Β― Error while running genhtml with coverage"; exit 2) diff --git a/contactos_foundation/README.md b/contactos_foundation/README.md new file mode 100644 index 0000000..e3f94a2 --- /dev/null +++ b/contactos_foundation/README.md @@ -0,0 +1,15 @@ +# Contactos Foundation + +[![Don't depend on me](https://img.shields.io/badge/platform-internal-lightgrey.svg)](https://github.com/ziqq/contactos) + +This package is the Darwin (iOS and macOS) implementation of the [`contactos`](https://github.com/ziqq/contactos/tree/main/contactos) plugin. + +## Usage + +This package is not meant to be used directly. It is automatically included in your app when you depend on the main [`contactos`](https://github.com/ziqq/contactos/tree/main/contactos) package. + +For more information on how to use the `contactos` plugin, please see the [main package's README](https://github.com/ziqq/contactos/blob/main/contactos/README.md). + +## Contributing + +This is part of a federated plugin. Contributions to the platform-specific implementations are welcome. Please see the [main repository](https://github.com/ziqq/contactos) for more information on how to contribute. diff --git a/contactos_foundation/analysis_options.yaml b/contactos_foundation/analysis_options.yaml new file mode 100644 index 0000000..ed15c99 --- /dev/null +++ b/contactos_foundation/analysis_options.yaml @@ -0,0 +1,235 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + # Build + - "build/**" + # Tests + - "test/**.mocks.dart" + - ".test_coverage.dart" + - ".coverage/**" + - "coverage/**" + # Assets + - "assets/**" + # Generated + - "**/generated/**" + # - '**/lib/l10n/**.arb' + - "**.g.dart" + - "**.gr.dart" + - "**.gql.dart" + - "**.freezed.dart" + - "**.config.dart" + - "**.mocks.dart" + - "**.gen.dart" + - "**.pb.dart" + - "**.pbenum.dart" + - "**.pbjson.dart" + # Flutter Version Manager + - ".fvm/**" + # Tools + - ".dart_tool/**" + - "scripts/**" + - "tool/**" + # Platform + - "ios/**" + - "android/**" + - "web/**" + - "macos/**" + - "windows/**" + - "linux/**" + + # Enable the following options to enable strong mode. + language: + strict-casts: true + strict-raw-types: true + strict-inference: true + + errors: + # Allow having TODOs in the code + todo: ignore + + # Info + directives_ordering: info + always_declare_return_types: info + + # Warning + unsafe_html: warning + missing_return: warning + missing_required_param: warning + no_logic_in_create_state: warning + empty_catches: warning + + # Error + always_use_package_imports: error + avoid_relative_lib_imports: error + avoid_slow_async_io: error + avoid_types_as_parameter_names: error + valid_regexps: error + always_require_non_null_named_parameters: error + +linter: + rules: + # Public packages + public_member_api_docs: true + lines_longer_than_80_chars: true + + # Enabling rules + always_use_package_imports: true + avoid_relative_lib_imports: true + + # Disable rules + sort_pub_dependencies: false + prefer_relative_imports: false + prefer_final_locals: false + avoid_escaping_inner_quotes: false + curly_braces_in_flow_control_structures: false + + # Enabled + use_named_constants: true + unnecessary_constructor_name: true + sort_constructors_first: true + exhaustive_cases: true + sort_unnamed_constructors_first: true + type_literal_in_constant_pattern: true + always_put_required_named_parameters_first: true + avoid_annotating_with_dynamic: true + avoid_bool_literals_in_conditional_expressions: true + avoid_double_and_int_checks: true + avoid_field_initializers_in_const_classes: true + avoid_implementing_value_types: true + avoid_js_rounded_ints: true + avoid_print: true + avoid_renaming_method_parameters: true + avoid_returning_null_for_void: true + avoid_single_cascade_in_expression_statements: true + avoid_slow_async_io: true + avoid_unnecessary_containers: true + avoid_unused_constructor_parameters: true + avoid_void_async: true + await_only_futures: true + cancel_subscriptions: true + cascade_invocations: true + close_sinks: true + control_flow_in_finally: true + empty_statements: true + collection_methods_unrelated_type: true + join_return_with_assignment: true + leading_newlines_in_multiline_strings: true + literal_only_boolean_expressions: true + missing_whitespace_between_adjacent_strings: true + no_adjacent_strings_in_list: true + no_logic_in_create_state: true + no_runtimeType_toString: true + only_throw_errors: true + overridden_fields: true + package_names: true + package_prefixed_library_names: true + parameter_assignments: true + prefer_asserts_in_initializer_lists: true + prefer_asserts_with_message: true + prefer_const_constructors: true + prefer_const_constructors_in_immutables: true + prefer_const_declarations: true + prefer_const_literals_to_create_immutables: true + prefer_constructors_over_static_methods: true + prefer_expression_function_bodies: true + prefer_final_in_for_each: true + prefer_foreach: true + prefer_if_elements_to_conditional_expressions: true + prefer_inlined_adds: true + prefer_int_literals: true + prefer_is_not_operator: true + prefer_null_aware_operators: true + prefer_typing_uninitialized_variables: true + prefer_void_to_null: true + provide_deprecation_message: true + sized_box_for_whitespace: true + sort_child_properties_last: true + test_types_in_equals: true + throw_in_finally: true + unnecessary_null_aware_assignments: true + unnecessary_overrides: true + unnecessary_parenthesis: true + unnecessary_raw_strings: true + unnecessary_statements: true + unnecessary_string_escapes: true + unnecessary_string_interpolations: true + # unsafe_html: true + use_full_hex_values_for_flutter_colors: true + use_raw_strings: true + use_string_buffers: true + valid_regexps: true + void_checks: true + + # Pedantic 1.9.0 + always_declare_return_types: true + annotate_overrides: true + avoid_empty_else: true + avoid_init_to_null: true + avoid_null_checks_in_equality_operators: true + avoid_return_types_on_setters: true + avoid_shadowing_type_parameters: true + avoid_types_as_parameter_names: true + camel_case_extensions: true + empty_catches: true + empty_constructor_bodies: true + library_names: true + library_prefixes: true + no_duplicate_case_values: true + null_closures: true + omit_local_variable_types: true + prefer_adjacent_string_concatenation: true + prefer_collection_literals: true + prefer_conditional_assignment: true + prefer_contains: true + prefer_final_fields: true + prefer_for_elements_to_map_fromIterable: true + prefer_generic_function_type_aliases: true + prefer_if_null_operators: true + prefer_is_empty: true + prefer_is_not_empty: true + prefer_iterable_whereType: true + prefer_single_quotes: true + prefer_spread_collections: true + recursive_getters: true + slash_for_doc_comments: true + type_init_formals: true + unawaited_futures: true + unnecessary_const: true + unnecessary_new: true + unnecessary_null_in_if_null_operators: true + unnecessary_this: true + unrelated_type_equality_checks: true + use_function_type_syntax_for_parameters: true + use_rethrow_when_possible: true + + # Effective_dart 1.2.0 + camel_case_types: true + file_names: true + non_constant_identifier_names: true + constant_identifier_names: true + directives_ordering: true + # package_api_docs: true + implementation_imports: true + prefer_interpolation_to_compose_strings: true + unnecessary_brace_in_string_interps: true + avoid_function_literals_in_foreach_calls: true + prefer_function_declarations_over_variables: true + unnecessary_lambdas: true + unnecessary_getters_setters: true + prefer_initializing_formals: true + avoid_catches_without_on_clauses: true + avoid_catching_errors: true + use_to_and_as_if_applicable: true + one_member_abstracts: true + avoid_classes_with_only_static_members: true + prefer_mixin: true + use_setters_to_change_properties: true + avoid_setters_without_getters: true + avoid_returning_this: true + type_annotate_public_apis: true + avoid_types_on_closure_parameters: true + avoid_private_typedef_functions: true + avoid_positional_boolean_parameters: true + hash_and_equals: true + avoid_equals_and_hash_code_on_mutable_classes: true \ No newline at end of file diff --git a/contactos_foundation/dart_dependency_validator.yaml b/contactos_foundation/dart_dependency_validator.yaml new file mode 100644 index 0000000..06cc68f --- /dev/null +++ b/contactos_foundation/dart_dependency_validator.yaml @@ -0,0 +1,8 @@ +ignore: + - dart_code_metrics + - integration_test + - flutter_lints + - collection + +exclude: + - "example/**" diff --git a/contactos_foundation/darwin/contactos_foundation.podspec b/contactos_foundation/darwin/contactos_foundation.podspec new file mode 100644 index 0000000..3a185c2 --- /dev/null +++ b/contactos_foundation/darwin/contactos_foundation.podspec @@ -0,0 +1,38 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'contactos_foundation' + s.version = '0.0.1' + s.summary = "A Flutter plugin to retrieve and manage contacts on iOS devices" + s.description = <<-DESC + A Flutter plugin to retrieve and manage contacts on iOS devices. + DESC + + s.homepage = 'https://github.com/ziqq/contactos/contactos_foundation' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = 'Anton Ustinoff' + + # Use local path (plugin shipped from local packages directory for Flutter). Remote :http not needed. + s.source = { :path => '.' } + s.source_files = 'contactos_foundation/Sources/contactos_foundation/**/*.swift' + s.requires_arc = true + + s.ios.dependency 'Flutter' + s.osx.dependency 'FlutterMacOS' + + s.ios.deployment_target = '13.0' + s.osx.deployment_target = '10.15' + + s.ios.frameworks = 'UIKit' + s.osx.frameworks = 'AppKit' + + s.swift_version = '5.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.xcconfig = { + 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', + 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', + } + s.resource_bundles = {'contactos_privacy' => ['contactos/Sources/contactos/Resources/PrivacyInfo.xcprivacy']} +end + diff --git a/contactos_foundation/darwin/contactos_foundation/Package.swift b/contactos_foundation/darwin/contactos_foundation/Package.swift new file mode 100644 index 0000000..9ae0001 --- /dev/null +++ b/contactos_foundation/darwin/contactos_foundation/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.9 + +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import PackageDescription + +let package = Package( + name: "contactos", + platforms: [ + .iOS("12.0"), + .macOS("10.14"), + ], + products: [ + .library(name: "contactos", targets: ["contactos"]) + ], + dependencies: [], + targets: [ + .target( + name: "contactos", + dependencies: [], + resources: [ + .process("Resources") + ] + ) + ] +) diff --git a/contactos_foundation/darwin/contactos_foundation/Sources/contactos_foundation/ContactosPlugin.swift b/contactos_foundation/darwin/contactos_foundation/Sources/contactos_foundation/ContactosPlugin.swift new file mode 100644 index 0000000..545d6f0 --- /dev/null +++ b/contactos_foundation/darwin/contactos_foundation/Sources/contactos_foundation/ContactosPlugin.swift @@ -0,0 +1,641 @@ +import Flutter +import UIKit +import Contacts +import ContactsUI + +@available(iOS 9.0, *) +public class ContactosPlugin: NSObject, FlutterPlugin, CNContactViewControllerDelegate, CNContactPickerDelegate { + private var result: FlutterResult? = nil + private var localizedLabels: Bool = true + static let FORM_OPERATION_CANCELED: Int = 1 + static let FORM_COULD_NOT_BE_OPEN: Int = 2 + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "github.com/ziqq/contactos", binaryMessenger: registrar.messenger()) + let instance = ContactosPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + instance.preLoadContactView() + } + + override init() { + super.init() + } + + private func getRootViewController() -> UIViewController? { + return UIApplication.shared.delegate?.window??.rootViewController + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getAvatar": + guard let contactId = (call.arguments as? [String:Any])?["identifier"] as? String else { + result(FlutterError(code: "MISSING_ID", message: "No contact identifier provided", details: nil)) + return + } + do { + let store = CNContactStore() + let keys = [CNContactImageDataKey, CNContactThumbnailImageDataKey] as [CNKeyDescriptor] + let cnContact = try store.unifiedContact(withIdentifier: contactId, keysToFetch: keys) + if let data = cnContact.imageData ?? cnContact.thumbnailImageData { + result(FlutterStandardTypedData(bytes: data)) + } else { + result(nil) + } + } catch { + result(FlutterError(code: "FETCH_ERROR", message: error.localizedDescription, details: nil)) + } + case "getContacts": + let arguments = call.arguments as! [String:Any] + result(getContacts(query: (arguments["query"] as? String), withThumbnails: arguments["withThumbnails"] as! Bool, + photoHighResolution: arguments["photoHighResolution"] as! Bool, phoneQuery: false, orderByGivenName: arguments["orderByGivenName"] as! Bool, + localizedLabels: arguments["iOSLocalizedLabels"] as! Bool )) + case "getContactsForPhone": + let arguments = call.arguments as! [String:Any] + result( + getContacts( + query: (arguments["phone"] as? String), + withThumbnails: arguments["withThumbnails"] as! Bool, + photoHighResolution: arguments["photoHighResolution"] as! Bool, + phoneQuery: true, + orderByGivenName: arguments["orderByGivenName"] as! Bool, + localizedLabels: arguments["iOSLocalizedLabels"] as! Bool + ) + ) + case "getContactsForEmail": + let arguments = call.arguments as! [String:Any] + result( + getContacts( + query: (arguments["email"] as? String), + withThumbnails: arguments["withThumbnails"] as! Bool, + photoHighResolution: arguments["photoHighResolution"] as! Bool, + phoneQuery: false, + emailQuery: true, + orderByGivenName: arguments["orderByGivenName"] as! Bool, + localizedLabels: arguments["iOSLocalizedLabels"] as! Bool + ) + ) + case "addContact": + let contact = dictionaryToContact(dictionary: call.arguments as! [String : Any]) + + let addResult = addContact(contact: contact) + if (addResult == "") { + result(nil) + } + else { + result(FlutterError(code: "", message: addResult, details: nil)) + } + case "deleteContact": + if(deleteContact(dictionary: call.arguments as! [String : Any])){ + result(nil) + } + else{ + result(FlutterError(code: "", message: "Failed to delete contact, make sure it has a valid identifier", details: nil)) + } + case "updateContact": + if(updateContact(dictionary: call.arguments as! [String: Any])) { + result(nil) + } + else { + result(FlutterError(code: "", message: "Failed to update contact, make sure it has a valid identifier", details: nil)) + } + case "openContactForm": + let arguments = call.arguments as! [String:Any] + localizedLabels = arguments["iOSLocalizedLabels"] as! Bool + self.result = result + _ = openContactForm() + case "openExistingContact": + let arguments = call.arguments as! [String : Any] + let contact = arguments["contact"] as! [String : Any] + localizedLabels = arguments["iOSLocalizedLabels"] as! Bool + self.result = result + _ = openExistingContact(contact: contact, result: result) + case "openDeviceContactPicker": + let arguments = call.arguments as! [String : Any] + openDeviceContactPicker(arguments: arguments, result: result); + default: + result(FlutterMethodNotImplemented) + } + } + + func getContacts(query : String?, withThumbnails: Bool, photoHighResolution: Bool, phoneQuery: Bool, emailQuery: Bool = false, orderByGivenName: Bool, localizedLabels: Bool) -> [[String:Any]]{ + + var contacts : [CNContact] = [] + var result = [[String:Any]]() + + //Create the store, keys & fetch request + let store = CNContactStore() + var keys = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), + CNContactEmailAddressesKey, + CNContactPhoneNumbersKey, + CNContactFamilyNameKey, + CNContactGivenNameKey, + CNContactMiddleNameKey, + CNContactNamePrefixKey, + CNContactNameSuffixKey, + CNContactPostalAddressesKey, + CNContactOrganizationNameKey, + CNContactJobTitleKey, + CNContactBirthdayKey] as [Any] + + if(withThumbnails){ + if(photoHighResolution){ + keys.append(CNContactImageDataKey) + } else { + keys.append(CNContactThumbnailImageDataKey) + } + } + + let fetchRequest = CNContactFetchRequest(keysToFetch: keys as! [CNKeyDescriptor]) + // Set the predicate if there is a query + if query != nil && !phoneQuery && !emailQuery { + fetchRequest.predicate = CNContact.predicateForContacts(matchingName: query!) + } + + if #available(iOS 11, *) { + if query != nil && phoneQuery { + let phoneNumberPredicate = CNPhoneNumber(stringValue: query!) + fetchRequest.predicate = CNContact.predicateForContacts(matching: phoneNumberPredicate) + } else if query != nil && emailQuery { + fetchRequest.predicate = CNContact.predicateForContacts(matchingEmailAddress: query!) + } + } + + // Fetch contacts + do{ + try store.enumerateContacts(with: fetchRequest, usingBlock: { (contact, stop) -> Void in + + if phoneQuery { + if #available(iOS 11, *) { + contacts.append(contact) + } else if query != nil && self.has(contact: contact, phone: query!){ + contacts.append(contact) + } + } else if emailQuery { + if #available(iOS 11, *) { + contacts.append(contact) + } else if query != nil && (contact.emailAddresses.contains { $0.value.caseInsensitiveCompare(query!) == .orderedSame}) { + contacts.append(contact) + } + } else { + contacts.append(contact) + } + + }) + } + catch let error as NSError { + print(error.localizedDescription) + return result + } + + if (orderByGivenName) { + contacts = contacts.sorted { (contactA, contactB) -> Bool in + contactA.givenName.lowercased() < contactB.givenName.lowercased() + } + } + + // Transform the CNContacts into dictionaries + for contact : CNContact in contacts{ + result.append(contactToDictionary(contact: contact, localizedLabels: localizedLabels)) + } + + return result + } + + private func has(contact: CNContact, phone: String) -> Bool { + if (!contact.phoneNumbers.isEmpty) { + let phoneNumberToCompareAgainst = phone.components(separatedBy: NSCharacterSet.decimalDigits.inverted).joined(separator: "") + for phoneNumber in contact.phoneNumbers { + + if let phoneNumberStruct = phoneNumber.value as CNPhoneNumber? { + let phoneNumberString = phoneNumberStruct.stringValue + let phoneNumberToCompare = phoneNumberString.components(separatedBy: NSCharacterSet.decimalDigits.inverted).joined(separator: "") + if phoneNumberToCompare == phoneNumberToCompareAgainst { + return true + } + } + } + } + return false + } + + func addContact(contact : CNMutableContact) -> String { + let store = CNContactStore() + do { + let saveRequest = CNSaveRequest() + saveRequest.add(contact, toContainerWithIdentifier: nil) + try store.execute(saveRequest) + } + catch { + return error.localizedDescription + } + return "" + } + + func openContactForm() -> [String:Any]? { + let contact = CNMutableContact.init() + let controller = CNContactViewController.init(forNewContact:contact) + controller.delegate = self + DispatchQueue.main.async { + let navigation = UINavigationController .init(rootViewController: controller) + if let viewController = self.getRootViewController() { + viewController.present(navigation, animated:true, completion: nil) + } + } + return nil + } + + func preLoadContactView() { + DispatchQueue.main.asyncAfter(deadline: .now()+5) { + NSLog("Preloading CNContactViewController") + let contactViewController = CNContactViewController.init(forNewContact: nil) + } + } + + @objc func cancelContactForm() { + if let result = self.result { + if let viewController = self.getRootViewController() { + viewController.dismiss(animated: true, completion: nil) + } + result(ContactosPlugin.FORM_OPERATION_CANCELED) + self.result = nil + } + } + + public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) { + viewController.dismiss(animated: true, completion: nil) + if let result = self.result { + if let contact = contact { + result(contactToDictionary(contact: contact, localizedLabels: localizedLabels)) + } else { + result(ContactosPlugin.FORM_OPERATION_CANCELED) + } + self.result = nil + } + } + + func openExistingContact(contact: [String:Any], result: FlutterResult ) -> [String:Any]? { + let store = CNContactStore() + do { + // Check to make sure dictionary has an identifier + guard let identifier = contact["identifier"] as? String else{ + result(ContactosPlugin.FORM_COULD_NOT_BE_OPEN) + return nil; + } + let backTitle = contact["backTitle"] as? String + + let keysToFetch = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), + CNContactIdentifierKey, + CNContactEmailAddressesKey, + CNContactBirthdayKey, + CNContactImageDataKey, + CNContactPhoneNumbersKey, + CNContactViewController.descriptorForRequiredKeys() + ] as! [CNKeyDescriptor] + let cnContact = try store.unifiedContact(withIdentifier: identifier, keysToFetch: keysToFetch) + let viewController = CNContactViewController(for: cnContact) + + viewController.navigationItem.backBarButtonItem = UIBarButtonItem.init(title: backTitle == nil ? "Cancel" : backTitle, style: UIBarButtonItem.Style.plain, target: self, action: #selector(cancelContactForm)) + viewController.delegate = self + DispatchQueue.main.async { + let navigation = UINavigationController .init(rootViewController: viewController) + var currentViewController = self.getRootViewController() + while let nextView = currentViewController?.presentedViewController { + currentViewController = nextView + } + let activityIndicatorView = UIActivityIndicatorView.init(style: .gray) + if let viewFrame = currentViewController?.view.frame { + activityIndicatorView.frame = viewFrame + } + activityIndicatorView.startAnimating() + activityIndicatorView.backgroundColor = UIColor.white + navigation.view.addSubview(activityIndicatorView) + currentViewController?.present(navigation, animated: true, completion: nil) + + DispatchQueue.main.asyncAfter(deadline: .now()+0.5 ){ + activityIndicatorView.removeFromSuperview() + } + } + return nil + } catch { + NSLog(error.localizedDescription) + result(ContactosPlugin.FORM_COULD_NOT_BE_OPEN) + return nil + } + } + + func openDeviceContactPicker(arguments: [String:Any], result: @escaping FlutterResult) { + localizedLabels = arguments["iOSLocalizedLabels"] as! Bool + self.result = result + + let contactPicker = CNContactPickerViewController() + contactPicker.delegate = self + //contactPicker!.displayedPropertyKeys = [CNContactPhoneNumbersKey]; + DispatchQueue.main.async { + if let rootViewController = self.getRootViewController() { + rootViewController.present(contactPicker, animated: true, completion: nil) + } + } + } + + // MARK:- CNContactPickerDelegate Method + + public func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) { + if let result = self.result { + result(contactToDictionary(contact: contact, localizedLabels: localizedLabels)) + self.result = nil + } + } + + public func contactPickerDidCancel(_ picker: CNContactPickerViewController) { + if let result = self.result { + result(ContactosPlugin.FORM_OPERATION_CANCELED) + self.result = nil + } + } + + + func deleteContact(dictionary : [String:Any]) -> Bool{ + guard let identifier = dictionary["identifier"] as? String else{ + return false; + } + let store = CNContactStore() + let keys = [CNContactIdentifierKey as NSString] + do{ + if let contact = try store.unifiedContact(withIdentifier: identifier, keysToFetch: keys).mutableCopy() as? CNMutableContact{ + let request = CNSaveRequest() + request.delete(contact) + try store.execute(request) + } + } + catch{ + print(error.localizedDescription) + return false; + } + return true; + } + + func updateContact(dictionary : [String:Any]) -> Bool{ + + // Check to make sure dictionary has an identifier + guard let identifier = dictionary["identifier"] as? String else{ + return false; + } + + let store = CNContactStore() + let keys = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), + CNContactEmailAddressesKey, + CNContactPhoneNumbersKey, + CNContactFamilyNameKey, + CNContactGivenNameKey, + CNContactMiddleNameKey, + CNContactNamePrefixKey, + CNContactNameSuffixKey, + CNContactPostalAddressesKey, + CNContactOrganizationNameKey, + CNContactImageDataKey, + CNContactJobTitleKey] as [Any] + do { + // Check if the contact exists + if let contact = try store.unifiedContact(withIdentifier: identifier, keysToFetch: keys as! [CNKeyDescriptor]).mutableCopy() as? CNMutableContact{ + + /// Update the contact that was retrieved from the store + //Simple fields + contact.givenName = dictionary["givenName"] as? String ?? "" + contact.familyName = dictionary["familyName"] as? String ?? "" + contact.middleName = dictionary["middleName"] as? String ?? "" + contact.namePrefix = dictionary["prefix"] as? String ?? "" + contact.nameSuffix = dictionary["suffix"] as? String ?? "" + contact.organizationName = dictionary["company"] as? String ?? "" + contact.jobTitle = dictionary["jobTitle"] as? String ?? "" + contact.imageData = (dictionary["avatar"] as? FlutterStandardTypedData)?.data + + //Phone numbers + if let phoneNumbers = dictionary["phones"] as? [[String:String]]{ + var updatedPhoneNumbers = [CNLabeledValue]() + for phone in phoneNumbers where phone["value"] != nil { + updatedPhoneNumbers.append(CNLabeledValue(label:getPhoneLabel(label: phone["label"]),value:CNPhoneNumber(stringValue: phone["value"]!))) + } + contact.phoneNumbers = updatedPhoneNumbers + } + + //Emails + if let emails = dictionary["emails"] as? [[String:String]]{ + var updatedEmails = [CNLabeledValue]() + for email in emails where nil != email["value"] { + let emailLabel = email["label"] ?? "" + updatedEmails.append(CNLabeledValue(label: getCommonLabel(label: emailLabel), value: email["value"]! as NSString)) + } + contact.emailAddresses = updatedEmails + } + + //Postal addresses + if let postalAddresses = dictionary["postalAddresses"] as? [[String:String]]{ + var updatedPostalAddresses = [CNLabeledValue]() + for postalAddress in postalAddresses{ + let newAddress = CNMutablePostalAddress() + newAddress.street = postalAddress["street"] ?? "" + newAddress.city = postalAddress["city"] ?? "" + newAddress.postalCode = postalAddress["postcode"] ?? "" + newAddress.country = postalAddress["country"] ?? "" + newAddress.state = postalAddress["region"] ?? "" + let label = postalAddress["label"] ?? "" + updatedPostalAddresses.append(CNLabeledValue(label: getCommonLabel(label: label), value: newAddress)) + } + contact.postalAddresses = updatedPostalAddresses + } + + // Attempt to update the contact + let request = CNSaveRequest() + request.update(contact) + try store.execute(request) + } + } + catch { + print(error.localizedDescription) + return false; + } + return true; + } + + func dictionaryToContact(dictionary : [String:Any]) -> CNMutableContact{ + let contact = CNMutableContact() + + //Simple fields + contact.givenName = dictionary["givenName"] as? String ?? "" + contact.familyName = dictionary["familyName"] as? String ?? "" + contact.middleName = dictionary["middleName"] as? String ?? "" + contact.namePrefix = dictionary["prefix"] as? String ?? "" + contact.nameSuffix = dictionary["suffix"] as? String ?? "" + contact.organizationName = dictionary["company"] as? String ?? "" + contact.jobTitle = dictionary["jobTitle"] as? String ?? "" + if let avatarData = (dictionary["avatar"] as? FlutterStandardTypedData)?.data { + contact.imageData = avatarData + } + + //Phone numbers + if let phoneNumbers = dictionary["phones"] as? [[String:String]]{ + for phone in phoneNumbers where phone["value"] != nil { + contact.phoneNumbers.append(CNLabeledValue(label:getPhoneLabel(label:phone["label"]),value:CNPhoneNumber(stringValue:phone["value"]!))) + } + } + + //Emails + if let emails = dictionary["emails"] as? [[String:String]]{ + for email in emails where nil != email["value"] { + let emailLabel = email["label"] ?? "" + contact.emailAddresses.append(CNLabeledValue(label:getCommonLabel(label: emailLabel), value:email["value"]! as NSString)) + } + } + + //Postal addresses + if let postalAddresses = dictionary["postalAddresses"] as? [[String:String]]{ + for postalAddress in postalAddresses{ + let newAddress = CNMutablePostalAddress() + newAddress.street = postalAddress["street"] ?? "" + newAddress.city = postalAddress["city"] ?? "" + newAddress.postalCode = postalAddress["postcode"] ?? "" + newAddress.country = postalAddress["country"] ?? "" + newAddress.state = postalAddress["region"] ?? "" + let label = postalAddress["label"] ?? "" + contact.postalAddresses.append(CNLabeledValue(label: getCommonLabel(label: label), value: newAddress)) + } + } + + //BIRTHDAY + if let birthday = dictionary["birthday"] as? String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + let date = formatter.date(from: birthday)! + contact.birthday = Calendar.current.dateComponents([.year, .month, .day], from: date) + } + + return contact + } + + func contactToDictionary(contact: CNContact, localizedLabels: Bool) -> [String:Any]{ + + var result = [String:Any]() + + //Simple fields + result["identifier"] = contact.identifier + result["displayName"] = CNContactFormatter.string(from: contact, style: CNContactFormatterStyle.fullName) + result["givenName"] = contact.givenName + result["familyName"] = contact.familyName + result["middleName"] = contact.middleName + result["prefix"] = contact.namePrefix + result["suffix"] = contact.nameSuffix + result["company"] = contact.organizationName + result["jobTitle"] = contact.jobTitle + if contact.isKeyAvailable(CNContactThumbnailImageDataKey) { + if let avatarData = contact.thumbnailImageData { + result["avatar"] = FlutterStandardTypedData(bytes: avatarData) + } + } + if contact.isKeyAvailable(CNContactImageDataKey) { + if let avatarData = contact.imageData { + result["avatar"] = FlutterStandardTypedData(bytes: avatarData) + } + } + + //Phone numbers + var phoneNumbers = [[String:String]]() + for phone in contact.phoneNumbers{ + var phoneDictionary = [String:String]() + phoneDictionary["value"] = phone.value.stringValue + phoneDictionary["label"] = "other" + if let label = phone.label{ + phoneDictionary["label"] = localizedLabels ? CNLabeledValue.localizedString(forLabel: label) : getRawPhoneLabel(label); + } + phoneNumbers.append(phoneDictionary) + } + result["phones"] = phoneNumbers + + //Emails + var emailAddresses = [[String:String]]() + for email in contact.emailAddresses{ + var emailDictionary = [String:String]() + emailDictionary["value"] = String(email.value) + emailDictionary["label"] = "other" + if let label = email.label{ + emailDictionary["label"] = localizedLabels ? CNLabeledValue.localizedString(forLabel: label) : getRawCommonLabel(label); + } + emailAddresses.append(emailDictionary) + } + result["emails"] = emailAddresses + + //Postal addresses + var postalAddresses = [[String:String]]() + for address in contact.postalAddresses{ + var addressDictionary = [String:String]() + addressDictionary["label"] = "" + if let label = address.label{ + addressDictionary["label"] = localizedLabels ? CNLabeledValue.localizedString(forLabel: label) : getRawCommonLabel(label); + } + addressDictionary["street"] = address.value.street + addressDictionary["city"] = address.value.city + addressDictionary["postcode"] = address.value.postalCode + addressDictionary["region"] = address.value.state + addressDictionary["country"] = address.value.country + + postalAddresses.append(addressDictionary) + } + result["postalAddresses"] = postalAddresses + + //BIRTHDAY + if let birthday : Date = contact.birthday?.date { + let formatter = DateFormatter() + let year = Calendar.current.component(.year, from: birthday) + formatter.dateFormat = year == 1 ? "--MM-dd" : "yyyy-MM-dd"; + result["birthday"] = formatter.string(from: birthday) + } + + return result + } + + func getPhoneLabel(label: String?) -> String{ + let labelValue = label ?? "" + switch(labelValue){ + case "main": return CNLabelPhoneNumberMain + case "mobile": return CNLabelPhoneNumberMobile + case "iPhone": return CNLabelPhoneNumberiPhone + case "work": return CNLabelWork + case "home": return CNLabelHome + case "other": return CNLabelOther + default: return labelValue + } + } + + func getCommonLabel(label:String?) -> String{ + let labelValue = label ?? "" + switch(labelValue){ + case "work": return CNLabelWork + case "home": return CNLabelHome + case "other": return CNLabelOther + default: return labelValue + } + } + + func getRawPhoneLabel(_ label: String?) -> String{ + let labelValue = label ?? "" + switch(labelValue){ + case CNLabelPhoneNumberMain: return "main" + case CNLabelPhoneNumberMobile: return "mobile" + case CNLabelPhoneNumberiPhone: return "iPhone" + case CNLabelWork: return "work" + case CNLabelHome: return "home" + case CNLabelOther: return "other" + default: return labelValue + } + } + + func getRawCommonLabel(_ label: String?) -> String{ + let labelValue = label ?? "" + switch(labelValue){ + case CNLabelWork: return "work" + case CNLabelHome: return "home" + case CNLabelOther: return "other" + default: return labelValue + } + } + +} diff --git a/contactos_foundation/darwin/contactos_foundation/Sources/contactos_foundation/Resources/PrivacyInfo.xcprivacy b/contactos_foundation/darwin/contactos_foundation/Sources/contactos_foundation/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..2d00b69 --- /dev/null +++ b/contactos_foundation/darwin/contactos_foundation/Sources/contactos_foundation/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,25 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + NSPrivacyCollectedDataTypes + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + + \ No newline at end of file diff --git a/contactos_foundation/example/.gitignore b/contactos_foundation/example/.gitignore new file mode 100644 index 0000000..e96ef60 --- /dev/null +++ b/contactos_foundation/example/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/contactos_foundation/example/.metadata b/contactos_foundation/example/.metadata new file mode 100644 index 0000000..a700013 --- /dev/null +++ b/contactos_foundation/example/.metadata @@ -0,0 +1,8 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 8f65fec5f5f7d7afbb0965f4a44bdb330a28fb19 + channel: alpha diff --git a/contactos_foundation/example/README.md b/contactos_foundation/example/README.md new file mode 100644 index 0000000..8399213 --- /dev/null +++ b/contactos_foundation/example/README.md @@ -0,0 +1,8 @@ +# contactos_example + +Demonstrates how to use the contactos plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](http://flutter.io/). diff --git a/contactos_foundation/example/ios/.gitignore b/contactos_foundation/example/ios/.gitignore new file mode 100644 index 0000000..14d6ec7 --- /dev/null +++ b/contactos_foundation/example/ios/.gitignore @@ -0,0 +1,43 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/app.flx +/Flutter/app.zip +/Flutter/App.framework +/Flutter/Flutter.framework +/Flutter/Generated.xcconfig +/Flutter/flutter_export_environment.sh +/ServiceDefinitions.json +/Flutter/Flutter.podspec + +Pods/ \ No newline at end of file diff --git a/contactos_foundation/example/ios/Flutter/AppFrameworkInfo.plist b/contactos_foundation/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1f6b98f --- /dev/null +++ b/contactos_foundation/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 13.0 + + diff --git a/contactos_foundation/example/ios/Flutter/Debug.xcconfig b/contactos_foundation/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..e8efba1 --- /dev/null +++ b/contactos_foundation/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/contactos_foundation/example/ios/Flutter/Release.xcconfig b/contactos_foundation/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..399e934 --- /dev/null +++ b/contactos_foundation/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/contactos_foundation/example/ios/Podfile b/contactos_foundation/example/ios/Podfile new file mode 100644 index 0000000..268e8f9 --- /dev/null +++ b/contactos_foundation/example/ios/Podfile @@ -0,0 +1,49 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + + ## dart: PermissionGroup.contacts + 'PERMISSION_CONTACTS=1', + ] + + end + end +end diff --git a/contactos_foundation/example/ios/Podfile.lock b/contactos_foundation/example/ios/Podfile.lock new file mode 100644 index 0000000..4a2dbe3 --- /dev/null +++ b/contactos_foundation/example/ios/Podfile.lock @@ -0,0 +1,35 @@ +PODS: + - contactos_foundation (0.0.1): + - Flutter + - FlutterMacOS + - Flutter (1.0.0) + - integration_test (0.0.1): + - Flutter + - permission_handler_apple (9.3.0): + - Flutter + +DEPENDENCIES: + - contactos_foundation (from `.symlinks/plugins/contactos_foundation/darwin`) + - Flutter (from `Flutter`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + +EXTERNAL SOURCES: + contactos_foundation: + :path: ".symlinks/plugins/contactos_foundation/darwin" + Flutter: + :path: Flutter + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + +SPEC CHECKSUMS: + contactos_foundation: 30cf4037c67f6003e6c2c35b61f748302e2632dd + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + +PODFILE CHECKSUM: 142419311492affa1242e9eb36f2905ca1a37b07 + +COCOAPODS: 1.16.2 diff --git a/contactos_foundation/example/ios/Runner.xcodeproj/project.pbxproj b/contactos_foundation/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..872fc57 --- /dev/null +++ b/contactos_foundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,525 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; + 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + D00F72E61F1A341EF236C241 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 376C65EDF2BBF4969C4BF795 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 376C65EDF2BBF4969C4BF795 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DC2CD4E154FC564256406BCB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + FADA5803582FD34ED02516C6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D00F72E61F1A341EF236C241 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0A611CA2056F00C87C8E2D3A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 376C65EDF2BBF4969C4BF795 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 6E34C24F55ECFA0978B626E3 /* Pods */ = { + isa = PBXGroup; + children = ( + FADA5803582FD34ED02516C6 /* Pods-Runner.debug.xcconfig */, + DC2CD4E154FC564256406BCB /* Pods-Runner.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 6E34C24F55ECFA0978B626E3 /* Pods */, + 0A611CA2056F00C87C8E2D3A /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 0069A5BAC5F591E956309FC9 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 7705EB8180C636DF7F0C2A3A /* [CP] Copy Pods Resources */, + AC8E7B761714740CD348D9B5 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 0910; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0069A5BAC5F591E956309FC9 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 7705EB8180C636DF7F0C2A3A /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/permission_handler_apple/permission_handler_apple_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/permission_handler_apple_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + AC8E7B761714740CD348D9B5 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/contactos_foundation/contactos_foundation.framework", + "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/contactos_foundation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = flutter.plugins.contactos.contactsServiceExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_VERSION = 4.0; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = flutter.plugins.contactos.contactsServiceExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_VERSION = 4.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/contactos_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/contactos_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/contactos_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/contactos_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/contactos_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/contactos_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/contactos_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/contactos_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..221227e --- /dev/null +++ b/contactos_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contactos_foundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/contactos_foundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/contactos_foundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/contactos_foundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/contactos_foundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/contactos_foundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/contactos_foundation/example/ios/Runner/AppDelegate.swift b/contactos_foundation/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..b636303 --- /dev/null +++ b/contactos_foundation/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d225b3c --- /dev/null +++ b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,121 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..28c6bf03016f6c994b70f38d1b7346e5831b531f GIT binary patch literal 564 zcmV-40?Yl0P)Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 literal 0 HcmV?d00001 diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f091b6b0bca859a3f474b03065bef75ba58a9e4c GIT binary patch literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ literal 0 HcmV?d00001 diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0ef06e7edb86cdfe0d15b4b0d98334a86163658 GIT binary patch literal 1716 zcmds$`#;kQ7{|XelZftyR5~xW7?MLxS4^|Hw3&P7^y)@A9Fj{Xm1~_CIV^XZ%SLBn zA;!r`GqGHg=7>xrB{?psZQs88ZaedDoagm^KF{a*>G|dJWRSe^I$DNW008I^+;Kjt z>9p3GNR^I;v>5_`+91i(*G;u5|L+Bu6M=(afLjtkya#yZ175|z$pU~>2#^Z_pCZ7o z1c6UNcv2B3?; zX%qdxCXQpdKRz=#b*q0P%b&o)5ZrNZt7$fiETSK_VaY=mb4GK`#~0K#~9^ zcY!`#Af+4h?UMR-gMKOmpuYeN5P*RKF!(tb`)oe0j2BH1l?=>y#S5pMqkx6i{*=V9JF%>N8`ewGhRE(|WohnD59R^$_36{4>S zDFlPC5|k?;SPsDo87!B{6*7eqmMdU|QZ84>6)Kd9wNfh90=y=TFQay-0__>=<4pk& zYDjgIhL-jQ9o>z32K)BgAH+HxamL{ZL~ozu)Qqe@a`FpH=oQRA8=L-m-1dam(Ix2V z?du;LdMO+ooBelr^_y4{|44tmgH^2hSzPFd;U^!1p>6d|o)(-01z{i&Kj@)z-yfWQ)V#3Uo!_U}q3u`(fOs`_f^ueFii1xBNUB z6MecwJN$CqV&vhc+)b(p4NzGGEgwWNs z@*lUV6LaduZH)4_g!cE<2G6#+hJrWd5(|p1Z;YJ7ifVHv+n49btR}dq?HHDjl{m$T z!jLZcGkb&XS2OG~u%&R$(X+Z`CWec%QKt>NGYvd5g20)PU(dOn^7%@6kQb}C(%=vr z{?RP(z~C9DPnL{q^@pVw@|Vx~@3v!9dCaBtbh2EdtoNHm4kGxp>i#ct)7p|$QJs+U z-a3qtcPvhihub?wnJqEt>zC@)2suY?%-96cYCm$Q8R%-8$PZYsx3~QOLMDf(piXMm zB=<63yQk1AdOz#-qsEDX>>c)EES%$owHKue;?B3)8aRd}m~_)>SL3h2(9X;|+2#7X z+#2)NpD%qJvCQ0a-uzZLmz*ms+l*N}w)3LRQ*6>|Ub-fyptY(keUxw+)jfwF5K{L9 z|Cl_w=`!l_o><384d&?)$6Nh(GAm=4p_;{qVn#hI8lqewW7~wUlyBM-4Z|)cZr?Rh z=xZ&Ol>4(CU85ea(CZ^aO@2N18K>ftl8>2MqetAR53_JA>Fal`^)1Y--Am~UDa4th zKfCYpcXky$XSFDWBMIl(q=Mxj$iMBX=|j9P)^fDmF(5(5$|?Cx}DKEJa&XZP%OyE`*GvvYQ4PV&!g2|L^Q z?YG}tx;sY@GzMmsY`7r$P+F_YLz)(e}% zyakqFB<6|x9R#TdoP{R$>o7y(-`$$p0NxJ6?2B8tH)4^yF(WhqGZlM3=9Ibs$%U1w zWzcss*_c0=v_+^bfb`kBFsI`d;ElwiU%frgRB%qBjn@!0U2zZehBn|{%uNIKBA7n= zzE`nnwTP85{g;8AkYxA68>#muXa!G>xH22D1I*SiD~7C?7Za+9y7j1SHiuSkKK*^O zsZ==KO(Ua#?YUpXl{ViynyT#Hzk=}5X$e04O@fsMQjb}EMuPWFO0e&8(2N(29$@Vd zn1h8Yd>6z(*p^E{c(L0Lg=wVdupg!z@WG;E0k|4a%s7Up5C0c)55XVK*|x9RQeZ1J@1v9MX;>n34(i>=YE@Iur`0Vah(inE3VUFZNqf~tSz{1fz3Fsn_x4F>o(Yo;kpqvBe-sbwH(*Y zu$JOl0b83zu$JMvy<#oH^Wl>aWL*?aDwnS0iEAwC?DK@aT)GHRLhnz2WCvf3Ba;o=aY7 z2{Asu5MEjGOY4O#Ggz@@J;q*0`kd2n8I3BeNuMmYZf{}pg=jTdTCrIIYuW~luKecn z+E-pHY%ohj@uS0%^ z&(OxwPFPD$+#~`H?fMvi9geVLci(`K?Kj|w{rZ9JgthFHV+=6vMbK~0)Ea<&WY-NC zy-PnZft_k2tfeQ*SuC=nUj4H%SQ&Y$gbH4#2sT0cU0SdFs=*W*4hKGpuR1{)mV;Qf5pw4? zfiQgy0w3fC*w&Bj#{&=7033qFR*<*61B4f9K%CQvxEn&bsWJ{&winp;FP!KBj=(P6 z4Z_n4L7cS;ao2)ax?Tm|I1pH|uLpDSRVghkA_UtFFuZ0b2#>!8;>-_0ELjQSD-DRd z4im;599VHDZYtnWZGAB25W-e(2VrzEh|etsv2YoP#VbIZ{aFkwPrzJ#JvCvA*mXS& z`}Q^v9(W4GiSs}#s7BaN!WA2bniM$0J(#;MR>uIJ^uvgD3GS^%*ikdW6-!VFUU?JV zZc2)4cMsX@j z5HQ^e3BUzOdm}yC-xA%SY``k$rbfk z;CHqifhU*jfGM@DkYCecD9vl*qr58l6x<8URB=&%{!Cu3RO*MrKZ4VO}V6R0a zZw3Eg^0iKWM1dcTYZ0>N899=r6?+adUiBKPciJw}L$=1f4cs^bio&cr9baLF>6#BM z(F}EXe-`F=f_@`A7+Q&|QaZ??Txp_dB#lg!NH=t3$G8&06MFhwR=Iu*Im0s_b2B@| znW>X}sy~m#EW)&6E&!*0%}8UAS)wjt+A(io#wGI@Z2S+Ms1Cxl%YVE800007ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 literal 0 HcmV?d00001 diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f9ed8f5cee1c98386d13b17e89f719e83555b2 GIT binary patch literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 literal 0 HcmV?d00001 diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..75b2d164a5a98e212cca15ea7bf2ab5de5108680 GIT binary patch literal 3831 zcmVjJBgitF5mAp-i>4+KS_oR{|13AP->1TD4=w)g|)JHOx|a2Wk1Va z!k)vP$UcQ#mdj%wNQoaJ!w>jv_6&JPyutpQps?s5dmDQ>`%?Bvj>o<%kYG!YW6H-z zu`g$@mp`;qDR!51QaS}|ZToSuAGcJ7$2HF0z`ln4t!#Yg46>;vGG9N9{V@9z#}6v* zfP?}r6b{*-C*)(S>NECI_E~{QYzN5SXRmVnP<=gzP+_Sp(Aza_hKlZ{C1D&l*(7IKXxQC1Z9#6wx}YrGcn~g%;icdw>T0Rf^w0{ z$_wn1J+C0@!jCV<%Go5LA45e{5gY9PvZp8uM$=1}XDI+9m7!A95L>q>>oe0$nC->i zeexUIvq%Uk<-$>DiDb?!In)lAmtuMWxvWlk`2>4lNuhSsjAf2*2tjT`y;@d}($o)S zn(+W&hJ1p0xy@oxP%AM15->wPLp{H!k)BdBD$toBpJh+crWdsNV)qsHaqLg2_s|Ih z`8E9z{E3sA!}5aKu?T!#enD(wLw?IT?k-yWVHZ8Akz4k5(TZJN^zZgm&zM28sfTD2BYJ|Fde3Xzh;;S` z=GXTnY4Xc)8nYoz6&vF;P7{xRF-{|2Xs5>a5)@BrnQ}I(_x7Cgpx#5&Td^4Q9_FnQ zX5so*;#8-J8#c$OlA&JyPp$LKUhC~-e~Ij!L%uSMu!-VZG7Hx-L{m2DVR2i=GR(_% zCVD!4N`I)&Q5S`?P&fQZ=4#Dgt_v2-DzkT}K(9gF0L(owe-Id$Rc2qZVLqI_M_DyO z9@LC#U28_LU{;wGZ&))}0R2P4MhajKCd^K#D+JJ&JIXZ_p#@+7J9A&P<0kdRujtQ_ zOy>3=C$kgi6$0pW06KaLz!21oOryKM3ZUOWqppndxfH}QpgjEJ`j7Tzn5bk6K&@RA?vl##y z$?V~1E(!wB5rH`>3nc&@)|#<1dN2cMzzm=PGhQ|Yppne(C-Vlt450IXc`J4R0W@I7 zd1e5uW6juvO%ni(WX7BsKx3MLngO7rHO;^R5I~0^nE^9^E_eYLgiR9&KnJ)pBbfno zSVnW$0R+&6jOOsZ82}nJ126+c|%svPo;TeUku<2G7%?$oft zyaO;tVo}(W)VsTUhq^XmFi#2z%-W9a{7mXn{uzivYQ_d6b7VJG{77naW(vHt-uhnY zVN#d!JTqVh(7r-lhtXVU6o})aZbDt_;&wJVGl2FKYFBFpU-#9U)z#(A%=IVnqytR$SY-sO( z($oNE09{D^@OuYPz&w~?9>Fl5`g9u&ecFGhqX=^#fmR=we0CJw+5xna*@oHnkahk+ z9aWeE3v|An+O5%?4fA&$Fgu~H_YmqR!yIU!bFCk4!#pAj%(lI(A5n)n@Id#M)O9Yx zJU9oKy{sRAIV3=5>(s8n{8ryJ!;ho}%pn6hZKTKbqk=&m=f*UnK$zW3YQP*)pw$O* zIfLA^!-bmBl6%d_n$#tP8Zd_(XdA*z*WH|E_yILwjtI~;jK#v-6jMl^?<%Y%`gvpwv&cFb$||^v4D&V=aNy?NGo620jL3VZnA%s zH~I|qPzB~e(;p;b^gJr7Ure#7?8%F0m4vzzPy^^(q4q1OdthF}Fi*RmVZN1OwTsAP zn9CZP`FazX3^kG(KodIZ=Kty8DLTy--UKfa1$6XugS zk%6v$Kmxt6U!YMx0JQ)0qX*{CXwZZk$vEROidEc7=J-1;peNat!vS<3P-FT5po>iE z!l3R+<`#x|+_hw!HjQGV=8!q|76y8L7N8gP3$%0kfush|u0uU^?dKBaeRSBUpOZ0c z62;D&Mdn2}N}xHRFTRI?zRv=>=AjHgH}`2k4WK=#AHB)UFrR-J87GgX*x5fL^W2#d z=(%K8-oZfMO=i{aWRDg=FX}UubM4eotRDcn;OR#{3q=*?3mE3_oJ-~prjhxh%PgQT zyn)Qozaq0@o&|LEgS{Ind4Swsr;b`u185hZPOBLL<`d2%^Yp1?oL)=jnLi;Zo0ZDliTtQ^b5SmfIMe{T==zZkbvn$KTQGlbG8w}s@M3TZnde;1Am46P3juKb zl9GU&3F=q`>j!`?SyH#r@O59%@aMX^rx}Nxe<>NqpUp5=lX1ojGDIR*-D^SDuvCKF z?3$xG(gVUsBERef_YjPFl^rU9EtD{pt z0CXwpN7BN3!8>hajGaTVk-wl=9rxmfWtIhC{mheHgStLi^+Nz12a?4r(fz)?3A%at zMlvQmL<2-R)-@G1wJ0^zQK%mR=r4d{Y3fHp){nWXUL#|CqXl(+v+qDh>FkF9`eWrW zfr^D%LNfOcTNvtx0JXR35J0~Jpi2#P3Q&80w+nqNfc}&G0A~*)lGHKv=^FE+b(37|)zL;KLF>oiGfb(?&1 zV3XRu!Sw>@quKiab%g6jun#oZ%!>V#A%+lNc?q>6+VvyAn=kf_6z^(TZUa4Eelh{{ zqFX-#dY(EV@7l$NE&kv9u9BR8&Ojd#ZGJ6l8_BW}^r?DIS_rU2(XaGOK z225E@kH5Opf+CgD^{y29jD4gHbGf{1MD6ggQ&%>UG4WyPh5q_tb`{@_34B?xfSO*| zZv8!)q;^o-bz`MuxXk*G^}(6)ACb@=Lfs`Hxoh>`Y0NE8QRQ!*p|SH@{r8=%RKd4p z+#Ty^-0kb=-H-O`nAA3_6>2z(D=~Tbs(n8LHxD0`R0_ATFqp-SdY3(bZ3;VUM?J=O zKCNsxsgt@|&nKMC=*+ZqmLHhX1KHbAJs{nGVMs6~TiF%Q)P@>!koa$%oS zjXa=!5>P`vC-a}ln!uH1ooeI&v?=?v7?1n~P(wZ~0>xWxd_Aw;+}9#eULM7M8&E?Y zC-ZLhi3RoM92SXUb-5i-Lmt5_rfjE{6y^+24`y$1lywLyHO!)Boa7438K4#iLe?rh z2O~YGSgFUBH?og*6=r9rme=peP~ah`(8Zt7V)j5!V0KPFf_mebo3z95U8(up$-+EA^9dTRLq>Yl)YMBuch9%=e5B`Vnb>o zt03=kq;k2TgGe4|lGne&zJa~h(UGutjP_zr?a7~#b)@15XNA>Dj(m=gg2Q5V4-$)D|Q9}R#002ovPDHLkV1o7DH3k3x literal 0 HcmV?d00001 diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..c4df70d39da7941ef3f6dcb7f06a192d8dcb308d GIT binary patch literal 1888 zcmV-m2cP(fP)x~L`~4d)Rspd&<9kFh{hn*KP1LP0~$;u(LfAu zp%fx&qLBcRHx$G|3q(bv@+b;o0*D|jwD-Q9uQR(l*ST}s+uPgQ-MeFwZ#GS?b332? z&Tk$&_miXn3IGq)AmQ)3sisq{raD4(k*bHvpCe-TdWq^NRTEVM)i9xbgQ&ccnUVx* zEY%vS%gDcSg=!tuIK8$Th2_((_h^+7;R|G{n06&O2#6%LK`a}n?h_fL18btz<@lFG za}xS}u?#DBMB> zw^b($1Z)`9G?eP95EKi&$eOy@K%h;ryrR3la%;>|o*>CgB(s>dDcNOXg}CK9SPmD? zmr-s{0wRmxUnbDrYfRvnZ@d z6johZ2sMX{YkGSKWd}m|@V7`Degt-43=2M?+jR%8{(H$&MLLmS;-|JxnX2pnz;el1jsvqQz}pGSF<`mqEXRQ5sC4#BbwnB_4` zc5bFE-Gb#JV3tox9fp-vVEN{(tOCpRse`S+@)?%pz+zVJXSooTrNCUg`R6`hxwb{) zC@{O6MKY8tfZ5@!yy=p5Y|#+myRL=^{tc(6YgAnkg3I(Cd!r5l;|;l-MQ8B`;*SCE z{u)uP^C$lOPM z5d~UhKhRRmvv{LIa^|oavk1$QiEApSrP@~Jjbg`<*dW4TO?4qG%a%sTPUFz(QtW5( zM)lA+5)0TvH~aBaOAs|}?u2FO;yc-CZ1gNM1dAxJ?%m?YsGR`}-xk2*dxC}r5j$d* zE!#Vtbo69h>V4V`BL%_&$} z+oJAo@jQ^Tk`;%xw-4G>hhb&)B?##U+(6Fi7nno`C<|#PVA%$Y{}N-?(Gc$1%tr4Pc}}hm~yY#fTOe!@v9s-ik$dX~|ygArPhByaXn8 zpI^FUjNWMsTFKTP3X7m?UK)3m zp6rI^_zxRYrx6_QmhoWoDR`fp4R7gu6;gdO)!KexaoO2D88F9x#TM1(9Bn7g;|?|o z)~$n&Lh#hCP6_LOPD>a)NmhW})LADx2kq=X7}7wYRj-0?dXr&bHaRWCfSqvzFa=sn z-8^gSyn-RmH=BZ{AJZ~!8n5621GbUJV7Qvs%JNv&$%Q17s_X%s-41vAPfIR>;x0Wlqr5?09S>x#%Qkt>?(&XjFRY}*L6BeQ3 z<6XEBh^S7>AbwGm@XP{RkeEKj6@_o%oV?hDuUpUJ+r#JZO?!IUc;r0R?>mi)*ZpQ) z#((dn=A#i_&EQn|hd)N$#A*fjBFuiHcYvo?@y1 z5|fV=a^a~d!c-%ZbMNqkMKiSzM{Yq=7_c&1H!mXk60Uv32dV;vMg&-kQ)Q{+PFtwc zj|-uQ;b^gts??J*9VxxOro}W~Q9j4Em|zSRv)(WSO9$F$s=Ydu%Q+5DOid~lwk&we zY%W(Z@ofdwPHncEZzZgmqS|!gTj3wQq9rxQy+^eNYKr1mj&?tm@wkO*9@UtnRMG>c aR{jt9+;fr}hV%pg00001^@s67{VYS000c7NklQEG_j zup^)eW&WUIApqy$=APz8jE@awGp)!bsTjDbrJO`$x^ZR^dr;>)LW>{ zs70vpsD38v)19rI=GNk1b(0?Js9~rjsQsu*K;@SD40RB-3^gKU-MYC7G!Bw{fZsqp zih4iIi;Hr_xZ033Iu{sQxLS=}yBXgLMn40d++>aQ0#%8D1EbGZp7+ z5=mK?t31BkVYbGOxE9`i748x`YgCMwL$qMsChbSGSE1`p{nSmadR zcQ#R)(?!~dmtD0+D2!K zR9%!Xp1oOJzm(vbLvT^$IKp@+W2=-}qTzTgVtQ!#Y7Gxz}stUIm<1;oBQ^Sh2X{F4ibaOOx;5ZGSNK z0maF^@(UtV$=p6DXLgRURwF95C=|U8?osGhgOED*b z7woJ_PWXBD>V-NjQAm{~T%sjyJ{5tn2f{G%?J!KRSrrGvQ1(^`YLA5B!~eycY(e5_ z*%aa{at13SxC(=7JT7$IQF~R3sy`Nn%EMv!$-8ZEAryB*yB1k&stni)=)8-ODo41g zkJu~roIgAih94tb=YsL%iH5@^b~kU9M-=aqgXIrbtxMpFy5mekFm#edF9z7RQ6V}R zBIhbXs~pMzt0VWy1Fi$^fh+1xxLDoK09&5&MJl(q#THjPm(0=z2H2Yfm^a&E)V+a5 zbi>08u;bJsDRUKR9(INSc7XyuWv(JsD+BB*0hS)FO&l&7MdViuur@-<-EHw>kHRGY zqoT}3fDv2-m{NhBG8X}+rgOEZ;amh*DqN?jEfQdqxdj08`Sr=C-KmT)qU1 z+9Cl)a1mgXxhQiHVB}l`m;-RpmKy?0*|yl?FXvJkFxuu!fKlcmz$kN(a}i*saM3nr z0!;a~_%Xqy24IxA2rz<+08=B-Q|2PT)O4;EaxP^6qixOv7-cRh?*T?zZU`{nIM-at zTKYWr9rJ=tppQ9I#Z#mLgINVB!pO-^FOcvFw6NhV0gztuO?g ztoA*C-52Q-Z-P#xB4HAY3KQVd%dz1S4PA3vHp0aa=zAO?FCt zC_GaTyVBg2F!bBr3U@Zy2iJgIAt>1sf$JWA9kh{;L+P*HfUBX1Zy{4MgNbDfBV_ly z!y#+753arsZUt@366jIC0klaC@ckuk!qu=pAyf7&QmiBUT^L1&tOHzsK)4n|pmrVT zs2($4=?s~VejTFHbFdDOwG;_58LkIj1Fh@{glkO#F1>a==ymJS$z;gdedT1zPx4Kj ztjS`y_C}%af-RtpehdQDt3a<=W5C4$)9W@QAse;WUry$WYmr51ml9lkeunUrE`-3e zmq1SgSOPNEE-Mf+AGJ$g0M;3@w!$Ej;hMh=v=I+Lpz^n%Pg^MgwyqOkNyu2c^of)C z1~ALor3}}+RiF*K4+4{(1%1j3pif1>sv0r^mTZ?5Jd-It!tfPfiG_p$AY*Vfak%FG z4z#;wLtw&E&?}w+eKG^=#jF7HQzr8rV0mY<1YAJ_uGz~$E13p?F^fPSzXSn$8UcI$ z8er9{5w5iv0qf8%70zV71T1IBB1N}R5Kp%NO0=5wJalZt8;xYp;b{1K) zHY>2wW-`Sl{=NpR%iu3(u6l&)rc%%cSA#aV7WCowfbFR4wcc{LQZv~o1u_`}EJA3>ki`?9CKYTA!rhO)if*zRdd}Kn zEPfYbhoVE~!FI_2YbC5qAj1kq;xP6%J8+?2PAs?`V3}nyFVD#sV3+uP`pi}{$l9U^ zSz}_M9f7RgnnRhaoIJgT8us!1aB&4!*vYF07Hp&}L zCRlop0oK4DL@ISz{2_BPlezc;xj2|I z23RlDNpi9LgTG_#(w%cMaS)%N`e>~1&a3<{Xy}>?WbF>OOLuO+j&hc^YohQ$4F&ze z+hwnro1puQjnKm;vFG~o>`kCeUIlkA-2tI?WBKCFLMBY=J{hpSsQ=PDtU$=duS_hq zHpymHt^uuV1q@uc4bFb{MdG*|VoW@15Osrqt2@8ll0qO=j*uOXn{M0UJX#SUztui9FN4)K3{9!y8PC-AHHvpVTU;x|-7P+taAtyglk#rjlH2 z5Gq8ik}BPaGiM{#Woyg;*&N9R2{J0V+WGB69cEtH7F?U~Kbi6ksi*`CFXsi931q7Y zGO82?whBhN%w1iDetv%~wM*Y;E^)@Vl?VDj-f*RX>{;o_=$fU!&KAXbuadYZ46Zbg z&6jMF=49$uL^73y;;N5jaHYv)BTyfh&`qVLYn?`o6BCA_z-0niZz=qPG!vonK3MW_ zo$V96zM!+kJRs{P-5-rQVse0VBH*n6A58)4uc&gfHMa{gIhV2fGf{st>E8sKyP-$8zp~wJX^A*@DI&-;8>gANXZj zU)R+Y)PB?=)a|Kj>8NXEu^S_h^7R`~Q&7*Kn!xyvzVv&^>?^iu;S~R2e-2fJx-oUb cX)(b1KSk$MOV07*qoM6N<$f&6$jw%VRuvdN2+38CZWny1cRtlsl+0_KtW)EU14Ei(F!UtWuj4IK+3{sK@>rh zs1Z;=(DD&U6+tlyL?UnHVN^&g6QhFi2#HS+*qz;(>63G(`|jRtW|nz$Pv7qTovP!^ zP_jES{mr@O-02w%!^a?^1ZP!_KmQiz0L~jZ=W@Qt`8wzOoclQsAS<5YdH;a(4bGLE zk8s}1If(PSIgVi!XE!5kA?~z*sobvNyohr;=Q_@h2@$6Flyej3J)D-6YfheRGl`HEcPk|~huT_2-U?PfL=4BPV)f1o!%rQ!NMt_MYw-5bUSwQ9Z&zC>u zOrl~UJglJNa%f50Ok}?WB{on`Ci`p^Y!xBA?m@rcJXLxtrE0FhRF3d*ir>yzO|BD$ z3V}HpFcCh6bTzY}Nt_(W%QYd3NG)jJ4<`F<1Od) zfQblTdC&h2lCz`>y?>|9o2CdvC8qZeIZt%jN;B7Hdn2l*k4M4MFEtq`q_#5?}c$b$pf_3y{Y!cRDafZBEj-*OD|gz#PBDeu3QoueOesLzB+O zxjf2wvf6Wwz>@AiOo2mO4=TkAV+g~%_n&R;)l#!cBxjuoD$aS-`IIJv7cdX%2{WT7 zOm%5rs(wqyPE^k5SIpUZ!&Lq4<~%{*>_Hu$2|~Xa;iX*tz8~G6O3uFOS?+)tWtdi| zV2b#;zRN!m@H&jd=!$7YY6_}|=!IU@=SjvGDFtL;aCtw06U;-v^0%k0FOyESt z1Wv$={b_H&8FiRV?MrzoHWd>%v6KTRU;-v^Miiz+@q`(BoT!+<37CKhoKb)|8!+RG z6BQFU^@fRW;s8!mOf2QViKQGk0TVER6EG1`#;Nm39Do^PoT!+<37AD!%oJe86(=et zZ~|sLzU>V-qYiU6V8$0GmU7_K8|Fd0B?+9Un1BhKAz#V~Fk^`mJtlCX#{^8^M8!me z8Yg;8-~>!e<-iG;h*0B1kBKm}hItVGY6WnjVpgnTTAC$rqQ^v)4KvOtpY|sIj@WYg zyw##ZZ5AC2IKNC;^hwg9BPk0wLStlmBr;E|$5GoAo$&Ui_;S9WY62n3)i49|T%C#i017z3J=$RF|KyZWnci*@lW4 z=AKhNN6+m`Q!V3Ye68|8y@%=am>YD0nG99M)NWc20%)gwO!96j7muR}Fr&54SxKP2 zP30S~lt=a*qDlbu3+Av57=9v&vr<6g0&`!8E2fq>I|EJGKs}t|{h7+KT@)LfIV-3K zK)r_fr2?}FFyn*MYoLC>oV-J~eavL2ho4a4^r{E-8m2hi>~hA?_vIG4a*KT;2eyl1 zh_hUvUJpNCFwBvRq5BI*srSle>c6%n`#VNsyC|MGa{(P&08p=C9+WUw9Hl<1o9T4M zdD=_C0F7#o8A_bRR?sFNmU0R6tW`ElnF8p53IdHo#S9(JoZCz}fHwJ6F<&?qrpVqE zte|m%89JQD+XwaPU#%#lVs-@-OL);|MdfINd6!XwP2h(eyafTUsoRkA%&@fe?9m@jw-v(yTTiV2(*fthQH9}SqmsRPVnwwbV$1E(_lkmo&S zF-truCU914_$jpqjr(>Ha4HkM4YMT>m~NosUu&UZ>zirfHo%N6PPs9^_o$WqPA0#5 z%tG>qFCL+b*0s?sZ;Sht0nE7Kl>OVXy=gjWxxK;OJ3yGd7-pZf7JYNcZo2*1SF`u6 zHJyRRxGw9mDlOiXqVMsNe#WX`fC`vrtjSQ%KmLcl(lC>ZOQzG^%iql2w-f_K@r?OE zwCICifM#L-HJyc7Gm>Ern?+Sk3&|Khmu4(~3qa$(m6Ub^U0E5RHq49za|XklN#?kP zl;EstdW?(_4D>kwjWy2f!LM)y?F94kyU3`W!6+AyId-89v}sXJpuic^NLL7GJItl~ zsiuB98AI-(#Mnm|=A-R6&2fwJ0JVSY#Q>&3$zFh|@;#%0qeF=j5Ajq@4i0tIIW z&}sk$&fGwoJpe&u-JeGLi^r?dO`m=y(QO{@h zQqAC7$rvz&5+mo3IqE?h=a~6m>%r5Quapvzq;{y~p zJpyXOBgD9VrW7@#p6l7O?o3feml(DtSL>D^R) zZUY%T2b0-vBAFN7VB;M88!~HuOXi4KcI6aRQ&h|XQ0A?m%j2=l1f0cGP}h(oVfJ`N zz#PpmFC*ieab)zJK<4?^k=g%OjPnkANzbAbmGZHoVRk*mTfm75s_cWVa`l*f$B@xu z5E*?&@seIo#*Y~1rBm!7sF9~~u6Wrj5oICUOuz}CS)jdNIznfzCA(stJ(7$c^e5wN z?lt>eYgbA!kvAR7zYSD&*r1$b|(@;9dcZ^67R0 zXAXJKa|5Sdmj!g578Nwt6d$sXuc&MWezA0Whd`94$h{{?1IwXP4)Tx4obDK%xoFZ_Z zjjHJ_P@R_e5blG@yEjnaJb`l;s%Lb2&=8$&Ct-fV`E^4CUs)=jTk!I}2d&n!f@)bm z@ z_4Dc86+3l2*p|~;o-Sb~oXb_RuLmoifDU^&Te$*FevycC0*nE3Xws8gsWp|Rj2>SM zns)qcYj?^2sd8?N!_w~4v+f-HCF|a$TNZDoNl$I1Uq87euoNgKb6&r26TNrfkUa@o zfdiFA@p{K&mH3b8i!lcoz)V{n8Q@g(vR4ns4r6w;K z>1~ecQR0-<^J|Ndg5fvVUM9g;lbu-){#ghGw(fg>L zh)T5Ljb%lWE;V9L!;Cqk>AV1(rULYF07ZBJbGb9qbSoLAd;in9{)95YqX$J43-dY7YU*k~vrM25 zxh5_IqO0LYZW%oxQ5HOzmk4x{atE*vipUk}sh88$b2tn?!ujEHn`tQLe&vo}nMb&{ zio`xzZ&GG6&ZyN3jnaQy#iVqXE9VT(3tWY$n-)uWDQ|tc{`?fq2F`oQ{;d3aWPg4Hp-(iE{ry>MIPWL> iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/contactos_foundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/contactos_foundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/contactos_foundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contactos_foundation/example/ios/Runner/Base.lproj/Main.storyboard b/contactos_foundation/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/contactos_foundation/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contactos_foundation/example/ios/Runner/Info.plist b/contactos_foundation/example/ios/Runner/Info.plist new file mode 100644 index 0000000..a61b535 --- /dev/null +++ b/contactos_foundation/example/ios/Runner/Info.plist @@ -0,0 +1,55 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + contactos_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + NSContactsUsageDescription + This app requires contacts access to function properly. + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/contactos_foundation/example/ios/Runner/Runner-Bridging-Header.h b/contactos_foundation/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..7335fdf --- /dev/null +++ b/contactos_foundation/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" \ No newline at end of file diff --git a/contactos_foundation/example/lib/main.dart b/contactos_foundation/example/lib/main.dart new file mode 100644 index 0000000..86291d3 --- /dev/null +++ b/contactos_foundation/example/lib/main.dart @@ -0,0 +1,99 @@ +import 'package:contactos_example/src/contacts_list_screen.dart'; +import 'package:contactos_example/src/navite_contacts_picker_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; + +void main() => runApp(const ContactsExampleApp()); + +/// iOS only: Localized labels language setting +/// is equal to CFBundleDevelopmentRegion value (Info.plist) of the iOS project +/// Set iOSLocalizedLabels=false if you always want english labels +/// whatever is the CFBundleDevelopmentRegion value. +const iOSLocalizedLabels = false; + +/// {@template main} +/// Example app for the `contactos` plugin. +/// {@endtemplate} +class ContactsExampleApp extends StatelessWidget { + /// {@macro main} + const ContactsExampleApp({super.key}); + + @override + Widget build(BuildContext context) => MaterialApp( + home: const _HomePage(), + routes: { + '/add': (_) => const AddContactScreen(), + '/contacts-list': (_) => const ContactsListScreen(), + '/native-contacts-picker': (_) => const NativeContactsPickerScreen(), + }, + ); +} + +class _HomePage extends StatefulWidget { + const _HomePage({ + super.key, // ignore: unused_element_parameter + }); + + @override + State<_HomePage> createState() => __HomePageState(); +} + +class __HomePageState extends State<_HomePage> { + @override + void initState() { + super.initState(); + _askPermissions(); + } + + Future _askPermissions([String? routeName]) async { + final navigator = Navigator.of(context); + final permissionStatus = await _getContactPermission(); + if (permissionStatus == PermissionStatus.granted) { + if (routeName != null) await navigator.pushNamed(routeName); + } else { + _handleInvalidPermissions(permissionStatus); + } + } + + Future _getContactPermission() async { + final permission = await Permission.contacts.status; + if (permission != PermissionStatus.granted && + permission != PermissionStatus.permanentlyDenied) { + final permissionStatus = await Permission.contacts.request(); + return permissionStatus; + } else { + return permission; + } + } + + void _handleInvalidPermissions(PermissionStatus permissionStatus) { + Widget? content; + if (permissionStatus == PermissionStatus.denied) { + content = const Text('Access to contact data denied'); + } else if (permissionStatus == PermissionStatus.permanentlyDenied) { + content = const Text('Contact data not available on device'); + } + if (content == null) return; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: content)); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Contacts Plugin Example')), + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton( + child: const Text('Contacts list'), + onPressed: () => _askPermissions('/contacts-list'), + ), + ElevatedButton( + child: const Text('Native Contacts picker'), + onPressed: () => _askPermissions('/native-contacts-picker'), + ), + ], + ), + ), + ); +} diff --git a/contactos_foundation/example/lib/src/contacts_list_screen.dart b/contactos_foundation/example/lib/src/contacts_list_screen.dart new file mode 100644 index 0000000..566e465 --- /dev/null +++ b/contactos_foundation/example/lib/src/contacts_list_screen.dart @@ -0,0 +1,588 @@ +/* + * Author: Anton Ustinoff | + * Date: 25 November 2025 + */ + +import 'dart:developer'; + +import 'package:collection/collection.dart'; +import 'package:contactos_example/main.dart' show iOSLocalizedLabels; +import 'package:contactos_foundation/contactos_foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +/// {@template contacts_list_screen} +/// Contacts list screen widget. +/// {@endtemplate} +class ContactsListScreen extends StatefulWidget { + /// {@macro contacts_list_screen} + const ContactsListScreen({super.key}); + + @override + State createState() => _ContactsListPageState(); +} + +class _ContactsListPageState extends State { + List? _contacts; + + @override + void initState() { + super.initState(); + refreshContacts(); + } + + Future refreshContacts() async { + // final contacts = + // await ContactosPluginFoundation + // .instance + // .getContactsForPhone("8554964652"); + + // Load without thumbnails initially. + final contacts = await ContactosPluginFoundation.instance.getContacts( + iOSLocalizedLabels: iOSLocalizedLabels, + withThumbnails: false, + ); + + _contacts = contacts; + if (!mounted) return; + setState(() {}); + + // Lazy load thumbnails after rendering initial contacts. + for (var contact in _contacts ?? contacts) { + ContactosPluginFoundation.instance.getAvatar(contact).then((avatar) { + contact = contact.copyWith(avatar: avatar); + if (mounted) setState(() {}); + }).ignore(); + } + } + + Future updateContact() async { + var contact = _contacts?.firstWhereOrNull( + (c) => c.familyName?.startsWith('Ninja') ?? false, + ); + if (contact == null) return; + await ContactosPluginFoundation.instance.updateContact(contact); + await refreshContacts(); + } + + Future _openContactForm() async { + try { + var _ = await ContactosPluginFoundation.instance.openContactForm( + iOSLocalizedLabels: iOSLocalizedLabels, + ); + await refreshContacts(); + } on FormOperationException catch (e) { + switch (e.errorCode) { + case FormOperationErrorCode.unknown: + case FormOperationErrorCode.canceled: + case FormOperationErrorCode.couldNotBeOpen: + default: + log(e.toString()); + } + } + } + + void contactOnDeviceHasBeenUpdated(Contact contact) { + if (!mounted) return; + final id = _contacts?.indexWhere((c) => c.identifier == contact.identifier); + if (id == null || id < 0) return; + _contacts?[id] = contact; + setState(() {}); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Contacts Plugin Example'), + actions: [ + IconButton(icon: const Icon(Icons.create), onPressed: _openContactForm), + ], + ), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () { + Navigator.of(context).pushNamed('/add').then((_) { + refreshContacts(); + }); + }, + ), + body: SafeArea( + child: _contacts != null + ? ListView.builder( + itemCount: _contacts?.length ?? 0, + itemBuilder: (context, index) { + final contact = _contacts?.elementAt(index); + if (contact == null) return const SizedBox.shrink(); + return ListTile( + onTap: () { + final route = MaterialPageRoute( + builder: (context) => _ContactDetailsPage( + contact, + onContactDeviceSave: contactOnDeviceHasBeenUpdated, + ), + ); + Navigator.of(context).push(route); + }, + leading: + (contact.avatar != null && + (contact.avatar?.length ?? 0) > 0) + ? CircleAvatar( + backgroundImage: MemoryImage(contact.avatar!), + ) + : CircleAvatar(child: Text(contact.initials())), + title: Text(contact.displayName ?? ''), + ); + }, + ) + : const Center(child: CircularProgressIndicator()), + ), + ); +} + +class _ContactDetailsPage extends StatefulWidget { + const _ContactDetailsPage( + this._contact, { + this.onContactDeviceSave, + super.key, // ignore: unused_element_parameter + }); + + final Contact _contact; + final void Function(Contact)? onContactDeviceSave; + + @override + State<_ContactDetailsPage> createState() => _ContactDetailsPageState(); +} + +class _ContactDetailsPageState extends State<_ContactDetailsPage> { + Future _openExistingContactOnDevice(BuildContext context) async { + try { + final contact = await ContactosPluginFoundation.instance + .openExistingContact( + widget._contact, + iOSLocalizedLabels: iOSLocalizedLabels, + ); + if (widget.onContactDeviceSave != null) { + widget.onContactDeviceSave?.call(contact); + } + if (!context.mounted) return; + Navigator.of(context).pop(); + } on FormOperationException catch (e) { + switch (e.errorCode) { + case FormOperationErrorCode.unknown: + case FormOperationErrorCode.canceled: + case FormOperationErrorCode.couldNotBeOpen: + default: + log(e.toString()); + } + } + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text(widget._contact.displayName ?? ''), + actions: [ + /* IconButton( + icon: Icon(Icons.share), + onPressed: () => shareVCFCard(context, contact: _contact), + ), */ + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + ContactosPluginFoundation.instance.deleteContact(widget._contact); + }, + ), + IconButton( + icon: const Icon(Icons.update), + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + _UpdateContactsPage(contact: widget._contact), + ), + ), + ), + IconButton( + icon: const Icon(Icons.edit), + onPressed: () => _openExistingContactOnDevice(context), + ), + ], + ), + body: SafeArea( + child: ListView( + children: [ + ListTile( + title: const Text('Name'), + trailing: Text(widget._contact.givenName ?? ''), + ), + ListTile( + title: const Text('Middle name'), + trailing: Text(widget._contact.middleName ?? ''), + ), + ListTile( + title: const Text('Family name'), + trailing: Text(widget._contact.familyName ?? ''), + ), + ListTile( + title: const Text('Prefix'), + trailing: Text(widget._contact.prefix ?? ''), + ), + ListTile( + title: const Text('Suffix'), + trailing: Text(widget._contact.suffix ?? ''), + ), + ListTile( + title: const Text('Birthday'), + trailing: Text( + widget._contact.birthday != null + ? DateFormat('dd-MM-yyyy').format(widget._contact.birthday!) + : '', + ), + ), + ListTile( + title: const Text('Company'), + trailing: Text(widget._contact.company ?? ''), + ), + ListTile( + title: const Text('Job'), + trailing: Text(widget._contact.jobTitle ?? ''), + ), + ListTile( + title: const Text('Account Type'), + trailing: Text( + (widget._contact.androidAccountType != null) + ? widget._contact.androidAccountType.toString() + : '', + ), + ), + _AddressesTile(widget._contact.postalAddresses!), + _ItemsTile('Phones', widget._contact.phones!), + _ItemsTile('Emails', widget._contact.emails!), + ], + ), + ), + ); +} + +@immutable +class _AddressesTile extends StatelessWidget { + const _AddressesTile( + this._addresses, { + super.key, // ignore: unused_element_parameter + }); + + final List _addresses; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile(title: Text('Addresses')), + Column( + children: [ + for (final a in _addresses) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + ListTile( + title: const Text('Street'), + trailing: Text(a.street ?? ''), + ), + ListTile( + title: const Text('Postcode'), + trailing: Text(a.postcode ?? ''), + ), + ListTile( + title: const Text('City'), + trailing: Text(a.city ?? ''), + ), + ListTile( + title: const Text('Region'), + trailing: Text(a.region ?? ''), + ), + ListTile( + title: const Text('Country'), + trailing: Text(a.country ?? ''), + ), + ], + ), + ), + ], + ), + ], + ); +} + +class _ItemsTile extends StatelessWidget { + const _ItemsTile( + this._title, + this._items, { + super.key, // ignore: unused_element_parameter + }); + + final List _items; + final String _title; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile(title: Text(_title)), + Column( + children: [ + for (final i in _items) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListTile( + title: Text(i.label ?? ''), + trailing: Text(i.value ?? ''), + ), + ), + ], + ), + ], + ); +} + +/// {@template add_contact_screen} +/// Add contact screen. +/// {@endtemplate} +class AddContactScreen extends StatefulWidget { + /// {@macro add_contact_screen} + const AddContactScreen({ + super.key, // ignore: unused_element_parameter + }); + + @override + State createState() => _AddContactScreenState(); +} + +class _AddContactScreenState extends State { + final GlobalKey _formKey = GlobalKey(); + + Contact$PostalAddress address = const Contact$PostalAddress(label: 'Home'); + Contact contact = const Contact(); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Add a contact'), + actions: [ + TextButton( + onPressed: () { + _formKey.currentState?.save(); + final newContact = contact.copyWith(postalAddresses: [address]); + ContactosPluginFoundation.instance.addContact(newContact); + Navigator.of(context).pop(); + }, + child: const Icon(Icons.save, color: Colors.white), + ), + ], + ), + body: Container( + padding: const EdgeInsets.all(12), + child: Form( + key: _formKey, + child: ListView( + children: [ + TextFormField( + decoration: const InputDecoration(labelText: 'First name'), + onSaved: (v) => contact.copyWith(givenName: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Middle name'), + onSaved: (v) => contact.copyWith(middleName: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Last name'), + onSaved: (v) => contact = contact.copyWith(familyName: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Prefix'), + onSaved: (v) => contact = contact.copyWith(prefix: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Suffix'), + onSaved: (v) => contact = contact.copyWith(suffix: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Phone'), + onSaved: (v) => contact = contact.copyWith( + phones: [Contact$Field(label: 'mobile', value: v)], + ), + keyboardType: TextInputType.phone, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'E-mail'), + onSaved: (v) => contact = contact.copyWith( + emails: [Contact$Field(label: 'work', value: v)], + ), + keyboardType: TextInputType.emailAddress, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Company'), + onSaved: (v) => contact = contact.copyWith(company: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Job'), + onSaved: (v) => contact = contact.copyWith(jobTitle: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Street'), + onSaved: (v) => address = address.copyWith(street: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'City'), + onSaved: (v) => address = address.copyWith(city: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Region'), + onSaved: (v) => address = address.copyWith(region: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Postal code'), + onSaved: (v) => address = address.copyWith(postcode: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Country'), + onSaved: (v) => address = address.copyWith(country: v), + ), + ], + ), + ), + ), + ); +} + +class _UpdateContactsPage extends StatefulWidget { + const _UpdateContactsPage({ + required this.contact, + super.key, // ignore: unused_element_parameter + }); + + final Contact? contact; + + @override + State<_UpdateContactsPage> createState() => __UpdateContactsPageState(); +} + +class __UpdateContactsPageState extends State<_UpdateContactsPage> { + final GlobalKey _formKey = GlobalKey(); + + Contact$PostalAddress address = const Contact$PostalAddress(label: 'Home'); + Contact? contact; + + @override + void initState() { + super.initState(); + contact = widget.contact; + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Update Contact'), + actions: [ + IconButton( + icon: const Icon(Icons.save, color: Colors.white), + onPressed: () async { + _formKey.currentState?.save(); + final navigator = Navigator.of(context); + final newContact = contact?.copyWith(postalAddresses: [address]); + if (newContact == null) return; + await ContactosPluginFoundation.instance.updateContact(newContact); + await navigator.pushReplacement( + MaterialPageRoute( + builder: (_) => const ContactsListScreen(), + ), + ); + }, + ), + ], + ), + body: Container( + padding: const EdgeInsets.all(12), + child: Form( + key: _formKey, + child: ListView( + children: [ + TextFormField( + initialValue: contact?.givenName ?? '', + decoration: const InputDecoration(labelText: 'First name'), + onSaved: (v) => contact = contact?.copyWith(givenName: v), + ), + TextFormField( + initialValue: contact?.middleName ?? '', + decoration: const InputDecoration(labelText: 'Middle name'), + onSaved: (v) => contact = contact?.copyWith(middleName: v), + ), + TextFormField( + initialValue: contact?.familyName ?? '', + decoration: const InputDecoration(labelText: 'Last name'), + onSaved: (v) => contact = contact?.copyWith(familyName: v), + ), + TextFormField( + initialValue: contact?.prefix ?? '', + decoration: const InputDecoration(labelText: 'Prefix'), + onSaved: (v) => contact = contact?.copyWith(prefix: v), + ), + TextFormField( + initialValue: contact?.suffix ?? '', + decoration: const InputDecoration(labelText: 'Suffix'), + onSaved: (v) => contact = contact?.copyWith(suffix: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Phone'), + onSaved: (v) => contact = contact?.copyWith( + phones: [Contact$Field(label: 'mobile', value: v)], + ), + keyboardType: TextInputType.phone, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'E-mail'), + onSaved: (v) => contact = contact?.copyWith( + emails: [Contact$Field(label: 'work', value: v)], + ), + keyboardType: TextInputType.emailAddress, + ), + TextFormField( + initialValue: contact?.company ?? '', + decoration: const InputDecoration(labelText: 'Company'), + onSaved: (v) => contact = contact?.copyWith(company: v), + ), + TextFormField( + initialValue: contact?.jobTitle ?? '', + decoration: const InputDecoration(labelText: 'Job'), + onSaved: (v) => contact = contact?.copyWith(jobTitle: v), + ), + TextFormField( + initialValue: address.street ?? '', + decoration: const InputDecoration(labelText: 'Street'), + onSaved: (v) => address = address.copyWith(street: v), + ), + TextFormField( + initialValue: address.city ?? '', + decoration: const InputDecoration(labelText: 'City'), + onSaved: (v) => address = address.copyWith(city: v), + ), + TextFormField( + initialValue: address.region ?? '', + decoration: const InputDecoration(labelText: 'Region'), + onSaved: (v) => address = address.copyWith(region: v), + ), + TextFormField( + initialValue: address.postcode ?? '', + decoration: const InputDecoration(labelText: 'Postal code'), + onSaved: (v) => address = address.copyWith(postcode: v), + ), + TextFormField( + initialValue: address.country ?? '', + decoration: const InputDecoration(labelText: 'Country'), + onSaved: (v) => address = address.copyWith(country: v), + ), + ], + ), + ), + ), + ); +} diff --git a/contactos_foundation/example/lib/src/navite_contacts_picker_screen.dart b/contactos_foundation/example/lib/src/navite_contacts_picker_screen.dart new file mode 100644 index 0000000..d7513e9 --- /dev/null +++ b/contactos_foundation/example/lib/src/navite_contacts_picker_screen.dart @@ -0,0 +1,59 @@ +/* + * Author: Anton Ustinoff | + * Date: 25 November 2025 + */ + +import 'dart:developer'; + +import 'package:contactos_example/main.dart' show iOSLocalizedLabels; +import 'package:contactos_foundation/contactos_foundation.dart'; +import 'package:flutter/material.dart'; + +/// {@template navite_contacts_picker_screen} +/// NativeContactsPickerScreen widget. +/// {@endtemplate} +class NativeContactsPickerScreen extends StatefulWidget { + /// {@macro navite_contacts_picker_screen} + const NativeContactsPickerScreen({super.key}); + + @override + State createState() => + _NativeContactsPickerScreenState(); +} + +class _NativeContactsPickerScreenState + extends State { + Contact? _contact; + + @override + void initState() { + super.initState(); + } + + Future _pickContact() async { + try { + final contact = await ContactosPluginFoundation.instance + .openDeviceContactPicker(iOSLocalizedLabels: iOSLocalizedLabels); + setState(() => _contact = contact); + } on Object catch (e) { + log(e.toString()); + } + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Contacts Picker Example')), + body: SafeArea( + child: Column( + children: [ + ElevatedButton( + onPressed: _pickContact, + child: const Text('Pick a contact'), + ), + if (_contact != null) + Text('Contact selected: ${_contact?.displayName}'), + ], + ), + ), + ); +} diff --git a/contactos_foundation/example/pubspec.lock b/contactos_foundation/example/pubspec.lock new file mode 100644 index 0000000..54cd7df --- /dev/null +++ b/contactos_foundation/example/pubspec.lock @@ -0,0 +1,343 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: "direct main" + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + contactos_foundation: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + contactos_platform_interface: + dependency: transitive + description: + path: "../../contactos_platform_interface" + relative: true + source: path + version: "1.0.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" + source: hosted + version: "0.17.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + url: "https://pub.dev" + source: hosted + version: "11.4.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + url: "https://pub.dev" + source: hosted + version: "12.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98 + url: "https://pub.dev" + source: hosted + version: "9.4.6" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" +sdks: + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.3" diff --git a/contactos_foundation/example/pubspec.yaml b/contactos_foundation/example/pubspec.yaml new file mode 100644 index 0000000..5199544 --- /dev/null +++ b/contactos_foundation/example/pubspec.yaml @@ -0,0 +1,34 @@ +name: contactos_example +description: Testbed for the contactos foundation implementation. +publish_to: none +version: 0.0.1 + + +environment: + sdk: '>=3.10.0 <4.0.0' + flutter: ">=3.38.3" + + +dependencies: + flutter: + sdk: flutter + + contactos_foundation: + path: ../ + + cupertino_icons: any + collection: any + intl: any + + permission_handler: ^11.4.0 + + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + + +flutter: + uses-material-design: true diff --git a/contactos_foundation/example/test/widget_test.dart b/contactos_foundation/example/test/widget_test.dart new file mode 100644 index 0000000..de20a2c --- /dev/null +++ b/contactos_foundation/example/test/widget_test.dart @@ -0,0 +1,33 @@ +// This is a basic Flutter widget test. +// To perform an interaction with a widget in your test, +// use the WidgetTester utility that Flutter +// provides. For example, you can send tap and scroll gestures. +// You can also use WidgetTester to find child widgets in the widget tree, +// read text, and verify that the values of widget properties are correct. + +import 'package:contactos_example/src/contacts_list_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() => group( + 'Widget_tests -', + () => group( + 'ContactsListScreents -', + () => testWidgets('First test', (tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const ContactsListScreen()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }), + ), +); diff --git a/contactos_foundation/lib/contactos_foundation.dart b/contactos_foundation/lib/contactos_foundation.dart new file mode 100644 index 0000000..3797a5a --- /dev/null +++ b/contactos_foundation/lib/contactos_foundation.dart @@ -0,0 +1,7 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'package:contactos_platform_interface/contactos_platform_interface.dart'; + +export 'src/contactos_foundation.dart'; diff --git a/contactos_foundation/lib/src/contactos_foundation.dart b/contactos_foundation/lib/src/contactos_foundation.dart new file mode 100644 index 0000000..0eb3731 --- /dev/null +++ b/contactos_foundation/lib/src/contactos_foundation.dart @@ -0,0 +1,161 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:contactos_platform_interface/contactos_platform_interface.dart'; +import 'package:flutter/foundation.dart' show visibleForOverriding; + +/// The iOS and macOS implementation of [ContactosPlatform]. +/// +/// This class implements the `package:contactos` +/// functionality for iOS and macOS. +class ContactosPluginFoundation extends ContactosPlatform { + /// Creates a new plugin for iOS and macOS implementation instance. + ContactosPluginFoundation._({ + @visibleForOverriding MethodChannelContactos? channel, + }) : _channel = channel ?? MethodChannelContactos.instance; + + /// Returns an instance using a specified [MethodChannelContactos]. + factory ContactosPluginFoundation._instanceFor({ + @visibleForOverriding MethodChannelContactos? channel, + }) => + ContactosPluginFoundation._(channel: channel); + + /// Returns the default instance + /// of [ContactosPluginFoundation]. + static ContactosPluginFoundation get instance => _instance; + + /// Returns an instance using the default [ContactosPluginFoundation]. + static final ContactosPluginFoundation _instance = + ContactosPluginFoundation._instanceFor(); + + /// The channel used to interact with the platform side of the plugin. + final MethodChannelContactos _channel; + + /// Registers this class + /// as the default instance of [ContactosPla]. + static void registerWith() { + ContactosPlatform.instance = ContactosPluginFoundation.instance; + } + + /// Fetches all contacts, or when specified, the contacts with a name + /// matching [query] + @override + Future> getContacts({ + String? query, + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.getContacts( + query: query, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Fetches all contacts, or when specified, the contacts with the phone + /// matching [phone] + @override + Future> getContactsForPhone( + String? phone, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.getContactsForPhone( + phone, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Fetches all contacts, or when specified, the contacts with the email + /// matching [email] + /// Works only on iOS + @override + Future> getContactsForEmail( + String email, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.getContactsForEmail( + email, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Loads the avatar for the given contact and returns it. If the user does + /// not have an avatar, then `null` is returned in that slot. Only implemented + /// on Android. + @override + Future getAvatar(Contact contact, {bool photoHighRes = true}) => + _channel.getAvatar(contact, photoHighRes: photoHighRes); + + /// Adds the [contact] to the device contact list + @override + Future addContact(Contact contact) => _channel.addContact(contact); + + /// Deletes the [contact] if it has a valid identifier + @override + Future deleteContact(Contact contact) => + _channel.deleteContact(contact); + + /// Updates the [contact] if it has a valid identifier + @override + Future updateContact(Contact contact) => + _channel.updateContact(contact); + + /// Opens the contact form with the fields prefilled with the values from the + @override + Future openContactForm({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.openContactForm( + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Opens the contact form with the fields prefilled with the values from the + /// [contact] parameter + @override + Future openExistingContact( + Contact contact, { + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.openExistingContact( + contact, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Displays the device/native contact picker dialog + /// and returns the contact selected by the user + @override + Future openDeviceContactPicker({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.openDeviceContactPicker( + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); +} diff --git a/contactos_foundation/pubspec.lock b/contactos_foundation/pubspec.lock new file mode 100644 index 0000000..a44201f --- /dev/null +++ b/contactos_foundation/pubspec.lock @@ -0,0 +1,419 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e" + url: "https://pub.dev" + source: hosted + version: "92.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139" + url: "https://pub.dev" + source: hosted + version: "8.12.1" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" + url: "https://pub.dev" + source: hosted + version: "4.11.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + contactos_platform_interface: + dependency: "direct main" + description: + path: "../contactos_platform_interface" + relative: true + source: path + version: "1.0.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + url: "https://pub.dev" + source: hosted + version: "3.1.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pigeon: + dependency: "direct dev" + description: + name: pigeon + sha256: "5480e09d2c3b1df269798ce5b2b73ae6469c4f6789d7d27ca610cb9c77b98246" + url: "https://pub.dev" + source: hosted + version: "26.1.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + url: "https://pub.dev" + source: hosted + version: "1.1.4" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.29.3" diff --git a/contactos_foundation/pubspec.yaml b/contactos_foundation/pubspec.yaml new file mode 100644 index 0000000..4baaebe --- /dev/null +++ b/contactos_foundation/pubspec.yaml @@ -0,0 +1,64 @@ +name: contactos_foundation +description: Android implementation of the contactos plugin. +version: 0.0.1 + +homepage: https://github.com/ziqq/contactos/contactos_foundation + +repository: https://github.com/ziqq/contactos/contactos_foundation + +issue_tracker: https://github.com/ziqq/contactos/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+contactos%22 + +publish_to: none + +funding: + - https://www.buymeacoffee.com/ziqq + - https://boosty.to/ziqq + +#screenshots: +# - description: 'Example of using the library to check app version.' +# path: example.png + +topics: + - contacts + + +environment: + sdk: '>=3.6.0 <4.0.0' + flutter: ">=3.29.0" + + +dependencies: + flutter: + sdk: flutter + + # contactos_platform_interface: ^1.0.0 + contactos_platform_interface: + path: ../contactos_platform_interface + + +dev_dependencies: + # Integration tests for Flutter + integration_test: + sdk: flutter + # Unit & Widget tests for Flutter + flutter_test: + sdk: flutter + + pigeon: ^26.0.2 + + # Linting + flutter_lints: ^6.0.0 + + +flutter: + plugin: + platforms: + ios: + pluginClass: ContactosPlugin + dartPluginClass: ContactosPluginFoundation + sharedDarwinSource: true + # macos: + # pluginClass: ContactosPlugin + # dartPluginClass: ContactosPluginFoundation + # sharedDarwinSource: true + diff --git a/contactos_ios/.gitkeep b/contactos_ios/.gitkeep deleted file mode 100644 index e69de29..0000000 From 42734608c3261476a4724ff830f7dcdc31e45b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Tue, 25 Nov 2025 17:36:13 +0400 Subject: [PATCH 06/35] feat(0.0.1): added tests directory for native plagin --- contactos_foundation/darwin/contactos_foundation/Tests/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 contactos_foundation/darwin/contactos_foundation/Tests/.keep diff --git a/contactos_foundation/darwin/contactos_foundation/Tests/.keep b/contactos_foundation/darwin/contactos_foundation/Tests/.keep new file mode 100644 index 0000000..e69de29 From 5e7ea353b5cae981b80e037759300264dad3dc43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Tue, 25 Nov 2025 18:13:07 +0400 Subject: [PATCH 07/35] chore: update --- .github/FUNDING.yml | 13 +++++++++++++ {.docs => .github/images}/example.gif | Bin 2 files changed, 13 insertions(+) create mode 100644 .github/FUNDING.yml rename {.docs => .github/images}/example.gif (100%) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..79d8aee --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +#patreon: ziqq +#open_collective: # Replace with a single Open Collective username +#ko_fi: # Replace with a single Ko-fi username +#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +#liberapay: # Replace with a single Liberapay username +#issuehunt: # Replace with a single IssueHunt username +#otechie: # Replace with a single Otechie username +#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: ['https://www.buymeacoffee.com/ziqq', 'https://boosty.to/ziqq'] \ No newline at end of file diff --git a/.docs/example.gif b/.github/images/example.gif similarity index 100% rename from .docs/example.gif rename to .github/images/example.gif From 1c227da01b20506ad35174be89dec6df30c6e254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Tue, 25 Nov 2025 18:13:33 +0400 Subject: [PATCH 08/35] chore: update --- contactos_foundation/pubspec.lock | 148 +----------------------------- contactos_foundation/pubspec.yaml | 5 +- 2 files changed, 3 insertions(+), 150 deletions(-) diff --git a/contactos_foundation/pubspec.lock b/contactos_foundation/pubspec.lock index a44201f..4bffce1 100644 --- a/contactos_foundation/pubspec.lock +++ b/contactos_foundation/pubspec.lock @@ -1,30 +1,6 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e" - url: "https://pub.dev" - source: hosted - version: "92.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e" - url: "https://pub.dev" - source: hosted - version: "9.0.0" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" async: dependency: transitive description: @@ -41,22 +17,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" - built_collection: - dependency: transitive - description: - name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" - source: hosted - version: "5.1.1" - built_value: - dependency: transitive - description: - name: built_value - sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139" - url: "https://pub.dev" - source: hosted - version: "8.12.1" characters: dependency: transitive description: @@ -73,14 +33,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" - code_builder: - dependency: transitive - description: - name: code_builder - sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" - url: "https://pub.dev" - source: hosted - version: "4.11.0" collection: dependency: transitive description: @@ -95,31 +47,7 @@ packages: path: "../contactos_platform_interface" relative: true source: path - version: "1.0.0" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b - url: "https://pub.dev" - source: hosted - version: "3.1.3" + version: "1.0.1" fake_async: dependency: transitive description: @@ -136,14 +64,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -172,22 +92,6 @@ packages: description: flutter source: sdk version: "0.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - graphs: - dependency: transitive - description: - name: graphs - sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" - url: "https://pub.dev" - source: hosted - version: "2.3.2" integration_test: dependency: "direct dev" description: flutter @@ -249,14 +153,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" path: dependency: transitive description: @@ -265,14 +161,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" - pigeon: - dependency: "direct dev" - description: - name: pigeon - sha256: "5480e09d2c3b1df269798ce5b2b73ae6469c4f6789d7d27ca610cb9c77b98246" - url: "https://pub.dev" - source: hosted - version: "26.1.2" platform: dependency: transitive description: @@ -297,14 +185,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.5" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" sky_engine: dependency: transitive description: flutter @@ -366,14 +246,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.7" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" vector_math: dependency: transitive description: @@ -390,14 +262,6 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" - watcher: - dependency: transitive - description: - name: watcher - sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" - url: "https://pub.dev" - source: hosted - version: "1.1.4" webdriver: dependency: transitive description: @@ -406,14 +270,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" + dart: ">=3.8.0 <4.0.0" flutter: ">=3.29.3" diff --git a/contactos_foundation/pubspec.yaml b/contactos_foundation/pubspec.yaml index 4baaebe..bad5b83 100644 --- a/contactos_foundation/pubspec.yaml +++ b/contactos_foundation/pubspec.yaml @@ -1,6 +1,7 @@ name: contactos_foundation description: Android implementation of the contactos plugin. version: 0.0.1 +publish_to: none homepage: https://github.com/ziqq/contactos/contactos_foundation @@ -8,8 +9,6 @@ repository: https://github.com/ziqq/contactos/contactos_foundation issue_tracker: https://github.com/ziqq/contactos/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+contactos%22 -publish_to: none - funding: - https://www.buymeacoffee.com/ziqq - https://boosty.to/ziqq @@ -44,8 +43,6 @@ dev_dependencies: flutter_test: sdk: flutter - pigeon: ^26.0.2 - # Linting flutter_lints: ^6.0.0 From 5636dd272b54a7ea74fb44b1004f4394884ec344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Tue, 25 Nov 2025 18:14:26 +0400 Subject: [PATCH 09/35] feat: add `android` implementation --- contactos_android/.gitignore | 74 ++ contactos_android/.pubignore | 5 + contactos_android/CHANGELOG.md | 2 + contactos_android/LICENSE | 27 + contactos_android/Makefile | 75 ++ contactos_android/{.gitkeep => README.md} | 0 contactos_android/analysis_options.yaml | 235 ++++ contactos_android/android/.gitignore | 7 + contactos_android/android/build.gradle | 58 + contactos_android/android/gradle.properties | 1 + .../gradle/wrapper/gradle-wrapper.properties | 6 + contactos_android/android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 15 + .../flutter/plugins/contactos/Contact.java | 109 ++ .../plugins/contactos/ContactosPlugin.java | 1003 +++++++++++++++++ .../java/flutter/plugins/contactos/Item.java | 97 ++ .../plugins/contactos/PostalAddress.java | 73 ++ .../contactsservice/ContactTest.java | 100 ++ .../dart_dependency_validator.yaml | 8 + contactos_android/example/.gitignore | 32 + contactos_android/example/.metadata | 8 + contactos_android/example/README.md | 8 + .../example/android/app/build.gradle | 60 + .../android/app/src/main/AndroidManifest.xml | 38 + .../contactos/example/MainActivity.java | 5 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values/styles.xml | 8 + .../example/android/build.gradle | 18 + .../example/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 7 + .../example/android/settings.gradle | 25 + contactos_android/example/lib/main.dart | 98 ++ .../lib/src/screens/contacts_list_screen.dart | 603 ++++++++++ .../navite_contacts_picker_screen.dart | 62 + contactos_android/example/pubspec.lock | 359 ++++++ contactos_android/example/pubspec.yaml | 44 + .../example/test/widget_test.dart | 33 + contactos_android/lib/contactos_android.dart | 7 + .../lib/src/contactos_android.dart | 161 +++ contactos_android/pubspec.lock | 275 +++++ contactos_android/pubspec.yaml | 58 + 46 files changed, 3820 insertions(+) create mode 100644 contactos_android/.gitignore create mode 100644 contactos_android/.pubignore create mode 100644 contactos_android/CHANGELOG.md create mode 100644 contactos_android/LICENSE create mode 100644 contactos_android/Makefile rename contactos_android/{.gitkeep => README.md} (100%) create mode 100644 contactos_android/analysis_options.yaml create mode 100644 contactos_android/android/.gitignore create mode 100644 contactos_android/android/build.gradle create mode 100644 contactos_android/android/gradle.properties create mode 100644 contactos_android/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 contactos_android/android/settings.gradle create mode 100644 contactos_android/android/src/main/AndroidManifest.xml create mode 100644 contactos_android/android/src/main/java/flutter/plugins/contactos/Contact.java create mode 100644 contactos_android/android/src/main/java/flutter/plugins/contactos/ContactosPlugin.java create mode 100644 contactos_android/android/src/main/java/flutter/plugins/contactos/Item.java create mode 100644 contactos_android/android/src/main/java/flutter/plugins/contactos/PostalAddress.java create mode 100644 contactos_android/android/src/test/java/flutter/plugins/contactsservice/contactsservice/ContactTest.java create mode 100644 contactos_android/dart_dependency_validator.yaml create mode 100644 contactos_android/example/.gitignore create mode 100644 contactos_android/example/.metadata create mode 100644 contactos_android/example/README.md create mode 100644 contactos_android/example/android/app/build.gradle create mode 100644 contactos_android/example/android/app/src/main/AndroidManifest.xml create mode 100644 contactos_android/example/android/app/src/main/java/flutter/plugins/contactos/example/MainActivity.java create mode 100644 contactos_android/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 contactos_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 contactos_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 contactos_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 contactos_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 contactos_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 contactos_android/example/android/app/src/main/res/values/styles.xml create mode 100644 contactos_android/example/android/build.gradle create mode 100644 contactos_android/example/android/gradle.properties create mode 100644 contactos_android/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 contactos_android/example/android/settings.gradle create mode 100644 contactos_android/example/lib/main.dart create mode 100644 contactos_android/example/lib/src/screens/contacts_list_screen.dart create mode 100644 contactos_android/example/lib/src/screens/navite_contacts_picker_screen.dart create mode 100644 contactos_android/example/pubspec.lock create mode 100644 contactos_android/example/pubspec.yaml create mode 100644 contactos_android/example/test/widget_test.dart create mode 100644 contactos_android/lib/contactos_android.dart create mode 100644 contactos_android/lib/src/contactos_android.dart create mode 100644 contactos_android/pubspec.lock create mode 100644 contactos_android/pubspec.yaml diff --git a/contactos_android/.gitignore b/contactos_android/.gitignore new file mode 100644 index 0000000..91f801f --- /dev/null +++ b/contactos_android/.gitignore @@ -0,0 +1,74 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +**/build +**/doc/api/ +**/Flutter/ephemeral/ +.dart_tool/ +.flutter-plugins-dependencies +.flutter-plugins +.pub-cache/ +.pub/ +.fvm/ + +# Platform-specific generated files +**/android/**/GeneratedPluginRegistrant.* +**/android/**/local.properties +**/android/**/.cxx/ +**/android/.kotlin +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +**/android/**/key.properties +**/android/**/*.keystore +**/android/**/*.jks +**/android/**/captures/ +**/android/**/.gradle +**/android/**/gradlew +**/android/**/gradle-wrapper.jar +**/android/**/gradlew.bat + +# Xcode-related +**/dgph +**/xcuserdata/ + +# Pana +log.pana.json + +# Test +.coverage/ +coverage/ +reports/ +/test/**/*.json +/test/.test_coverage.dart +!/test/**/fixtures/*.json +integration_test/screenshots + +# Temp +.temp/ +temp/ +.tmp/ +tmp/ \ No newline at end of file diff --git a/contactos_android/.pubignore b/contactos_android/.pubignore new file mode 100644 index 0000000..7b5ee92 --- /dev/null +++ b/contactos_android/.pubignore @@ -0,0 +1,5 @@ +pubspec.lock +reports/ +coverage/ +build/ +example/ \ No newline at end of file diff --git a/contactos_android/CHANGELOG.md b/contactos_android/CHANGELOG.md new file mode 100644 index 0000000..1e64072 --- /dev/null +++ b/contactos_android/CHANGELOG.md @@ -0,0 +1,2 @@ +## 0.0.1 +- **ADDED**: Initial release \ No newline at end of file diff --git a/contactos_android/LICENSE b/contactos_android/LICENSE new file mode 100644 index 0000000..4de1fa1 --- /dev/null +++ b/contactos_android/LICENSE @@ -0,0 +1,27 @@ +BSD 3-Clause License + +Copyright (c) 2025, Anton Ustinoff +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. 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. +3. Neither the name of the copyright holder 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 THE COPYRIGHT HOLDER OR CONTRIBUTORS 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/contactos_android/Makefile b/contactos_android/Makefile new file mode 100644 index 0000000..a9ec8a7 --- /dev/null +++ b/contactos_android/Makefile @@ -0,0 +1,75 @@ +SHELL :=/bin/bash -e -o pipefail +PWD :=$(shell pwd) + +.DEFAULT_GOAL := all +.PHONY: all +all: ## build pipeline +all: get format test-unit publish-check + +.PHONY: ci +ci: ## CI build pipeline +ci: all + +.PHONY: precommit +precommit: ## validate the branch before commit +precommit: all + +.PHONY: help +help: ## Help dialog + @echo 'Usage: make ' + @echo '' + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: doctor +doctor: ## Check flutter doctor + @fvm flutter doctor + +.PHONY: version +version: ## Check flutter version + @fvm flutter --version + +.PHONY: format +format: ## Format code + @fvm dart format -l 80 lib test || (echo "Β―\_(ツ)_/Β― Format code error"; exit 1) + +.PHONY: fix +fix: format ## Fix code + @fvm dart fix --apply lib + +.PHONY: clean +clean: ## Clean flutter + @fvm flutter clean + +.PHONY: get +get: ## Get dependencies + @fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos_platform_interface dependencies error"; exit 1) + +.PHONY: analyze +analyze: get format ## Analyze code + @fvm flutter analyze --fatal-warnings --no-fatal-infos lib/ test/ || (echo "Β―\_(ツ)_/Β― Analyze code error"; exit 1) + +.PHONY: check +check: analyze ## Check the code + @fvm dart pub global activate pana + @pana --json --no-warning --line-length 80 > log.pana.json + +.PHONY: publish-check +publish-check: ## Check the code before publish + @fvm dart pub publish --dry-run + +.PHONY: publish +publish: ## Publish package + @fvm dart pub publish --server=https://pub.dartlang.org || (echo "Β―\_(ツ)_/Β― Publish error"; exit 1) + +.PHONY: coverage +coverage: ## Runs get coverage + @lcov --summary coverage/lcov.info + +.PHONY: run-genhtml +run-genhtml: ## Runs generage coverage html + @genhtml coverage/lcov.info -o coverage/html + +.PHONY: test-unit +test-unit: ## Runs unit tests + @fvm flutter test --coverage || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) + @genhtml coverage/lcov.info --output=coverage -o coverage/html || (echo "Β―\_(ツ)_/Β― Error while running genhtml with coverage"; exit 2) diff --git a/contactos_android/.gitkeep b/contactos_android/README.md similarity index 100% rename from contactos_android/.gitkeep rename to contactos_android/README.md diff --git a/contactos_android/analysis_options.yaml b/contactos_android/analysis_options.yaml new file mode 100644 index 0000000..ed15c99 --- /dev/null +++ b/contactos_android/analysis_options.yaml @@ -0,0 +1,235 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + # Build + - "build/**" + # Tests + - "test/**.mocks.dart" + - ".test_coverage.dart" + - ".coverage/**" + - "coverage/**" + # Assets + - "assets/**" + # Generated + - "**/generated/**" + # - '**/lib/l10n/**.arb' + - "**.g.dart" + - "**.gr.dart" + - "**.gql.dart" + - "**.freezed.dart" + - "**.config.dart" + - "**.mocks.dart" + - "**.gen.dart" + - "**.pb.dart" + - "**.pbenum.dart" + - "**.pbjson.dart" + # Flutter Version Manager + - ".fvm/**" + # Tools + - ".dart_tool/**" + - "scripts/**" + - "tool/**" + # Platform + - "ios/**" + - "android/**" + - "web/**" + - "macos/**" + - "windows/**" + - "linux/**" + + # Enable the following options to enable strong mode. + language: + strict-casts: true + strict-raw-types: true + strict-inference: true + + errors: + # Allow having TODOs in the code + todo: ignore + + # Info + directives_ordering: info + always_declare_return_types: info + + # Warning + unsafe_html: warning + missing_return: warning + missing_required_param: warning + no_logic_in_create_state: warning + empty_catches: warning + + # Error + always_use_package_imports: error + avoid_relative_lib_imports: error + avoid_slow_async_io: error + avoid_types_as_parameter_names: error + valid_regexps: error + always_require_non_null_named_parameters: error + +linter: + rules: + # Public packages + public_member_api_docs: true + lines_longer_than_80_chars: true + + # Enabling rules + always_use_package_imports: true + avoid_relative_lib_imports: true + + # Disable rules + sort_pub_dependencies: false + prefer_relative_imports: false + prefer_final_locals: false + avoid_escaping_inner_quotes: false + curly_braces_in_flow_control_structures: false + + # Enabled + use_named_constants: true + unnecessary_constructor_name: true + sort_constructors_first: true + exhaustive_cases: true + sort_unnamed_constructors_first: true + type_literal_in_constant_pattern: true + always_put_required_named_parameters_first: true + avoid_annotating_with_dynamic: true + avoid_bool_literals_in_conditional_expressions: true + avoid_double_and_int_checks: true + avoid_field_initializers_in_const_classes: true + avoid_implementing_value_types: true + avoid_js_rounded_ints: true + avoid_print: true + avoid_renaming_method_parameters: true + avoid_returning_null_for_void: true + avoid_single_cascade_in_expression_statements: true + avoid_slow_async_io: true + avoid_unnecessary_containers: true + avoid_unused_constructor_parameters: true + avoid_void_async: true + await_only_futures: true + cancel_subscriptions: true + cascade_invocations: true + close_sinks: true + control_flow_in_finally: true + empty_statements: true + collection_methods_unrelated_type: true + join_return_with_assignment: true + leading_newlines_in_multiline_strings: true + literal_only_boolean_expressions: true + missing_whitespace_between_adjacent_strings: true + no_adjacent_strings_in_list: true + no_logic_in_create_state: true + no_runtimeType_toString: true + only_throw_errors: true + overridden_fields: true + package_names: true + package_prefixed_library_names: true + parameter_assignments: true + prefer_asserts_in_initializer_lists: true + prefer_asserts_with_message: true + prefer_const_constructors: true + prefer_const_constructors_in_immutables: true + prefer_const_declarations: true + prefer_const_literals_to_create_immutables: true + prefer_constructors_over_static_methods: true + prefer_expression_function_bodies: true + prefer_final_in_for_each: true + prefer_foreach: true + prefer_if_elements_to_conditional_expressions: true + prefer_inlined_adds: true + prefer_int_literals: true + prefer_is_not_operator: true + prefer_null_aware_operators: true + prefer_typing_uninitialized_variables: true + prefer_void_to_null: true + provide_deprecation_message: true + sized_box_for_whitespace: true + sort_child_properties_last: true + test_types_in_equals: true + throw_in_finally: true + unnecessary_null_aware_assignments: true + unnecessary_overrides: true + unnecessary_parenthesis: true + unnecessary_raw_strings: true + unnecessary_statements: true + unnecessary_string_escapes: true + unnecessary_string_interpolations: true + # unsafe_html: true + use_full_hex_values_for_flutter_colors: true + use_raw_strings: true + use_string_buffers: true + valid_regexps: true + void_checks: true + + # Pedantic 1.9.0 + always_declare_return_types: true + annotate_overrides: true + avoid_empty_else: true + avoid_init_to_null: true + avoid_null_checks_in_equality_operators: true + avoid_return_types_on_setters: true + avoid_shadowing_type_parameters: true + avoid_types_as_parameter_names: true + camel_case_extensions: true + empty_catches: true + empty_constructor_bodies: true + library_names: true + library_prefixes: true + no_duplicate_case_values: true + null_closures: true + omit_local_variable_types: true + prefer_adjacent_string_concatenation: true + prefer_collection_literals: true + prefer_conditional_assignment: true + prefer_contains: true + prefer_final_fields: true + prefer_for_elements_to_map_fromIterable: true + prefer_generic_function_type_aliases: true + prefer_if_null_operators: true + prefer_is_empty: true + prefer_is_not_empty: true + prefer_iterable_whereType: true + prefer_single_quotes: true + prefer_spread_collections: true + recursive_getters: true + slash_for_doc_comments: true + type_init_formals: true + unawaited_futures: true + unnecessary_const: true + unnecessary_new: true + unnecessary_null_in_if_null_operators: true + unnecessary_this: true + unrelated_type_equality_checks: true + use_function_type_syntax_for_parameters: true + use_rethrow_when_possible: true + + # Effective_dart 1.2.0 + camel_case_types: true + file_names: true + non_constant_identifier_names: true + constant_identifier_names: true + directives_ordering: true + # package_api_docs: true + implementation_imports: true + prefer_interpolation_to_compose_strings: true + unnecessary_brace_in_string_interps: true + avoid_function_literals_in_foreach_calls: true + prefer_function_declarations_over_variables: true + unnecessary_lambdas: true + unnecessary_getters_setters: true + prefer_initializing_formals: true + avoid_catches_without_on_clauses: true + avoid_catching_errors: true + use_to_and_as_if_applicable: true + one_member_abstracts: true + avoid_classes_with_only_static_members: true + prefer_mixin: true + use_setters_to_change_properties: true + avoid_setters_without_getters: true + avoid_returning_this: true + type_annotate_public_apis: true + avoid_types_on_closure_parameters: true + avoid_private_typedef_functions: true + avoid_positional_boolean_parameters: true + hash_and_equals: true + avoid_equals_and_hash_code_on_mutable_classes: true \ No newline at end of file diff --git a/contactos_android/android/.gitignore b/contactos_android/android/.gitignore new file mode 100644 index 0000000..733bfea --- /dev/null +++ b/contactos_android/android/.gitignore @@ -0,0 +1,7 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java \ No newline at end of file diff --git a/contactos_android/android/build.gradle b/contactos_android/android/build.gradle new file mode 100644 index 0000000..960b83c --- /dev/null +++ b/contactos_android/android/build.gradle @@ -0,0 +1,58 @@ +plugins { + id "com.android.library" + id "kotlin-android" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file("local.properties") +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader("UTF-8") { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty("flutter.versionCode") +if (flutterVersionCode == null) { + flutterVersionCode = "1" +} + +def flutterVersionName = localProperties.getProperty("flutter.versionName") +if (flutterVersionName == null) { + flutterVersionName = "1.0" +} + +android { + namespace 'flutter.plugins.contactos' + compileSdkVersion flutter.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion flutter.minSdkVersion + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + testImplementation 'junit:junit:4.12' + testImplementation 'com.google.truth:truth:1.0' +} + +flutter { + source = "../.." +} \ No newline at end of file diff --git a/contactos_android/android/gradle.properties b/contactos_android/android/gradle.properties new file mode 100644 index 0000000..8bd86f6 --- /dev/null +++ b/contactos_android/android/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx1536M diff --git a/contactos_android/android/gradle/wrapper/gradle-wrapper.properties b/contactos_android/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..af2445b --- /dev/null +++ b/contactos_android/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Apr 10 22:57:03 IST 2021 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip diff --git a/contactos_android/android/settings.gradle b/contactos_android/android/settings.gradle new file mode 100644 index 0000000..438f72d --- /dev/null +++ b/contactos_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'contactos' diff --git a/contactos_android/android/src/main/AndroidManifest.xml b/contactos_android/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a4f006c --- /dev/null +++ b/contactos_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/contactos_android/android/src/main/java/flutter/plugins/contactos/Contact.java b/contactos_android/android/src/main/java/flutter/plugins/contactos/Contact.java new file mode 100644 index 0000000..fcb9175 --- /dev/null +++ b/contactos_android/android/src/main/java/flutter/plugins/contactos/Contact.java @@ -0,0 +1,109 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package flutter.plugins.contactos; + +import java.util.ArrayList; +import java.util.HashMap; + +public class Contact implements Comparable { + + Contact(String id) { + this.identifier = id; + } + + private Contact() { + } + + String identifier; + String displayName, givenName, middleName, familyName, prefix, suffix, company, jobTitle, note, birthday, androidAccountType, androidAccountName; + ArrayList emails = new ArrayList<>(); + ArrayList phones = new ArrayList<>(); + ArrayList postalAddresses = new ArrayList<>(); + byte[] avatar = new byte[0]; + + HashMap toMap() { + HashMap contactMap = new HashMap<>(); + contactMap.put("identifier", identifier); + contactMap.put("displayName", displayName); + contactMap.put("givenName", givenName); + contactMap.put("middleName", middleName); + contactMap.put("familyName", familyName); + contactMap.put("prefix", prefix); + contactMap.put("suffix", suffix); + contactMap.put("company", company); + contactMap.put("jobTitle", jobTitle); + contactMap.put("avatar", avatar); + contactMap.put("note", note); + contactMap.put("birthday", birthday); + contactMap.put("androidAccountType", androidAccountType); + contactMap.put("androidAccountName", androidAccountName); + + ArrayList> emailsMap = new ArrayList<>(); + for (Item email : emails) { + emailsMap.add(email.toMap()); + } + contactMap.put("emails", emailsMap); + + ArrayList> phonesMap = new ArrayList<>(); + for (Item phone : phones) { + phonesMap.add(phone.toMap()); + } + contactMap.put("phones", phonesMap); + + ArrayList> addressesMap = new ArrayList<>(); + for (PostalAddress address : postalAddresses) { + addressesMap.add(address.toMap()); + } + contactMap.put("postalAddresses", addressesMap); + + return contactMap; + } + + @SuppressWarnings("unchecked") + static Contact fromMap(HashMap map) { + Contact contact = new Contact(); + contact.identifier = (String) map.get("identifier"); + contact.givenName = (String) map.get("givenName"); + contact.middleName = (String) map.get("middleName"); + contact.familyName = (String) map.get("familyName"); + contact.prefix = (String) map.get("prefix"); + contact.suffix = (String) map.get("suffix"); + contact.company = (String) map.get("company"); + contact.jobTitle = (String) map.get("jobTitle"); + contact.avatar = (byte[]) map.get("avatar"); + contact.note = (String) map.get("note"); + contact.birthday = (String) map.get("birthday"); + contact.androidAccountType = (String) map.get("androidAccountType"); + contact.androidAccountName = (String) map.get("androidAccountName"); + + ArrayList emails = (ArrayList) map.get("emails"); + if (emails != null) { + for (HashMap email : emails) { + contact.emails.add(Item.fromMap(email)); + } + } + ArrayList phones = (ArrayList) map.get("phones"); + if (phones != null) { + for (HashMap phone : phones) { + contact.phones.add(Item.fromMap(phone)); + } + } + ArrayList postalAddresses = (ArrayList) map.get("postalAddresses"); + if (postalAddresses != null) { + for (HashMap postalAddress : postalAddresses) { + contact.postalAddresses.add(PostalAddress.fromMap(postalAddress)); + } + } + return contact; + } + + @Override + public int compareTo(Contact contact) { + String givenName1 = this.givenName == null ? "" : this.givenName.toLowerCase(); + String givenName2 = contact == null ? "" + : (contact.givenName == null ? "" : contact.givenName.toLowerCase()); + return givenName1.compareTo(givenName2); + } +} \ No newline at end of file diff --git a/contactos_android/android/src/main/java/flutter/plugins/contactos/ContactosPlugin.java b/contactos_android/android/src/main/java/flutter/plugins/contactos/ContactosPlugin.java new file mode 100644 index 0000000..c6cee3e --- /dev/null +++ b/contactos_android/android/src/main/java/flutter/plugins/contactos/ContactosPlugin.java @@ -0,0 +1,1003 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package flutter.plugins.contactos; + +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.provider.BaseColumns; +import android.provider.ContactsContract; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry; + +import static android.app.Activity.RESULT_CANCELED; +import static android.provider.ContactsContract.CommonDataKinds; +import static android.provider.ContactsContract.CommonDataKinds.Email; +import static android.provider.ContactsContract.CommonDataKinds.Organization; +import static android.provider.ContactsContract.CommonDataKinds.Phone; +import static android.provider.ContactsContract.CommonDataKinds.StructuredName; +import static android.provider.ContactsContract.CommonDataKinds.StructuredPostal; + +/** + * Updated ContactosPlugin, using PluginRegistry.ActivityResultListener and ExecutorService + * instead of ActivityPluginBinding.ActivityResultListener and AsyncTask. + */ +public class ContactosPlugin implements + MethodChannel.MethodCallHandler, + FlutterPlugin, + ActivityAware +{ + private static final int FORM_OPERATION_CANCELED = 1; + private static final int FORM_COULD_NOT_BE_OPEN = 2; + + private static final String LOG_TAG = "contacts"; + + private ContentResolver contentResolver; + private BaseContactosDelegate delegate; + private MethodChannel methodChannel; + private Resources resources; + + // Thread pool for asynchronous operations (replacement for AsyncTask) + private final ExecutorService executor = new ThreadPoolExecutor( + 0, + 10, + 60, + TimeUnit.SECONDS, + new ArrayBlockingQueue<>(1000) + ); + + // Handler for returning the result to the main (UI) thread + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + + // region FlutterPlugin + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + this.resources = binding.getApplicationContext().getResources(); + initInstance(binding.getBinaryMessenger(), binding.getApplicationContext()); + this.delegate = new ContactosDelegate(binding.getApplicationContext()); + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + if (methodChannel != null) { + methodChannel.setMethodCallHandler(null); + methodChannel = null; + } + contentResolver = null; + delegate = null; + resources = null; + } + // endregion + + // region ActivityAware + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { + if (delegate instanceof ContactosDelegate) { + ((ContactosDelegate) delegate).bindToActivity(binding); + } + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + if (delegate instanceof ContactosDelegate) { + ((ContactosDelegate) delegate).unbindActivity(); + } + } + + @Override + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { + if (delegate instanceof ContactosDelegate) { + ((ContactosDelegate) delegate).bindToActivity(binding); + } + } + + @Override + public void onDetachedFromActivity() { + if (delegate instanceof ContactosDelegate) { + ((ContactosDelegate) delegate).unbindActivity(); + } + } + // endregion + + // region Initialization + private void initInstance(BinaryMessenger messenger, Context context) { + methodChannel = new MethodChannel(messenger, "github.com/ziqq/contactos"); + methodChannel.setMethodCallHandler(this); + contentResolver = context.getContentResolver(); + } + // endregion + + // region MethodCallHandler + @Override + public void onMethodCall(MethodCall call, MethodChannel.Result result) { + switch (call.method) { + case "getContacts": { + final String query = call.argument("query"); + final boolean withThumbnails = call.argument("withThumbnails"); + final boolean orderByGivenName = call.argument("orderByGivenName"); + final boolean photoHighResolution = call.argument("photoHighResolution"); + final boolean androidLocalizedLabels = call.argument("androidLocalizedLabels"); + + getContacts( + "getContacts", + query, + withThumbnails, + photoHighResolution, + orderByGivenName, + androidLocalizedLabels, + result + ); + break; + } + case "getContactsForPhone": { + final String phone = call.argument("phone"); + final boolean withThumbnails = call.argument("withThumbnails"); + final boolean photoHighResolution = call.argument("photoHighResolution"); + final boolean orderByGivenName = call.argument("orderByGivenName"); + final boolean androidLocalizedLabels = call.argument("androidLocalizedLabels"); + + getContacts( + "getContactsForPhone", + phone, + withThumbnails, + photoHighResolution, + orderByGivenName, + androidLocalizedLabels, + result + ); + break; + } + case "getContactsForEmail": { + final String email = call.argument("email"); + final boolean withThumbnails = call.argument("withThumbnails"); + final boolean photoHighResolution = call.argument("photoHighResolution"); + final boolean orderByGivenName = call.argument("orderByGivenName"); + final boolean androidLocalizedLabels = call.argument("androidLocalizedLabels"); + + getContacts( + "getContactsForEmail", + email, + withThumbnails, + photoHighResolution, + orderByGivenName, + androidLocalizedLabels, + result + ); + break; + } + case "getAvatar": { + final HashMap map = call.argument("contact"); + final boolean photoHighResolution = call.argument("photoHighResolution"); + final Contact contact = Contact.fromMap(map); + getAvatar(contact, photoHighResolution, result); + break; + } + case "addContact": { + final Contact contact = Contact.fromMap((HashMap) call.arguments); + if (addContact(contact)) { + result.success(null); + } else { + result.error(null, "Failed to add the contact", null); + } + break; + } + case "deleteContact": { + final Contact contact = Contact.fromMap((HashMap) call.arguments); + if (deleteContact(contact)) { + result.success(null); + } else { + result.error(null, "Failed to delete the contact, make sure it has a valid identifier", null); + } + break; + } + case "updateContact": { + final Contact contact = Contact.fromMap((HashMap) call.arguments); + if (updateContact(contact)) { + result.success(null); + } else { + result.error(null, "Failed to update the contact, make sure it has a valid identifier", null); + } + break; + } + case "openExistingContact": { + final HashMap map = call.argument("contact"); + final boolean localizedLabels = call.argument("androidLocalizedLabels"); + final Contact contact = Contact.fromMap(map); + if (delegate != null) { + delegate.setResult(result); + delegate.setLocalizedLabels(localizedLabels); + delegate.openExistingContact(contact); + } else { + result.success(FORM_COULD_NOT_BE_OPEN); + } + break; + } + case "openContactForm": { + final boolean localizedLabels = call.argument("androidLocalizedLabels"); + if (delegate != null) { + delegate.setResult(result); + delegate.setLocalizedLabels(localizedLabels); + delegate.openContactForm(); + } else { + result.success(FORM_COULD_NOT_BE_OPEN); + } + break; + } + case "openDeviceContactPicker": { + final boolean localizedLabels = call.argument("androidLocalizedLabels"); + openDeviceContactPicker(result, localizedLabels); + break; + } + default: { + result.notImplemented(); + } + } + } + // endregion + + // region Asynchronous contact retrieval (AsyncTask replacement) + private static final String[] PROJECTION = { + ContactsContract.Data.CONTACT_ID, + ContactsContract.Profile.DISPLAY_NAME, + ContactsContract.Contacts.Data.MIMETYPE, + ContactsContract.RawContacts.ACCOUNT_TYPE, + ContactsContract.RawContacts.ACCOUNT_NAME, + StructuredName.DISPLAY_NAME, + StructuredName.GIVEN_NAME, + StructuredName.MIDDLE_NAME, + StructuredName.FAMILY_NAME, + StructuredName.PREFIX, + StructuredName.SUFFIX, + CommonDataKinds.Note.NOTE, + Phone.NUMBER, + Phone.TYPE, + Phone.LABEL, + Email.ADDRESS, + Email.TYPE, + Email.LABEL, + Organization.COMPANY, + Organization.TITLE, + StructuredPostal.FORMATTED_ADDRESS, + StructuredPostal.TYPE, + StructuredPostal.LABEL, + StructuredPostal.STREET, + StructuredPostal.POBOX, + StructuredPostal.NEIGHBORHOOD, + StructuredPostal.CITY, + StructuredPostal.REGION, + StructuredPostal.POSTCODE, + StructuredPostal.COUNTRY, + }; + + // A unified method for getting contacts in the background. + private void getContacts( + final String callMethod, + final String param, + final boolean withThumbnails, + final boolean photoHighResolution, + final boolean orderByGivenName, + final boolean localizedLabels, + final MethodChannel.Result result + ) { + executor.execute(() -> { + ArrayList contacts; + + switch (callMethod) { + case "openDeviceContactPicker": + contacts = getContactsFrom(getCursor(null, param), localizedLabels); + break; + case "getContacts": + contacts = getContactsFrom(getCursor(param, null), localizedLabels); + break; + case "getContactsForPhone": + contacts = getContactsFrom(getCursorForPhone(param), localizedLabels); + break; + case "getContactsForEmail": + contacts = getContactsFrom(getCursorForEmail(param), localizedLabels); + break; + default: + contacts = null; + break; + } + + if (contacts != null && withThumbnails) { + for (Contact c : contacts) { + byte[] avatar = loadContactPhotoHighRes(c.identifier, photoHighResolution, contentResolver); + c.avatar = (avatar != null) ? avatar : new byte[0]; + } + } + + if (contacts != null && orderByGivenName) { + Collections.sort(contacts, Comparator.naturalOrder()); + } + + final ArrayList contactMaps = new ArrayList<>(); + if (contacts != null) { + for (Contact c : contacts) { + contactMaps.add(c.toMap()); + } + } + + // Returning the result to the main thread + mainHandler.post(() -> { + if (contacts == null) { + result.notImplemented(); + } else { + result.success(contactMaps); + } + }); + }); + } + // endregion + + private void openDeviceContactPicker(MethodChannel.Result result, boolean localizedLabels) { + if (delegate != null) { + delegate.setResult(result); + delegate.setLocalizedLabels(localizedLabels); + delegate.openContactPicker(); + } else { + result.success(FORM_COULD_NOT_BE_OPEN); + } + } + + private ArrayList getContactsFrom(Cursor cursor, boolean localizedLabels) { + HashMap map = new LinkedHashMap<>(); + + if (cursor != null) { + while (cursor.moveToNext()) { + String contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID)); + if (!map.containsKey(contactId)) { + map.put(contactId, new Contact(contactId)); + } + Contact contact = map.get(contactId); + + String mimeType = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.MIMETYPE)); + contact.displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)); + contact.androidAccountType = cursor.getString(cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_TYPE)); + contact.androidAccountName = cursor.getString(cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_NAME)); + + if (CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { + contact.givenName = cursor.getString(cursor.getColumnIndex(StructuredName.GIVEN_NAME)); + contact.middleName = cursor.getString(cursor.getColumnIndex(StructuredName.MIDDLE_NAME)); + contact.familyName = cursor.getString(cursor.getColumnIndex(StructuredName.FAMILY_NAME)); + contact.prefix = cursor.getString(cursor.getColumnIndex(StructuredName.PREFIX)); + contact.suffix = cursor.getString(cursor.getColumnIndex(StructuredName.SUFFIX)); + } + else if (CommonDataKinds.Note.CONTENT_ITEM_TYPE.equals(mimeType)) { + contact.note = cursor.getString(cursor.getColumnIndex(CommonDataKinds.Note.NOTE)); + } + else if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) { + String phoneNumber = cursor.getString(cursor.getColumnIndex(Phone.NUMBER)); + if (!TextUtils.isEmpty(phoneNumber)) { + int type = cursor.getInt(cursor.getColumnIndex(Phone.TYPE)); + String label = Item.getPhoneLabel(resources, type, cursor, localizedLabels); + contact.phones.add(new Item(label, phoneNumber, type)); + } + } + else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) { + String email = cursor.getString(cursor.getColumnIndex(Email.ADDRESS)); + int type = cursor.getInt(cursor.getColumnIndex(Email.TYPE)); + if (!TextUtils.isEmpty(email)) { + String label = Item.getEmailLabel(resources, type, cursor, localizedLabels); + contact.emails.add(new Item(label, email, type)); + } + } + else if (CommonDataKinds.Organization.CONTENT_ITEM_TYPE.equals(mimeType)) { + contact.company = cursor.getString(cursor.getColumnIndex(Organization.COMPANY)); + contact.jobTitle = cursor.getString(cursor.getColumnIndex(Organization.TITLE)); + } + else if (CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) { + int type = cursor.getInt(cursor.getColumnIndex(StructuredPostal.TYPE)); + String label = PostalAddress.getLabel(resources, type, cursor, localizedLabels); + String street = cursor.getString(cursor.getColumnIndex(StructuredPostal.STREET)); + String city = cursor.getString(cursor.getColumnIndex(StructuredPostal.CITY)); + String postcode = cursor.getString(cursor.getColumnIndex(StructuredPostal.POSTCODE)); + String region = cursor.getString(cursor.getColumnIndex(StructuredPostal.REGION)); + String country = cursor.getString(cursor.getColumnIndex(StructuredPostal.COUNTRY)); + contact.postalAddresses.add( + new PostalAddress(label, street, city, postcode, region, country, type) + ); + } + else if (CommonDataKinds.Event.CONTENT_ITEM_TYPE.equals(mimeType)) { + int eventType = cursor.getInt(cursor.getColumnIndex(CommonDataKinds.Event.TYPE)); + if (eventType == CommonDataKinds.Event.TYPE_BIRTHDAY) { + contact.birthday = cursor.getString(cursor.getColumnIndex(CommonDataKinds.Event.START_DATE)); + } + } + } + cursor.close(); + } + + return new ArrayList<>(map.values()); + } + + private Cursor getCursor(String query, String rawContactId) { + String selection = "(" + + ContactsContract.Data.MIMETYPE + "=? OR " + + ContactsContract.Data.MIMETYPE + "=? OR " + + ContactsContract.Data.MIMETYPE + "=? OR " + + ContactsContract.Data.MIMETYPE + "=? OR " + + ContactsContract.Data.MIMETYPE + "=? OR " + + ContactsContract.Data.MIMETYPE + "=? OR " + + ContactsContract.Data.MIMETYPE + "=? OR " + + ContactsContract.RawContacts.ACCOUNT_TYPE + "=?" + + ")"; + ArrayList selectionArgs = new ArrayList<>(Arrays.asList( + CommonDataKinds.Note.CONTENT_ITEM_TYPE, + Email.CONTENT_ITEM_TYPE, + Phone.CONTENT_ITEM_TYPE, + StructuredName.CONTENT_ITEM_TYPE, + Organization.CONTENT_ITEM_TYPE, + StructuredPostal.CONTENT_ITEM_TYPE, + CommonDataKinds.Event.CONTENT_ITEM_TYPE, + ContactsContract.RawContacts.ACCOUNT_TYPE + )); + + if (query != null) { + // Search by DISPLAY_NAME_PRIMARY + selection = ContactsContract.Contacts.DISPLAY_NAME_PRIMARY + " LIKE ?"; + selectionArgs.clear(); + selectionArgs.add(query + "%"); + } + if (rawContactId != null) { + selectionArgs.add(rawContactId); + selection += " AND " + ContactsContract.Data.CONTACT_ID + " =?"; + } + return contentResolver.query( + ContactsContract.Data.CONTENT_URI, + PROJECTION, + selection, + selectionArgs.toArray(new String[0]), + null + ); + } + + private Cursor getCursorForPhone(String phone) { + if (phone == null || phone.isEmpty()) return null; + + Uri uri = Uri.withAppendedPath( + ContactsContract.PhoneLookup.CONTENT_FILTER_URI, + Uri.encode(phone) + ); + String[] projection = new String[]{BaseColumns._ID}; + + ArrayList contactIds = new ArrayList<>(); + Cursor phoneCursor = contentResolver.query(uri, projection, null, null, null); + if (phoneCursor != null) { + while (phoneCursor.moveToNext()) { + int idIndex = phoneCursor.getColumnIndex(BaseColumns._ID); + if (idIndex >= 0) { + contactIds.add(phoneCursor.getString(idIndex)); + } + } + phoneCursor.close(); + } + + if (!contactIds.isEmpty()) { + String contactIdsList = contactIds.toString().replace("[", "(").replace("]", ")"); + String contactSelection = ContactsContract.Data.CONTACT_ID + " IN " + contactIdsList; + return contentResolver.query( + ContactsContract.Data.CONTENT_URI, + PROJECTION, + contactSelection, + null, + null + ); + } + return null; + } + + private Cursor getCursorForEmail(String email) { + if (email == null || email.isEmpty()) return null; + String selection = Email.ADDRESS + " LIKE ?"; + String[] selectionArgs = new String[]{"%" + email + "%"}; + return contentResolver.query( + ContactsContract.Data.CONTENT_URI, + PROJECTION, + selection, + selectionArgs, + null + ); + } + + // region Working with avatar (replacement of AsyncTask) + private void getAvatar(final Contact contact, final boolean highRes, final MethodChannel.Result result) { + executor.execute(() -> { + byte[] avatar = loadContactPhotoHighRes(contact.identifier, highRes, contentResolver); + mainHandler.post(() -> result.success(avatar)); + }); + } + + private static byte[] loadContactPhotoHighRes( + final String identifier, + final boolean photoHighResolution, + final ContentResolver resolver + ) { + try { + if (identifier == null || identifier.isEmpty()) { + return null; + } + long contactId = Long.parseLong(identifier); + Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId); + InputStream input = ContactsContract.Contacts.openContactPhotoInputStream( + resolver, uri, photoHighResolution + ); + if (input == null) return null; + + Bitmap bitmap = BitmapFactory.decodeStream(input); + input.close(); + + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); + byte[] bytes = stream.toByteArray(); + stream.close(); + return bytes; + } catch (IOException ex) { + Log.e(LOG_TAG, ex.getMessage()); + return null; + } + } + // endregion + + // region CRUD: addContact, deleteContact, updateContact + private boolean addContact(Contact contact) { + try { + ArrayList ops = new ArrayList<>(); + + ContentProviderOperation.Builder op = ContentProviderOperation + .newInsert(ContactsContract.RawContacts.CONTENT_URI) + .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null) + .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null); + ops.add(op.build()); + + // Name + op = ContentProviderOperation + .newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) + .withValue(StructuredName.GIVEN_NAME, contact.givenName) + .withValue(StructuredName.MIDDLE_NAME, contact.middleName) + .withValue(StructuredName.FAMILY_NAME, contact.familyName) + .withValue(StructuredName.PREFIX, contact.prefix) + .withValue(StructuredName.SUFFIX, contact.suffix); + ops.add(op.build()); + + // Note + op = ContentProviderOperation + .newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Note.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Note.NOTE, contact.note); + ops.add(op.build()); + + // Organization + op = ContentProviderOperation + .newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Organization.CONTENT_ITEM_TYPE) + .withValue(Organization.COMPANY, contact.company) + .withValue(Organization.TITLE, contact.jobTitle); + ops.add(op.build()); + + // Photo + op = ContentProviderOperation + .newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.IS_SUPER_PRIMARY, 1) + .withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, contact.avatar) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE); + ops.add(op.build()); + op.withYieldAllowed(true); + + // Phones + for (Item phone : contact.phones) { + op = ContentProviderOperation + .newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Phone.NUMBER, phone.value); + + if (phone.type == CommonDataKinds.Phone.TYPE_CUSTOM) { + op.withValue(CommonDataKinds.Phone.TYPE, CommonDataKinds.BaseTypes.TYPE_CUSTOM); + op.withValue(CommonDataKinds.Phone.LABEL, phone.label); + } else { + op.withValue(CommonDataKinds.Phone.TYPE, phone.type); + } + ops.add(op.build()); + } + + // E-mail + for (Item email : contact.emails) { + op = ContentProviderOperation + .newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Email.ADDRESS, email.value) + .withValue(CommonDataKinds.Email.TYPE, email.type); + ops.add(op.build()); + } + + // Addresses + for (PostalAddress address : contact.postalAddresses) { + op = ContentProviderOperation + .newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.StructuredPostal.TYPE, address.type) + .withValue(CommonDataKinds.StructuredPostal.LABEL, address.label) + .withValue(CommonDataKinds.StructuredPostal.STREET, address.street) + .withValue(CommonDataKinds.StructuredPostal.CITY, address.city) + .withValue(CommonDataKinds.StructuredPostal.REGION, address.region) + .withValue(CommonDataKinds.StructuredPostal.POSTCODE, address.postcode) + .withValue(CommonDataKinds.StructuredPostal.COUNTRY, address.country); + ops.add(op.build()); + } + + // Birthday + op = ContentProviderOperation + .newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Event.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Event.TYPE, CommonDataKinds.Event.TYPE_BIRTHDAY) + .withValue(CommonDataKinds.Event.START_DATE, contact.birthday); + ops.add(op.build()); + + contentResolver.applyBatch(ContactsContract.AUTHORITY, ops); + return true; + } catch (Exception e) { + return false; + } + } + + private boolean deleteContact(Contact contact) { + try { + ArrayList ops = new ArrayList<>(); + ops.add(ContentProviderOperation + .newDelete(ContactsContract.RawContacts.CONTENT_URI) + .withSelection( + ContactsContract.RawContacts.CONTACT_ID + "=?", + new String[]{String.valueOf(contact.identifier)} + ) + .build()); + contentResolver.applyBatch(ContactsContract.AUTHORITY, ops); + return true; + } catch (Exception e) { + return false; + } + } + + private boolean updateContact(Contact contact) { + try { + ArrayList ops = new ArrayList<>(); + ContentProviderOperation.Builder op; + + // Removed old data (except for the name) + op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) + .withSelection( + ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", + new String[]{contact.identifier, CommonDataKinds.Organization.CONTENT_ITEM_TYPE} + ); + ops.add(op.build()); + + op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) + .withSelection( + ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", + new String[]{contact.identifier, CommonDataKinds.Phone.CONTENT_ITEM_TYPE} + ); + ops.add(op.build()); + + op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) + .withSelection( + ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", + new String[]{contact.identifier, CommonDataKinds.Email.CONTENT_ITEM_TYPE} + ); + ops.add(op.build()); + + op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) + .withSelection( + ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", + new String[]{contact.identifier, CommonDataKinds.Note.CONTENT_ITEM_TYPE} + ); + ops.add(op.build()); + + op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) + .withSelection( + ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", + new String[]{contact.identifier, CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE} + ); + ops.add(op.build()); + + // Photo + op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) + .withSelection( + ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", + new String[]{contact.identifier, CommonDataKinds.Photo.CONTENT_ITEM_TYPE} + ); + ops.add(op.build()); + + // Update name + op = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) + .withSelection( + ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", + new String[]{contact.identifier, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE} + ) + .withValue(StructuredName.GIVEN_NAME, contact.givenName) + .withValue(StructuredName.MIDDLE_NAME, contact.middleName) + .withValue(StructuredName.FAMILY_NAME, contact.familyName) + .withValue(StructuredName.PREFIX, contact.prefix) + .withValue(StructuredName.SUFFIX, contact.suffix); + ops.add(op.build()); + + // Add new organization + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Organization.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier) + .withValue(Organization.TYPE, Organization.TYPE_WORK) + .withValue(Organization.COMPANY, contact.company) + .withValue(Organization.TITLE, contact.jobTitle); + ops.add(op.build()); + + // Note + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Note.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier) + .withValue(CommonDataKinds.Note.NOTE, contact.note); + ops.add(op.build()); + + // Photo + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier) + .withValue(ContactsContract.Data.IS_SUPER_PRIMARY, 1) + .withValue(CommonDataKinds.Photo.PHOTO, contact.avatar) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE); + ops.add(op.build()); + + // Phone's + for (Item phone : contact.phones) { + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier) + .withValue(Phone.NUMBER, phone.value); + + if (phone.type == CommonDataKinds.Phone.TYPE_CUSTOM) { + op.withValue(CommonDataKinds.Phone.TYPE, CommonDataKinds.BaseTypes.TYPE_CUSTOM); + op.withValue(CommonDataKinds.Phone.LABEL, phone.label); + } else { + op.withValue(CommonDataKinds.Phone.TYPE, phone.type); + } + ops.add(op.build()); + } + + // Email + for (Item email : contact.emails) { + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier) + .withValue(CommonDataKinds.Email.ADDRESS, email.value) + .withValue(CommonDataKinds.Email.TYPE, email.type); + ops.add(op.build()); + } + + // Addresses + for (PostalAddress address : contact.postalAddresses) { + op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier) + .withValue(CommonDataKinds.StructuredPostal.TYPE, address.type) + .withValue(StructuredPostal.STREET, address.street) + .withValue(StructuredPostal.CITY, address.city) + .withValue(StructuredPostal.REGION, address.region) + .withValue(StructuredPostal.POSTCODE, address.postcode) + .withValue(StructuredPostal.COUNTRY, address.country); + ops.add(op.build()); + } + + contentResolver.applyBatch(ContactsContract.AUTHORITY, ops); + return true; + } catch (Exception e) { + Log.e("TAG", "Exception encountered while updating contact: ", e); + return false; + } + } + // endregion + + // region Deleates for ActivityResult + private class BaseContactosDelegate implements PluginRegistry.ActivityResultListener { + private static final int REQUEST_OPEN_CONTACT_FORM = 52941; + private static final int REQUEST_OPEN_EXISTING_CONTACT = 52942; + private static final int REQUEST_OPEN_CONTACT_PICKER = 52943; + + private MethodChannel.Result result; + private boolean localizedLabels; + + void setResult(MethodChannel.Result result) { + this.result = result; + } + + void setLocalizedLabels(boolean localizedLabels) { + this.localizedLabels = localizedLabels; + } + + void finishWithResult(Object value) { + if (this.result != null) { + this.result.success(value); + this.result = null; + } + } + + @Override + public boolean onActivityResult(int requestCode, int resultCode, Intent intent) { + if (requestCode == REQUEST_OPEN_EXISTING_CONTACT || requestCode == REQUEST_OPEN_CONTACT_FORM) { + try { + if (intent != null && intent.getData() != null) { + Uri ur = intent.getData(); + finishWithResult(getContactByIdentifier(ur.getLastPathSegment())); + } else { + finishWithResult(FORM_OPERATION_CANCELED); + } + } catch (NullPointerException e) { + finishWithResult(FORM_OPERATION_CANCELED); + } + return true; + } + + if (requestCode == REQUEST_OPEN_CONTACT_PICKER) { + if (resultCode == RESULT_CANCELED) { + finishWithResult(FORM_OPERATION_CANCELED); + return true; + } + if (intent == null) { + finishWithResult(FORM_COULD_NOT_BE_OPEN); + return true; + } + Uri contactUri = intent.getData(); + if (contactUri == null) { + finishWithResult(FORM_COULD_NOT_BE_OPEN); + return true; + } + Cursor cursor = contentResolver.query(contactUri, null, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + String id = contactUri.getLastPathSegment(); + getContacts("openDeviceContactPicker", id, false, false, false, this.localizedLabels, this.result); + } else { + Log.e(LOG_TAG, "onActivityResult: cursor.moveToFirst() == false"); + finishWithResult(FORM_OPERATION_CANCELED); + } + if (cursor != null) cursor.close(); + return true; + } + + finishWithResult(FORM_COULD_NOT_BE_OPEN); + return false; + } + + void openExistingContact(Contact contact) { + String identifier = contact.identifier; + try { + HashMap contactMapFromDevice = getContactByIdentifier(identifier); + if (contactMapFromDevice != null) { + Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, identifier); + Intent intent = new Intent(Intent.ACTION_EDIT); + intent.setDataAndType(uri, ContactsContract.Contacts.CONTENT_ITEM_TYPE); + intent.putExtra("finishActivityOnSaveCompleted", true); + startIntent(intent, REQUEST_OPEN_EXISTING_CONTACT); + } else { + finishWithResult(FORM_COULD_NOT_BE_OPEN); + } + } catch (Exception e) { + finishWithResult(FORM_COULD_NOT_BE_OPEN); + } + } + + void openContactForm() { + try { + Intent intent = new Intent(Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI); + intent.putExtra("finishActivityOnSaveCompleted", true); + startIntent(intent, REQUEST_OPEN_CONTACT_FORM); + } catch (Exception e) { + finishWithResult(FORM_COULD_NOT_BE_OPEN); + } + } + + void openContactPicker() { + Intent intent = new Intent(Intent.ACTION_PICK); + intent.setType(ContactsContract.Contacts.CONTENT_TYPE); + startIntent(intent, REQUEST_OPEN_CONTACT_PICKER); + } + + void startIntent(Intent intent, int request) { + // Overridden in the successor ContactosDelegate + } + + HashMap getContactByIdentifier(String identifier) { + Cursor cursor = contentResolver.query( + ContactsContract.Data.CONTENT_URI, + PROJECTION, + ContactsContract.RawContacts.CONTACT_ID + " = ?", + new String[]{identifier}, + null + ); + ArrayList matchingContacts; + try { + matchingContacts = getContactsFrom(cursor, localizedLabels); + } finally { + if (cursor != null) cursor.close(); + } + if (!matchingContacts.isEmpty()) { + return matchingContacts.get(0).toMap(); + } + return null; + } + } + + private class ContactosDelegate extends BaseContactosDelegate { + private final Context context; + private ActivityPluginBinding activityPluginBinding; + + ContactosDelegate(Context context) { + this.context = context; + } + + void bindToActivity(ActivityPluginBinding binding) { + this.activityPluginBinding = binding; + this.activityPluginBinding.addActivityResultListener(this); + } + + void unbindActivity() { + if (this.activityPluginBinding != null) { + this.activityPluginBinding.removeActivityResultListener(this); + } + this.activityPluginBinding = null; + } + + @Override + void startIntent(Intent intent, int request) { + if (this.activityPluginBinding != null) { + if (intent.resolveActivity(context.getPackageManager()) != null) { + this.activityPluginBinding.getActivity().startActivityForResult(intent, request); + } else { + finishWithResult(FORM_COULD_NOT_BE_OPEN); + } + } else { + context.startActivity(intent); + } + } + } + // endregion +} \ No newline at end of file diff --git a/contactos_android/android/src/main/java/flutter/plugins/contactos/Item.java b/contactos_android/android/src/main/java/flutter/plugins/contactos/Item.java new file mode 100644 index 0000000..ccc5abb --- /dev/null +++ b/contactos_android/android/src/main/java/flutter/plugins/contactos/Item.java @@ -0,0 +1,97 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package flutter.plugins.contactos; + +import android.content.res.Resources; +import android.database.Cursor; + +import static android.provider.ContactsContract.CommonDataKinds; + +import java.util.HashMap; + +/*** + * Represents an object which has a label and a value + * such as an email or a phone + ***/ +public class Item { + + public String label, value; + int type; + + public Item(String label, String value, int type) { + this.label = label; + this.value = value; + this.type = type; + } + + HashMap toMap() { + HashMap result = new HashMap<>(); + result.put("label", label); + result.put("value", value); + result.put("type", String.valueOf(type)); + return result; + } + + public static Item fromMap(HashMap map) { + String label = map.get("label"); + String value = map.get("value"); + String type = map.get("type"); + return new Item(label, value, type != null ? Integer.parseInt(type) : -1); + } + + public static String getPhoneLabel(Resources resources, int type, Cursor cursor, boolean localizedLabels) { + if (localizedLabels) { + CharSequence localizedLabel = CommonDataKinds.Phone.getTypeLabel(resources, type, ""); + return localizedLabel.toString().toLowerCase(); + } else { + switch (type) { + case CommonDataKinds.Phone.TYPE_HOME: + return "home"; + case CommonDataKinds.Phone.TYPE_WORK: + return "work"; + case CommonDataKinds.Phone.TYPE_MOBILE: + return "mobile"; + case CommonDataKinds.Phone.TYPE_FAX_WORK: + return "fax work"; + case CommonDataKinds.Phone.TYPE_FAX_HOME: + return "fax home"; + case CommonDataKinds.Phone.TYPE_MAIN: + return "main"; + case CommonDataKinds.Phone.TYPE_COMPANY_MAIN: + return "company"; + case CommonDataKinds.Phone.TYPE_PAGER: + return "pager"; + case CommonDataKinds.Phone.TYPE_CUSTOM: + if (cursor.getString(cursor.getColumnIndex(CommonDataKinds.Phone.LABEL)) != null) { + return cursor.getString(cursor.getColumnIndex(CommonDataKinds.Phone.LABEL)).toLowerCase(); + } else return ""; + default: + return "other"; + } + } + } + + public static String getEmailLabel(Resources resources, int type, Cursor cursor, boolean localizedLabels) { + if (localizedLabels) { + CharSequence localizedLabel = CommonDataKinds.Email.getTypeLabel(resources, type, ""); + return localizedLabel.toString().toLowerCase(); + } else { + switch (type) { + case CommonDataKinds.Email.TYPE_HOME: + return "home"; + case CommonDataKinds.Email.TYPE_WORK: + return "work"; + case CommonDataKinds.Email.TYPE_MOBILE: + return "mobile"; + case CommonDataKinds.Email.TYPE_CUSTOM: + if (cursor.getString(cursor.getColumnIndex(CommonDataKinds.Email.LABEL)) != null) { + return cursor.getString(cursor.getColumnIndex(CommonDataKinds.Email.LABEL)).toLowerCase(); + } else return ""; + default: + return "other"; + } + } + } +} \ No newline at end of file diff --git a/contactos_android/android/src/main/java/flutter/plugins/contactos/PostalAddress.java b/contactos_android/android/src/main/java/flutter/plugins/contactos/PostalAddress.java new file mode 100644 index 0000000..048c56a --- /dev/null +++ b/contactos_android/android/src/main/java/flutter/plugins/contactos/PostalAddress.java @@ -0,0 +1,73 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package flutter.plugins.contactos; + +import android.annotation.TargetApi; +import android.content.res.Resources; +import android.database.Cursor; +import android.os.Build; + +import static android.provider.ContactsContract.CommonDataKinds; +import static android.provider.ContactsContract.CommonDataKinds.StructuredPostal; + +import java.util.HashMap; + +@TargetApi(Build.VERSION_CODES.ECLAIR) +public class PostalAddress { + + public String label, street, city, postcode, region, country; + int type; + + public PostalAddress(String label, String street, String city, String postcode, String region, String country, int type) { + this.label = label; + this.street = street; + this.city = city; + this.postcode = postcode; + this.region = region; + this.country = country; + this.type = type; + } + + HashMap toMap() { + HashMap result = new HashMap<>(); + result.put("label", label); + result.put("street", street); + result.put("city", city); + result.put("postcode", postcode); + result.put("region", region); + result.put("country", country); + result.put("type", String.valueOf(type)); + return result; + } + + public static PostalAddress fromMap(HashMap map) { + String label = map.get("label"); + String street = map.get("street"); + String city = map.get("city"); + String postcode = map.get("postcode"); + String region = map.get("region"); + String country = map.get("country"); + String type = map.get("type"); + return new PostalAddress(label, street, city, postcode, region, country, type != null ? Integer.parseInt(type) : -1); + } + + public static String getLabel(Resources resources, int type, Cursor cursor, boolean localizedLabels) { + if (localizedLabels) { + CharSequence localizedLabel = CommonDataKinds.StructuredPostal.getTypeLabel(resources, type, ""); + return localizedLabel.toString().toLowerCase(); + } else { + switch (cursor.getInt(cursor.getColumnIndex(StructuredPostal.TYPE))) { + case StructuredPostal.TYPE_HOME: + return "home"; + case StructuredPostal.TYPE_WORK: + return "work"; + case StructuredPostal.TYPE_CUSTOM: + final String label = cursor.getString(cursor.getColumnIndex(StructuredPostal.LABEL)); + return label != null ? label : ""; + } + return "other"; + } + } +} diff --git a/contactos_android/android/src/test/java/flutter/plugins/contactsservice/contactsservice/ContactTest.java b/contactos_android/android/src/test/java/flutter/plugins/contactsservice/contactsservice/ContactTest.java new file mode 100644 index 0000000..0589220 --- /dev/null +++ b/contactos_android/android/src/test/java/flutter/plugins/contactsservice/contactsservice/ContactTest.java @@ -0,0 +1,100 @@ +package flutter.plugins.contactos; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; + +public class ContactTest { + + @Test + public void compareTo_nullParam() { + Contact contact1 = new Contact("id"); + contact1.givenName = "givenName"; + + Contact contact2 = new Contact("id2"); + + assertThat(contact1.compareTo(contact2)) + .isGreaterThan(0); + } + + @Test + public void compareTo_largerParam() { + Contact contact1 = new Contact("id"); + contact1.givenName = "a"; + + Contact contact2 = new Contact("id2"); + contact2.givenName = "b"; + + assertThat(contact1.compareTo(contact2)) + .isLessThan(0); + } + + @Test + public void compareTo_smallerParam() { + Contact contact1 = new Contact("id"); + contact1.givenName = "b"; + + Contact contact2 = new Contact("id2"); + contact2.givenName = "a"; + + assertThat(contact1.compareTo(contact2)) + .isGreaterThan(0); + } + + @Test + public void compareTo_givenNameNull() { + Contact contact1 = new Contact("id"); + contact1.givenName = null; + + Contact contact2 = new Contact("id2"); + contact2.givenName = null; + + assertThat(contact1.compareTo(contact2)) + .isEqualTo(0); + } + + @Test + public void compareTo_currentContactGivenNameNull() { + Contact contact1 = new Contact("id"); + contact1.givenName = null; + + Contact contact2 = new Contact("id2"); + contact2.givenName = "b"; + + assertThat(contact1.compareTo(contact2)) + .isLessThan(0); + } + + @Test + public void compareTo_nullContact() { + Contact contact1 = new Contact("id"); + contact1.givenName = "a"; + + assertThat(contact1.compareTo(null)) + .isGreaterThan(0); + } + + @Test + public void compareTo_transitiveCompare() { + Contact contact1 = new Contact("id"); + contact1.givenName = "b"; + + Contact contact2 = new Contact("id2"); + contact2.givenName = "a"; + + Contact contact3 = new Contact("id3"); + contact3.givenName = null; + + // b > a + assertThat(contact1.compareTo(contact2)) + .isGreaterThan(0); + + // a > null + assertThat(contact2.compareTo(contact3)) + .isGreaterThan(0); + + // This implies => b > null + assertThat(contact1.compareTo(contact3)) + .isGreaterThan(0); + } +} \ No newline at end of file diff --git a/contactos_android/dart_dependency_validator.yaml b/contactos_android/dart_dependency_validator.yaml new file mode 100644 index 0000000..06cc68f --- /dev/null +++ b/contactos_android/dart_dependency_validator.yaml @@ -0,0 +1,8 @@ +ignore: + - dart_code_metrics + - integration_test + - flutter_lints + - collection + +exclude: + - "example/**" diff --git a/contactos_android/example/.gitignore b/contactos_android/example/.gitignore new file mode 100644 index 0000000..e96ef60 --- /dev/null +++ b/contactos_android/example/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/contactos_android/example/.metadata b/contactos_android/example/.metadata new file mode 100644 index 0000000..a700013 --- /dev/null +++ b/contactos_android/example/.metadata @@ -0,0 +1,8 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 8f65fec5f5f7d7afbb0965f4a44bdb330a28fb19 + channel: alpha diff --git a/contactos_android/example/README.md b/contactos_android/example/README.md new file mode 100644 index 0000000..8399213 --- /dev/null +++ b/contactos_android/example/README.md @@ -0,0 +1,8 @@ +# contactos_example + +Demonstrates how to use the contactos plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](http://flutter.io/). diff --git a/contactos_android/example/android/app/build.gradle b/contactos_android/example/android/app/build.gradle new file mode 100644 index 0000000..ae7b755 --- /dev/null +++ b/contactos_android/example/android/app/build.gradle @@ -0,0 +1,60 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file("local.properties") +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader("UTF-8") { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty("flutter.versionCode") +if (flutterVersionCode == null) { + flutterVersionCode = "1" +} + +def flutterVersionName = localProperties.getProperty("flutter.versionName") +if (flutterVersionName == null) { + flutterVersionName = "1.0" +} + +android { + namespace 'flutter.plugins.contactos.example' + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + applicationId "flutter.plugins.contactos.example" + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + signingConfig = signingConfigs.debug + } + } +} + +flutter { + source = "../.." +} \ No newline at end of file diff --git a/contactos_android/example/android/app/src/main/AndroidManifest.xml b/contactos_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..844374c --- /dev/null +++ b/contactos_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + diff --git a/contactos_android/example/android/app/src/main/java/flutter/plugins/contactos/example/MainActivity.java b/contactos_android/example/android/app/src/main/java/flutter/plugins/contactos/example/MainActivity.java new file mode 100644 index 0000000..271e05b --- /dev/null +++ b/contactos_android/example/android/app/src/main/java/flutter/plugins/contactos/example/MainActivity.java @@ -0,0 +1,5 @@ +package flutter.plugins.contactos.example; + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity {} diff --git a/contactos_android/example/android/app/src/main/res/drawable/launch_background.xml b/contactos_android/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/contactos_android/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/contactos_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/contactos_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/contactos_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/contactos_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/contactos_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/contactos_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/contactos_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/contactos_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/contactos_android/example/android/app/src/main/res/values/styles.xml b/contactos_android/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..00fa441 --- /dev/null +++ b/contactos_android/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/contactos_android/example/android/build.gradle b/contactos_android/example/android/build.gradle new file mode 100644 index 0000000..8e9de9d --- /dev/null +++ b/contactos_android/example/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = "../build" +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/contactos_android/example/android/gradle.properties b/contactos_android/example/android/gradle.properties new file mode 100644 index 0000000..f3dfe33 --- /dev/null +++ b/contactos_android/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/contactos_android/example/android/gradle/wrapper/gradle-wrapper.properties b/contactos_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/contactos_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/contactos_android/example/android/settings.gradle b/contactos_android/example/android/settings.gradle new file mode 100644 index 0000000..65b4c17 --- /dev/null +++ b/contactos_android/example/android/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.13.0" apply false + id "org.jetbrains.kotlin.android" version "2.2.0" apply false +} + +include ":app" \ No newline at end of file diff --git a/contactos_android/example/lib/main.dart b/contactos_android/example/lib/main.dart new file mode 100644 index 0000000..17359cd --- /dev/null +++ b/contactos_android/example/lib/main.dart @@ -0,0 +1,98 @@ +import 'package:contactos_example/src/screens/contacts_list_screen.dart'; +import 'package:contactos_example/src/screens/navite_contacts_picker_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; + +void main() => runApp(const ContactsExampleApp()); + +/// Android only: Localized labels language setting +/// Set androidLocalizedLabels=false if you always want english labels +/// whatever is the device language setting. +const kAndroidLocalizedLabels = false; + +/// {@template main} +/// Example app for the `contactos` plugin. +/// {@endtemplate} +class ContactsExampleApp extends StatelessWidget { + /// {@macro main} + const ContactsExampleApp({super.key}); + + @override + Widget build(BuildContext context) => MaterialApp( + home: const _HomePage(), + routes: { + '/add': (_) => const AddContactScreen(), + '/contacts-list': (_) => const ContactsListScreen(), + '/native-contacts-picker': (_) => const NativeContactsPickerScreen(), + }, + ); +} + +class _HomePage extends StatefulWidget { + const _HomePage({ + super.key, // ignore: unused_element_parameter + }); + + @override + State<_HomePage> createState() => __HomePageState(); +} + +class __HomePageState extends State<_HomePage> { + @override + void initState() { + super.initState(); + _askPermissions(); + } + + Future _askPermissions([String? routeName]) async { + final navigator = Navigator.of(context); + final permissionStatus = await _getContactPermission(); + if (permissionStatus == PermissionStatus.granted) { + if (routeName != null) await navigator.pushNamed(routeName); + } else { + _handleInvalidPermissions(permissionStatus); + } + } + + Future _getContactPermission() async { + final permission = await Permission.contacts.status; + if (permission != PermissionStatus.granted && + permission != PermissionStatus.permanentlyDenied) { + final permissionStatus = await Permission.contacts.request(); + return permissionStatus; + } else { + return permission; + } + } + + void _handleInvalidPermissions(PermissionStatus permissionStatus) { + Widget? content; + if (permissionStatus == PermissionStatus.denied) { + content = const Text('Access to contact data denied'); + } else if (permissionStatus == PermissionStatus.permanentlyDenied) { + content = const Text('Contact data not available on device'); + } + if (content == null) return; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: content)); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Contacts Plugin Example')), + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton( + child: const Text('Contacts list'), + onPressed: () => _askPermissions('/contacts-list'), + ), + ElevatedButton( + child: const Text('Native Contacts picker'), + onPressed: () => _askPermissions('/native-contacts-picker'), + ), + ], + ), + ), + ); +} diff --git a/contactos_android/example/lib/src/screens/contacts_list_screen.dart b/contactos_android/example/lib/src/screens/contacts_list_screen.dart new file mode 100644 index 0000000..d7c4e5a --- /dev/null +++ b/contactos_android/example/lib/src/screens/contacts_list_screen.dart @@ -0,0 +1,603 @@ +/* + * Author: Anton Ustinoff | + * Date: 25 November 2025 + */ + +import 'dart:developer'; + +import 'package:collection/collection.dart'; +import 'package:contactos_android/contactos_android.dart'; +import 'package:contactos_example/main.dart' show kAndroidLocalizedLabels; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +/// {@template contacts_list_screen} +/// Contacts list screen widget. +/// {@endtemplate} +class ContactsListScreen extends StatefulWidget { + /// {@macro contacts_list_screen} + const ContactsListScreen({super.key}); + + @override + State createState() => _ContactsListPageState(); +} + +/// State for widget [ContactsListScreen]. +class _ContactsListPageState extends State { + List? _contacts; + + @override + void initState() { + super.initState(); + refreshContacts(); + } + + Future refreshContacts() async { + // final contacts = + // await ContactosPluginAndroid + // .instance + // .getContactsForPhone("8554964652"); + + // Load without thumbnails initially. + final contacts = await ContactosPluginAndroid.instance.getContacts( + androidLocalizedLabels: kAndroidLocalizedLabels, + withThumbnails: false, + ); + + _contacts = contacts; + if (!mounted) return; + setState(() {}); + + // Lazy load thumbnails after rendering initial contacts. + for (var contact in _contacts ?? contacts) { + ContactosPluginAndroid.instance.getAvatar(contact).then((avatar) { + contact = contact.copyWith(avatar: avatar); + if (mounted) setState(() {}); + }).ignore(); + } + } + + Future updateContact() async { + var contact = _contacts?.firstWhereOrNull( + (c) => c.familyName?.startsWith('Ninja') ?? false, + ); + if (contact == null) return; + await ContactosPluginAndroid.instance.updateContact(contact); + await refreshContacts(); + } + + Future _openContactForm() async { + try { + var _ = await ContactosPluginAndroid.instance.openContactForm( + androidLocalizedLabels: kAndroidLocalizedLabels, + ); + await refreshContacts(); + } on FormOperationException catch (e) { + switch (e.errorCode) { + case FormOperationErrorCode.unknown: + case FormOperationErrorCode.canceled: + case FormOperationErrorCode.couldNotBeOpen: + default: + log(e.toString()); + } + } + } + + void contactOnDeviceHasBeenUpdated(Contact contact) { + if (!mounted) return; + final id = _contacts?.indexWhere((c) => c.identifier == contact.identifier); + if (id == null || id < 0) return; + _contacts?[id] = contact; + setState(() {}); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Contacts Plugin Example'), + actions: [ + IconButton(icon: const Icon(Icons.create), onPressed: _openContactForm), + ], + ), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () { + Navigator.of(context).pushNamed('/add').then((_) { + refreshContacts(); + }); + }, + ), + body: SafeArea( + child: _contacts != null + ? ListView.builder( + itemCount: _contacts?.length ?? 0, + itemBuilder: (context, index) { + final contact = _contacts?.elementAt(index); + if (contact == null) return const SizedBox.shrink(); + return ListTile( + onTap: () { + final route = MaterialPageRoute( + builder: (context) => _ContactDetailsPage( + contact, + onContactDeviceSave: contactOnDeviceHasBeenUpdated, + ), + ); + Navigator.of(context).push(route); + }, + leading: + (contact.avatar != null && + (contact.avatar?.length ?? 0) > 0) + ? CircleAvatar( + backgroundImage: MemoryImage(contact.avatar!), + ) + : CircleAvatar(child: Text(contact.initials())), + title: Text(contact.displayName ?? ''), + ); + }, + ) + : const Center(child: CircularProgressIndicator()), + ), + ); +} + +/// Contact details page. +/// {@macro add_contact_screen} +class _ContactDetailsPage extends StatefulWidget { + /// {@macro add_contact_screen} + const _ContactDetailsPage( + this._contact, { + this.onContactDeviceSave, + super.key, // ignore: unused_element_parameter + }); + + final Contact _contact; + final void Function(Contact)? onContactDeviceSave; + + @override + State<_ContactDetailsPage> createState() => _ContactDetailsPageState(); +} + +/// State for widget [_ContactDetailsPage]. +class _ContactDetailsPageState extends State<_ContactDetailsPage> { + Future _openExistingContactOnDevice(BuildContext context) async { + try { + final contact = await ContactosPluginAndroid.instance.openExistingContact( + widget._contact, + androidLocalizedLabels: kAndroidLocalizedLabels, + ); + if (widget.onContactDeviceSave != null) { + widget.onContactDeviceSave?.call(contact); + } + if (!context.mounted) return; + Navigator.of(context).pop(); + } on FormOperationException catch (e) { + switch (e.errorCode) { + case FormOperationErrorCode.unknown: + case FormOperationErrorCode.canceled: + case FormOperationErrorCode.couldNotBeOpen: + default: + log(e.toString()); + } + } + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text(widget._contact.displayName ?? ''), + actions: [ + /* IconButton( + icon: Icon(Icons.share), + onPressed: () => shareVCFCard(context, contact: _contact), + ), */ + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + ContactosPluginAndroid.instance.deleteContact(widget._contact); + }, + ), + IconButton( + icon: const Icon(Icons.update), + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + _UpdateContactsPage(contact: widget._contact), + ), + ), + ), + IconButton( + icon: const Icon(Icons.edit), + onPressed: () => _openExistingContactOnDevice(context), + ), + ], + ), + body: SafeArea( + child: ListView( + children: [ + ListTile( + title: const Text('Name'), + trailing: Text(widget._contact.givenName ?? ''), + ), + ListTile( + title: const Text('Middle name'), + trailing: Text(widget._contact.middleName ?? ''), + ), + ListTile( + title: const Text('Family name'), + trailing: Text(widget._contact.familyName ?? ''), + ), + ListTile( + title: const Text('Prefix'), + trailing: Text(widget._contact.prefix ?? ''), + ), + ListTile( + title: const Text('Suffix'), + trailing: Text(widget._contact.suffix ?? ''), + ), + ListTile( + title: const Text('Birthday'), + trailing: Text( + widget._contact.birthday != null + ? DateFormat('dd-MM-yyyy').format(widget._contact.birthday!) + : '', + ), + ), + ListTile( + title: const Text('Company'), + trailing: Text(widget._contact.company ?? ''), + ), + ListTile( + title: const Text('Job'), + trailing: Text(widget._contact.jobTitle ?? ''), + ), + ListTile( + title: const Text('Account Type'), + trailing: Text( + (widget._contact.androidAccountType != null) + ? widget._contact.androidAccountType.toString() + : '', + ), + ), + _AddressesTile(widget._contact.postalAddresses!), + _ItemsTile('Phones', widget._contact.phones!), + _ItemsTile('Emails', widget._contact.emails!), + ], + ), + ), + ); +} + +/// Tile for contact addresses. +/// {@macro add_contact_screen} +class _AddressesTile extends StatelessWidget { + /// {@macro add_contact_screen} + const _AddressesTile( + this._addresses, { + super.key, // ignore: unused_element_parameter + }); + + final List _addresses; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile(title: Text('Addresses')), + Column( + children: [ + for (final a in _addresses) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + ListTile( + title: const Text('Street'), + trailing: Text(a.street ?? ''), + ), + ListTile( + title: const Text('Postcode'), + trailing: Text(a.postcode ?? ''), + ), + ListTile( + title: const Text('City'), + trailing: Text(a.city ?? ''), + ), + ListTile( + title: const Text('Region'), + trailing: Text(a.region ?? ''), + ), + ListTile( + title: const Text('Country'), + trailing: Text(a.country ?? ''), + ), + ], + ), + ), + ], + ), + ], + ); +} + +/// Tile for contact items like phones, emails etc. +/// {@macro add_contact_screen} +class _ItemsTile extends StatelessWidget { + /// {@macro add_contact_screen} + const _ItemsTile( + this._title, + this._items, { + super.key, // ignore: unused_element_parameter + }); + + final List _items; + final String _title; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile(title: Text(_title)), + Column( + children: [ + for (final i in _items) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListTile( + title: Text(i.label ?? ''), + trailing: Text(i.value ?? ''), + ), + ), + ], + ), + ], + ); +} + +/// Add contact screen. +/// {@macro add_contact_screen} +class AddContactScreen extends StatefulWidget { + /// {@macro add_contact_screen} + const AddContactScreen({ + super.key, // ignore: unused_element_parameter + }); + + @override + State createState() => _AddContactScreenState(); +} + +/// State for widget [AddContactScreen]. +class _AddContactScreenState extends State { + final GlobalKey _formKey = GlobalKey(); + + Contact$PostalAddress address = const Contact$PostalAddress(label: 'Home'); + Contact contact = const Contact(); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Add a contact'), + actions: [ + TextButton( + onPressed: () { + _formKey.currentState?.save(); + final newContact = contact.copyWith(postalAddresses: [address]); + ContactosPluginAndroid.instance.addContact(newContact); + Navigator.of(context).pop(); + }, + child: const Icon(Icons.save, color: Colors.white), + ), + ], + ), + body: Container( + padding: const EdgeInsets.all(12), + child: Form( + key: _formKey, + child: ListView( + children: [ + TextFormField( + decoration: const InputDecoration(labelText: 'First name'), + onSaved: (v) => contact.copyWith(givenName: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Middle name'), + onSaved: (v) => contact.copyWith(middleName: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Last name'), + onSaved: (v) => contact = contact.copyWith(familyName: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Prefix'), + onSaved: (v) => contact = contact.copyWith(prefix: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Suffix'), + onSaved: (v) => contact = contact.copyWith(suffix: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Phone'), + onSaved: (v) => contact = contact.copyWith( + phones: [Contact$Field(label: 'mobile', value: v)], + ), + keyboardType: TextInputType.phone, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'E-mail'), + onSaved: (v) => contact = contact.copyWith( + emails: [Contact$Field(label: 'work', value: v)], + ), + keyboardType: TextInputType.emailAddress, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Company'), + onSaved: (v) => contact = contact.copyWith(company: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Job'), + onSaved: (v) => contact = contact.copyWith(jobTitle: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Street'), + onSaved: (v) => address = address.copyWith(street: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'City'), + onSaved: (v) => address = address.copyWith(city: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Region'), + onSaved: (v) => address = address.copyWith(region: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Postal code'), + onSaved: (v) => address = address.copyWith(postcode: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Country'), + onSaved: (v) => address = address.copyWith(country: v), + ), + ], + ), + ), + ), + ); +} + +/// Update contact screen. +/// {@macro add_contact_screen} +class _UpdateContactsPage extends StatefulWidget { + /// {@macro add_contact_screen} + const _UpdateContactsPage({ + required this.contact, + super.key, // ignore: unused_element_parameter + }); + + final Contact? contact; + + @override + State<_UpdateContactsPage> createState() => __UpdateContactsPageState(); +} + +/// State for widget [_UpdateContactsPage]. +class __UpdateContactsPageState extends State<_UpdateContactsPage> { + final GlobalKey _formKey = GlobalKey(); + + Contact$PostalAddress address = const Contact$PostalAddress(label: 'Home'); + Contact? contact; + + @override + void initState() { + super.initState(); + contact = widget.contact; + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Update Contact'), + actions: [ + IconButton( + icon: const Icon(Icons.save, color: Colors.white), + onPressed: () async { + final navigator = Navigator.of(context); + final newContact = contact?.copyWith(postalAddresses: [address]); + if (newContact == null) return; + _formKey.currentState?.save(); + await ContactosPluginAndroid.instance.updateContact(newContact); + navigator + .pushReplacement( + MaterialPageRoute( + builder: (_) => const ContactsListScreen(), + ), + ) + .ignore(); + }, + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(12), + child: Form( + key: _formKey, + child: ListView( + children: [ + TextFormField( + initialValue: contact?.givenName ?? '', + decoration: const InputDecoration(labelText: 'First name'), + onSaved: (v) => contact = contact?.copyWith(givenName: v), + ), + TextFormField( + initialValue: contact?.middleName ?? '', + decoration: const InputDecoration(labelText: 'Middle name'), + onSaved: (v) => contact = contact?.copyWith(middleName: v), + ), + TextFormField( + initialValue: contact?.familyName ?? '', + decoration: const InputDecoration(labelText: 'Last name'), + onSaved: (v) => contact = contact?.copyWith(familyName: v), + ), + TextFormField( + initialValue: contact?.prefix ?? '', + decoration: const InputDecoration(labelText: 'Prefix'), + onSaved: (v) => contact = contact?.copyWith(prefix: v), + ), + TextFormField( + initialValue: contact?.suffix ?? '', + decoration: const InputDecoration(labelText: 'Suffix'), + onSaved: (v) => contact = contact?.copyWith(suffix: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Phone'), + onSaved: (v) => contact = contact?.copyWith( + phones: [Contact$Field(label: 'mobile', value: v)], + ), + keyboardType: TextInputType.phone, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'E-mail'), + onSaved: (v) => contact = contact?.copyWith( + emails: [Contact$Field(label: 'work', value: v)], + ), + keyboardType: TextInputType.emailAddress, + ), + TextFormField( + initialValue: contact?.company ?? '', + decoration: const InputDecoration(labelText: 'Company'), + onSaved: (v) => contact = contact?.copyWith(company: v), + ), + TextFormField( + initialValue: contact?.jobTitle ?? '', + decoration: const InputDecoration(labelText: 'Job'), + onSaved: (v) => contact = contact?.copyWith(jobTitle: v), + ), + TextFormField( + initialValue: address.street ?? '', + decoration: const InputDecoration(labelText: 'Street'), + onSaved: (v) => address = address.copyWith(street: v), + ), + TextFormField( + initialValue: address.city ?? '', + decoration: const InputDecoration(labelText: 'City'), + onSaved: (v) => address = address.copyWith(city: v), + ), + TextFormField( + initialValue: address.region ?? '', + decoration: const InputDecoration(labelText: 'Region'), + onSaved: (v) => address = address.copyWith(region: v), + ), + TextFormField( + initialValue: address.postcode ?? '', + decoration: const InputDecoration(labelText: 'Postal code'), + onSaved: (v) => address = address.copyWith(postcode: v), + ), + TextFormField( + initialValue: address.country ?? '', + decoration: const InputDecoration(labelText: 'Country'), + onSaved: (v) => address = address.copyWith(country: v), + ), + ], + ), + ), + ), + ); +} diff --git a/contactos_android/example/lib/src/screens/navite_contacts_picker_screen.dart b/contactos_android/example/lib/src/screens/navite_contacts_picker_screen.dart new file mode 100644 index 0000000..f2a0627 --- /dev/null +++ b/contactos_android/example/lib/src/screens/navite_contacts_picker_screen.dart @@ -0,0 +1,62 @@ +/* + * Author: Anton Ustinoff | + * Date: 25 November 2025 + */ + +import 'dart:developer'; + +import 'package:contactos_android/contactos_android.dart'; +import 'package:contactos_example/main.dart' show kAndroidLocalizedLabels; +import 'package:flutter/material.dart'; + +/// {@template navite_contacts_picker_screen} +/// NativeContactsPickerScreen widget. +/// {@endtemplate} +class NativeContactsPickerScreen extends StatefulWidget { + /// {@macro navite_contacts_picker_screen} + const NativeContactsPickerScreen({super.key}); + + @override + State createState() => + _NativeContactsPickerScreenState(); +} + +/// State for widget [NativeContactsPickerScreen]. +class _NativeContactsPickerScreenState + extends State { + Contact? _contact; + + @override + void initState() { + super.initState(); + } + + Future _pickContact() async { + try { + final contact = await ContactosPluginAndroid.instance + .openDeviceContactPicker( + androidLocalizedLabels: kAndroidLocalizedLabels, + ); + setState(() => _contact = contact); + } on Object catch (e) { + log(e.toString()); + } + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Contacts Picker Example')), + body: SafeArea( + child: Column( + children: [ + ElevatedButton( + onPressed: _pickContact, + child: const Text('Pick a contact'), + ), + if (_contact != null) + Text('Contact selected: ${_contact?.displayName}'), + ], + ), + ), + ); +} diff --git a/contactos_android/example/pubspec.lock b/contactos_android/example/pubspec.lock new file mode 100644 index 0000000..8ebdf41 --- /dev/null +++ b/contactos_android/example/pubspec.lock @@ -0,0 +1,359 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: "direct main" + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + contactos_android: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + contactos_platform_interface: + dependency: transitive + description: + path: "../../contactos_platform_interface" + relative: true + source: path + version: "1.0.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" + source: hosted + version: "0.17.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98 + url: "https://pub.dev" + source: hosted + version: "9.4.6" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" +sdks: + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.3" diff --git a/contactos_android/example/pubspec.yaml b/contactos_android/example/pubspec.yaml new file mode 100644 index 0000000..8f146d9 --- /dev/null +++ b/contactos_android/example/pubspec.yaml @@ -0,0 +1,44 @@ +name: contactos_example +description: Testbed for the contactos plugin implementation for Android. +publish_to: 'none' +version: 0.0.1 + + +environment: + sdk: '>=3.10.0 <4.0.0' + flutter: ">=3.38.3" + + +dependencies: + flutter: + sdk: flutter + + contactos_android: + # When depending on this package from a real application you should use: + # contactos: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + intl: any + collection: any + cupertino_icons: any + + permission_handler: ^12.0.1 + + +dev_dependencies: + # Integration tests for Flutter + integration_test: + sdk: flutter + # Unit & Widget tests for Flutter + flutter_test: + sdk: flutter + + # Linting + flutter_lints: ^6.0.0 + + +flutter: + uses-material-design: true diff --git a/contactos_android/example/test/widget_test.dart b/contactos_android/example/test/widget_test.dart new file mode 100644 index 0000000..38699e8 --- /dev/null +++ b/contactos_android/example/test/widget_test.dart @@ -0,0 +1,33 @@ +// This is a basic Flutter widget test. +// To perform an interaction with a widget in your test, +// use the WidgetTester utility that Flutter +// provides. For example, you can send tap and scroll gestures. +// You can also use WidgetTester to find child widgets in the widget tree, +// read text, and verify that the values of widget properties are correct. + +import 'package:contactos_example/src/screens/contacts_list_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() => group( + 'Widget_tests -', + () => group( + 'ContactsListScreents -', + () => testWidgets('First test', (tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const ContactsListScreen()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }), + ), + ); diff --git a/contactos_android/lib/contactos_android.dart b/contactos_android/lib/contactos_android.dart new file mode 100644 index 0000000..f6ac248 --- /dev/null +++ b/contactos_android/lib/contactos_android.dart @@ -0,0 +1,7 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'package:contactos_platform_interface/contactos_platform_interface.dart'; + +export 'src/contactos_android.dart'; diff --git a/contactos_android/lib/src/contactos_android.dart b/contactos_android/lib/src/contactos_android.dart new file mode 100644 index 0000000..264250e --- /dev/null +++ b/contactos_android/lib/src/contactos_android.dart @@ -0,0 +1,161 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:contactos_platform_interface/contactos_platform_interface.dart'; +import 'package:flutter/foundation.dart' show visibleForOverriding; + +/// The Android implementation of [ContactosPlatform]. +/// +/// This class implements the `package:contactos` +/// functionality for iOS and macOS. +class ContactosPluginAndroid extends ContactosPlatform { + /// Creates a new plugin for iOS and macOS implementation instance. + ContactosPluginAndroid._({ + @visibleForOverriding MethodChannelContactos? channel, + }) : _channel = channel ?? MethodChannelContactos.instance; + + /// Returns an instance using a specified [MethodChannelContactos]. + factory ContactosPluginAndroid._instanceFor({ + @visibleForOverriding MethodChannelContactos? channel, + }) => + ContactosPluginAndroid._(channel: channel); + + /// Returns the default instance + /// of [ContactosPluginAndroid]. + static ContactosPluginAndroid get instance => _instance; + + /// Returns an instance using the default [ContactosPluginAndroid]. + static final ContactosPluginAndroid _instance = + ContactosPluginAndroid._instanceFor(); + + /// The channel used to interact with the platform side of the plugin. + final MethodChannelContactos _channel; + + /// Registers this class + /// as the default instance of [ContactosPla]. + static void registerWith() { + ContactosPlatform.instance = ContactosPluginAndroid.instance; + } + + /// Fetches all contacts, or when specified, the contacts with a name + /// matching [query] + @override + Future> getContacts({ + String? query, + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.getContacts( + query: query, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Fetches all contacts, or when specified, the contacts with the phone + /// matching [phone] + @override + Future> getContactsForPhone( + String? phone, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.getContactsForPhone( + phone, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Fetches all contacts, or when specified, the contacts with the email + /// matching [email] + /// Works only on iOS + @override + Future> getContactsForEmail( + String email, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.getContactsForEmail( + email, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Loads the avatar for the given contact and returns it. If the user does + /// not have an avatar, then `null` is returned in that slot. Only implemented + /// on Android. + @override + Future getAvatar(Contact contact, {bool photoHighRes = true}) => + _channel.getAvatar(contact, photoHighRes: photoHighRes); + + /// Adds the [contact] to the device contact list + @override + Future addContact(Contact contact) => _channel.addContact(contact); + + /// Deletes the [contact] if it has a valid identifier + @override + Future deleteContact(Contact contact) => + _channel.deleteContact(contact); + + /// Updates the [contact] if it has a valid identifier + @override + Future updateContact(Contact contact) => + _channel.updateContact(contact); + + /// Opens the contact form with the fields prefilled with the values from the + @override + Future openContactForm({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.openContactForm( + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Opens the contact form with the fields prefilled with the values from the + /// [contact] parameter + @override + Future openExistingContact( + Contact contact, { + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.openExistingContact( + contact, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Displays the device/native contact picker dialog + /// and returns the contact selected by the user + @override + Future openDeviceContactPicker({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.openDeviceContactPicker( + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); +} diff --git a/contactos_android/pubspec.lock b/contactos_android/pubspec.lock new file mode 100644 index 0000000..4bffce1 --- /dev/null +++ b/contactos_android/pubspec.lock @@ -0,0 +1,275 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + contactos_platform_interface: + dependency: "direct main" + description: + path: "../contactos_platform_interface" + relative: true + source: path + version: "1.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" +sdks: + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.29.3" diff --git a/contactos_android/pubspec.yaml b/contactos_android/pubspec.yaml new file mode 100644 index 0000000..b0676b5 --- /dev/null +++ b/contactos_android/pubspec.yaml @@ -0,0 +1,58 @@ +name: contactos_android +description: Android implementation of the contactos plugin. +version: 0.0.1 +publish_to: none + +homepage: https://github.com/ziqq/contactos/contactos_android + +repository: https://github.com/ziqq/contactos/contactos_android + +issue_tracker: https://github.com/ziqq/contactos/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+contactos%22 + +funding: + - https://www.buymeacoffee.com/ziqq + - https://boosty.to/ziqq + +#screenshots: +# - description: 'Example of using the library to check app version.' +# path: example.png + +topics: + - contacts + + +environment: + sdk: '>=3.6.0 <4.0.0' + flutter: ">=3.29.0" + + +dependencies: + flutter: + sdk: flutter + + # contactos_platform_interface: ^1.0.1 + contactos_platform_interface: + path: ../contactos_platform_interface + + +dev_dependencies: + # Integration tests for Flutter + integration_test: + sdk: flutter + # Unit & Widget tests for Flutter + flutter_test: + sdk: flutter + + # Linting + flutter_lints: ^6.0.0 + + +flutter: + plugin: + implements: contactos + platforms: + android: + package: flutter.plugins.contactos + pluginClass: ContactosPlugin + dartPluginClass: ContactosPluginAndroid + From 00d27a8f86c293aceb974f18a2195c525e9144c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Tue, 25 Nov 2025 18:14:36 +0400 Subject: [PATCH 10/35] chore: update --- LICENSE | 14 ++++++-------- contactos/LICENSE | 14 ++++++-------- contactos/example/pubspec.lock | 28 ++++++++++++++-------------- contactos/pubspec.lock | 26 +++++++++++++------------- 4 files changed, 39 insertions(+), 43 deletions(-) diff --git a/LICENSE b/LICENSE index 789dcc0..4de1fa1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,18 +1,16 @@ BSD 3-Clause License -Copyright (c) 2025, Anton Ustinoff +Copyright (c) 2025, Anton Ustinoff All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. 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. - +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. 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. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/contactos/LICENSE b/contactos/LICENSE index 789dcc0..4de1fa1 100644 --- a/contactos/LICENSE +++ b/contactos/LICENSE @@ -1,18 +1,16 @@ BSD 3-Clause License -Copyright (c) 2025, Anton Ustinoff +Copyright (c) 2025, Anton Ustinoff All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. 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. - +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. 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. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/contactos/example/pubspec.lock b/contactos/example/pubspec.lock index 676fbb8..b426336 100644 --- a/contactos/example/pubspec.lock +++ b/contactos/example/pubspec.lock @@ -47,7 +47,7 @@ packages: path: ".." relative: true source: path - version: "1.0.5" + version: "1.0.6" contactos_platform_interface: dependency: "direct main" description: @@ -122,26 +122,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" matcher: dependency: transitive description: @@ -162,10 +162,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" path: dependency: transitive description: @@ -303,18 +303,18 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.7" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -340,5 +340,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.29.3" diff --git a/contactos/pubspec.lock b/contactos/pubspec.lock index d2a1da7..377b8ba 100644 --- a/contactos/pubspec.lock +++ b/contactos/pubspec.lock @@ -87,26 +87,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -135,10 +135,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" path: dependency: transitive description: @@ -204,18 +204,18 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.7" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -225,5 +225,5 @@ packages: source: hosted version: "15.0.0" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.29.3" From bb250075dd48d20ccf3b5b45454dc5489438015e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 10:09:15 +0400 Subject: [PATCH 11/35] chore(1.0.1): update make --- contactos_platform_interface/Makefile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contactos_platform_interface/Makefile b/contactos_platform_interface/Makefile index 1439f93..307ee6c 100644 --- a/contactos_platform_interface/Makefile +++ b/contactos_platform_interface/Makefile @@ -50,16 +50,17 @@ get: ## Get dependencies .PHONY: analyze analyze: get format ## Analyze code - @fvm dart analyze --fatal-infos --fatal-warnings + @fvm dart analyze --fatal-infos --fatal-warnings || (echo "Β―\_(ツ)_/Β― Analyze code error"; exit 1) .PHONY: check check: analyze ## Check the code + @fvm dart pub global deactivate pana > /dev/null 2>&1 || true @fvm dart pub global activate pana - @pana --json --no-warning --line-length 80 > log.pana.json + @fvm dart pub global run pana --json > log.pana.json || (echo "Β―\_(ツ)_/Β― Pana analysis error"; exit 1) .PHONY: publish-check publish-check: check ## Check the code before publish - @fvm dart pub publish --dry-run + @fvm dart pub publish --dry-run || (echo "Β―\_(ツ)_/Β― Publish check error"; exit 1) .PHONY: publish publish: ## Publish package From f7a9e004427ae98c675c3148c5cc84c59863c691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 10:57:03 +0400 Subject: [PATCH 12/35] chore(1.0.1): update make --- contactos_platform_interface/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contactos_platform_interface/Makefile b/contactos_platform_interface/Makefile index 307ee6c..d1032a7 100644 --- a/contactos_platform_interface/Makefile +++ b/contactos_platform_interface/Makefile @@ -76,7 +76,7 @@ run-genhtml: ## Runs generage coverage html .PHONY: test-unit test-unit: ## Runs unit tests - @fvm flutter test --coverage || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) + @fvm flutter test --coverage test/contactos_platform_interface_test.dart || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) @genhtml coverage/lcov.info --output=coverage -o coverage/html || (echo "Β―\_(ツ)_/Β― Error while running genhtml with coverage"; exit 2) .PHONY: tag From f74248cf24cb2eb2ac86b17ff1f486db7164b2e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 10:57:17 +0400 Subject: [PATCH 13/35] feat(1.0.1): added more tests --- .../lib/src/method_channel_contactos.dart | 2 +- .../lib/src/types.dart | 4 +- .../contactos_platform_interface_test.dart | 230 +---------- .../test/method_channel_contactos_test.dart | 124 ------ .../contactos_platform_interface_test.dart | 226 +++++++++++ .../src/method_channel_contactos_test.dart | 254 ++++++++++++ .../test/src/types_test.dart | 379 ++++++++++++++++++ 7 files changed, 875 insertions(+), 344 deletions(-) delete mode 100644 contactos_platform_interface/test/method_channel_contactos_test.dart create mode 100644 contactos_platform_interface/test/src/contactos_platform_interface_test.dart create mode 100644 contactos_platform_interface/test/src/method_channel_contactos_test.dart create mode 100644 contactos_platform_interface/test/src/types_test.dart diff --git a/contactos_platform_interface/lib/src/method_channel_contactos.dart b/contactos_platform_interface/lib/src/method_channel_contactos.dart index 1cb691a..a6a7102 100644 --- a/contactos_platform_interface/lib/src/method_channel_contactos.dart +++ b/contactos_platform_interface/lib/src/method_channel_contactos.dart @@ -164,7 +164,7 @@ class MethodChannelContactos extends ContactosPlatform { // result contains either : // - an List of contacts containing 0 or 1 contact // - a FormOperationErrorCode value - if (result case List resultList) { + if (result case List resultList) { if (resultList.isEmpty) return null; result = resultList.first; } diff --git a/contactos_platform_interface/lib/src/types.dart b/contactos_platform_interface/lib/src/types.dart index f9a7ed1..7e89fb3 100644 --- a/contactos_platform_interface/lib/src/types.dart +++ b/contactos_platform_interface/lib/src/types.dart @@ -526,13 +526,13 @@ class FormOperationException implements Exception { /// with a [FormOperationErrorCode.couldNotBeOpen] /// {@macro form_operation_exception} const factory FormOperationException.couldNotBeOpen() = - FormOperationException$Canceled; + FormOperationException$CouldNotBeOpen; /// Creates a [FormOperationException] /// with a [FormOperationErrorCode.unknown] /// {@macro form_operation_exception} const factory FormOperationException.unknown() = - FormOperationException$Canceled; + FormOperationException$Unknown; /// The error code associated with this exception final FormOperationErrorCode? errorCode; diff --git a/contactos_platform_interface/test/contactos_platform_interface_test.dart b/contactos_platform_interface/test/contactos_platform_interface_test.dart index 7a94afa..f0c2d32 100644 --- a/contactos_platform_interface/test/contactos_platform_interface_test.dart +++ b/contactos_platform_interface/test/contactos_platform_interface_test.dart @@ -1,222 +1,18 @@ -// Copyright 2025 Anton Ustinoff. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +/* + * Author: Anton Ustinoff | + * Date: 26 November 2025 + */ -import 'dart:typed_data'; - -import 'package:contactos_platform_interface/contactos_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - group(ContactosPlatform, () { - test('disallows implementing interface', () { - expect( - () { - ContactosPlatform.instance = IllegalImplementation(); - }, - // In versions of `package:plugin_platform_interface` prior to fixing - // https://github.com/flutter/flutter/issues/109339, an attempt to - // implement a platform interface using `implements` would sometimes - // throw a `NoSuchMethodError` and other times throw an - // `AssertionError`. After the issue is fixed, an `AssertionError` will - // always be thrown. For the purpose of this test, we don't really care - // what exception is thrown, so just allow any exception. - throwsA(anything), - ); - }); +import 'src/contactos_platform_interface_test.dart' + as contactos_platform_interface_test; +import 'src/method_channel_contactos_test.dart' + as method_channel_contactos_test; +import 'src/types_test.dart' as types_test; - test('supports MockPlatformInterfaceMixin', () { - ContactosPlatform.instance = MockContactosPlatformImplementation(); +void main() => group('Unit_tests -', () { + contactos_platform_interface_test.main(); + method_channel_contactos_test.main(); + types_test.main(); }); - }); -} - -/// An implementation using `implements` that isn't a mock, which isn't allowed. -class IllegalImplementation implements ContactosPlatform { - // Intentionally declare self as not a mock to trigger the - // compliance check. - @override - bool get isMock => false; - - @override - Future addContact(Contact contact) { - throw UnimplementedError(); - } - - @override - Future deleteContact(Contact contact) { - throw UnimplementedError(); - } - - @override - Future getAvatar(Contact contact, {bool photoHighRes = true}) { - throw UnimplementedError(); - } - - @override - Future> getContacts({ - String? query, - bool withThumbnails = true, - bool photoHighResolution = true, - bool orderByGivenName = true, - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future> getContactsForEmail( - String email, { - bool withThumbnails = true, - bool photoHighResolution = true, - bool orderByGivenName = true, - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future> getContactsForPhone( - String? phone, { - bool withThumbnails = true, - bool photoHighResolution = true, - bool orderByGivenName = true, - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future openContactForm({ - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future openDeviceContactPicker({ - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future openExistingContact( - Contact contact, { - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future updateContact(Contact contact) { - throw UnimplementedError(); - } - - @override - ContactosPlatform delegateFor({required MethodChannelContactos channel}) { - throw UnimplementedError(); - } -} - -class MockContactosPlatformImplementation - with MockPlatformInterfaceMixin - implements ContactosPlatform { - @override - bool get isMock => false; - - @override - Future addContact(Contact contact) { - throw UnimplementedError(); - } - - @override - Future deleteContact(Contact contact) { - throw UnimplementedError(); - } - - @override - Future getAvatar(Contact contact, {bool photoHighRes = true}) { - throw UnimplementedError(); - } - - @override - Future> getContacts({ - String? query, - bool withThumbnails = true, - bool photoHighResolution = true, - bool orderByGivenName = true, - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future> getContactsForEmail( - String email, { - bool withThumbnails = true, - bool photoHighResolution = true, - bool orderByGivenName = true, - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future> getContactsForPhone( - String? phone, { - bool withThumbnails = true, - bool photoHighResolution = true, - bool orderByGivenName = true, - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future openContactForm({ - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future openDeviceContactPicker({ - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future openExistingContact( - Contact contact, { - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future updateContact(Contact contact) { - throw UnimplementedError(); - } - - @override - ContactosPlatform delegateFor({required MethodChannelContactos channel}) { - throw UnimplementedError(); - } -} diff --git a/contactos_platform_interface/test/method_channel_contactos_test.dart b/contactos_platform_interface/test/method_channel_contactos_test.dart deleted file mode 100644 index cf9cc72..0000000 --- a/contactos_platform_interface/test/method_channel_contactos_test.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:contactos_platform_interface/contactos_platform_interface.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('MethodChannelContactos -', () { - const channel = MethodChannel('github.com/ziqq/contactos'); - - late MethodChannelContactos contactos; - final log = []; - - setUp(() { - TestWidgetsFlutterBinding.ensureInitialized(); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'getContacts': - return [ - { - 'identifier': '1', - 'displayName': 'John Doe', - 'phones': [ - {'label': 'mobile', 'value': '+123456789'} - ], - 'emails': [ - {'label': 'work', 'value': 'johndoe@example.com'} - ], - } - ]; - case 'getContactsForEmail': - return [ - { - 'identifier': '2', - 'displayName': 'Jane Doe', - 'emails': [ - {'label': 'home', 'value': 'janedoe@example.com'} - ], - } - ]; - case 'getContactsForPhone': - return [ - { - 'identifier': '3', - 'displayName': 'Alice Smith', - 'phones': [ - {'label': 'home', 'value': '+987654321'} - ], - } - ]; - case 'addContact': - case 'deleteContact': - case 'updateContact': - return null; - case 'getAvatar': - return Uint8List.fromList([0, 1, 2, 3, 4, 5]); - default: - return null; - } - }); - - contactos = MethodChannelContactos.instance; - log.clear(); - }); - - tearDown(() async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, null); - }); - - test('getContacts should return list of contacts', () async { - final contacts = await contactos.getContacts(); - expect(contacts, isNotEmpty); - expect(contacts.first.identifier, '1'); - expect(contacts.first.displayName, 'John Doe'); - expect(contacts.first.phones?.first.value, '+123456789'); - expect(contacts.first.emails?.first.value, 'johndoe@example.com'); - expect(log.single.method, 'getContacts'); - }); - - test('getContactsForEmail should return list of contacts', () async { - final contacts = - await contactos.getContactsForEmail('janedoe@example.com'); - expect(contacts, isNotEmpty); - expect(contacts.first.identifier, '2'); - expect(contacts.first.displayName, 'Jane Doe'); - expect(contacts.first.emails?.first.value, 'janedoe@example.com'); - expect(log.single.method, 'getContactsForEmail'); - }); - - test('getContactsForPhone should return list of contacts', () async { - final contacts = await contactos.getContactsForPhone('+987654321'); - expect(contacts, isNotEmpty); - expect(contacts.first.identifier, '3'); - expect(contacts.first.displayName, 'Alice Smith'); - expect(contacts.first.phones?.first.value, '+987654321'); - expect(log.single.method, 'getContactsForPhone'); - }); - - test('getAvatar should return Uint8List', () async { - final avatar = await contactos.getAvatar(const Contact(identifier: '1')); - expect(avatar, isNotNull); - expect(avatar, isA()); - expect(log.single.method, 'getAvatar'); - }); - - test('addContact should call method channel', () async { - await contactos.addContact( - const Contact(identifier: '4', displayName: 'New Contact')); - expect(log.single.method, 'addContact'); - }); - - test('deleteContact should call method channel', () async { - await contactos.deleteContact(const Contact(identifier: '5')); - expect(log.single.method, 'deleteContact'); - }); - - test('updateContact should call method channel', () async { - await contactos.updateContact( - const Contact(identifier: '6', displayName: 'Updated Contact')); - expect(log.single.method, 'updateContact'); - }); - }); -} diff --git a/contactos_platform_interface/test/src/contactos_platform_interface_test.dart b/contactos_platform_interface/test/src/contactos_platform_interface_test.dart new file mode 100644 index 0000000..22ad176 --- /dev/null +++ b/contactos_platform_interface/test/src/contactos_platform_interface_test.dart @@ -0,0 +1,226 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:contactos_platform_interface/contactos_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ContactosPlatform -', () { + setUp(TestWidgetsFlutterBinding.ensureInitialized); + + group('instance -', () { + test('is MethodChannelContactos by default', () { + expect(ContactosPlatform.instance, isA()); + }); + + test('cannot be implemented with `implements`', () { + expect( + () { + ContactosPlatform.instance = ImplementsContactosPlatform(); + }, + throwsA(isA()), + ); + }); + + test('can be extended', () { + ContactosPlatform.instance = ExtendsContactosPlatform(); + }); + + test('isMock returns false by default', () { + // ignore: deprecated_member_use_from_same_package + expect(ContactosPlatform.instance.isMock, isFalse); + }); + }); + + group('delegateFor', () { + test('throws UnimplementedError', () { + final platform = ExtendsContactosPlatform(); + ContactosPlatform.instance = platform; + + expect( + () => ContactosPlatform.instanceFor( + channel: MethodChannelContactos.instance, + ), + throwsUnimplementedError, + ); + }); + }); + }); +} + +class ImplementsContactosPlatform implements ContactosPlatform { + @override + Future addContact(Contact contact) { + throw UnimplementedError(); + } + + @override + ContactosPlatform delegateFor({required MethodChannelContactos channel}) { + throw UnimplementedError(); + } + + @override + Future deleteContact(Contact contact) { + throw UnimplementedError(); + } + + @override + Future getAvatar(Contact contact, {bool photoHighRes = true}) { + throw UnimplementedError(); + } + + @override + Future> getContacts({ + String? query, + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future> getContactsForEmail( + String email, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future> getContactsForPhone( + String? phone, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + bool get isMock => true; + + @override + Future openContactForm({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future openDeviceContactPicker({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future openExistingContact( + Contact contact, { + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future updateContact(Contact contact) { + throw UnimplementedError(); + } +} + +class ExtendsContactosPlatform extends ContactosPlatform { + @override + Future addContact(Contact contact) { + throw UnimplementedError(); + } + + @override + Future deleteContact(Contact contact) { + throw UnimplementedError(); + } + + @override + Future getAvatar(Contact contact, {bool photoHighRes = true}) { + throw UnimplementedError(); + } + + @override + Future> getContacts({ + String? query, + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future> getContactsForEmail( + String email, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future> getContactsForPhone( + String? phone, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future openContactForm({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future openDeviceContactPicker({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future openExistingContact( + Contact contact, { + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future updateContact(Contact contact) { + throw UnimplementedError(); + } +} diff --git a/contactos_platform_interface/test/src/method_channel_contactos_test.dart b/contactos_platform_interface/test/src/method_channel_contactos_test.dart new file mode 100644 index 0000000..e41e9fb --- /dev/null +++ b/contactos_platform_interface/test/src/method_channel_contactos_test.dart @@ -0,0 +1,254 @@ +import 'package:contactos_platform_interface/contactos_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('MethodChannelContactos -', () { + const channel = MethodChannel('github.com/ziqq/contactos'); + + late MethodChannelContactos contactos; + final log = []; + + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'getContacts': + return [ + { + 'identifier': '1', + 'displayName': 'John Doe', + 'phones': [ + {'label': 'mobile', 'value': '+123456789'} + ], + 'emails': [ + {'label': 'work', 'value': 'johndoe@example.com'} + ], + } + ]; + case 'getContactsForEmail': + return [ + { + 'identifier': '2', + 'displayName': 'Jane Doe', + 'emails': [ + {'label': 'home', 'value': 'janedoe@example.com'} + ], + } + ]; + case 'getContactsForPhone': + return [ + { + 'identifier': '3', + 'displayName': 'Alice Smith', + 'phones': [ + {'label': 'home', 'value': '+987654321'} + ], + } + ]; + case 'addContact': + case 'deleteContact': + case 'updateContact': + return null; + case 'getAvatar': + return Uint8List.fromList([0, 1, 2, 3, 4, 5]); + default: + return null; + } + }); + + contactos = MethodChannelContactos.instance; + log.clear(); + }); + + tearDown(() async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + group('getContacts -', () { + test('should return list of contacts', () async { + final contacts = await contactos.getContacts(); + expect(contacts, isNotEmpty); + expect(contacts.first.identifier, '1'); + expect(contacts.first.displayName, 'John Doe'); + expect(contacts.first.phones?.first.value, '+123456789'); + expect(contacts.first.emails?.first.value, 'johndoe@example.com'); + expect(log.single.method, 'getContacts'); + }); + }); + + group('getContactsForEmail -', () { + test('should return list of contacts', () async { + final contacts = + await contactos.getContactsForEmail('janedoe@example.com'); + expect(contacts, isNotEmpty); + expect(contacts.first.identifier, '2'); + expect(contacts.first.displayName, 'Jane Doe'); + expect(contacts.first.emails?.first.value, 'janedoe@example.com'); + expect(log.single.method, 'getContactsForEmail'); + }); + }); + + group('getContactsForPhone -', () { + test('should return list of contacts', () async { + final contacts = await contactos.getContactsForPhone('+987654321'); + expect(contacts, isNotEmpty); + expect(contacts.first.identifier, '3'); + expect(contacts.first.displayName, 'Alice Smith'); + expect(contacts.first.phones?.first.value, '+987654321'); + expect(log.single.method, 'getContactsForPhone'); + }); + + test('returns empty list if phone is null or empty', () async { + var contacts = await contactos.getContactsForPhone(null); + expect(contacts, isEmpty); + + contacts = await contactos.getContactsForPhone(''); + expect(contacts, isEmpty); + }); + }); + + group('getAvatar -', () { + test('should return Uint8List', () async { + final avatar = + await contactos.getAvatar(const Contact(identifier: '1')); + expect(avatar, isNotNull); + expect(avatar, isA()); + expect(log.single.method, 'getAvatar'); + }); + }); + + group('addContact -', () { + test('should call method channel', () async { + await contactos.addContact( + const Contact(identifier: '4', displayName: 'New Contact')); + expect(log.single.method, 'addContact'); + }); + }); + + group('deleteContact -', () { + test('should call method channel', () async { + await contactos.deleteContact(const Contact(identifier: '5')); + expect(log.single.method, 'deleteContact'); + }); + }); + + group('updateContact -', () { + test('should call method channel', () async { + await contactos.updateContact( + const Contact(identifier: '6', displayName: 'Updated Contact')); + expect(log.single.method, 'updateContact'); + }); + }); + + group('openContactForm -', () { + test('returns contact on success', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + if (methodCall.method == 'openContactForm') { + return {'identifier': 'new_id', 'displayName': 'New Contact'}; + } + return null; + }); + + final contact = await contactos.openContactForm(); + expect(contact.identifier, 'new_id'); + expect(contact.displayName, 'New Contact'); + }); + + test('throws canceled exception', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + if (methodCall.method == 'openContactForm') { + return 1; // Canceled code + } + return null; + }); + + expect( + () => contactos.openContactForm(), + throwsA(isA()), + ); + }); + + test('throws couldNotBeOpen exception', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + if (methodCall.method == 'openContactForm') { + return 2; // CouldNotBeOpen code + } + return null; + }); + + expect( + () => contactos.openContactForm(), + throwsA(isA()), + ); + }); + + test('throws unknown exception for other codes', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + if (methodCall.method == 'openContactForm') { + return 999; // Unknown code + } + return null; + }); + + expect( + () => contactos.openContactForm(), + throwsA(isA()), + ); + }); + }); + + group('openDeviceContactPicker -', () { + test('returns contact from list', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + if (methodCall.method == 'openDeviceContactPicker') { + return [ + {'identifier': 'picked_id', 'displayName': 'Picked Contact'} + ]; + } + return null; + }); + + final contact = await contactos.openDeviceContactPicker(); + expect(contact?.identifier, 'picked_id'); + }); + + test('returns null if list is empty', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + if (methodCall.method == 'openDeviceContactPicker') { + return []; + } + return null; + }); + + final contact = await contactos.openDeviceContactPicker(); + expect(contact, isNull); + }); + }); + + group('openExistingContact -', () { + test('returns updated contact', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + if (methodCall.method == 'openExistingContact') { + return {'identifier': 'id', 'displayName': 'Updated Name'}; + } + return null; + }); + + final contact = await contactos + .openExistingContact(const Contact(identifier: 'id')); + expect(contact.displayName, 'Updated Name'); + }); + }); + }); +} diff --git a/contactos_platform_interface/test/src/types_test.dart b/contactos_platform_interface/test/src/types_test.dart new file mode 100644 index 0000000..a88d722 --- /dev/null +++ b/contactos_platform_interface/test/src/types_test.dart @@ -0,0 +1,379 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:contactos_platform_interface/contactos_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('AndroidAccountType -', () { + group('fromString -', () { + test('returns correct enum', () { + expect( + AndroidAccountType.fromString('com.google'), + AndroidAccountType.google, + ); + expect( + AndroidAccountType.fromString('com.whatsapp'), + AndroidAccountType.whatsapp, + ); + expect( + AndroidAccountType.fromString('com.facebook'), + AndroidAccountType.facebook, + ); + expect( + AndroidAccountType.fromString('other'), + AndroidAccountType.other, + ); + expect(AndroidAccountType.fromString(null), null); + }); + }); + }); + + group('Contact -', () { + group('fromJson -', () { + test('parses correctly', () { + final json = { + 'identifier': 'id', + 'displayName': 'Display Name', + 'givenName': 'Given', + 'middleName': 'Middle', + 'prefix': 'Mr.', + 'suffix': 'Jr.', + 'familyName': 'Family', + 'company': 'Company', + 'jobTitle': 'Job', + 'androidAccountType': 'com.google', + 'androidAccountName': 'account', + 'emails': [ + {'label': 'work', 'value': 'email@example.com'} + ], + 'phones': [ + {'label': 'mobile', 'value': '1234567890'} + ], + 'postalAddresses': [ + { + 'label': 'home', + 'street': 'Street', + 'city': 'City', + 'postcode': '12345', + 'region': 'Region', + 'country': 'Country', + } + ], + 'avatar': [1, 2, 3], + 'birthday': '2000-01-01', + }; + + final contact = Contact.fromJson(json); + + expect(contact.identifier, 'id'); + expect(contact.displayName, 'Display Name'); + expect(contact.givenName, 'Given'); + expect(contact.middleName, 'Middle'); + expect(contact.prefix, 'Mr.'); + expect(contact.suffix, 'Jr.'); + expect(contact.familyName, 'Family'); + expect(contact.company, 'Company'); + expect(contact.jobTitle, 'Job'); + expect(contact.androidAccountType, AndroidAccountType.google); + expect(contact.androidAccountName, 'account'); + expect(contact.emails?.first.label, 'work'); + expect(contact.emails?.first.value, 'email@example.com'); + expect(contact.phones?.first.label, 'mobile'); + expect(contact.phones?.first.value, '1234567890'); + expect(contact.postalAddresses?.first.label, 'home'); + expect(contact.postalAddresses?.first.street, 'Street'); + expect(contact.avatar, Uint8List.fromList([1, 2, 3])); + expect(contact.birthday, DateTime(2000, 1, 1)); + }); + + test('handles invalid birthday', () { + final json = { + 'birthday': 'invalid', + }; + final contact = Contact.fromJson(json); + expect(contact.birthday, null); + }); + }); + + group('toJson -', () { + test('serializes correctly', () { + final contact = Contact( + identifier: 'id', + displayName: 'Display Name', + givenName: 'Given', + middleName: 'Middle', + prefix: 'Mr.', + suffix: 'Jr.', + familyName: 'Family', + company: 'Company', + jobTitle: 'Job', + androidAccountType: AndroidAccountType.google, + androidAccountTypeRaw: 'com.google', + androidAccountName: 'account', + emails: const [ + Contact$Field(label: 'work', value: 'email@example.com') + ], + phones: const [Contact$Field(label: 'mobile', value: '1234567890')], + postalAddresses: const [ + Contact$PostalAddress( + label: 'home', + street: 'Street', + city: 'City', + postcode: '12345', + region: 'Region', + country: 'Country', + ) + ], + avatar: Uint8List.fromList([1, 2, 3]), + birthday: DateTime(2000, 1, 1), + ); + + final json = contact.toJson(); + + expect(json['identifier'], 'id'); + expect(json['displayName'], 'Display Name'); + expect(json['birthday'], '2000-01-01'); + expect(json['emails'], isA>()); + expect(json['phones'], isA>()); + expect(json['postalAddresses'], isA>()); + }); + }); + + group('copyWith -', () { + test('works correctly', () { + const contact = Contact(identifier: 'id', givenName: 'Given'); + final copy = contact.copyWith(givenName: 'New Given'); + expect(copy.identifier, 'id'); + expect(copy.givenName, 'New Given'); + }); + + test('preserves values when arguments are null', () { + const contact = Contact(identifier: 'id', givenName: 'Given'); + final copy = contact.copyWith(identifier: 'newId'); + expect(copy.identifier, 'newId'); + expect(copy.givenName, 'Given'); + }); + }); + + group('initials -', () { + test('returns correct initials', () { + const contact1 = Contact(givenName: 'John', familyName: 'Doe'); + expect(contact1.initials(), 'JD'); + + const contact2 = Contact(givenName: 'John'); + expect(contact2.initials(), 'J'); + + const contact3 = Contact(familyName: 'Doe'); + expect(contact3.initials(), 'D'); + + const contact4 = Contact(); + expect(contact4.initials(), ''); + }); + }); + + group('operator +', () { + test('merges contacts', () { + const contact1 = Contact(givenName: 'John', emails: []); + const contact2 = Contact( + familyName: 'Doe', + emails: [Contact$Field(label: 'work', value: 'email')], + ); + final merged = contact1 + contact2; + expect(merged.givenName, 'John'); + expect(merged.familyName, 'Doe'); + expect(merged.emails?.length, 1); + }); + + test('merges contacts with existing lists', () { + const contact1 = Contact( + emails: [Contact$Field(label: 'home', value: 'home@email.com')], + ); + const contact2 = Contact( + emails: [Contact$Field(label: 'work', value: 'work@email.com')], + ); + final merged = contact1 + contact2; + expect(merged.emails?.length, 2); + }); + + test('merges contacts with null lists on left side', () { + const contact1 = Contact(givenName: 'A'); + const contact2 = Contact( + emails: [Contact$Field(label: 'a', value: 'a')], + phones: [Contact$Field(label: 'b', value: 'b')], + postalAddresses: [Contact$PostalAddress(street: 's')], + ); + final merged = contact1 + contact2; + expect(merged.emails?.length, 1); + expect(merged.phones?.length, 1); + expect(merged.postalAddresses?.length, 1); + }); + + test('merges contacts with non-null lists', () { + const contact1 = Contact( + phones: [Contact$Field(label: 'a', value: 'a')], + postalAddresses: [Contact$PostalAddress(street: 's1')], + ); + const contact2 = Contact( + phones: [Contact$Field(label: 'b', value: 'b')], + postalAddresses: [Contact$PostalAddress(street: 's2')], + ); + final merged = contact1 + contact2; + expect(merged.phones?.length, 2); + expect(merged.postalAddresses?.length, 2); + }); + }); + + group('equality and hashCode -', () { + test('works correctly', () { + const contact1 = Contact(identifier: 'id'); + const contact2 = Contact(identifier: 'id'); + const contact3 = Contact(identifier: 'other'); + + expect(contact1, contact2); + expect(contact1.hashCode, contact2.hashCode); + expect(contact1, isNot(contact3)); + }); + }); + }); + + group(r'Contact$PostalAddress -', () { + group('toString -', () { + test('returns formatted address', () { + const address = Contact$PostalAddress( + street: 'Street', + city: 'City', + country: 'Country', + ); + expect(address.toString(), 'Street, City, Country'); + }); + + test('returns formatted address with all fields', () { + const address = Contact$PostalAddress( + street: 'Street', + city: 'City', + region: 'Region', + postcode: '12345', + country: 'Country', + ); + expect(address.toString(), 'Street, City, Region 12345, Country'); + }); + + test('returns formatted address with missing fields', () { + const address1 = Contact$PostalAddress(city: 'City'); + expect(address1.toString(), 'City'); + + const address2 = Contact$PostalAddress(region: 'Region'); + expect(address2.toString(), 'Region'); + + const address3 = Contact$PostalAddress(postcode: '12345'); + expect(address3.toString(), '12345'); + + const address4 = Contact$PostalAddress(country: 'Country'); + expect(address4.toString(), 'Country'); + }); + + test('returns formatted address with combinations', () { + expect( + const Contact$PostalAddress(street: 'S', region: 'R').toString(), + 'S, R', + ); + expect( + const Contact$PostalAddress(street: 'S', postcode: 'P').toString(), + 'S P', + ); + expect( + const Contact$PostalAddress(street: 'S', country: 'C').toString(), + 'S, C', + ); + }); + }); + + group('equality and hashCode -', () { + test('works correctly', () { + const address1 = Contact$PostalAddress(street: 'Street'); + const address2 = Contact$PostalAddress(street: 'Street'); + const address3 = Contact$PostalAddress(street: 'Other'); + + expect(address1, address2); + expect(address1.hashCode, address2.hashCode); + expect(address1, isNot(address3)); + }); + }); + + group('copyWith -', () { + test('works correctly', () { + const address = Contact$PostalAddress(street: 'Street'); + final copy = address.copyWith(street: 'New Street'); + expect(copy.street, 'New Street'); + }); + + test('preserves values when arguments are null', () { + const address = Contact$PostalAddress(street: 'Street', city: 'City'); + final copy = address.copyWith(city: 'New City'); + expect(copy.street, 'Street'); + expect(copy.city, 'New City'); + }); + }); + }); + + group(r'Contact$Field -', () { + group('equality and hashCode -', () { + test('works correctly', () { + const field1 = Contact$Field(label: 'label', value: 'value'); + const field2 = Contact$Field(label: 'label', value: 'value'); + const field3 = Contact$Field(label: 'other', value: 'value'); + + expect(field1, field2); + expect(field1.hashCode, field2.hashCode); + expect(field1, isNot(field3)); + expect(field1, isNot('string')); + }); + }); + }); + + group('FormOperationException -', () { + group('toString -', () { + test('returns correct message', () { + const exception = FormOperationException.canceled(); + expect(exception.toString(), + 'FormOperationException: FormOperationErrorCode.canceled'); + }); + }); + + group('factories -', () { + test('create correct exceptions', () { + expect( + const FormOperationException.canceled(), + isA(), + ); + expect( + const FormOperationException.couldNotBeOpen(), + isA(), + ); + expect( + const FormOperationException.unknown(), + isA(), + ); + }); + + test('subclasses can be instantiated directly', () { + expect( + const FormOperationException$Canceled(), + isA(), + ); + expect( + const FormOperationException$CouldNotBeOpen(), + isA(), + ); + expect( + const FormOperationException$Unknown(), + isA(), + ); + }); + }); + }); +} From 0ea28a18e67d371a4a4dc6d3af7893f8d46c7a86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 11:06:34 +0400 Subject: [PATCH 14/35] chore(1.0.1): update `README` --- contactos_platform_interface/README.md | 43 ++++++++++++++++---------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/contactos_platform_interface/README.md b/contactos_platform_interface/README.md index 7b5cf2d..b5b42f4 100644 --- a/contactos_platform_interface/README.md +++ b/contactos_platform_interface/README.md @@ -1,29 +1,40 @@ # contactos_platform_interface -A common platform interface for the [`contactos`][1] plugin. +[![pub package](https://img.shields.io/pub/v/contactos_platform_interface.svg)](https://pub.dev/packages/contactos_platform_interface) +[![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) +[![style: very_good_analysis](https://img.shields.io/badge/style-very_good_analysis-B22C11.svg)](https://pub.dev/packages/very_good_analysis) + +A common platform interface for the [`contactos`](https://pub.dev/packages/contactos) plugin. This interface allows platform-specific implementations of the `contactos` plugin, as well as the plugin itself, to ensure they are supporting the same interface. -# Usage +## Architecture -To implement a new platform-specific implementation of `contactos`, extend -[`ContactosPlatform`][2] with implementations that perform the platform-specific behaviors, -and when you register your plugin. +The `contactos` plugin uses the [federated plugin architecture](https://flutter.dev/go/federated-plugins). + +- **`contactos`**: The app-facing package that developers depend on. +- **`contactos_platform_interface`**: This package. It declares the interface that platform packages must implement. +- **`contactos_android`**, **`contactos_foundation`**: Platform-specific implementations. -Please note that the plugin tooling only registers the native and/or Dart classes -listed in your package's `pubspec.yaml`, so if you intend to implement more than -one class, you will need to manually register the second class -(as can be seen in the Android and iOS implementations). +## Usage + +To implement a new platform-specific implementation of `contactos`, extend +[`ContactosPlatform`](lib/src/contactos_platform_interface.dart) with implementations that perform the platform-specific behaviors. -# Note on breaking changes +### Example -Strongly prefer non-breaking changes (such as adding a method to the interface) -over breaking changes for this package. +```dart +class ContactosWindows extends ContactosPlatform { + static void registerWith() { + ContactosPlatform.instance = ContactosWindows(); + } -See https://flutter.dev/go/platform-interface-breaking-changes for a discussion -on why a less-clean interface is preferable to a breaking change. + @override + Future> getContacts({String? query, ...}) { + // Implementation for Windows + } -[1]: ../contactos -[2]: lib/contactos_platform_interface.dart \ No newline at end of file + // ... implement other methods +} \ No newline at end of file From 696bdab7c42c0ddde114beb792caeaafeeeb16b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 11:06:45 +0400 Subject: [PATCH 15/35] chore(1.0.1): bump dependencies --- contactos_platform_interface/pubspec.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/contactos_platform_interface/pubspec.yaml b/contactos_platform_interface/pubspec.yaml index 1bfb754..50715b6 100644 --- a/contactos_platform_interface/pubspec.yaml +++ b/contactos_platform_interface/pubspec.yaml @@ -10,7 +10,6 @@ issue_tracker: https://github.com/ziqq/contactos/issues?q=is%3Aissue+is%3Aopen+l funding: - https://www.buymeacoffee.com/ziqq - - https://www.patreon.com/ziqq - https://boosty.to/ziqq topics: @@ -25,11 +24,11 @@ environment: dependencies: flutter: sdk: flutter - plugin_platform_interface: ^2.1.7 + plugin_platform_interface: ^2.1.8 # Utilities - collection: '>=1.19.0 <2.0.0' - meta: '>=1.15.0 <2.0.0' + collection: '^1.16.0' + meta: '^1.16.0' dev_dependencies: From f06517de382976b6f51866e3066df062e3d2c29b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 11:09:46 +0400 Subject: [PATCH 16/35] chore(1.0.2): update `CHANGELOG` --- contactos_platform_interface/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contactos_platform_interface/CHANGELOG.md b/contactos_platform_interface/CHANGELOG.md index 3a90c72..b700c2a 100644 --- a/contactos_platform_interface/CHANGELOG.md +++ b/contactos_platform_interface/CHANGELOG.md @@ -1,5 +1,7 @@ ## 1.0.1 - **ADDED**: `LICENSE` file to the package +- **ADDED**: Tests +- **CHANGED**: Bump dependencies ## 1.0.0 - **ADDED**: Initial release \ No newline at end of file From 1c95521b831052931b8072d64f48aab3cc433cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 11:17:47 +0400 Subject: [PATCH 17/35] feat: added tests --- .../test/contactos_foundation_test.dart | 273 ++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 contactos_foundation/test/contactos_foundation_test.dart diff --git a/contactos_foundation/test/contactos_foundation_test.dart b/contactos_foundation/test/contactos_foundation_test.dart new file mode 100644 index 0000000..3bc3132 --- /dev/null +++ b/contactos_foundation/test/contactos_foundation_test.dart @@ -0,0 +1,273 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:contactos_foundation/contactos_foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ContactosPluginFoundation -', () { + const channel = MethodChannel('github.com/ziqq/contactos'); + final log = []; + + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'getContacts': + case 'getContactsForEmail': + case 'getContactsForPhone': + return [ + {'identifier': 'id', 'displayName': 'Name'} + ]; + case 'openContactForm': + case 'openExistingContact': + return {'identifier': 'id', 'displayName': 'Name'}; + case 'openDeviceContactPicker': + return [ + {'identifier': 'id', 'displayName': 'Name'} + ]; + case 'getAvatar': + return Uint8List.fromList([0, 1, 2]); + default: + return null; + } + }); + log.clear(); + }); + + tearDown(log.clear); + + test('registerWith registers the instance', () { + ContactosPluginFoundation.registerWith(); + expect(ContactosPlatform.instance, isA()); + }); + + group('getContacts -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginFoundation.instance.getContacts( + query: 'query', + withThumbnails: false, + photoHighResolution: false, + orderByGivenName: false, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getContacts', + arguments: { + 'query': 'query', + 'withThumbnails': false, + 'photoHighResolution': false, + 'orderByGivenName': false, + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('getContactsForPhone -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginFoundation.instance.getContactsForPhone( + '123', + withThumbnails: false, + photoHighResolution: false, + orderByGivenName: false, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getContactsForPhone', + arguments: { + 'phone': '123', + 'withThumbnails': false, + 'photoHighResolution': false, + 'orderByGivenName': false, + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('getContactsForEmail -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginFoundation.instance.getContactsForEmail( + 'test@example.com', + withThumbnails: false, + photoHighResolution: false, + orderByGivenName: false, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getContactsForEmail', + arguments: { + 'email': 'test@example.com', + 'withThumbnails': false, + 'photoHighResolution': false, + 'orderByGivenName': false, + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('getAvatar -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginFoundation.instance.getAvatar( + contact, + photoHighRes: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getAvatar', + arguments: { + 'contact': contact.toJson(), + 'identifier': 'id', + 'photoHighResolution': false, + }, + ), + ); + }); + }); + + group('addContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginFoundation.instance.addContact(contact); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'addContact', + arguments: contact.toJson(), + ), + ); + }); + }); + + group('deleteContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginFoundation.instance.deleteContact(contact); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'deleteContact', + arguments: contact.toJson(), + ), + ); + }); + }); + + group('updateContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginFoundation.instance.updateContact(contact); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'updateContact', + arguments: contact.toJson(), + ), + ); + }); + }); + + group('openContactForm -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginFoundation.instance.openContactForm( + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'openContactForm', + arguments: { + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('openExistingContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginFoundation.instance.openExistingContact( + contact, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'openExistingContact', + arguments: { + 'contact': contact.toJson(), + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('openDeviceContactPicker -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginFoundation.instance.openDeviceContactPicker( + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'openDeviceContactPicker', + arguments: { + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + }); +} From 1f7757c057f14f745da4fc740b63ab97fac8da63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 11:17:53 +0400 Subject: [PATCH 18/35] chore: update make --- contactos_foundation/Makefile | 45 ++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/contactos_foundation/Makefile b/contactos_foundation/Makefile index a9ec8a7..0f16681 100644 --- a/contactos_foundation/Makefile +++ b/contactos_foundation/Makefile @@ -4,7 +4,7 @@ PWD :=$(shell pwd) .DEFAULT_GOAL := all .PHONY: all all: ## build pipeline -all: get format test-unit publish-check +all: format check test-unit publish-check .PHONY: ci ci: ## CI build pipeline @@ -36,6 +36,10 @@ format: ## Format code fix: format ## Fix code @fvm dart fix --apply lib +.PHONY: clean-cache +clean-cache: ## Clean the pub cache + @fvm flutter pub cache repair + .PHONY: clean clean: ## Clean flutter @fvm flutter clean @@ -46,16 +50,17 @@ get: ## Get dependencies .PHONY: analyze analyze: get format ## Analyze code - @fvm flutter analyze --fatal-warnings --no-fatal-infos lib/ test/ || (echo "Β―\_(ツ)_/Β― Analyze code error"; exit 1) + @fvm dart analyze --fatal-infos --fatal-warnings || (echo "Β―\_(ツ)_/Β― Analyze code error"; exit 1) .PHONY: check check: analyze ## Check the code + @fvm dart pub global deactivate pana > /dev/null 2>&1 || true @fvm dart pub global activate pana - @pana --json --no-warning --line-length 80 > log.pana.json + @fvm dart pub global run pana --json > log.pana.json || (echo "Β―\_(ツ)_/Β― Pana analysis error"; exit 1) .PHONY: publish-check -publish-check: ## Check the code before publish - @fvm dart pub publish --dry-run +publish-check: check ## Check the code before publish + @fvm dart pub publish --dry-run || (echo "Β―\_(ツ)_/Β― Publish check error"; exit 1) .PHONY: publish publish: ## Publish package @@ -71,5 +76,33 @@ run-genhtml: ## Runs generage coverage html .PHONY: test-unit test-unit: ## Runs unit tests - @fvm flutter test --coverage || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) + @fvm flutter test --coverage test/contactos_foundation_test.dart || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) @genhtml coverage/lcov.info --output=coverage -o coverage/html || (echo "Β―\_(ツ)_/Β― Error while running genhtml with coverage"; exit 2) + +.PHONY: tag +tag: ## Add a tag to the current commit + @fvm dart run tool/tag.dart + +.PHONY: tag-add +tag-add: ## Add TAG. E.g: make tag-add TAG=v1.0.0 + @if [ -z "$(TAG)" ]; then echo "Β―\_(ツ)_/Β― TAG is not set"; exit 1; fi + @echo "" + @echo "START ADDING TAG: $(TAG)" + @echo "" + @git tag $(TAG) + @git push origin $(TAG) + @echo "" + @echo "CREATED AND PUSHED TAG $(TAG)" + @echo "" + +.PHONY: tag-remove +tag-remove: ## Delete TAG. E.g: make tag-delete TAG=v1.0.0 + @if [ -z "$(TAG)" ]; then echo "Β―\_(ツ)_/Β― TAG is not set"; exit 1; fi + @echo "" + @echo "START REMOVING TAG: $(TAG)" + @echo "" + @git tag -d $(TAG) + @git push origin --delete $(TAG) + @echo "" + @echo "DELETED TAG $(TAG) LOCALLY AND REMOTELY" + @echo "" From f862e0e4c80f8646ec132bdd0ac0a8dffa98eb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 11:18:57 +0400 Subject: [PATCH 19/35] chore: swith to remote `contactos_platform_interface` --- contactos_foundation/pubspec.lock | 7 ++++--- contactos_foundation/pubspec.yaml | 9 +++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/contactos_foundation/pubspec.lock b/contactos_foundation/pubspec.lock index 4bffce1..5285f9e 100644 --- a/contactos_foundation/pubspec.lock +++ b/contactos_foundation/pubspec.lock @@ -44,9 +44,10 @@ packages: contactos_platform_interface: dependency: "direct main" description: - path: "../contactos_platform_interface" - relative: true - source: path + name: contactos_platform_interface + sha256: "6fc797fa685c84ef7e1ec8fc8c6aae1f9f09274bcc5ea26e435781a4fa907628" + url: "https://pub.dev" + source: hosted version: "1.0.1" fake_async: dependency: transitive diff --git a/contactos_foundation/pubspec.yaml b/contactos_foundation/pubspec.yaml index bad5b83..f38d3b6 100644 --- a/contactos_foundation/pubspec.yaml +++ b/contactos_foundation/pubspec.yaml @@ -1,7 +1,8 @@ name: contactos_foundation description: Android implementation of the contactos plugin. version: 0.0.1 -publish_to: none + +# publish_to: none homepage: https://github.com/ziqq/contactos/contactos_foundation @@ -30,9 +31,9 @@ dependencies: flutter: sdk: flutter - # contactos_platform_interface: ^1.0.0 - contactos_platform_interface: - path: ../contactos_platform_interface + contactos_platform_interface: ^1.0.1 + # contactos_platform_interface: + # path: ../contactos_platform_interface dev_dependencies: From 3e515627182c6fb88de956845808fc0c9d919126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 11:21:19 +0400 Subject: [PATCH 20/35] chore: update --- contactos_foundation/README.md | 2 +- contactos_foundation/example/pubspec.lock | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/contactos_foundation/README.md b/contactos_foundation/README.md index e3f94a2..cf92b57 100644 --- a/contactos_foundation/README.md +++ b/contactos_foundation/README.md @@ -2,7 +2,7 @@ [![Don't depend on me](https://img.shields.io/badge/platform-internal-lightgrey.svg)](https://github.com/ziqq/contactos) -This package is the Darwin (iOS and macOS) implementation of the [`contactos`](https://github.com/ziqq/contactos/tree/main/contactos) plugin. +This package is the Darwin (iOS and ~~macOS~~) implementation of the [`contactos`](https://github.com/ziqq/contactos/tree/main/contactos) plugin. ## Usage diff --git a/contactos_foundation/example/pubspec.lock b/contactos_foundation/example/pubspec.lock index 54cd7df..c08500a 100644 --- a/contactos_foundation/example/pubspec.lock +++ b/contactos_foundation/example/pubspec.lock @@ -51,10 +51,11 @@ packages: contactos_platform_interface: dependency: transitive description: - path: "../../contactos_platform_interface" - relative: true - source: path - version: "1.0.0" + name: contactos_platform_interface + sha256: "6fc797fa685c84ef7e1ec8fc8c6aae1f9f09274bcc5ea26e435781a4fa907628" + url: "https://pub.dev" + source: hosted + version: "1.0.1" cupertino_icons: dependency: "direct main" description: From 05599e96d4b51b0d081c62ca54495b0edfd73598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 11:27:42 +0400 Subject: [PATCH 21/35] chore: update make --- contactos_android/Makefile | 45 +++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/contactos_android/Makefile b/contactos_android/Makefile index a9ec8a7..bb2b44c 100644 --- a/contactos_android/Makefile +++ b/contactos_android/Makefile @@ -4,7 +4,7 @@ PWD :=$(shell pwd) .DEFAULT_GOAL := all .PHONY: all all: ## build pipeline -all: get format test-unit publish-check +all: format check test-unit publish-check .PHONY: ci ci: ## CI build pipeline @@ -36,6 +36,10 @@ format: ## Format code fix: format ## Fix code @fvm dart fix --apply lib +.PHONY: clean-cache +clean-cache: ## Clean the pub cache + @fvm flutter pub cache repair + .PHONY: clean clean: ## Clean flutter @fvm flutter clean @@ -46,16 +50,17 @@ get: ## Get dependencies .PHONY: analyze analyze: get format ## Analyze code - @fvm flutter analyze --fatal-warnings --no-fatal-infos lib/ test/ || (echo "Β―\_(ツ)_/Β― Analyze code error"; exit 1) + @fvm dart analyze --fatal-infos --fatal-warnings || (echo "Β―\_(ツ)_/Β― Analyze code error"; exit 1) .PHONY: check check: analyze ## Check the code + @fvm dart pub global deactivate pana > /dev/null 2>&1 || true @fvm dart pub global activate pana - @pana --json --no-warning --line-length 80 > log.pana.json + @fvm dart pub global run pana --json > log.pana.json || (echo "Β―\_(ツ)_/Β― Pana analysis error"; exit 1) .PHONY: publish-check -publish-check: ## Check the code before publish - @fvm dart pub publish --dry-run +publish-check: check ## Check the code before publish + @fvm dart pub publish --dry-run || (echo "Β―\_(ツ)_/Β― Publish check error"; exit 1) .PHONY: publish publish: ## Publish package @@ -71,5 +76,33 @@ run-genhtml: ## Runs generage coverage html .PHONY: test-unit test-unit: ## Runs unit tests - @fvm flutter test --coverage || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) + @fvm flutter test --coverage test/contactos_android_test.dart || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) @genhtml coverage/lcov.info --output=coverage -o coverage/html || (echo "Β―\_(ツ)_/Β― Error while running genhtml with coverage"; exit 2) + +.PHONY: tag +tag: ## Add a tag to the current commit + @fvm dart run tool/tag.dart + +.PHONY: tag-add +tag-add: ## Add TAG. E.g: make tag-add TAG=v1.0.0 + @if [ -z "$(TAG)" ]; then echo "Β―\_(ツ)_/Β― TAG is not set"; exit 1; fi + @echo "" + @echo "START ADDING TAG: $(TAG)" + @echo "" + @git tag $(TAG) + @git push origin $(TAG) + @echo "" + @echo "CREATED AND PUSHED TAG $(TAG)" + @echo "" + +.PHONY: tag-remove +tag-remove: ## Delete TAG. E.g: make tag-delete TAG=v1.0.0 + @if [ -z "$(TAG)" ]; then echo "Β―\_(ツ)_/Β― TAG is not set"; exit 1; fi + @echo "" + @echo "START REMOVING TAG: $(TAG)" + @echo "" + @git tag -d $(TAG) + @git push origin --delete $(TAG) + @echo "" + @echo "DELETED TAG $(TAG) LOCALLY AND REMOTELY" + @echo "" From e99c375b4e677ed15a16ad788ec99eaea69faaaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 11:27:50 +0400 Subject: [PATCH 22/35] feat: added tests --- .../test/contactos_android_test.dart | 273 ++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 contactos_android/test/contactos_android_test.dart diff --git a/contactos_android/test/contactos_android_test.dart b/contactos_android/test/contactos_android_test.dart new file mode 100644 index 0000000..ebe7cf3 --- /dev/null +++ b/contactos_android/test/contactos_android_test.dart @@ -0,0 +1,273 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:contactos_android/contactos_android.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ContactosPluginAndroid -', () { + const channel = MethodChannel('github.com/ziqq/contactos'); + final log = []; + + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'getContacts': + case 'getContactsForEmail': + case 'getContactsForPhone': + return [ + {'identifier': 'id', 'displayName': 'Name'} + ]; + case 'openContactForm': + case 'openExistingContact': + return {'identifier': 'id', 'displayName': 'Name'}; + case 'openDeviceContactPicker': + return [ + {'identifier': 'id', 'displayName': 'Name'} + ]; + case 'getAvatar': + return Uint8List.fromList([0, 1, 2]); + default: + return null; + } + }); + log.clear(); + }); + + tearDown(log.clear); + + test('registerWith registers the instance', () { + ContactosPluginAndroid.registerWith(); + expect(ContactosPlatform.instance, isA()); + }); + + group('getContacts -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginAndroid.instance.getContacts( + query: 'query', + withThumbnails: false, + photoHighResolution: false, + orderByGivenName: false, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getContacts', + arguments: { + 'query': 'query', + 'withThumbnails': false, + 'photoHighResolution': false, + 'orderByGivenName': false, + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('getContactsForPhone -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginAndroid.instance.getContactsForPhone( + '123', + withThumbnails: false, + photoHighResolution: false, + orderByGivenName: false, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getContactsForPhone', + arguments: { + 'phone': '123', + 'withThumbnails': false, + 'photoHighResolution': false, + 'orderByGivenName': false, + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('getContactsForEmail -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginAndroid.instance.getContactsForEmail( + 'test@example.com', + withThumbnails: false, + photoHighResolution: false, + orderByGivenName: false, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getContactsForEmail', + arguments: { + 'email': 'test@example.com', + 'withThumbnails': false, + 'photoHighResolution': false, + 'orderByGivenName': false, + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('getAvatar -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginAndroid.instance.getAvatar( + contact, + photoHighRes: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getAvatar', + arguments: { + 'contact': contact.toJson(), + 'identifier': 'id', + 'photoHighResolution': false, + }, + ), + ); + }); + }); + + group('addContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginAndroid.instance.addContact(contact); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'addContact', + arguments: contact.toJson(), + ), + ); + }); + }); + + group('deleteContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginAndroid.instance.deleteContact(contact); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'deleteContact', + arguments: contact.toJson(), + ), + ); + }); + }); + + group('updateContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginAndroid.instance.updateContact(contact); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'updateContact', + arguments: contact.toJson(), + ), + ); + }); + }); + + group('openContactForm -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginAndroid.instance.openContactForm( + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'openContactForm', + arguments: { + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('openExistingContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginAndroid.instance.openExistingContact( + contact, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'openExistingContact', + arguments: { + 'contact': contact.toJson(), + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('openDeviceContactPicker -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginAndroid.instance.openDeviceContactPicker( + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'openDeviceContactPicker', + arguments: { + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + }); +} From 08d0167db3684c789b16a8aa673cb6745be67326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 11:30:51 +0400 Subject: [PATCH 23/35] feat: add `README` --- contactos_android/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/contactos_android/README.md b/contactos_android/README.md index e69de29..7ef9eeb 100644 --- a/contactos_android/README.md +++ b/contactos_android/README.md @@ -0,0 +1,15 @@ +# Contactos Android + +[![Don't depend on me](https://img.shields.io/badge/platform-internal-lightgrey.svg)](https://github.com/ziqq/contactos) + +This package is the Android implementation of the [`contactos`](https://github.com/ziqq/contactos/tree/main/contactos) plugin. + +## Usage + +This package is not meant to be used directly. It is automatically included in your app when you depend on the main [`contactos`](https://github.com/ziqq/contactos/tree/main/contactos) package. + +For more information on how to use the `contactos` plugin, please see the [main package's README](https://github.com/ziqq/contactos/blob/main/contactos/README.md). + +## Contributing + +This is part of a federated plugin. Contributions to the platform-specific implementations are welcome. Please see the [main repository](https://github.com/ziqq/contactos) for more information on how to contribute. From f29db4ed3a7b59898446e391ca4320b5da6e7e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 11:32:56 +0400 Subject: [PATCH 24/35] chore: update --- contactos_android/pubspec.lock | 7 ++++--- contactos_android/pubspec.yaml | 9 +++++---- contactos_foundation/pubspec.yaml | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/contactos_android/pubspec.lock b/contactos_android/pubspec.lock index 4bffce1..5285f9e 100644 --- a/contactos_android/pubspec.lock +++ b/contactos_android/pubspec.lock @@ -44,9 +44,10 @@ packages: contactos_platform_interface: dependency: "direct main" description: - path: "../contactos_platform_interface" - relative: true - source: path + name: contactos_platform_interface + sha256: "6fc797fa685c84ef7e1ec8fc8c6aae1f9f09274bcc5ea26e435781a4fa907628" + url: "https://pub.dev" + source: hosted version: "1.0.1" fake_async: dependency: transitive diff --git a/contactos_android/pubspec.yaml b/contactos_android/pubspec.yaml index b0676b5..4157d90 100644 --- a/contactos_android/pubspec.yaml +++ b/contactos_android/pubspec.yaml @@ -1,7 +1,8 @@ name: contactos_android description: Android implementation of the contactos plugin. version: 0.0.1 -publish_to: none + +# publish_to: none homepage: https://github.com/ziqq/contactos/contactos_android @@ -30,9 +31,9 @@ dependencies: flutter: sdk: flutter - # contactos_platform_interface: ^1.0.1 - contactos_platform_interface: - path: ../contactos_platform_interface + contactos_platform_interface: ^1.0.1 + # contactos_platform_interface: + # path: ../contactos_platform_interface dev_dependencies: diff --git a/contactos_foundation/pubspec.yaml b/contactos_foundation/pubspec.yaml index f38d3b6..411ba12 100644 --- a/contactos_foundation/pubspec.yaml +++ b/contactos_foundation/pubspec.yaml @@ -1,6 +1,6 @@ name: contactos_foundation -description: Android implementation of the contactos plugin. -version: 0.0.1 +description: iOS implementation of the contactos plugin. +version: 0.0.2 # publish_to: none From 99c2ee55c625f164eec3e152f387ba0ed3e7cee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 11:34:09 +0400 Subject: [PATCH 25/35] chore: update --- contactos_foundation/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contactos_foundation/CHANGELOG.md b/contactos_foundation/CHANGELOG.md index 1e64072..a471847 100644 --- a/contactos_foundation/CHANGELOG.md +++ b/contactos_foundation/CHANGELOG.md @@ -1,2 +1,5 @@ +## 0.0.2 +- **CHANGED**: Package metadata + ## 0.0.1 - **ADDED**: Initial release \ No newline at end of file From 14121d9a7e7f7545382011f538e420f44e44f797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 11:37:01 +0400 Subject: [PATCH 26/35] chore: update example --- contactos_foundation/example/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contactos_foundation/example/pubspec.lock b/contactos_foundation/example/pubspec.lock index c08500a..ad961d7 100644 --- a/contactos_foundation/example/pubspec.lock +++ b/contactos_foundation/example/pubspec.lock @@ -47,7 +47,7 @@ packages: path: ".." relative: true source: path - version: "0.0.1" + version: "0.0.2" contactos_platform_interface: dependency: transitive description: From 3e06f54cc78a8cb7ef0f93b932e1448653737245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 11:49:50 +0400 Subject: [PATCH 27/35] chore: update example --- contactos_android/example/pubspec.lock | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contactos_android/example/pubspec.lock b/contactos_android/example/pubspec.lock index 8ebdf41..5ae71a0 100644 --- a/contactos_android/example/pubspec.lock +++ b/contactos_android/example/pubspec.lock @@ -51,9 +51,10 @@ packages: contactos_platform_interface: dependency: transitive description: - path: "../../contactos_platform_interface" - relative: true - source: path + name: contactos_platform_interface + sha256: "6fc797fa685c84ef7e1ec8fc8c6aae1f9f09274bcc5ea26e435781a4fa907628" + url: "https://pub.dev" + source: hosted version: "1.0.1" cupertino_icons: dependency: "direct main" From bd536e4c98c62b410fc43cb0f73add9e4df0224a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 12:41:29 +0400 Subject: [PATCH 28/35] feat(2.0.0): Migrate to platform implementations --- .../workflows/checkout_platform_interface.yml | 171 --- .github/workflows/publish.yml | 160 ++- contactos/CHANGELOG.md | 3 + contactos/Makefile | 11 +- contactos/README.md | 5 +- contactos/android/.gitignore | 7 - contactos/android/build.gradle | 58 - contactos/android/gradle.properties | 1 - .../gradle/wrapper/gradle-wrapper.properties | 6 - contactos/android/settings.gradle | 1 - .../android/src/main/AndroidManifest.xml | 15 - .../flutter/plugins/contactos/Contact.java | 109 -- .../plugins/contactos/ContactosPlugin.java | 1003 ----------------- .../java/flutter/plugins/contactos/Item.java | 97 -- .../plugins/contactos/PostalAddress.java | 73 -- .../contactsservice/ContactTest.java | 100 -- contactos/example/pubspec.lock | 22 +- contactos/ios/contactos.podspec | 29 - contactos/ios/contactos/Package.swift | 28 - .../Sources/contactos/ContactosPlugin.h | 4 - .../Sources/contactos/ContactosPlugin.m | 13 - .../Sources/contactos/ContactosPlugin.swift | 636 ----------- .../contactos/Resources/PrivacyInfo.xcprivacy | 25 - contactos/lib/src/contactos.dart | 57 +- contactos/lib/src/contactos_legacy.dart | 139 +++ contactos/pubspec.lock | 85 +- contactos/pubspec.yaml | 65 +- contactos/test/contactos_test.dart | 331 +----- contactos/test/src/contactos_legacy_test.dart | 319 ++++++ contactos/test/src/contactos_test.dart | 221 ++++ contactos_android/tool/tag.dart | 76 ++ contactos_foundation/tool/tag.dart | 76 ++ 32 files changed, 1177 insertions(+), 2769 deletions(-) delete mode 100644 .github/workflows/checkout_platform_interface.yml delete mode 100644 contactos/android/.gitignore delete mode 100644 contactos/android/build.gradle delete mode 100644 contactos/android/gradle.properties delete mode 100644 contactos/android/gradle/wrapper/gradle-wrapper.properties delete mode 100644 contactos/android/settings.gradle delete mode 100644 contactos/android/src/main/AndroidManifest.xml delete mode 100644 contactos/android/src/main/java/flutter/plugins/contactos/Contact.java delete mode 100644 contactos/android/src/main/java/flutter/plugins/contactos/ContactosPlugin.java delete mode 100644 contactos/android/src/main/java/flutter/plugins/contactos/Item.java delete mode 100644 contactos/android/src/main/java/flutter/plugins/contactos/PostalAddress.java delete mode 100644 contactos/android/src/test/java/flutter/plugins/contactsservice/contactsservice/ContactTest.java delete mode 100644 contactos/ios/contactos.podspec delete mode 100644 contactos/ios/contactos/Package.swift delete mode 100644 contactos/ios/contactos/Sources/contactos/ContactosPlugin.h delete mode 100644 contactos/ios/contactos/Sources/contactos/ContactosPlugin.m delete mode 100644 contactos/ios/contactos/Sources/contactos/ContactosPlugin.swift delete mode 100644 contactos/ios/contactos/Sources/contactos/Resources/PrivacyInfo.xcprivacy create mode 100644 contactos/lib/src/contactos_legacy.dart create mode 100644 contactos/test/src/contactos_legacy_test.dart create mode 100644 contactos/test/src/contactos_test.dart create mode 100644 contactos_android/tool/tag.dart create mode 100644 contactos_foundation/tool/tag.dart diff --git a/.github/workflows/checkout_platform_interface.yml b/.github/workflows/checkout_platform_interface.yml deleted file mode 100644 index 348e94d..0000000 --- a/.github/workflows/checkout_platform_interface.yml +++ /dev/null @@ -1,171 +0,0 @@ -name: "Checkout platform interface" - - -on: - workflow_call: - - -jobs: - checkout: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./contactos_platform_interface/ - env: - pub-cache-name: pub - threshold: 50 - timeout-minutes: 15 - steps: - - name: πŸš‚ Get latest code - id: checkout - uses: actions/checkout@v4 - with: - sparse-checkout: | - .github - pubspec.yaml - lib - test - analysis_options.yaml - CHANGELOG.md - - - name: πŸ‘· Install flutter - uses: subosito/flutter-action@v2 - id: install-flutter - with: - channel: 'stable' - - - name: πŸ”Ž Check flutter version - id: check-flutter-version - run: flutter --version - - - name: πŸ“€ Restore Pub modules - id: cache-pub-restore - uses: actions/cache/restore@v4 - with: - path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} - - - name: πŸ—„οΈ Export Pub cache directory - id: export-pub-cache - timeout-minutes: 1 - run: | - export PUB_CACHE=$PWD/.pub_cache/ - export PATH="$PATH":"$HOME/.pub-cache/bin" - echo "${HOME}/.pub-cache/bin" >> $GITHUB_PATH - - - name: πŸ‘· Install Dependencies - id: install-dependencies - timeout-minutes: 2 - run: | - apt-get update && apt-get install -y lcov - flutter pub get --no-example - - - name: πŸ“₯ Save Pub modules - id: cache-pub-save - uses: actions/cache/save@v4 - with: - path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} - - - name: πŸ”Ž Check content - id: check_files - uses: andstor/file-existence-action@v1 - with: - files: "README.md, CHANGELOG.md, LICENSE" - - - name: ❔ File exists - if: steps.check_files.outputs.files_exists == 'true' - run: echo Content is ok! - - - name: πŸ”Ž Check description - run: echo | grep -q Description README.md ; echo $? - - - name: πŸ‘· Install dependencies - run: flutter pub get - - - name: πŸ§ͺ Run dependency validator - run: | - dart pub global activate dependency_validator - dart pub global run dependency_validator:dependency_validator - - - name: πŸ”Ž Check format - id: check_format - run: dart format --set-exit-if-changed -l 80 -o none lib/ - - - name: πŸ“ˆ Check analyzer - id: check_analyzer - run: dart analyze --fatal-infos --fatal-warnings lib/ - - testing: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./contactos_platform_interface/ - env: - pub-cache-name: pub - threshold: 50 - timeout-minutes: 15 - steps: - - name: πŸš‚ Get latest code - id: checkout - uses: actions/checkout@v4 - with: - sparse-checkout: | - .github - pubspec.yaml - lib - test - analysis_options.yaml - CHANGELOG.md - - - name: πŸ‘· Install flutter - uses: subosito/flutter-action@v2 - id: install-flutter - with: - channel: 'stable' - - - name: πŸ”Ž Check flutter version - id: check-flutter-version - run: flutter --version - - - name: πŸ“€ Restore Pub modules - id: cache-pub-restore - uses: actions/cache/restore@v4 - with: - path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} - - - name: πŸ—„οΈ Export Pub cache directory - id: export-pub-cache - timeout-minutes: 1 - run: | - export PUB_CACHE=$PWD/.pub_cache/ - export PATH="$PATH":"$HOME/.pub-cache/bin" - echo "${HOME}/.pub-cache/bin" >> $GITHUB_PATH - - - name: πŸ‘· Install Dependencies - id: install-dependencies - timeout-minutes: 2 - run: | - apt-get update && apt-get install -y lcov - flutter pub get --no-example - - - name: πŸ“₯ Save Pub modules - id: cache-pub-save - uses: actions/cache/save@v4 - with: - path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} - - - name: πŸ§ͺ Run tests - timeout-minutes: 2 - env: - CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} - if: success() - run: | - flutter test --coverage - bash <(curl -s https://codecov.io/bash) -f coverage/lcov.info \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 92c10cf..609ffb9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,16 +2,160 @@ name: Publish to pub.dev on: workflow_dispatch: + inputs: + packages: + description: "Packages to publish (comma-separated: android,ios,interface,main)" + required: false + default: "android,ios,interface,main" push: tags: - "v[0-9]+.[0-9]+.[0-9]+*" + - "contactos-v[0-9]+.[0-9]+.[0-9]+*" + - "contactos-android-v[0-9]+.[0-9]+.[0-9]+*" + - "contactos-foundation-v[0-9]+.[0-9]+.[0-9]+*" + - "contactos-platform-interface-v[0-9]+.[0-9]+.[0-9]+*" + +permissions: + contents: read + id-token: write # Required for authentication with Google Cloud/Pub jobs: - publish: - uses: ziqq/flutter_ci_workflows/.github/workflows/publish_to_pub.yml@main - with: - PANA_TOTAL: '110' - secrets: - PUB_CREDENTIAL_JSON: ${{ secrets.PUB_CREDENTIAL_JSON }} - PUB_OAUTH_ACCESS_TOKEN: ${{ secrets.PUB_OAUTH_ACCESS_TOKEN }} - PUB_OAUTH_REFRESH_TOKEN: ${{ secrets.PUB_OAUTH_REFRESH_TOKEN }} \ No newline at end of file + prepare: + name: "Prepare publish configuration" + runs-on: ubuntu-latest + outputs: + publish_android: ${{ steps.determine_packages.outputs.publish_android }} + publish_foundation: ${{ steps.determine_packages.outputs.publish_foundation }} + publish_interface: ${{ steps.determine_packages.outputs.publish_interface }} + publish_main: ${{ steps.determine_packages.outputs.publish_main }} + steps: + - name: Determine packages to publish + id: determine_packages + shell: bash + run: | + PACKAGES="" + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + if [[ "${{ github.event.inputs.packages }}" != "" ]]; then + PACKAGES="${{ github.event.inputs.packages }}" + else + PACKAGES="android,ios,interface,main" + fi + elif [[ "${{ github.ref }}" =~ ^refs/tags/(.*)-v.*$ ]]; then + PACKAGE_NAME="${BASH_REMATCH[1]}" + if [[ "$PACKAGE_NAME" == "contactos" ]]; then + PACKAGES="main" + elif [[ "$PACKAGE_NAME" == "contactos_android" ]]; then + PACKAGES="android" + elif [[ "$PACKAGE_NAME" == "contactos_foundation" ]]; then + PACKAGES="ios" + elif [[ "$PACKAGE_NAME" == "contactos_platform_interface" ]]; then + PACKAGES="interface" + fi + fi + + echo "Selected packages: $PACKAGES" + + if [[ "$PACKAGES" == *"android"* ]]; then echo "publish_android=true" >> $GITHUB_OUTPUT; else echo "publish_android=false" >> $GITHUB_OUTPUT; fi + if [[ "$PACKAGES" == *"ios"* ]]; then echo "publish_foundation=true" >> $GITHUB_OUTPUT; else echo "publish_foundation=false" >> $GITHUB_OUTPUT; fi + if [[ "$PACKAGES" == *"interface"* ]]; then echo "publish_interface=true" >> $GITHUB_OUTPUT; else echo "publish_interface=false" >> $GITHUB_OUTPUT; fi + if [[ "$PACKAGES" == *"main"* ]]; then echo "publish_main=true" >> $GITHUB_OUTPUT; else echo "publish_main=false" >> $GITHUB_OUTPUT; fi + + publish_interface: + needs: prepare + if: needs.prepare.outputs.publish_interface == 'true' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - name: Setup Pub Credentials + shell: bash + env: + PUB_CREDENTIAL_JSON: ${{ secrets.PUB_CREDENTIAL_JSON }} + run: | + mkdir -p ~/.config/dart + echo "$PUB_CREDENTIAL_JSON" > ~/.config/dart/pub-credentials.json + - name: Publish + working-directory: contactos_platform_interface + run: | + flutter pub get + dart pub publish --force + + publish_foundation: + needs: prepare + if: needs.prepare.outputs.publish_foundation == 'true' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - name: Setup Pub Credentials + shell: bash + env: + PUB_CREDENTIAL_JSON: ${{ secrets.PUB_CREDENTIAL_JSON }} + run: | + mkdir -p ~/.config/dart + echo "$PUB_CREDENTIAL_JSON" > ~/.config/dart/pub-credentials.json + - name: Publish + working-directory: contactos_foundation + run: | + flutter pub get + dart pub publish --force + + publish_android: + needs: prepare + if: needs.prepare.outputs.publish_android == 'true' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - name: Setup Pub Credentials + shell: bash + env: + PUB_CREDENTIAL_JSON: ${{ secrets.PUB_CREDENTIAL_JSON }} + run: | + mkdir -p ~/.config/dart + echo "$PUB_CREDENTIAL_JSON" > ~/.config/dart/pub-credentials.json + - name: Publish + working-directory: contactos_android + run: | + flutter pub get + dart pub publish --force + + publish_main: + needs: prepare + if: needs.prepare.outputs.publish_main == 'true' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - name: Setup Pub Credentials + shell: bash + env: + PUB_CREDENTIAL_JSON: ${{ secrets.PUB_CREDENTIAL_JSON }} + run: | + mkdir -p ~/.config/dart + echo "$PUB_CREDENTIAL_JSON" > ~/.config/dart/pub-credentials.json + - name: Publish + working-directory: contactos + run: | + flutter pub get + dart pub publish --force diff --git a/contactos/CHANGELOG.md b/contactos/CHANGELOG.md index 6b708fd..6f3baf2 100644 --- a/contactos/CHANGELOG.md +++ b/contactos/CHANGELOG.md @@ -1,3 +1,6 @@ +## 2.0.0-pre.1 +- **CHANGED**: Migrate to platform implementations + ## 1.0.1 - 1.0.6 - **CHANGED**: Refactoring - **FIXED**: Syntax error diff --git a/contactos/Makefile b/contactos/Makefile index d432654..e731423 100644 --- a/contactos/Makefile +++ b/contactos/Makefile @@ -46,20 +46,21 @@ clean: ## Clean flutter .PHONY: get get: ## Get dependencies - @fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos dependencies error"; exit 1) + @fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos_platform_interface dependencies error"; exit 1) .PHONY: analyze analyze: get format ## Analyze code - @fvm dart analyze --fatal-infos --fatal-warnings + @fvm dart analyze --fatal-infos --fatal-warnings || (echo "Β―\_(ツ)_/Β― Analyze code error"; exit 1) .PHONY: check check: analyze ## Check the code + @fvm dart pub global deactivate pana > /dev/null 2>&1 || true @fvm dart pub global activate pana - @pana --json --no-warning --line-length 80 > log.pana.json + @fvm dart pub global run pana --json > log.pana.json || (echo "Β―\_(ツ)_/Β― Pana analysis error"; exit 1) .PHONY: publish-check publish-check: check ## Check the code before publish - @fvm dart pub publish --dry-run + @fvm dart pub publish --dry-run || (echo "Β―\_(ツ)_/Β― Publish check error"; exit 1) .PHONY: publish publish: ## Publish package @@ -75,7 +76,7 @@ run-genhtml: ## Runs generage coverage html .PHONY: test-unit test-unit: ## Runs unit tests - @fvm flutter test --coverage || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) + @fvm flutter test --coverage test/contactos_test.dart || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) @genhtml coverage/lcov.info --output=coverage -o coverage/html || (echo "Β―\_(ツ)_/Β― Error while running genhtml with coverage"; exit 2) .PHONY: tag diff --git a/contactos/README.md b/contactos/README.md index 1a21d75..75c30ff 100644 --- a/contactos/README.md +++ b/contactos/README.md @@ -18,7 +18,7 @@ To use this plugin, add `contactos` as a [dependency in your `pubspec.yaml` file For example: ```yaml dependencies: - contactos: ^1.0.0 + contactos: ``` @@ -121,7 +121,7 @@ List postalAddresses = []; Uint8List avatar; ``` -![Example](https://raw.githubusercontent.com/ziqq/contactos/refs/heads/main/.docs/example.gif "Example screenshot") +![Example](https://raw.githubusercontent.com/ziqq/contactos/refs/heads/main/.github/images/example.gif "Example screenshot") ## Changelog @@ -142,7 +142,6 @@ Refer to the [Changelog](https://github.com/ziqq/contactos/blob/main/CHANGELOG.m ## Contributions Contributions are welcome! If you find a bug or want a feature, please fill an issue. - If you want to contribute code please create a pull request under the master branch. diff --git a/contactos/android/.gitignore b/contactos/android/.gitignore deleted file mode 100644 index 733bfea..0000000 --- a/contactos/android/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java \ No newline at end of file diff --git a/contactos/android/build.gradle b/contactos/android/build.gradle deleted file mode 100644 index 960b83c..0000000 --- a/contactos/android/build.gradle +++ /dev/null @@ -1,58 +0,0 @@ -plugins { - id "com.android.library" - id "kotlin-android" -} - -def localProperties = new Properties() -def localPropertiesFile = rootProject.file("local.properties") -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader("UTF-8") { reader -> - localProperties.load(reader) - } -} - -def flutterVersionCode = localProperties.getProperty("flutter.versionCode") -if (flutterVersionCode == null) { - flutterVersionCode = "1" -} - -def flutterVersionName = localProperties.getProperty("flutter.versionName") -if (flutterVersionName == null) { - flutterVersionName = "1.0" -} - -android { - namespace 'flutter.plugins.contactos' - compileSdkVersion flutter.compileSdkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - minSdkVersion flutter.minSdkVersion - minSdkVersion flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } -} - -dependencies { - testImplementation 'junit:junit:4.12' - testImplementation 'com.google.truth:truth:1.0' -} - -flutter { - source = "../.." -} \ No newline at end of file diff --git a/contactos/android/gradle.properties b/contactos/android/gradle.properties deleted file mode 100644 index 8bd86f6..0000000 --- a/contactos/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/contactos/android/gradle/wrapper/gradle-wrapper.properties b/contactos/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index af2445b..0000000 --- a/contactos/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Sat Apr 10 22:57:03 IST 2021 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip diff --git a/contactos/android/settings.gradle b/contactos/android/settings.gradle deleted file mode 100644 index 438f72d..0000000 --- a/contactos/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'contactos' diff --git a/contactos/android/src/main/AndroidManifest.xml b/contactos/android/src/main/AndroidManifest.xml deleted file mode 100644 index a4f006c..0000000 --- a/contactos/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/contactos/android/src/main/java/flutter/plugins/contactos/Contact.java b/contactos/android/src/main/java/flutter/plugins/contactos/Contact.java deleted file mode 100644 index fcb9175..0000000 --- a/contactos/android/src/main/java/flutter/plugins/contactos/Contact.java +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2025 Anton Ustinoff. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package flutter.plugins.contactos; - -import java.util.ArrayList; -import java.util.HashMap; - -public class Contact implements Comparable { - - Contact(String id) { - this.identifier = id; - } - - private Contact() { - } - - String identifier; - String displayName, givenName, middleName, familyName, prefix, suffix, company, jobTitle, note, birthday, androidAccountType, androidAccountName; - ArrayList emails = new ArrayList<>(); - ArrayList phones = new ArrayList<>(); - ArrayList postalAddresses = new ArrayList<>(); - byte[] avatar = new byte[0]; - - HashMap toMap() { - HashMap contactMap = new HashMap<>(); - contactMap.put("identifier", identifier); - contactMap.put("displayName", displayName); - contactMap.put("givenName", givenName); - contactMap.put("middleName", middleName); - contactMap.put("familyName", familyName); - contactMap.put("prefix", prefix); - contactMap.put("suffix", suffix); - contactMap.put("company", company); - contactMap.put("jobTitle", jobTitle); - contactMap.put("avatar", avatar); - contactMap.put("note", note); - contactMap.put("birthday", birthday); - contactMap.put("androidAccountType", androidAccountType); - contactMap.put("androidAccountName", androidAccountName); - - ArrayList> emailsMap = new ArrayList<>(); - for (Item email : emails) { - emailsMap.add(email.toMap()); - } - contactMap.put("emails", emailsMap); - - ArrayList> phonesMap = new ArrayList<>(); - for (Item phone : phones) { - phonesMap.add(phone.toMap()); - } - contactMap.put("phones", phonesMap); - - ArrayList> addressesMap = new ArrayList<>(); - for (PostalAddress address : postalAddresses) { - addressesMap.add(address.toMap()); - } - contactMap.put("postalAddresses", addressesMap); - - return contactMap; - } - - @SuppressWarnings("unchecked") - static Contact fromMap(HashMap map) { - Contact contact = new Contact(); - contact.identifier = (String) map.get("identifier"); - contact.givenName = (String) map.get("givenName"); - contact.middleName = (String) map.get("middleName"); - contact.familyName = (String) map.get("familyName"); - contact.prefix = (String) map.get("prefix"); - contact.suffix = (String) map.get("suffix"); - contact.company = (String) map.get("company"); - contact.jobTitle = (String) map.get("jobTitle"); - contact.avatar = (byte[]) map.get("avatar"); - contact.note = (String) map.get("note"); - contact.birthday = (String) map.get("birthday"); - contact.androidAccountType = (String) map.get("androidAccountType"); - contact.androidAccountName = (String) map.get("androidAccountName"); - - ArrayList emails = (ArrayList) map.get("emails"); - if (emails != null) { - for (HashMap email : emails) { - contact.emails.add(Item.fromMap(email)); - } - } - ArrayList phones = (ArrayList) map.get("phones"); - if (phones != null) { - for (HashMap phone : phones) { - contact.phones.add(Item.fromMap(phone)); - } - } - ArrayList postalAddresses = (ArrayList) map.get("postalAddresses"); - if (postalAddresses != null) { - for (HashMap postalAddress : postalAddresses) { - contact.postalAddresses.add(PostalAddress.fromMap(postalAddress)); - } - } - return contact; - } - - @Override - public int compareTo(Contact contact) { - String givenName1 = this.givenName == null ? "" : this.givenName.toLowerCase(); - String givenName2 = contact == null ? "" - : (contact.givenName == null ? "" : contact.givenName.toLowerCase()); - return givenName1.compareTo(givenName2); - } -} \ No newline at end of file diff --git a/contactos/android/src/main/java/flutter/plugins/contactos/ContactosPlugin.java b/contactos/android/src/main/java/flutter/plugins/contactos/ContactosPlugin.java deleted file mode 100644 index c6cee3e..0000000 --- a/contactos/android/src/main/java/flutter/plugins/contactos/ContactosPlugin.java +++ /dev/null @@ -1,1003 +0,0 @@ -// Copyright 2025 Anton Ustinoff. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package flutter.plugins.contactos; - -import android.content.ContentProviderOperation; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.provider.BaseColumns; -import android.provider.ContactsContract; -import android.text.TextUtils; -import android.util.Log; - -import androidx.annotation.NonNull; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.embedding.engine.plugins.activity.ActivityAware; -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry; - -import static android.app.Activity.RESULT_CANCELED; -import static android.provider.ContactsContract.CommonDataKinds; -import static android.provider.ContactsContract.CommonDataKinds.Email; -import static android.provider.ContactsContract.CommonDataKinds.Organization; -import static android.provider.ContactsContract.CommonDataKinds.Phone; -import static android.provider.ContactsContract.CommonDataKinds.StructuredName; -import static android.provider.ContactsContract.CommonDataKinds.StructuredPostal; - -/** - * Updated ContactosPlugin, using PluginRegistry.ActivityResultListener and ExecutorService - * instead of ActivityPluginBinding.ActivityResultListener and AsyncTask. - */ -public class ContactosPlugin implements - MethodChannel.MethodCallHandler, - FlutterPlugin, - ActivityAware -{ - private static final int FORM_OPERATION_CANCELED = 1; - private static final int FORM_COULD_NOT_BE_OPEN = 2; - - private static final String LOG_TAG = "contacts"; - - private ContentResolver contentResolver; - private BaseContactosDelegate delegate; - private MethodChannel methodChannel; - private Resources resources; - - // Thread pool for asynchronous operations (replacement for AsyncTask) - private final ExecutorService executor = new ThreadPoolExecutor( - 0, - 10, - 60, - TimeUnit.SECONDS, - new ArrayBlockingQueue<>(1000) - ); - - // Handler for returning the result to the main (UI) thread - private final Handler mainHandler = new Handler(Looper.getMainLooper()); - - // region FlutterPlugin - @Override - public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { - this.resources = binding.getApplicationContext().getResources(); - initInstance(binding.getBinaryMessenger(), binding.getApplicationContext()); - this.delegate = new ContactosDelegate(binding.getApplicationContext()); - } - - @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - if (methodChannel != null) { - methodChannel.setMethodCallHandler(null); - methodChannel = null; - } - contentResolver = null; - delegate = null; - resources = null; - } - // endregion - - // region ActivityAware - @Override - public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { - if (delegate instanceof ContactosDelegate) { - ((ContactosDelegate) delegate).bindToActivity(binding); - } - } - - @Override - public void onDetachedFromActivityForConfigChanges() { - if (delegate instanceof ContactosDelegate) { - ((ContactosDelegate) delegate).unbindActivity(); - } - } - - @Override - public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { - if (delegate instanceof ContactosDelegate) { - ((ContactosDelegate) delegate).bindToActivity(binding); - } - } - - @Override - public void onDetachedFromActivity() { - if (delegate instanceof ContactosDelegate) { - ((ContactosDelegate) delegate).unbindActivity(); - } - } - // endregion - - // region Initialization - private void initInstance(BinaryMessenger messenger, Context context) { - methodChannel = new MethodChannel(messenger, "github.com/ziqq/contactos"); - methodChannel.setMethodCallHandler(this); - contentResolver = context.getContentResolver(); - } - // endregion - - // region MethodCallHandler - @Override - public void onMethodCall(MethodCall call, MethodChannel.Result result) { - switch (call.method) { - case "getContacts": { - final String query = call.argument("query"); - final boolean withThumbnails = call.argument("withThumbnails"); - final boolean orderByGivenName = call.argument("orderByGivenName"); - final boolean photoHighResolution = call.argument("photoHighResolution"); - final boolean androidLocalizedLabels = call.argument("androidLocalizedLabels"); - - getContacts( - "getContacts", - query, - withThumbnails, - photoHighResolution, - orderByGivenName, - androidLocalizedLabels, - result - ); - break; - } - case "getContactsForPhone": { - final String phone = call.argument("phone"); - final boolean withThumbnails = call.argument("withThumbnails"); - final boolean photoHighResolution = call.argument("photoHighResolution"); - final boolean orderByGivenName = call.argument("orderByGivenName"); - final boolean androidLocalizedLabels = call.argument("androidLocalizedLabels"); - - getContacts( - "getContactsForPhone", - phone, - withThumbnails, - photoHighResolution, - orderByGivenName, - androidLocalizedLabels, - result - ); - break; - } - case "getContactsForEmail": { - final String email = call.argument("email"); - final boolean withThumbnails = call.argument("withThumbnails"); - final boolean photoHighResolution = call.argument("photoHighResolution"); - final boolean orderByGivenName = call.argument("orderByGivenName"); - final boolean androidLocalizedLabels = call.argument("androidLocalizedLabels"); - - getContacts( - "getContactsForEmail", - email, - withThumbnails, - photoHighResolution, - orderByGivenName, - androidLocalizedLabels, - result - ); - break; - } - case "getAvatar": { - final HashMap map = call.argument("contact"); - final boolean photoHighResolution = call.argument("photoHighResolution"); - final Contact contact = Contact.fromMap(map); - getAvatar(contact, photoHighResolution, result); - break; - } - case "addContact": { - final Contact contact = Contact.fromMap((HashMap) call.arguments); - if (addContact(contact)) { - result.success(null); - } else { - result.error(null, "Failed to add the contact", null); - } - break; - } - case "deleteContact": { - final Contact contact = Contact.fromMap((HashMap) call.arguments); - if (deleteContact(contact)) { - result.success(null); - } else { - result.error(null, "Failed to delete the contact, make sure it has a valid identifier", null); - } - break; - } - case "updateContact": { - final Contact contact = Contact.fromMap((HashMap) call.arguments); - if (updateContact(contact)) { - result.success(null); - } else { - result.error(null, "Failed to update the contact, make sure it has a valid identifier", null); - } - break; - } - case "openExistingContact": { - final HashMap map = call.argument("contact"); - final boolean localizedLabels = call.argument("androidLocalizedLabels"); - final Contact contact = Contact.fromMap(map); - if (delegate != null) { - delegate.setResult(result); - delegate.setLocalizedLabels(localizedLabels); - delegate.openExistingContact(contact); - } else { - result.success(FORM_COULD_NOT_BE_OPEN); - } - break; - } - case "openContactForm": { - final boolean localizedLabels = call.argument("androidLocalizedLabels"); - if (delegate != null) { - delegate.setResult(result); - delegate.setLocalizedLabels(localizedLabels); - delegate.openContactForm(); - } else { - result.success(FORM_COULD_NOT_BE_OPEN); - } - break; - } - case "openDeviceContactPicker": { - final boolean localizedLabels = call.argument("androidLocalizedLabels"); - openDeviceContactPicker(result, localizedLabels); - break; - } - default: { - result.notImplemented(); - } - } - } - // endregion - - // region Asynchronous contact retrieval (AsyncTask replacement) - private static final String[] PROJECTION = { - ContactsContract.Data.CONTACT_ID, - ContactsContract.Profile.DISPLAY_NAME, - ContactsContract.Contacts.Data.MIMETYPE, - ContactsContract.RawContacts.ACCOUNT_TYPE, - ContactsContract.RawContacts.ACCOUNT_NAME, - StructuredName.DISPLAY_NAME, - StructuredName.GIVEN_NAME, - StructuredName.MIDDLE_NAME, - StructuredName.FAMILY_NAME, - StructuredName.PREFIX, - StructuredName.SUFFIX, - CommonDataKinds.Note.NOTE, - Phone.NUMBER, - Phone.TYPE, - Phone.LABEL, - Email.ADDRESS, - Email.TYPE, - Email.LABEL, - Organization.COMPANY, - Organization.TITLE, - StructuredPostal.FORMATTED_ADDRESS, - StructuredPostal.TYPE, - StructuredPostal.LABEL, - StructuredPostal.STREET, - StructuredPostal.POBOX, - StructuredPostal.NEIGHBORHOOD, - StructuredPostal.CITY, - StructuredPostal.REGION, - StructuredPostal.POSTCODE, - StructuredPostal.COUNTRY, - }; - - // A unified method for getting contacts in the background. - private void getContacts( - final String callMethod, - final String param, - final boolean withThumbnails, - final boolean photoHighResolution, - final boolean orderByGivenName, - final boolean localizedLabels, - final MethodChannel.Result result - ) { - executor.execute(() -> { - ArrayList contacts; - - switch (callMethod) { - case "openDeviceContactPicker": - contacts = getContactsFrom(getCursor(null, param), localizedLabels); - break; - case "getContacts": - contacts = getContactsFrom(getCursor(param, null), localizedLabels); - break; - case "getContactsForPhone": - contacts = getContactsFrom(getCursorForPhone(param), localizedLabels); - break; - case "getContactsForEmail": - contacts = getContactsFrom(getCursorForEmail(param), localizedLabels); - break; - default: - contacts = null; - break; - } - - if (contacts != null && withThumbnails) { - for (Contact c : contacts) { - byte[] avatar = loadContactPhotoHighRes(c.identifier, photoHighResolution, contentResolver); - c.avatar = (avatar != null) ? avatar : new byte[0]; - } - } - - if (contacts != null && orderByGivenName) { - Collections.sort(contacts, Comparator.naturalOrder()); - } - - final ArrayList contactMaps = new ArrayList<>(); - if (contacts != null) { - for (Contact c : contacts) { - contactMaps.add(c.toMap()); - } - } - - // Returning the result to the main thread - mainHandler.post(() -> { - if (contacts == null) { - result.notImplemented(); - } else { - result.success(contactMaps); - } - }); - }); - } - // endregion - - private void openDeviceContactPicker(MethodChannel.Result result, boolean localizedLabels) { - if (delegate != null) { - delegate.setResult(result); - delegate.setLocalizedLabels(localizedLabels); - delegate.openContactPicker(); - } else { - result.success(FORM_COULD_NOT_BE_OPEN); - } - } - - private ArrayList getContactsFrom(Cursor cursor, boolean localizedLabels) { - HashMap map = new LinkedHashMap<>(); - - if (cursor != null) { - while (cursor.moveToNext()) { - String contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID)); - if (!map.containsKey(contactId)) { - map.put(contactId, new Contact(contactId)); - } - Contact contact = map.get(contactId); - - String mimeType = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.MIMETYPE)); - contact.displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)); - contact.androidAccountType = cursor.getString(cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_TYPE)); - contact.androidAccountName = cursor.getString(cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_NAME)); - - if (CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { - contact.givenName = cursor.getString(cursor.getColumnIndex(StructuredName.GIVEN_NAME)); - contact.middleName = cursor.getString(cursor.getColumnIndex(StructuredName.MIDDLE_NAME)); - contact.familyName = cursor.getString(cursor.getColumnIndex(StructuredName.FAMILY_NAME)); - contact.prefix = cursor.getString(cursor.getColumnIndex(StructuredName.PREFIX)); - contact.suffix = cursor.getString(cursor.getColumnIndex(StructuredName.SUFFIX)); - } - else if (CommonDataKinds.Note.CONTENT_ITEM_TYPE.equals(mimeType)) { - contact.note = cursor.getString(cursor.getColumnIndex(CommonDataKinds.Note.NOTE)); - } - else if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) { - String phoneNumber = cursor.getString(cursor.getColumnIndex(Phone.NUMBER)); - if (!TextUtils.isEmpty(phoneNumber)) { - int type = cursor.getInt(cursor.getColumnIndex(Phone.TYPE)); - String label = Item.getPhoneLabel(resources, type, cursor, localizedLabels); - contact.phones.add(new Item(label, phoneNumber, type)); - } - } - else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) { - String email = cursor.getString(cursor.getColumnIndex(Email.ADDRESS)); - int type = cursor.getInt(cursor.getColumnIndex(Email.TYPE)); - if (!TextUtils.isEmpty(email)) { - String label = Item.getEmailLabel(resources, type, cursor, localizedLabels); - contact.emails.add(new Item(label, email, type)); - } - } - else if (CommonDataKinds.Organization.CONTENT_ITEM_TYPE.equals(mimeType)) { - contact.company = cursor.getString(cursor.getColumnIndex(Organization.COMPANY)); - contact.jobTitle = cursor.getString(cursor.getColumnIndex(Organization.TITLE)); - } - else if (CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) { - int type = cursor.getInt(cursor.getColumnIndex(StructuredPostal.TYPE)); - String label = PostalAddress.getLabel(resources, type, cursor, localizedLabels); - String street = cursor.getString(cursor.getColumnIndex(StructuredPostal.STREET)); - String city = cursor.getString(cursor.getColumnIndex(StructuredPostal.CITY)); - String postcode = cursor.getString(cursor.getColumnIndex(StructuredPostal.POSTCODE)); - String region = cursor.getString(cursor.getColumnIndex(StructuredPostal.REGION)); - String country = cursor.getString(cursor.getColumnIndex(StructuredPostal.COUNTRY)); - contact.postalAddresses.add( - new PostalAddress(label, street, city, postcode, region, country, type) - ); - } - else if (CommonDataKinds.Event.CONTENT_ITEM_TYPE.equals(mimeType)) { - int eventType = cursor.getInt(cursor.getColumnIndex(CommonDataKinds.Event.TYPE)); - if (eventType == CommonDataKinds.Event.TYPE_BIRTHDAY) { - contact.birthday = cursor.getString(cursor.getColumnIndex(CommonDataKinds.Event.START_DATE)); - } - } - } - cursor.close(); - } - - return new ArrayList<>(map.values()); - } - - private Cursor getCursor(String query, String rawContactId) { - String selection = "(" + - ContactsContract.Data.MIMETYPE + "=? OR " + - ContactsContract.Data.MIMETYPE + "=? OR " + - ContactsContract.Data.MIMETYPE + "=? OR " + - ContactsContract.Data.MIMETYPE + "=? OR " + - ContactsContract.Data.MIMETYPE + "=? OR " + - ContactsContract.Data.MIMETYPE + "=? OR " + - ContactsContract.Data.MIMETYPE + "=? OR " + - ContactsContract.RawContacts.ACCOUNT_TYPE + "=?" + - ")"; - ArrayList selectionArgs = new ArrayList<>(Arrays.asList( - CommonDataKinds.Note.CONTENT_ITEM_TYPE, - Email.CONTENT_ITEM_TYPE, - Phone.CONTENT_ITEM_TYPE, - StructuredName.CONTENT_ITEM_TYPE, - Organization.CONTENT_ITEM_TYPE, - StructuredPostal.CONTENT_ITEM_TYPE, - CommonDataKinds.Event.CONTENT_ITEM_TYPE, - ContactsContract.RawContacts.ACCOUNT_TYPE - )); - - if (query != null) { - // Search by DISPLAY_NAME_PRIMARY - selection = ContactsContract.Contacts.DISPLAY_NAME_PRIMARY + " LIKE ?"; - selectionArgs.clear(); - selectionArgs.add(query + "%"); - } - if (rawContactId != null) { - selectionArgs.add(rawContactId); - selection += " AND " + ContactsContract.Data.CONTACT_ID + " =?"; - } - return contentResolver.query( - ContactsContract.Data.CONTENT_URI, - PROJECTION, - selection, - selectionArgs.toArray(new String[0]), - null - ); - } - - private Cursor getCursorForPhone(String phone) { - if (phone == null || phone.isEmpty()) return null; - - Uri uri = Uri.withAppendedPath( - ContactsContract.PhoneLookup.CONTENT_FILTER_URI, - Uri.encode(phone) - ); - String[] projection = new String[]{BaseColumns._ID}; - - ArrayList contactIds = new ArrayList<>(); - Cursor phoneCursor = contentResolver.query(uri, projection, null, null, null); - if (phoneCursor != null) { - while (phoneCursor.moveToNext()) { - int idIndex = phoneCursor.getColumnIndex(BaseColumns._ID); - if (idIndex >= 0) { - contactIds.add(phoneCursor.getString(idIndex)); - } - } - phoneCursor.close(); - } - - if (!contactIds.isEmpty()) { - String contactIdsList = contactIds.toString().replace("[", "(").replace("]", ")"); - String contactSelection = ContactsContract.Data.CONTACT_ID + " IN " + contactIdsList; - return contentResolver.query( - ContactsContract.Data.CONTENT_URI, - PROJECTION, - contactSelection, - null, - null - ); - } - return null; - } - - private Cursor getCursorForEmail(String email) { - if (email == null || email.isEmpty()) return null; - String selection = Email.ADDRESS + " LIKE ?"; - String[] selectionArgs = new String[]{"%" + email + "%"}; - return contentResolver.query( - ContactsContract.Data.CONTENT_URI, - PROJECTION, - selection, - selectionArgs, - null - ); - } - - // region Working with avatar (replacement of AsyncTask) - private void getAvatar(final Contact contact, final boolean highRes, final MethodChannel.Result result) { - executor.execute(() -> { - byte[] avatar = loadContactPhotoHighRes(contact.identifier, highRes, contentResolver); - mainHandler.post(() -> result.success(avatar)); - }); - } - - private static byte[] loadContactPhotoHighRes( - final String identifier, - final boolean photoHighResolution, - final ContentResolver resolver - ) { - try { - if (identifier == null || identifier.isEmpty()) { - return null; - } - long contactId = Long.parseLong(identifier); - Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId); - InputStream input = ContactsContract.Contacts.openContactPhotoInputStream( - resolver, uri, photoHighResolution - ); - if (input == null) return null; - - Bitmap bitmap = BitmapFactory.decodeStream(input); - input.close(); - - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); - byte[] bytes = stream.toByteArray(); - stream.close(); - return bytes; - } catch (IOException ex) { - Log.e(LOG_TAG, ex.getMessage()); - return null; - } - } - // endregion - - // region CRUD: addContact, deleteContact, updateContact - private boolean addContact(Contact contact) { - try { - ArrayList ops = new ArrayList<>(); - - ContentProviderOperation.Builder op = ContentProviderOperation - .newInsert(ContactsContract.RawContacts.CONTENT_URI) - .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null) - .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null); - ops.add(op.build()); - - // Name - op = ContentProviderOperation - .newInsert(ContactsContract.Data.CONTENT_URI) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) - .withValue(StructuredName.GIVEN_NAME, contact.givenName) - .withValue(StructuredName.MIDDLE_NAME, contact.middleName) - .withValue(StructuredName.FAMILY_NAME, contact.familyName) - .withValue(StructuredName.PREFIX, contact.prefix) - .withValue(StructuredName.SUFFIX, contact.suffix); - ops.add(op.build()); - - // Note - op = ContentProviderOperation - .newInsert(ContactsContract.Data.CONTENT_URI) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Note.CONTENT_ITEM_TYPE) - .withValue(CommonDataKinds.Note.NOTE, contact.note); - ops.add(op.build()); - - // Organization - op = ContentProviderOperation - .newInsert(ContactsContract.Data.CONTENT_URI) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Organization.CONTENT_ITEM_TYPE) - .withValue(Organization.COMPANY, contact.company) - .withValue(Organization.TITLE, contact.jobTitle); - ops.add(op.build()); - - // Photo - op = ContentProviderOperation - .newInsert(ContactsContract.Data.CONTENT_URI) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.IS_SUPER_PRIMARY, 1) - .withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, contact.avatar) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE); - ops.add(op.build()); - op.withYieldAllowed(true); - - // Phones - for (Item phone : contact.phones) { - op = ContentProviderOperation - .newInsert(ContactsContract.Data.CONTENT_URI) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) - .withValue(CommonDataKinds.Phone.NUMBER, phone.value); - - if (phone.type == CommonDataKinds.Phone.TYPE_CUSTOM) { - op.withValue(CommonDataKinds.Phone.TYPE, CommonDataKinds.BaseTypes.TYPE_CUSTOM); - op.withValue(CommonDataKinds.Phone.LABEL, phone.label); - } else { - op.withValue(CommonDataKinds.Phone.TYPE, phone.type); - } - ops.add(op.build()); - } - - // E-mail - for (Item email : contact.emails) { - op = ContentProviderOperation - .newInsert(ContactsContract.Data.CONTENT_URI) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE) - .withValue(CommonDataKinds.Email.ADDRESS, email.value) - .withValue(CommonDataKinds.Email.TYPE, email.type); - ops.add(op.build()); - } - - // Addresses - for (PostalAddress address : contact.postalAddresses) { - op = ContentProviderOperation - .newInsert(ContactsContract.Data.CONTENT_URI) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) - .withValue(CommonDataKinds.StructuredPostal.TYPE, address.type) - .withValue(CommonDataKinds.StructuredPostal.LABEL, address.label) - .withValue(CommonDataKinds.StructuredPostal.STREET, address.street) - .withValue(CommonDataKinds.StructuredPostal.CITY, address.city) - .withValue(CommonDataKinds.StructuredPostal.REGION, address.region) - .withValue(CommonDataKinds.StructuredPostal.POSTCODE, address.postcode) - .withValue(CommonDataKinds.StructuredPostal.COUNTRY, address.country); - ops.add(op.build()); - } - - // Birthday - op = ContentProviderOperation - .newInsert(ContactsContract.Data.CONTENT_URI) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Event.CONTENT_ITEM_TYPE) - .withValue(CommonDataKinds.Event.TYPE, CommonDataKinds.Event.TYPE_BIRTHDAY) - .withValue(CommonDataKinds.Event.START_DATE, contact.birthday); - ops.add(op.build()); - - contentResolver.applyBatch(ContactsContract.AUTHORITY, ops); - return true; - } catch (Exception e) { - return false; - } - } - - private boolean deleteContact(Contact contact) { - try { - ArrayList ops = new ArrayList<>(); - ops.add(ContentProviderOperation - .newDelete(ContactsContract.RawContacts.CONTENT_URI) - .withSelection( - ContactsContract.RawContacts.CONTACT_ID + "=?", - new String[]{String.valueOf(contact.identifier)} - ) - .build()); - contentResolver.applyBatch(ContactsContract.AUTHORITY, ops); - return true; - } catch (Exception e) { - return false; - } - } - - private boolean updateContact(Contact contact) { - try { - ArrayList ops = new ArrayList<>(); - ContentProviderOperation.Builder op; - - // Removed old data (except for the name) - op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) - .withSelection( - ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", - new String[]{contact.identifier, CommonDataKinds.Organization.CONTENT_ITEM_TYPE} - ); - ops.add(op.build()); - - op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) - .withSelection( - ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", - new String[]{contact.identifier, CommonDataKinds.Phone.CONTENT_ITEM_TYPE} - ); - ops.add(op.build()); - - op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) - .withSelection( - ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", - new String[]{contact.identifier, CommonDataKinds.Email.CONTENT_ITEM_TYPE} - ); - ops.add(op.build()); - - op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) - .withSelection( - ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", - new String[]{contact.identifier, CommonDataKinds.Note.CONTENT_ITEM_TYPE} - ); - ops.add(op.build()); - - op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) - .withSelection( - ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", - new String[]{contact.identifier, CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE} - ); - ops.add(op.build()); - - // Photo - op = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) - .withSelection( - ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", - new String[]{contact.identifier, CommonDataKinds.Photo.CONTENT_ITEM_TYPE} - ); - ops.add(op.build()); - - // Update name - op = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) - .withSelection( - ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", - new String[]{contact.identifier, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE} - ) - .withValue(StructuredName.GIVEN_NAME, contact.givenName) - .withValue(StructuredName.MIDDLE_NAME, contact.middleName) - .withValue(StructuredName.FAMILY_NAME, contact.familyName) - .withValue(StructuredName.PREFIX, contact.prefix) - .withValue(StructuredName.SUFFIX, contact.suffix); - ops.add(op.build()); - - // Add new organization - op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Organization.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier) - .withValue(Organization.TYPE, Organization.TYPE_WORK) - .withValue(Organization.COMPANY, contact.company) - .withValue(Organization.TITLE, contact.jobTitle); - ops.add(op.build()); - - // Note - op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Note.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier) - .withValue(CommonDataKinds.Note.NOTE, contact.note); - ops.add(op.build()); - - // Photo - op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier) - .withValue(ContactsContract.Data.IS_SUPER_PRIMARY, 1) - .withValue(CommonDataKinds.Photo.PHOTO, contact.avatar) - .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE); - ops.add(op.build()); - - // Phone's - for (Item phone : contact.phones) { - op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier) - .withValue(Phone.NUMBER, phone.value); - - if (phone.type == CommonDataKinds.Phone.TYPE_CUSTOM) { - op.withValue(CommonDataKinds.Phone.TYPE, CommonDataKinds.BaseTypes.TYPE_CUSTOM); - op.withValue(CommonDataKinds.Phone.LABEL, phone.label); - } else { - op.withValue(CommonDataKinds.Phone.TYPE, phone.type); - } - ops.add(op.build()); - } - - // Email - for (Item email : contact.emails) { - op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier) - .withValue(CommonDataKinds.Email.ADDRESS, email.value) - .withValue(CommonDataKinds.Email.TYPE, email.type); - ops.add(op.build()); - } - - // Addresses - for (PostalAddress address : contact.postalAddresses) { - op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier) - .withValue(CommonDataKinds.StructuredPostal.TYPE, address.type) - .withValue(StructuredPostal.STREET, address.street) - .withValue(StructuredPostal.CITY, address.city) - .withValue(StructuredPostal.REGION, address.region) - .withValue(StructuredPostal.POSTCODE, address.postcode) - .withValue(StructuredPostal.COUNTRY, address.country); - ops.add(op.build()); - } - - contentResolver.applyBatch(ContactsContract.AUTHORITY, ops); - return true; - } catch (Exception e) { - Log.e("TAG", "Exception encountered while updating contact: ", e); - return false; - } - } - // endregion - - // region Deleates for ActivityResult - private class BaseContactosDelegate implements PluginRegistry.ActivityResultListener { - private static final int REQUEST_OPEN_CONTACT_FORM = 52941; - private static final int REQUEST_OPEN_EXISTING_CONTACT = 52942; - private static final int REQUEST_OPEN_CONTACT_PICKER = 52943; - - private MethodChannel.Result result; - private boolean localizedLabels; - - void setResult(MethodChannel.Result result) { - this.result = result; - } - - void setLocalizedLabels(boolean localizedLabels) { - this.localizedLabels = localizedLabels; - } - - void finishWithResult(Object value) { - if (this.result != null) { - this.result.success(value); - this.result = null; - } - } - - @Override - public boolean onActivityResult(int requestCode, int resultCode, Intent intent) { - if (requestCode == REQUEST_OPEN_EXISTING_CONTACT || requestCode == REQUEST_OPEN_CONTACT_FORM) { - try { - if (intent != null && intent.getData() != null) { - Uri ur = intent.getData(); - finishWithResult(getContactByIdentifier(ur.getLastPathSegment())); - } else { - finishWithResult(FORM_OPERATION_CANCELED); - } - } catch (NullPointerException e) { - finishWithResult(FORM_OPERATION_CANCELED); - } - return true; - } - - if (requestCode == REQUEST_OPEN_CONTACT_PICKER) { - if (resultCode == RESULT_CANCELED) { - finishWithResult(FORM_OPERATION_CANCELED); - return true; - } - if (intent == null) { - finishWithResult(FORM_COULD_NOT_BE_OPEN); - return true; - } - Uri contactUri = intent.getData(); - if (contactUri == null) { - finishWithResult(FORM_COULD_NOT_BE_OPEN); - return true; - } - Cursor cursor = contentResolver.query(contactUri, null, null, null, null); - if (cursor != null && cursor.moveToFirst()) { - String id = contactUri.getLastPathSegment(); - getContacts("openDeviceContactPicker", id, false, false, false, this.localizedLabels, this.result); - } else { - Log.e(LOG_TAG, "onActivityResult: cursor.moveToFirst() == false"); - finishWithResult(FORM_OPERATION_CANCELED); - } - if (cursor != null) cursor.close(); - return true; - } - - finishWithResult(FORM_COULD_NOT_BE_OPEN); - return false; - } - - void openExistingContact(Contact contact) { - String identifier = contact.identifier; - try { - HashMap contactMapFromDevice = getContactByIdentifier(identifier); - if (contactMapFromDevice != null) { - Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, identifier); - Intent intent = new Intent(Intent.ACTION_EDIT); - intent.setDataAndType(uri, ContactsContract.Contacts.CONTENT_ITEM_TYPE); - intent.putExtra("finishActivityOnSaveCompleted", true); - startIntent(intent, REQUEST_OPEN_EXISTING_CONTACT); - } else { - finishWithResult(FORM_COULD_NOT_BE_OPEN); - } - } catch (Exception e) { - finishWithResult(FORM_COULD_NOT_BE_OPEN); - } - } - - void openContactForm() { - try { - Intent intent = new Intent(Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI); - intent.putExtra("finishActivityOnSaveCompleted", true); - startIntent(intent, REQUEST_OPEN_CONTACT_FORM); - } catch (Exception e) { - finishWithResult(FORM_COULD_NOT_BE_OPEN); - } - } - - void openContactPicker() { - Intent intent = new Intent(Intent.ACTION_PICK); - intent.setType(ContactsContract.Contacts.CONTENT_TYPE); - startIntent(intent, REQUEST_OPEN_CONTACT_PICKER); - } - - void startIntent(Intent intent, int request) { - // Overridden in the successor ContactosDelegate - } - - HashMap getContactByIdentifier(String identifier) { - Cursor cursor = contentResolver.query( - ContactsContract.Data.CONTENT_URI, - PROJECTION, - ContactsContract.RawContacts.CONTACT_ID + " = ?", - new String[]{identifier}, - null - ); - ArrayList matchingContacts; - try { - matchingContacts = getContactsFrom(cursor, localizedLabels); - } finally { - if (cursor != null) cursor.close(); - } - if (!matchingContacts.isEmpty()) { - return matchingContacts.get(0).toMap(); - } - return null; - } - } - - private class ContactosDelegate extends BaseContactosDelegate { - private final Context context; - private ActivityPluginBinding activityPluginBinding; - - ContactosDelegate(Context context) { - this.context = context; - } - - void bindToActivity(ActivityPluginBinding binding) { - this.activityPluginBinding = binding; - this.activityPluginBinding.addActivityResultListener(this); - } - - void unbindActivity() { - if (this.activityPluginBinding != null) { - this.activityPluginBinding.removeActivityResultListener(this); - } - this.activityPluginBinding = null; - } - - @Override - void startIntent(Intent intent, int request) { - if (this.activityPluginBinding != null) { - if (intent.resolveActivity(context.getPackageManager()) != null) { - this.activityPluginBinding.getActivity().startActivityForResult(intent, request); - } else { - finishWithResult(FORM_COULD_NOT_BE_OPEN); - } - } else { - context.startActivity(intent); - } - } - } - // endregion -} \ No newline at end of file diff --git a/contactos/android/src/main/java/flutter/plugins/contactos/Item.java b/contactos/android/src/main/java/flutter/plugins/contactos/Item.java deleted file mode 100644 index ccc5abb..0000000 --- a/contactos/android/src/main/java/flutter/plugins/contactos/Item.java +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2025 Anton Ustinoff. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package flutter.plugins.contactos; - -import android.content.res.Resources; -import android.database.Cursor; - -import static android.provider.ContactsContract.CommonDataKinds; - -import java.util.HashMap; - -/*** - * Represents an object which has a label and a value - * such as an email or a phone - ***/ -public class Item { - - public String label, value; - int type; - - public Item(String label, String value, int type) { - this.label = label; - this.value = value; - this.type = type; - } - - HashMap toMap() { - HashMap result = new HashMap<>(); - result.put("label", label); - result.put("value", value); - result.put("type", String.valueOf(type)); - return result; - } - - public static Item fromMap(HashMap map) { - String label = map.get("label"); - String value = map.get("value"); - String type = map.get("type"); - return new Item(label, value, type != null ? Integer.parseInt(type) : -1); - } - - public static String getPhoneLabel(Resources resources, int type, Cursor cursor, boolean localizedLabels) { - if (localizedLabels) { - CharSequence localizedLabel = CommonDataKinds.Phone.getTypeLabel(resources, type, ""); - return localizedLabel.toString().toLowerCase(); - } else { - switch (type) { - case CommonDataKinds.Phone.TYPE_HOME: - return "home"; - case CommonDataKinds.Phone.TYPE_WORK: - return "work"; - case CommonDataKinds.Phone.TYPE_MOBILE: - return "mobile"; - case CommonDataKinds.Phone.TYPE_FAX_WORK: - return "fax work"; - case CommonDataKinds.Phone.TYPE_FAX_HOME: - return "fax home"; - case CommonDataKinds.Phone.TYPE_MAIN: - return "main"; - case CommonDataKinds.Phone.TYPE_COMPANY_MAIN: - return "company"; - case CommonDataKinds.Phone.TYPE_PAGER: - return "pager"; - case CommonDataKinds.Phone.TYPE_CUSTOM: - if (cursor.getString(cursor.getColumnIndex(CommonDataKinds.Phone.LABEL)) != null) { - return cursor.getString(cursor.getColumnIndex(CommonDataKinds.Phone.LABEL)).toLowerCase(); - } else return ""; - default: - return "other"; - } - } - } - - public static String getEmailLabel(Resources resources, int type, Cursor cursor, boolean localizedLabels) { - if (localizedLabels) { - CharSequence localizedLabel = CommonDataKinds.Email.getTypeLabel(resources, type, ""); - return localizedLabel.toString().toLowerCase(); - } else { - switch (type) { - case CommonDataKinds.Email.TYPE_HOME: - return "home"; - case CommonDataKinds.Email.TYPE_WORK: - return "work"; - case CommonDataKinds.Email.TYPE_MOBILE: - return "mobile"; - case CommonDataKinds.Email.TYPE_CUSTOM: - if (cursor.getString(cursor.getColumnIndex(CommonDataKinds.Email.LABEL)) != null) { - return cursor.getString(cursor.getColumnIndex(CommonDataKinds.Email.LABEL)).toLowerCase(); - } else return ""; - default: - return "other"; - } - } - } -} \ No newline at end of file diff --git a/contactos/android/src/main/java/flutter/plugins/contactos/PostalAddress.java b/contactos/android/src/main/java/flutter/plugins/contactos/PostalAddress.java deleted file mode 100644 index 048c56a..0000000 --- a/contactos/android/src/main/java/flutter/plugins/contactos/PostalAddress.java +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2025 Anton Ustinoff. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package flutter.plugins.contactos; - -import android.annotation.TargetApi; -import android.content.res.Resources; -import android.database.Cursor; -import android.os.Build; - -import static android.provider.ContactsContract.CommonDataKinds; -import static android.provider.ContactsContract.CommonDataKinds.StructuredPostal; - -import java.util.HashMap; - -@TargetApi(Build.VERSION_CODES.ECLAIR) -public class PostalAddress { - - public String label, street, city, postcode, region, country; - int type; - - public PostalAddress(String label, String street, String city, String postcode, String region, String country, int type) { - this.label = label; - this.street = street; - this.city = city; - this.postcode = postcode; - this.region = region; - this.country = country; - this.type = type; - } - - HashMap toMap() { - HashMap result = new HashMap<>(); - result.put("label", label); - result.put("street", street); - result.put("city", city); - result.put("postcode", postcode); - result.put("region", region); - result.put("country", country); - result.put("type", String.valueOf(type)); - return result; - } - - public static PostalAddress fromMap(HashMap map) { - String label = map.get("label"); - String street = map.get("street"); - String city = map.get("city"); - String postcode = map.get("postcode"); - String region = map.get("region"); - String country = map.get("country"); - String type = map.get("type"); - return new PostalAddress(label, street, city, postcode, region, country, type != null ? Integer.parseInt(type) : -1); - } - - public static String getLabel(Resources resources, int type, Cursor cursor, boolean localizedLabels) { - if (localizedLabels) { - CharSequence localizedLabel = CommonDataKinds.StructuredPostal.getTypeLabel(resources, type, ""); - return localizedLabel.toString().toLowerCase(); - } else { - switch (cursor.getInt(cursor.getColumnIndex(StructuredPostal.TYPE))) { - case StructuredPostal.TYPE_HOME: - return "home"; - case StructuredPostal.TYPE_WORK: - return "work"; - case StructuredPostal.TYPE_CUSTOM: - final String label = cursor.getString(cursor.getColumnIndex(StructuredPostal.LABEL)); - return label != null ? label : ""; - } - return "other"; - } - } -} diff --git a/contactos/android/src/test/java/flutter/plugins/contactsservice/contactsservice/ContactTest.java b/contactos/android/src/test/java/flutter/plugins/contactsservice/contactsservice/ContactTest.java deleted file mode 100644 index 0589220..0000000 --- a/contactos/android/src/test/java/flutter/plugins/contactsservice/contactsservice/ContactTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package flutter.plugins.contactos; - -import static com.google.common.truth.Truth.assertThat; - -import org.junit.Test; - -public class ContactTest { - - @Test - public void compareTo_nullParam() { - Contact contact1 = new Contact("id"); - contact1.givenName = "givenName"; - - Contact contact2 = new Contact("id2"); - - assertThat(contact1.compareTo(contact2)) - .isGreaterThan(0); - } - - @Test - public void compareTo_largerParam() { - Contact contact1 = new Contact("id"); - contact1.givenName = "a"; - - Contact contact2 = new Contact("id2"); - contact2.givenName = "b"; - - assertThat(contact1.compareTo(contact2)) - .isLessThan(0); - } - - @Test - public void compareTo_smallerParam() { - Contact contact1 = new Contact("id"); - contact1.givenName = "b"; - - Contact contact2 = new Contact("id2"); - contact2.givenName = "a"; - - assertThat(contact1.compareTo(contact2)) - .isGreaterThan(0); - } - - @Test - public void compareTo_givenNameNull() { - Contact contact1 = new Contact("id"); - contact1.givenName = null; - - Contact contact2 = new Contact("id2"); - contact2.givenName = null; - - assertThat(contact1.compareTo(contact2)) - .isEqualTo(0); - } - - @Test - public void compareTo_currentContactGivenNameNull() { - Contact contact1 = new Contact("id"); - contact1.givenName = null; - - Contact contact2 = new Contact("id2"); - contact2.givenName = "b"; - - assertThat(contact1.compareTo(contact2)) - .isLessThan(0); - } - - @Test - public void compareTo_nullContact() { - Contact contact1 = new Contact("id"); - contact1.givenName = "a"; - - assertThat(contact1.compareTo(null)) - .isGreaterThan(0); - } - - @Test - public void compareTo_transitiveCompare() { - Contact contact1 = new Contact("id"); - contact1.givenName = "b"; - - Contact contact2 = new Contact("id2"); - contact2.givenName = "a"; - - Contact contact3 = new Contact("id3"); - contact3.givenName = null; - - // b > a - assertThat(contact1.compareTo(contact2)) - .isGreaterThan(0); - - // a > null - assertThat(contact2.compareTo(contact3)) - .isGreaterThan(0); - - // This implies => b > null - assertThat(contact1.compareTo(contact3)) - .isGreaterThan(0); - } -} \ No newline at end of file diff --git a/contactos/example/pubspec.lock b/contactos/example/pubspec.lock index b426336..8f6bbf7 100644 --- a/contactos/example/pubspec.lock +++ b/contactos/example/pubspec.lock @@ -47,15 +47,31 @@ packages: path: ".." relative: true source: path - version: "1.0.6" + version: "2.0.0-pre.1" + contactos_android: + dependency: transitive + description: + name: contactos_android + sha256: "9847cb2a2955c02e793dc9af803ec8130b458e6e95ec859b860431f38d7526a5" + url: "https://pub.dev" + source: hosted + version: "0.0.1" + contactos_foundation: + dependency: transitive + description: + name: contactos_foundation + sha256: "8dbcf1dd698e9776e12f23635b6a2b73fc9fcea936113b3079d6081f5d40f534" + url: "https://pub.dev" + source: hosted + version: "0.0.2" contactos_platform_interface: dependency: "direct main" description: name: contactos_platform_interface - sha256: "91cb1912e5b6f5126d43e1eb21a8385f837e6b637642449a526fc60038214529" + sha256: "6fc797fa685c84ef7e1ec8fc8c6aae1f9f09274bcc5ea26e435781a4fa907628" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" cupertino_icons: dependency: "direct main" description: diff --git a/contactos/ios/contactos.podspec b/contactos/ios/contactos.podspec deleted file mode 100644 index 641697c..0000000 --- a/contactos/ios/contactos.podspec +++ /dev/null @@ -1,29 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'contactos' - s.version = '1.0.6' - s.summary = "A Flutter plugin to retrieve and manage contacts on iOS devices" - s.description = <<-DESC - A Flutter plugin to retrieve and manage contacts on iOS devices. - DESC - s.homepage = 'https://github.com/ziqq/contactos/contactos' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Anton Ustinoff' => 'a.a.ustinoff@gmail.com' } - s.source = { :http => 'https://github.com/ziqq/contactos/contactos' } - s.public_header_files = 'contactos/Sources/contactos/**/*.h' - s.source_files = 'contactos/Sources/contactos/**/*.{swift,m,h}' - s.ios.dependency 'Flutter' - s.osx.dependency 'FlutterMacOS' - s.ios.deployment_target = '12.0' - s.osx.deployment_target = '10.14' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } - s.xcconfig = { - 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', - 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', - } - s.swift_version = '5.0' - s.resource_bundles = {'contactos_privacy' => ['contactos/Sources/contactos/Resources/PrivacyInfo.xcprivacy']} -end - diff --git a/contactos/ios/contactos/Package.swift b/contactos/ios/contactos/Package.swift deleted file mode 100644 index 9ae0001..0000000 --- a/contactos/ios/contactos/Package.swift +++ /dev/null @@ -1,28 +0,0 @@ -// swift-tools-version: 5.9 - -// Copyright 2025 Anton Ustinoff. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import PackageDescription - -let package = Package( - name: "contactos", - platforms: [ - .iOS("12.0"), - .macOS("10.14"), - ], - products: [ - .library(name: "contactos", targets: ["contactos"]) - ], - dependencies: [], - targets: [ - .target( - name: "contactos", - dependencies: [], - resources: [ - .process("Resources") - ] - ) - ] -) diff --git a/contactos/ios/contactos/Sources/contactos/ContactosPlugin.h b/contactos/ios/contactos/Sources/contactos/ContactosPlugin.h deleted file mode 100644 index 1354260..0000000 --- a/contactos/ios/contactos/Sources/contactos/ContactosPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface ContactosPlugin : NSObject -@end diff --git a/contactos/ios/contactos/Sources/contactos/ContactosPlugin.m b/contactos/ios/contactos/Sources/contactos/ContactosPlugin.m deleted file mode 100644 index 509f4ab..0000000 --- a/contactos/ios/contactos/Sources/contactos/ContactosPlugin.m +++ /dev/null @@ -1,13 +0,0 @@ -#import "ContactosPlugin.h" - -#if __has_include() -#import -#else -#import "contactos-Swift.h" -#endif - -@implementation ContactosPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftContactosPlugin registerWithRegistrar:registrar]; -} -@end diff --git a/contactos/ios/contactos/Sources/contactos/ContactosPlugin.swift b/contactos/ios/contactos/Sources/contactos/ContactosPlugin.swift deleted file mode 100644 index e0ede27..0000000 --- a/contactos/ios/contactos/Sources/contactos/ContactosPlugin.swift +++ /dev/null @@ -1,636 +0,0 @@ -import Flutter -import UIKit -import Contacts -import ContactsUI - -@available(iOS 9.0, *) -public class SwiftContactosPlugin: NSObject, FlutterPlugin, CNContactViewControllerDelegate, CNContactPickerDelegate { - private var result: FlutterResult? = nil - private var localizedLabels: Bool = true - private let rootViewController: UIViewController - static let FORM_OPERATION_CANCELED: Int = 1 - static let FORM_COULD_NOT_BE_OPEN: Int = 2 - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "github.com/ziqq/contactos", binaryMessenger: registrar.messenger()) - guard let rootViewController = UIApplication.shared.delegate?.window??.rootViewController else { - NSLog("ContactosPlugin: Failed to get rootViewController") - return - } - let instance = SwiftContactosPlugin(rootViewController) - registrar.addMethodCallDelegate(instance, channel: channel) - instance.preLoadContactView() - } - - init(_ rootViewController: UIViewController) { - self.rootViewController = rootViewController - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "getAvatar": - guard let contactId = (call.arguments as? [String:Any])?["identifier"] as? String else { - result(FlutterError(code: "MISSING_ID", message: "No contact identifier provided", details: nil)) - return - } - do { - let store = CNContactStore() - let keys = [CNContactImageDataKey, CNContactThumbnailImageDataKey] as [CNKeyDescriptor] - let cnContact = try store.unifiedContact(withIdentifier: contactId, keysToFetch: keys) - if let data = cnContact.imageData ?? cnContact.thumbnailImageData { - result(FlutterStandardTypedData(bytes: data)) - } else { - result(nil) - } - } catch { - result(FlutterError(code: "FETCH_ERROR", message: error.localizedDescription, details: nil)) - } - case "getContacts": - let arguments = call.arguments as! [String:Any] - result(getContacts(query: (arguments["query"] as? String), withThumbnails: arguments["withThumbnails"] as! Bool, - photoHighResolution: arguments["photoHighResolution"] as! Bool, phoneQuery: false, orderByGivenName: arguments["orderByGivenName"] as! Bool, - localizedLabels: arguments["iOSLocalizedLabels"] as! Bool )) - case "getContactsForPhone": - let arguments = call.arguments as! [String:Any] - result( - getContacts( - query: (arguments["phone"] as? String), - withThumbnails: arguments["withThumbnails"] as! Bool, - photoHighResolution: arguments["photoHighResolution"] as! Bool, - phoneQuery: true, - orderByGivenName: arguments["orderByGivenName"] as! Bool, - localizedLabels: arguments["iOSLocalizedLabels"] as! Bool - ) - ) - case "getContactsForEmail": - let arguments = call.arguments as! [String:Any] - result( - getContacts( - query: (arguments["email"] as? String), - withThumbnails: arguments["withThumbnails"] as! Bool, - photoHighResolution: arguments["photoHighResolution"] as! Bool, - phoneQuery: false, - emailQuery: true, - orderByGivenName: arguments["orderByGivenName"] as! Bool, - localizedLabels: arguments["iOSLocalizedLabels"] as! Bool - ) - ) - case "addContact": - let contact = dictionaryToContact(dictionary: call.arguments as! [String : Any]) - - let addResult = addContact(contact: contact) - if (addResult == "") { - result(nil) - } - else { - result(FlutterError(code: "", message: addResult, details: nil)) - } - case "deleteContact": - if(deleteContact(dictionary: call.arguments as! [String : Any])){ - result(nil) - } - else{ - result(FlutterError(code: "", message: "Failed to delete contact, make sure it has a valid identifier", details: nil)) - } - case "updateContact": - if(updateContact(dictionary: call.arguments as! [String: Any])) { - result(nil) - } - else { - result(FlutterError(code: "", message: "Failed to update contact, make sure it has a valid identifier", details: nil)) - } - case "openContactForm": - let arguments = call.arguments as! [String:Any] - localizedLabels = arguments["iOSLocalizedLabels"] as! Bool - self.result = result - _ = openContactForm() - case "openExistingContact": - let arguments = call.arguments as! [String : Any] - let contact = arguments["contact"] as! [String : Any] - localizedLabels = arguments["iOSLocalizedLabels"] as! Bool - self.result = result - _ = openExistingContact(contact: contact, result: result) - case "openDeviceContactPicker": - let arguments = call.arguments as! [String : Any] - openDeviceContactPicker(arguments: arguments, result: result); - default: - result(FlutterMethodNotImplemented) - } - } - - func getContacts(query : String?, withThumbnails: Bool, photoHighResolution: Bool, phoneQuery: Bool, emailQuery: Bool = false, orderByGivenName: Bool, localizedLabels: Bool) -> [[String:Any]]{ - - var contacts : [CNContact] = [] - var result = [[String:Any]]() - - //Create the store, keys & fetch request - let store = CNContactStore() - var keys = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), - CNContactEmailAddressesKey, - CNContactPhoneNumbersKey, - CNContactFamilyNameKey, - CNContactGivenNameKey, - CNContactMiddleNameKey, - CNContactNamePrefixKey, - CNContactNameSuffixKey, - CNContactPostalAddressesKey, - CNContactOrganizationNameKey, - CNContactJobTitleKey, - CNContactBirthdayKey] as [Any] - - if(withThumbnails){ - if(photoHighResolution){ - keys.append(CNContactImageDataKey) - } else { - keys.append(CNContactThumbnailImageDataKey) - } - } - - let fetchRequest = CNContactFetchRequest(keysToFetch: keys as! [CNKeyDescriptor]) - // Set the predicate if there is a query - if query != nil && !phoneQuery && !emailQuery { - fetchRequest.predicate = CNContact.predicateForContacts(matchingName: query!) - } - - if #available(iOS 11, *) { - if query != nil && phoneQuery { - let phoneNumberPredicate = CNPhoneNumber(stringValue: query!) - fetchRequest.predicate = CNContact.predicateForContacts(matching: phoneNumberPredicate) - } else if query != nil && emailQuery { - fetchRequest.predicate = CNContact.predicateForContacts(matchingEmailAddress: query!) - } - } - - // Fetch contacts - do{ - try store.enumerateContacts(with: fetchRequest, usingBlock: { (contact, stop) -> Void in - - if phoneQuery { - if #available(iOS 11, *) { - contacts.append(contact) - } else if query != nil && self.has(contact: contact, phone: query!){ - contacts.append(contact) - } - } else if emailQuery { - if #available(iOS 11, *) { - contacts.append(contact) - } else if query != nil && (contact.emailAddresses.contains { $0.value.caseInsensitiveCompare(query!) == .orderedSame}) { - contacts.append(contact) - } - } else { - contacts.append(contact) - } - - }) - } - catch let error as NSError { - print(error.localizedDescription) - return result - } - - if (orderByGivenName) { - contacts = contacts.sorted { (contactA, contactB) -> Bool in - contactA.givenName.lowercased() < contactB.givenName.lowercased() - } - } - - // Transform the CNContacts into dictionaries - for contact : CNContact in contacts{ - result.append(contactToDictionary(contact: contact, localizedLabels: localizedLabels)) - } - - return result - } - - private func has(contact: CNContact, phone: String) -> Bool { - if (!contact.phoneNumbers.isEmpty) { - let phoneNumberToCompareAgainst = phone.components(separatedBy: NSCharacterSet.decimalDigits.inverted).joined(separator: "") - for phoneNumber in contact.phoneNumbers { - - if let phoneNumberStruct = phoneNumber.value as CNPhoneNumber? { - let phoneNumberString = phoneNumberStruct.stringValue - let phoneNumberToCompare = phoneNumberString.components(separatedBy: NSCharacterSet.decimalDigits.inverted).joined(separator: "") - if phoneNumberToCompare == phoneNumberToCompareAgainst { - return true - } - } - } - } - return false - } - - func addContact(contact : CNMutableContact) -> String { - let store = CNContactStore() - do { - let saveRequest = CNSaveRequest() - saveRequest.add(contact, toContainerWithIdentifier: nil) - try store.execute(saveRequest) - } - catch { - return error.localizedDescription - } - return "" - } - - func openContactForm() -> [String:Any]? { - let contact = CNMutableContact.init() - let controller = CNContactViewController.init(forNewContact:contact) - controller.delegate = self - DispatchQueue.main.async { - let navigation = UINavigationController .init(rootViewController: controller) - let viewController : UIViewController? = UIApplication.shared.delegate?.window??.rootViewController - viewController?.present(navigation, animated:true, completion: nil) - } - return nil - } - - func preLoadContactView() { - DispatchQueue.main.asyncAfter(deadline: .now()+5) { - NSLog("Preloading CNContactViewController") - let contactViewController = CNContactViewController.init(forNewContact: nil) - } - } - - @objc func cancelContactForm() { - if let result = self.result { - let viewController : UIViewController? = UIApplication.shared.delegate?.window??.rootViewController - viewController?.dismiss(animated: true, completion: nil) - result(SwiftContactosPlugin.FORM_OPERATION_CANCELED) - self.result = nil - } - } - - public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) { - viewController.dismiss(animated: true, completion: nil) - if let result = self.result { - if let contact = contact { - result(contactToDictionary(contact: contact, localizedLabels: localizedLabels)) - } else { - result(SwiftContactosPlugin.FORM_OPERATION_CANCELED) - } - self.result = nil - } - } - - func openExistingContact(contact: [String:Any], result: FlutterResult ) -> [String:Any]? { - let store = CNContactStore() - do { - // Check to make sure dictionary has an identifier - guard let identifier = contact["identifier"] as? String else{ - result(SwiftContactosPlugin.FORM_COULD_NOT_BE_OPEN) - return nil; - } - let backTitle = contact["backTitle"] as? String - - let keysToFetch = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), - CNContactIdentifierKey, - CNContactEmailAddressesKey, - CNContactBirthdayKey, - CNContactImageDataKey, - CNContactPhoneNumbersKey, - CNContactViewController.descriptorForRequiredKeys() - ] as! [CNKeyDescriptor] - let cnContact = try store.unifiedContact(withIdentifier: identifier, keysToFetch: keysToFetch) - let viewController = CNContactViewController(for: cnContact) - - viewController.navigationItem.backBarButtonItem = UIBarButtonItem.init(title: backTitle == nil ? "Cancel" : backTitle, style: UIBarButtonItem.Style.plain, target: self, action: #selector(cancelContactForm)) - viewController.delegate = self - DispatchQueue.main.async { - let navigation = UINavigationController .init(rootViewController: viewController) - var currentViewController = UIApplication.shared.keyWindow?.rootViewController - while let nextView = currentViewController?.presentedViewController { - currentViewController = nextView - } - let activityIndicatorView = UIActivityIndicatorView.init(style: UIActivityIndicatorView.Style.gray) - activityIndicatorView.frame = (UIApplication.shared.keyWindow?.frame)! - activityIndicatorView.startAnimating() - activityIndicatorView.backgroundColor = UIColor.white - navigation.view.addSubview(activityIndicatorView) - currentViewController!.present(navigation, animated: true, completion: nil) - - DispatchQueue.main.asyncAfter(deadline: .now()+0.5 ){ - activityIndicatorView.removeFromSuperview() - } - } - return nil - } catch { - NSLog(error.localizedDescription) - result(SwiftContactosPlugin.FORM_COULD_NOT_BE_OPEN) - return nil - } - } - - func openDeviceContactPicker(arguments: [String:Any], result: @escaping FlutterResult) { - localizedLabels = arguments["iOSLocalizedLabels"] as! Bool - self.result = result - - let contactPicker = CNContactPickerViewController() - contactPicker.delegate = self - //contactPicker!.displayedPropertyKeys = [CNContactPhoneNumbersKey]; - DispatchQueue.main.async { - self.rootViewController.present(contactPicker, animated: true, completion: nil) - } - } - - //MARK:- CNContactPickerDelegate Method - - public func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) { - if let result = self.result { - result(contactToDictionary(contact: contact, localizedLabels: localizedLabels)) - self.result = nil - } - } - - public func contactPickerDidCancel(_ picker: CNContactPickerViewController) { - if let result = self.result { - result(SwiftContactosPlugin.FORM_OPERATION_CANCELED) - self.result = nil - } - } - - - func deleteContact(dictionary : [String:Any]) -> Bool{ - guard let identifier = dictionary["identifier"] as? String else{ - return false; - } - let store = CNContactStore() - let keys = [CNContactIdentifierKey as NSString] - do{ - if let contact = try store.unifiedContact(withIdentifier: identifier, keysToFetch: keys).mutableCopy() as? CNMutableContact{ - let request = CNSaveRequest() - request.delete(contact) - try store.execute(request) - } - } - catch{ - print(error.localizedDescription) - return false; - } - return true; - } - - func updateContact(dictionary : [String:Any]) -> Bool{ - - // Check to make sure dictionary has an identifier - guard let identifier = dictionary["identifier"] as? String else{ - return false; - } - - let store = CNContactStore() - let keys = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), - CNContactEmailAddressesKey, - CNContactPhoneNumbersKey, - CNContactFamilyNameKey, - CNContactGivenNameKey, - CNContactMiddleNameKey, - CNContactNamePrefixKey, - CNContactNameSuffixKey, - CNContactPostalAddressesKey, - CNContactOrganizationNameKey, - CNContactImageDataKey, - CNContactJobTitleKey] as [Any] - do { - // Check if the contact exists - if let contact = try store.unifiedContact(withIdentifier: identifier, keysToFetch: keys as! [CNKeyDescriptor]).mutableCopy() as? CNMutableContact{ - - /// Update the contact that was retrieved from the store - //Simple fields - contact.givenName = dictionary["givenName"] as? String ?? "" - contact.familyName = dictionary["familyName"] as? String ?? "" - contact.middleName = dictionary["middleName"] as? String ?? "" - contact.namePrefix = dictionary["prefix"] as? String ?? "" - contact.nameSuffix = dictionary["suffix"] as? String ?? "" - contact.organizationName = dictionary["company"] as? String ?? "" - contact.jobTitle = dictionary["jobTitle"] as? String ?? "" - contact.imageData = (dictionary["avatar"] as? FlutterStandardTypedData)?.data - - //Phone numbers - if let phoneNumbers = dictionary["phones"] as? [[String:String]]{ - var updatedPhoneNumbers = [CNLabeledValue]() - for phone in phoneNumbers where phone["value"] != nil { - updatedPhoneNumbers.append(CNLabeledValue(label:getPhoneLabel(label: phone["label"]),value:CNPhoneNumber(stringValue: phone["value"]!))) - } - contact.phoneNumbers = updatedPhoneNumbers - } - - //Emails - if let emails = dictionary["emails"] as? [[String:String]]{ - var updatedEmails = [CNLabeledValue]() - for email in emails where nil != email["value"] { - let emailLabel = email["label"] ?? "" - updatedEmails.append(CNLabeledValue(label: getCommonLabel(label: emailLabel), value: email["value"]! as NSString)) - } - contact.emailAddresses = updatedEmails - } - - //Postal addresses - if let postalAddresses = dictionary["postalAddresses"] as? [[String:String]]{ - var updatedPostalAddresses = [CNLabeledValue]() - for postalAddress in postalAddresses{ - let newAddress = CNMutablePostalAddress() - newAddress.street = postalAddress["street"] ?? "" - newAddress.city = postalAddress["city"] ?? "" - newAddress.postalCode = postalAddress["postcode"] ?? "" - newAddress.country = postalAddress["country"] ?? "" - newAddress.state = postalAddress["region"] ?? "" - let label = postalAddress["label"] ?? "" - updatedPostalAddresses.append(CNLabeledValue(label: getCommonLabel(label: label), value: newAddress)) - } - contact.postalAddresses = updatedPostalAddresses - } - - // Attempt to update the contact - let request = CNSaveRequest() - request.update(contact) - try store.execute(request) - } - } - catch { - print(error.localizedDescription) - return false; - } - return true; - } - - func dictionaryToContact(dictionary : [String:Any]) -> CNMutableContact{ - let contact = CNMutableContact() - - //Simple fields - contact.givenName = dictionary["givenName"] as? String ?? "" - contact.familyName = dictionary["familyName"] as? String ?? "" - contact.middleName = dictionary["middleName"] as? String ?? "" - contact.namePrefix = dictionary["prefix"] as? String ?? "" - contact.nameSuffix = dictionary["suffix"] as? String ?? "" - contact.organizationName = dictionary["company"] as? String ?? "" - contact.jobTitle = dictionary["jobTitle"] as? String ?? "" - if let avatarData = (dictionary["avatar"] as? FlutterStandardTypedData)?.data { - contact.imageData = avatarData - } - - //Phone numbers - if let phoneNumbers = dictionary["phones"] as? [[String:String]]{ - for phone in phoneNumbers where phone["value"] != nil { - contact.phoneNumbers.append(CNLabeledValue(label:getPhoneLabel(label:phone["label"]),value:CNPhoneNumber(stringValue:phone["value"]!))) - } - } - - //Emails - if let emails = dictionary["emails"] as? [[String:String]]{ - for email in emails where nil != email["value"] { - let emailLabel = email["label"] ?? "" - contact.emailAddresses.append(CNLabeledValue(label:getCommonLabel(label: emailLabel), value:email["value"]! as NSString)) - } - } - - //Postal addresses - if let postalAddresses = dictionary["postalAddresses"] as? [[String:String]]{ - for postalAddress in postalAddresses{ - let newAddress = CNMutablePostalAddress() - newAddress.street = postalAddress["street"] ?? "" - newAddress.city = postalAddress["city"] ?? "" - newAddress.postalCode = postalAddress["postcode"] ?? "" - newAddress.country = postalAddress["country"] ?? "" - newAddress.state = postalAddress["region"] ?? "" - let label = postalAddress["label"] ?? "" - contact.postalAddresses.append(CNLabeledValue(label: getCommonLabel(label: label), value: newAddress)) - } - } - - //BIRTHDAY - if let birthday = dictionary["birthday"] as? String { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd" - let date = formatter.date(from: birthday)! - contact.birthday = Calendar.current.dateComponents([.year, .month, .day], from: date) - } - - return contact - } - - func contactToDictionary(contact: CNContact, localizedLabels: Bool) -> [String:Any]{ - - var result = [String:Any]() - - //Simple fields - result["identifier"] = contact.identifier - result["displayName"] = CNContactFormatter.string(from: contact, style: CNContactFormatterStyle.fullName) - result["givenName"] = contact.givenName - result["familyName"] = contact.familyName - result["middleName"] = contact.middleName - result["prefix"] = contact.namePrefix - result["suffix"] = contact.nameSuffix - result["company"] = contact.organizationName - result["jobTitle"] = contact.jobTitle - if contact.isKeyAvailable(CNContactThumbnailImageDataKey) { - if let avatarData = contact.thumbnailImageData { - result["avatar"] = FlutterStandardTypedData(bytes: avatarData) - } - } - if contact.isKeyAvailable(CNContactImageDataKey) { - if let avatarData = contact.imageData { - result["avatar"] = FlutterStandardTypedData(bytes: avatarData) - } - } - - //Phone numbers - var phoneNumbers = [[String:String]]() - for phone in contact.phoneNumbers{ - var phoneDictionary = [String:String]() - phoneDictionary["value"] = phone.value.stringValue - phoneDictionary["label"] = "other" - if let label = phone.label{ - phoneDictionary["label"] = localizedLabels ? CNLabeledValue.localizedString(forLabel: label) : getRawPhoneLabel(label); - } - phoneNumbers.append(phoneDictionary) - } - result["phones"] = phoneNumbers - - //Emails - var emailAddresses = [[String:String]]() - for email in contact.emailAddresses{ - var emailDictionary = [String:String]() - emailDictionary["value"] = String(email.value) - emailDictionary["label"] = "other" - if let label = email.label{ - emailDictionary["label"] = localizedLabels ? CNLabeledValue.localizedString(forLabel: label) : getRawCommonLabel(label); - } - emailAddresses.append(emailDictionary) - } - result["emails"] = emailAddresses - - //Postal addresses - var postalAddresses = [[String:String]]() - for address in contact.postalAddresses{ - var addressDictionary = [String:String]() - addressDictionary["label"] = "" - if let label = address.label{ - addressDictionary["label"] = localizedLabels ? CNLabeledValue.localizedString(forLabel: label) : getRawCommonLabel(label); - } - addressDictionary["street"] = address.value.street - addressDictionary["city"] = address.value.city - addressDictionary["postcode"] = address.value.postalCode - addressDictionary["region"] = address.value.state - addressDictionary["country"] = address.value.country - - postalAddresses.append(addressDictionary) - } - result["postalAddresses"] = postalAddresses - - //BIRTHDAY - if let birthday : Date = contact.birthday?.date { - let formatter = DateFormatter() - let year = Calendar.current.component(.year, from: birthday) - formatter.dateFormat = year == 1 ? "--MM-dd" : "yyyy-MM-dd"; - result["birthday"] = formatter.string(from: birthday) - } - - return result - } - - func getPhoneLabel(label: String?) -> String{ - let labelValue = label ?? "" - switch(labelValue){ - case "main": return CNLabelPhoneNumberMain - case "mobile": return CNLabelPhoneNumberMobile - case "iPhone": return CNLabelPhoneNumberiPhone - case "work": return CNLabelWork - case "home": return CNLabelHome - case "other": return CNLabelOther - default: return labelValue - } - } - - func getCommonLabel(label:String?) -> String{ - let labelValue = label ?? "" - switch(labelValue){ - case "work": return CNLabelWork - case "home": return CNLabelHome - case "other": return CNLabelOther - default: return labelValue - } - } - - func getRawPhoneLabel(_ label: String?) -> String{ - let labelValue = label ?? "" - switch(labelValue){ - case CNLabelPhoneNumberMain: return "main" - case CNLabelPhoneNumberMobile: return "mobile" - case CNLabelPhoneNumberiPhone: return "iPhone" - case CNLabelWork: return "work" - case CNLabelHome: return "home" - case CNLabelOther: return "other" - default: return labelValue - } - } - - func getRawCommonLabel(_ label: String?) -> String{ - let labelValue = label ?? "" - switch(labelValue){ - case CNLabelWork: return "work" - case CNLabelHome: return "home" - case CNLabelOther: return "other" - default: return labelValue - } - } - -} diff --git a/contactos/ios/contactos/Sources/contactos/Resources/PrivacyInfo.xcprivacy b/contactos/ios/contactos/Sources/contactos/Resources/PrivacyInfo.xcprivacy deleted file mode 100644 index 2d00b69..0000000 --- a/contactos/ios/contactos/Sources/contactos/Resources/PrivacyInfo.xcprivacy +++ /dev/null @@ -1,25 +0,0 @@ - - - - - NSPrivacyTracking - - NSPrivacyTrackingDomains - - - NSPrivacyCollectedDataTypes - - - NSPrivacyAccessedAPITypes - - - NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryUserDefaults - NSPrivacyAccessedAPITypeReasons - - CA92.1 - - - - - \ No newline at end of file diff --git a/contactos/lib/src/contactos.dart b/contactos/lib/src/contactos.dart index fcd56d6..4132218 100644 --- a/contactos/lib/src/contactos.dart +++ b/contactos/lib/src/contactos.dart @@ -2,54 +2,51 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: sort_constructors_first + import 'package:contactos_platform_interface/contactos_platform_interface.dart'; import 'package:flutter/foundation.dart'; +/// {@template contactos} /// The iOS implementation of [ContactosPlatform]. /// /// This class implements the `package:contactos` functionality for iOS. +/// {@endtemplate} class Contactos extends ContactosPlatform { - Contactos._({@visibleForTesting MethodChannelContactos? channel}) - : channel = channel ?? MethodChannelContactos.instance, - super(); + /// Use [SharePlus.instance] to access the [share] method. + /// {@macro contactos} + Contactos._(this._platform); - /// Underlying channel-based implementation. - final MethodChannelContactos channel; + /// Platform interface + final ContactosPlatform _platform; + /// Singleton instance (instance API). static Contactos? _instance; - /// Returns the [Contactos] singleton instance. - /// Also registers this as the default platform implementation. - // ignore: prefer_constructors_over_static_methods - static Contactos get instance => _instance ??= Contactos._(); - - /// Registers this class as the default instance of [Contactos]. - static void registerWith() { - ContactosPlatform.instance = Contactos.instance; - } + /// The default instance of [Contactos]. + static final Contactos instance = + _instance ??= Contactos._(ContactosPlatform.instance); - @protected - @override - ContactosPlatform delegateFor({ - required MethodChannelContactos channel, - }) => - Contactos._(channel: channel); + /// Create a custom instance of [Contactos]. + /// Use this constructor for testing purposes only. + @visibleForTesting + factory Contactos.custom(ContactosPlatform platform) => Contactos._(platform); @override - Future addContact(Contact c) => channel.addContact(c); + Future addContact(Contact c) => _platform.addContact(c); @override - Future deleteContact(Contact c) => channel.deleteContact(c); + Future deleteContact(Contact c) => _platform.deleteContact(c); @override - Future updateContact(Contact c) => channel.updateContact(c); + Future updateContact(Contact c) => _platform.updateContact(c); @override Future getAvatar( Contact contact, { bool photoHighRes = true, }) => - channel.getAvatar(contact, photoHighRes: photoHighRes); + _platform.getAvatar(contact, photoHighRes: photoHighRes); @override Future> getContacts({ @@ -60,7 +57,7 @@ class Contactos extends ContactosPlatform { bool iOSLocalizedLabels = true, bool androidLocalizedLabels = true, }) => - channel.getContacts( + _platform.getContacts( query: query, withThumbnails: withThumbnails, photoHighResolution: photoHighResolution, @@ -78,7 +75,7 @@ class Contactos extends ContactosPlatform { bool iOSLocalizedLabels = true, bool androidLocalizedLabels = true, }) => - channel.getContactsForEmail( + _platform.getContactsForEmail( email, withThumbnails: withThumbnails, photoHighResolution: photoHighResolution, @@ -96,7 +93,7 @@ class Contactos extends ContactosPlatform { bool iOSLocalizedLabels = true, bool androidLocalizedLabels = true, }) => - channel.getContactsForPhone( + _platform.getContactsForPhone( phone, withThumbnails: withThumbnails, photoHighResolution: photoHighResolution, @@ -110,7 +107,7 @@ class Contactos extends ContactosPlatform { bool iOSLocalizedLabels = true, bool androidLocalizedLabels = true, }) => - channel.openContactForm( + _platform.openContactForm( iOSLocalizedLabels: iOSLocalizedLabels, androidLocalizedLabels: androidLocalizedLabels, ); @@ -120,7 +117,7 @@ class Contactos extends ContactosPlatform { bool iOSLocalizedLabels = true, bool androidLocalizedLabels = true, }) => - channel.openDeviceContactPicker( + _platform.openDeviceContactPicker( iOSLocalizedLabels: iOSLocalizedLabels, androidLocalizedLabels: androidLocalizedLabels, ); @@ -131,7 +128,7 @@ class Contactos extends ContactosPlatform { bool iOSLocalizedLabels = true, bool androidLocalizedLabels = true, }) => - channel.openExistingContact( + _platform.openExistingContact( contact, iOSLocalizedLabels: iOSLocalizedLabels, androidLocalizedLabels: androidLocalizedLabels, diff --git a/contactos/lib/src/contactos_legacy.dart b/contactos/lib/src/contactos_legacy.dart new file mode 100644 index 0000000..fcd56d6 --- /dev/null +++ b/contactos/lib/src/contactos_legacy.dart @@ -0,0 +1,139 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:contactos_platform_interface/contactos_platform_interface.dart'; +import 'package:flutter/foundation.dart'; + +/// The iOS implementation of [ContactosPlatform]. +/// +/// This class implements the `package:contactos` functionality for iOS. +class Contactos extends ContactosPlatform { + Contactos._({@visibleForTesting MethodChannelContactos? channel}) + : channel = channel ?? MethodChannelContactos.instance, + super(); + + /// Underlying channel-based implementation. + final MethodChannelContactos channel; + + static Contactos? _instance; + + /// Returns the [Contactos] singleton instance. + /// Also registers this as the default platform implementation. + // ignore: prefer_constructors_over_static_methods + static Contactos get instance => _instance ??= Contactos._(); + + /// Registers this class as the default instance of [Contactos]. + static void registerWith() { + ContactosPlatform.instance = Contactos.instance; + } + + @protected + @override + ContactosPlatform delegateFor({ + required MethodChannelContactos channel, + }) => + Contactos._(channel: channel); + + @override + Future addContact(Contact c) => channel.addContact(c); + + @override + Future deleteContact(Contact c) => channel.deleteContact(c); + + @override + Future updateContact(Contact c) => channel.updateContact(c); + + @override + Future getAvatar( + Contact contact, { + bool photoHighRes = true, + }) => + channel.getAvatar(contact, photoHighRes: photoHighRes); + + @override + Future> getContacts({ + String? query, + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + channel.getContacts( + query: query, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + @override + Future> getContactsForEmail( + String email, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + channel.getContactsForEmail( + email, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + @override + Future> getContactsForPhone( + String? phone, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + channel.getContactsForPhone( + phone, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + @override + Future openContactForm({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + channel.openContactForm( + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + @override + Future openDeviceContactPicker({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + channel.openDeviceContactPicker( + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + @override + Future openExistingContact( + Contact contact, { + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + channel.openExistingContact( + contact, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); +} diff --git a/contactos/pubspec.lock b/contactos/pubspec.lock index 377b8ba..a2519b1 100644 --- a/contactos/pubspec.lock +++ b/contactos/pubspec.lock @@ -41,14 +41,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + contactos_android: + dependency: "direct main" + description: + name: contactos_android + sha256: "9847cb2a2955c02e793dc9af803ec8130b458e6e95ec859b860431f38d7526a5" + url: "https://pub.dev" + source: hosted + version: "0.0.1" + contactos_foundation: + dependency: "direct main" + description: + name: contactos_foundation + sha256: "8dbcf1dd698e9776e12f23635b6a2b73fc9fcea936113b3079d6081f5d40f534" + url: "https://pub.dev" + source: hosted + version: "0.0.2" contactos_platform_interface: dependency: "direct main" description: name: contactos_platform_interface - sha256: "91cb1912e5b6f5126d43e1eb21a8385f837e6b637642449a526fc60038214529" + sha256: "6fc797fa685c84ef7e1ec8fc8c6aae1f9f09274bcc5ea26e435781a4fa907628" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" fake_async: dependency: transitive description: @@ -57,24 +73,47 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" l: dependency: "direct dev" description: @@ -111,10 +150,10 @@ packages: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.0.0" matcher: dependency: transitive description: @@ -147,6 +186,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -155,6 +202,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" sky_engine: dependency: transitive description: flutter @@ -192,6 +247,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" term_glyph: dependency: transitive description: @@ -224,6 +287,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.0" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" sdks: - dart: ">=3.8.0-0 <4.0.0" + dart: ">=3.8.0 <4.0.0" flutter: ">=3.29.3" diff --git a/contactos/pubspec.yaml b/contactos/pubspec.yaml index 41f98c6..71309b6 100644 --- a/contactos/pubspec.yaml +++ b/contactos/pubspec.yaml @@ -1,37 +1,62 @@ name: contactos -description: Android and iOS implementation of the contactos plugin. +description: Plugin to access and manage contacts on Android and iOS. + +version: 2.0.0-pre.1 + +homepage: https://github.com/ziqq/contactos + repository: https://github.com/ziqq/contactos/contactos + issue_tracker: https://github.com/ziqq/contactos/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+contactos%22 -version: 1.0.6 + +#screenshots: +# - description: 'Example of using the library to get contacts.' +# path: example.png + +funding: + - https://www.buymeacoffee.com/ziqq + - https://boosty.to/ziqq + +platforms: + android: + ios: + +topics: + - contacts + environment: sdk: '>=3.6.0 <4.0.0' flutter: ">=3.29.3" -flutter: - plugin: - implements: contactos - platforms: - android: - package: flutter.plugins.contactos - pluginClass: ContactosPlugin - dartPluginClass: Contactos - ios: - pluginClass: ContactosPlugin - dartPluginClass: Contactos dependencies: flutter: sdk: flutter - contactos_platform_interface: ^1.0.0 + contactos_android: ^0.0.1 + # contactos_android: + # path: ../contactos_android + + contactos_foundation: ^0.0.2 + # contactos_foundation: + # path: ../contactos_foundation + + contactos_platform_interface: ^1.0.1 + # contactos_platform_interface: + # path: ../contactos_platform_interface + dev_dependencies: + # Integration tests for Flutter + integration_test: + sdk: flutter + # Unit & Widget tests for Flutter flutter_test: sdk: flutter # Linting - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 # Utilities collection: ^1.19.1 @@ -39,5 +64,11 @@ dev_dependencies: # Logging l: ^5.0.0 -topics: - - contacts + +flutter: + plugin: + platforms: + android: + default_package: contactos_android + ios: + default_package: contactos_foundation \ No newline at end of file diff --git a/contactos/test/contactos_test.dart b/contactos/test/contactos_test.dart index bae9480..413c608 100644 --- a/contactos/test/contactos_test.dart +++ b/contactos/test/contactos_test.dart @@ -1,323 +1,14 @@ -import 'package:contactos/contactos.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() => group( - 'Unit_tests -', - () => group('Contactos -', () { - const channel = MethodChannel('github.com/ziqq/contactos'); - - final log = []; - - setUp(() { - TestWidgetsFlutterBinding.ensureInitialized(); - TestWidgetsFlutterBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - channel, - (methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'getContacts': - case 'getContactsForPhone': - case 'getContactsForEmail': - return [ - {'givenName': 'givenName1'}, - { - 'givenName': 'givenName2', - 'postalAddresses': [ - {'label': 'label'} - ], - 'emails': [ - {'label': 'label'} - ], - 'birthday': '1994-02-01' - }, - ]; - case 'getAvatar': - return Uint8List.fromList([0, 1, 2, 3]); - default: - return null; - } - }, - ); - }); - - tearDown(() { - log.clear(); - TestWidgetsFlutterBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, null); - }); - - test('should get contacts', () async { - final contacts = await Contactos.instance.getContacts(); - expect(contacts.length, 2); - expect(contacts, everyElement(isInstanceOf())); - expect(contacts[0].givenName, 'givenName1'); - expect(contacts[1].postalAddresses![0].label, 'label'); - expect(contacts[1].emails![0].label, 'label'); - expect(contacts[1].birthday, DateTime(1994, 2, 1)); - }); - - test('should get avatar for contact identifiers', () async { - const contact = Contact(givenName: 'givenName'); - - final avatar = await Contactos.instance.getAvatar(contact); - - expect(log, [ - isMethodCall('getAvatar', arguments: { - 'contact': contact.toJson(), - 'identifier': contact.identifier, - 'photoHighResolution': true, - }) - ]); - - expect(avatar, Uint8List.fromList([0, 1, 2, 3])); - }); - - group('getContactsForPhone', () { - test('returns empty list when no phone number specified', () async { - final contacts = await Contactos.instance.getContactsForPhone(null); - expect(contacts.length, equals(0)); - }); - - /// Tests whether phone number argument is not null and plugin call is - /// fired. - /// - /// Whether contact is returned or not depends on the plaform - /// implementation which cannot be tested in unit tests. - test('returns contacts if phone number supplied', () async { - final contacts = - await Contactos.instance.getContactsForPhone('1234567890'); - expect(contacts.length, equals(2)); - expect(contacts[0].givenName, 'givenName1'); - expect(contacts[1].givenName, 'givenName2'); - }); - }); - - group('getContactsForEmail', () { - /// Just tests whether the plugin call is fired. - test('returns contacts when email is supplied', () async { - final contacts = await Contactos.instance.getContactsForEmail( - 'abc@example.net', - ); - expect(contacts.length, equals(2)); - expect(contacts[0].givenName, 'givenName1'); - expect(contacts[1].givenName, 'givenName2'); - }); - }); +/* + * Author: Anton Ustinoff | + * Date: 26 November 2025 + */ - test('should get low-res avatar for contact identifiers', () async { - const contact = Contact(givenName: 'givenName'); - - await Contactos.instance.getAvatar(contact, photoHighRes: false); - - expect(log, [ - isMethodCall('getAvatar', arguments: { - 'contact': contact.toJson(), - 'identifier': contact.identifier, - 'photoHighResolution': false, - }) - ]); - }); - - test('should add contact', () async { - await Contactos.instance.addContact(const Contact( - givenName: 'givenName', - emails: [Contact$Field(label: 'label')], - phones: [Contact$Field(label: 'label')], - postalAddresses: [Contact$PostalAddress(label: 'label')], - )); - _expectMethodCall(log, 'addContact'); - }); - - test('should delete contact', () async { - await Contactos.instance.deleteContact(const Contact( - givenName: 'givenName', - emails: [Contact$Field(label: 'label')], - phones: [Contact$Field(label: 'label')], - postalAddresses: [Contact$PostalAddress(label: 'label')], - )); - _expectMethodCall(log, 'deleteContact'); - }); - - test('should provide initials for contact', () { - var contact1 = const Contact( - givenName: 'givenName', - familyName: 'familyName', - ); - var contact2 = const Contact(givenName: 'givenName'); - var contact3 = const Contact(familyName: 'familyName'); - var contact4 = const Contact(); - - expect(contact1.initials(), 'GF'); - expect(contact2.initials(), 'G'); - expect(contact3.initials(), 'F'); - expect(contact4.initials(), ''); - }); - - test('should update contact', () async { - await Contactos.instance.updateContact(const Contact( - givenName: 'givenName', - emails: [Contact$Field(label: 'label')], - phones: [Contact$Field(label: 'label')], - postalAddresses: [Contact$PostalAddress(label: 'label')], - )); - _expectMethodCall(log, 'updateContact'); - }); - - test('should show contacts are equal', () { - var contact1 = const Contact( - givenName: 'givenName', - familyName: 'familyName', - emails: [ - Contact$Field(label: 'Home', value: 'home@example.com'), - Contact$Field(label: 'Work', value: 'work@example.com'), - ], - ); - var contact2 = const Contact( - givenName: 'givenName', - familyName: 'familyName', - emails: [ - Contact$Field(label: 'Work', value: 'work@example.com'), - Contact$Field(label: 'Home', value: 'home@example.com'), - ], - ); - expect(contact1 == contact2, true); - expect(contact1.hashCode, contact2.hashCode); - }); - - test('should show contacts are not equal', () { - var contact1 = const Contact( - givenName: 'givenName', - familyName: 'familyName', - emails: [ - Contact$Field(label: 'Home', value: 'home@example.com'), - Contact$Field(label: 'Work', value: 'work@example.com'), - ]); - var contact2 = const Contact( - givenName: 'givenName', - familyName: 'familyName', - emails: [ - Contact$Field(label: 'Office', value: 'office@example.com'), - Contact$Field(label: 'Home', value: 'home@example.com'), - ]); - expect(contact1 == contact2, false); - expect(contact1.hashCode, contact2.hashCode); - }); - - test('should produce a valid merged contact', () { - var contact1 = const Contact( - givenName: 'givenName', - familyName: 'familyName', - emails: [ - Contact$Field(label: 'Home', value: 'home@example.com'), - Contact$Field(label: 'Work', value: 'work@example.com'), - ], - phones: [], - postalAddresses: []); - var contact2 = const Contact(familyName: 'familyName', phones: [ - Contact$Field(label: 'Mobile', value: '111-222-3344') - ], emails: [ - Contact$Field(label: 'Mobile', value: 'mobile@example.com'), - ], postalAddresses: [ - Contact$PostalAddress( - label: 'Home', - street: '1234 Middle-of Rd', - city: 'Nowhere', - postcode: '12345', - region: null, - country: null) - ]); - var mergedContact = const Contact( - givenName: 'givenName', - familyName: 'familyName', - emails: [ - Contact$Field(label: 'Home', value: 'home@example.com'), - Contact$Field(label: 'Mobile', value: 'mobile@example.com'), - Contact$Field(label: 'Work', value: 'work@example.com'), - ], - phones: [ - Contact$Field(label: 'Mobile', value: '111-222-3344') - ], - postalAddresses: [ - Contact$PostalAddress( - label: 'Home', - street: '1234 Middle-of Rd', - city: 'Nowhere', - postcode: '12345', - region: null, - country: null) - ]); - - expect(contact1 + contact2, mergedContact); - }); - - test('should provide a valid merged contact, with no extra info', () { - var contact1 = const Contact(familyName: 'familyName'); - var contact2 = const Contact(); - expect(contact1 + contact2, contact1); - }); +import 'package:flutter_test/flutter_test.dart'; - test('should provide a map of the contact', () { - var contact = - const Contact(givenName: 'givenName', familyName: 'familyName'); - expect(contact.toJson(), { - 'identifier': null, - 'displayName': null, - 'givenName': 'givenName', - 'middleName': null, - 'familyName': 'familyName', - 'prefix': null, - 'suffix': null, - 'company': null, - 'jobTitle': null, - 'androidAccountType': null, - 'androidAccountName': null, - 'emails': const [], - 'phones': const [], - 'postalAddresses': const [], - 'avatar': null, - 'birthday': null - }); - }); - }), - ); +import 'src//contactos_legacy_test.dart' as contactos_legacy_test; +import 'src/contactos_test.dart' as contactos_test; -void _expectMethodCall(List log, String methodName) { - expect(log, [ - isMethodCall( - methodName, - arguments: { - 'identifier': null, - 'displayName': null, - 'givenName': 'givenName', - 'middleName': null, - 'familyName': null, - 'prefix': null, - 'suffix': null, - 'company': null, - 'jobTitle': null, - 'androidAccountType': null, - 'androidAccountName': null, - 'emails': [ - {'label': 'label', 'value': null} - ], - 'phones': [ - {'label': 'label', 'value': null} - ], - 'postalAddresses': [ - { - 'label': 'label', - 'street': null, - 'city': null, - 'postcode': null, - 'region': null, - 'country': null - } - ], - 'avatar': null, - 'birthday': null - }, - ), - ]); -} +void main() => group('ContactosPlugin -', () { + contactos_legacy_test.main(); + contactos_test.main(); + }); diff --git a/contactos/test/src/contactos_legacy_test.dart b/contactos/test/src/contactos_legacy_test.dart new file mode 100644 index 0000000..6fda829 --- /dev/null +++ b/contactos/test/src/contactos_legacy_test.dart @@ -0,0 +1,319 @@ +import 'package:contactos/contactos.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() => group('ContactosLegacy -', () { + const channel = MethodChannel('github.com/ziqq/contactos'); + final log = []; + + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + TestWidgetsFlutterBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + channel, + (methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'getContacts': + case 'getContactsForPhone': + case 'getContactsForEmail': + return [ + {'givenName': 'givenName1'}, + { + 'givenName': 'givenName2', + 'postalAddresses': [ + {'label': 'label'} + ], + 'emails': [ + {'label': 'label'} + ], + 'birthday': '1994-02-01' + }, + ]; + case 'getAvatar': + return Uint8List.fromList([0, 1, 2, 3]); + default: + return null; + } + }, + ); + }); + + tearDown(() { + log.clear(); + TestWidgetsFlutterBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + test('should get contacts', () async { + final contacts = await Contactos.instance.getContacts(); + expect(contacts.length, 2); + expect(contacts, everyElement(isInstanceOf())); + expect(contacts[0].givenName, 'givenName1'); + expect(contacts[1].postalAddresses![0].label, 'label'); + expect(contacts[1].emails![0].label, 'label'); + expect(contacts[1].birthday, DateTime(1994, 2, 1)); + }); + + test('should get avatar for contact identifiers', () async { + const contact = Contact(givenName: 'givenName'); + + final avatar = await Contactos.instance.getAvatar(contact); + + expect(log, [ + isMethodCall('getAvatar', arguments: { + 'contact': contact.toJson(), + 'identifier': contact.identifier, + 'photoHighResolution': true, + }) + ]); + + expect(avatar, Uint8List.fromList([0, 1, 2, 3])); + }); + + group('getContactsForPhone', () { + test('returns empty list when no phone number specified', () async { + final contacts = await Contactos.instance.getContactsForPhone(null); + expect(contacts.length, equals(0)); + }); + + /// Tests whether phone number argument is not null and plugin call is + /// fired. + /// + /// Whether contact is returned or not depends on the plaform + /// implementation which cannot be tested in unit tests. + test('returns contacts if phone number supplied', () async { + final contacts = + await Contactos.instance.getContactsForPhone('1234567890'); + expect(contacts.length, equals(2)); + expect(contacts[0].givenName, 'givenName1'); + expect(contacts[1].givenName, 'givenName2'); + }); + }); + + group('getContactsForEmail', () { + /// Just tests whether the plugin call is fired. + test('returns contacts when email is supplied', () async { + final contacts = await Contactos.instance.getContactsForEmail( + 'abc@example.net', + ); + expect(contacts.length, equals(2)); + expect(contacts[0].givenName, 'givenName1'); + expect(contacts[1].givenName, 'givenName2'); + }); + }); + + test('should get low-res avatar for contact identifiers', () async { + const contact = Contact(givenName: 'givenName'); + + await Contactos.instance.getAvatar(contact, photoHighRes: false); + + expect(log, [ + isMethodCall('getAvatar', arguments: { + 'contact': contact.toJson(), + 'identifier': contact.identifier, + 'photoHighResolution': false, + }) + ]); + }); + + test('should add contact', () async { + await Contactos.instance.addContact(const Contact( + givenName: 'givenName', + emails: [Contact$Field(label: 'label')], + phones: [Contact$Field(label: 'label')], + postalAddresses: [Contact$PostalAddress(label: 'label')], + )); + _expectMethodCall(log, 'addContact'); + }); + + test('should delete contact', () async { + await Contactos.instance.deleteContact(const Contact( + givenName: 'givenName', + emails: [Contact$Field(label: 'label')], + phones: [Contact$Field(label: 'label')], + postalAddresses: [Contact$PostalAddress(label: 'label')], + )); + _expectMethodCall(log, 'deleteContact'); + }); + + test('should provide initials for contact', () { + var contact1 = const Contact( + givenName: 'givenName', + familyName: 'familyName', + ); + var contact2 = const Contact(givenName: 'givenName'); + var contact3 = const Contact(familyName: 'familyName'); + var contact4 = const Contact(); + + expect(contact1.initials(), 'GF'); + expect(contact2.initials(), 'G'); + expect(contact3.initials(), 'F'); + expect(contact4.initials(), ''); + }); + + test('should update contact', () async { + await Contactos.instance.updateContact(const Contact( + givenName: 'givenName', + emails: [Contact$Field(label: 'label')], + phones: [Contact$Field(label: 'label')], + postalAddresses: [Contact$PostalAddress(label: 'label')], + )); + _expectMethodCall(log, 'updateContact'); + }); + + test('should show contacts are equal', () { + var contact1 = const Contact( + givenName: 'givenName', + familyName: 'familyName', + emails: [ + Contact$Field(label: 'Home', value: 'home@example.com'), + Contact$Field(label: 'Work', value: 'work@example.com'), + ], + ); + var contact2 = const Contact( + givenName: 'givenName', + familyName: 'familyName', + emails: [ + Contact$Field(label: 'Work', value: 'work@example.com'), + Contact$Field(label: 'Home', value: 'home@example.com'), + ], + ); + expect(contact1 == contact2, true); + expect(contact1.hashCode, contact2.hashCode); + }); + + test('should show contacts are not equal', () { + var contact1 = const Contact( + givenName: 'givenName', + familyName: 'familyName', + emails: [ + Contact$Field(label: 'Home', value: 'home@example.com'), + Contact$Field(label: 'Work', value: 'work@example.com'), + ]); + var contact2 = const Contact( + givenName: 'givenName', + familyName: 'familyName', + emails: [ + Contact$Field(label: 'Office', value: 'office@example.com'), + Contact$Field(label: 'Home', value: 'home@example.com'), + ]); + expect(contact1 == contact2, false); + expect(contact1.hashCode, contact2.hashCode); + }); + + test('should produce a valid merged contact', () { + var contact1 = const Contact( + givenName: 'givenName', + familyName: 'familyName', + emails: [ + Contact$Field(label: 'Home', value: 'home@example.com'), + Contact$Field(label: 'Work', value: 'work@example.com'), + ], + phones: [], + postalAddresses: []); + var contact2 = const Contact(familyName: 'familyName', phones: [ + Contact$Field(label: 'Mobile', value: '111-222-3344') + ], emails: [ + Contact$Field(label: 'Mobile', value: 'mobile@example.com'), + ], postalAddresses: [ + Contact$PostalAddress( + label: 'Home', + street: '1234 Middle-of Rd', + city: 'Nowhere', + postcode: '12345', + region: null, + country: null) + ]); + var mergedContact = const Contact( + givenName: 'givenName', + familyName: 'familyName', + emails: [ + Contact$Field(label: 'Home', value: 'home@example.com'), + Contact$Field(label: 'Mobile', value: 'mobile@example.com'), + Contact$Field(label: 'Work', value: 'work@example.com'), + ], + phones: [ + Contact$Field(label: 'Mobile', value: '111-222-3344') + ], + postalAddresses: [ + Contact$PostalAddress( + label: 'Home', + street: '1234 Middle-of Rd', + city: 'Nowhere', + postcode: '12345', + region: null, + country: null) + ]); + + expect(contact1 + contact2, mergedContact); + }); + + test('should provide a valid merged contact, with no extra info', () { + var contact1 = const Contact(familyName: 'familyName'); + var contact2 = const Contact(); + expect(contact1 + contact2, contact1); + }); + + test('should provide a map of the contact', () { + var contact = + const Contact(givenName: 'givenName', familyName: 'familyName'); + expect(contact.toJson(), { + 'identifier': null, + 'displayName': null, + 'givenName': 'givenName', + 'middleName': null, + 'familyName': 'familyName', + 'prefix': null, + 'suffix': null, + 'company': null, + 'jobTitle': null, + 'androidAccountType': null, + 'androidAccountName': null, + 'emails': const [], + 'phones': const [], + 'postalAddresses': const [], + 'avatar': null, + 'birthday': null + }); + }); + }); + +void _expectMethodCall(List log, String methodName) { + expect(log, [ + isMethodCall( + methodName, + arguments: { + 'identifier': null, + 'displayName': null, + 'givenName': 'givenName', + 'middleName': null, + 'familyName': null, + 'prefix': null, + 'suffix': null, + 'company': null, + 'jobTitle': null, + 'androidAccountType': null, + 'androidAccountName': null, + 'emails': [ + {'label': 'label', 'value': null} + ], + 'phones': [ + {'label': 'label', 'value': null} + ], + 'postalAddresses': [ + { + 'label': 'label', + 'street': null, + 'city': null, + 'postcode': null, + 'region': null, + 'country': null + } + ], + 'avatar': null, + 'birthday': null + }, + ), + ]); +} diff --git a/contactos/test/src/contactos_test.dart b/contactos/test/src/contactos_test.dart new file mode 100644 index 0000000..7714603 --- /dev/null +++ b/contactos/test/src/contactos_test.dart @@ -0,0 +1,221 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:contactos/contactos.dart'; +import 'package:contactos_platform_interface/contactos_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockContactosPlatform extends ContactosPlatform + with MockPlatformInterfaceMixin { + List contacts = []; + Contact? lastAddedContact; + Contact? lastDeletedContact; + Contact? lastUpdatedContact; + Contact? lastAvatarContact; + bool? lastAvatarHighRes; + + @override + Future addContact(Contact c) async { + lastAddedContact = c; + } + + @override + Future deleteContact(Contact c) async { + lastDeletedContact = c; + } + + @override + Future updateContact(Contact c) async { + lastUpdatedContact = c; + } + + @override + Future getAvatar( + Contact contact, { + bool photoHighRes = true, + }) async { + lastAvatarContact = contact; + lastAvatarHighRes = photoHighRes; + return Uint8List.fromList([1, 2, 3]); + } + + @override + Future> getContacts({ + String? query, + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) async => + contacts; + + @override + Future> getContactsForEmail( + String email, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) async => + contacts + .where((c) => c.emails?.any((e) => e.value == email) ?? false) + .toList(); + + @override + Future> getContactsForPhone( + String? phone, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) async => + contacts + .where((c) => c.phones?.any((p) => p.value == phone) ?? false) + .toList(); + + @override + Future openContactForm({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) async => + const Contact(identifier: 'new_contact'); + + @override + Future openDeviceContactPicker({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) async => + const Contact(identifier: 'picked_contact'); + + @override + Future openExistingContact( + Contact contact, { + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) async => + contact; +} + +void main() { + group('Contactos -', () { + late MockContactosPlatform mockPlatform; + late Contactos contactos; + + setUp(() { + mockPlatform = MockContactosPlatform(); + contactos = Contactos.custom(mockPlatform); + }); + + group('instance -', () { + test('returns the default instance', () { + expect(Contactos.instance, isA()); + }); + }); + + group('addContact -', () { + test('delegates to platform', () async { + const contact = Contact(identifier: '1', displayName: 'Test'); + await contactos.addContact(contact); + expect(mockPlatform.lastAddedContact, contact); + }); + }); + + group('deleteContact -', () { + test('delegates to platform', () async { + const contact = Contact(identifier: '1', displayName: 'Test'); + await contactos.deleteContact(contact); + expect(mockPlatform.lastDeletedContact, contact); + }); + }); + + group('updateContact -', () { + test('delegates to platform', () async { + const contact = Contact(identifier: '1', displayName: 'Test'); + await contactos.updateContact(contact); + expect(mockPlatform.lastUpdatedContact, contact); + }); + }); + + group('getAvatar -', () { + test('delegates to platform', () async { + const contact = Contact(identifier: '1', displayName: 'Test'); + final result = await contactos.getAvatar(contact, photoHighRes: false); + expect(mockPlatform.lastAvatarContact, contact); + expect(mockPlatform.lastAvatarHighRes, false); + expect(result, isNotNull); + }); + }); + + group('getContacts -', () { + test('delegates to platform', () async { + mockPlatform.contacts = [ + const Contact(identifier: '1', displayName: 'Test') + ]; + final result = await contactos.getContacts(); + expect(result.length, 1); + expect(result.first.identifier, '1'); + }); + }); + + group('getContactsForEmail -', () { + test('delegates to platform', () async { + mockPlatform.contacts = [ + const Contact( + identifier: '1', + displayName: 'Test', + emails: [Contact$Field(value: 'test@example.com')], + ), + const Contact(identifier: '2', displayName: 'Test 2'), + ]; + final result = await contactos.getContactsForEmail('test@example.com'); + expect(result.length, 1); + expect(result.first.identifier, '1'); + }); + }); + + group('getContactsForPhone -', () { + test('delegates to platform', () async { + mockPlatform.contacts = [ + const Contact( + identifier: '1', + displayName: 'Test', + phones: [Contact$Field(value: '123456')], + ), + const Contact(identifier: '2', displayName: 'Test 2'), + ]; + final result = await contactos.getContactsForPhone('123456'); + expect(result.length, 1); + expect(result.first.identifier, '1'); + }); + }); + + group('openContactForm -', () { + test('delegates to platform', () async { + final result = await contactos.openContactForm(); + expect(result.identifier, 'new_contact'); + }); + }); + + group('openDeviceContactPicker -', () { + test('delegates to platform', () async { + final result = await contactos.openDeviceContactPicker(); + expect(result?.identifier, 'picked_contact'); + }); + }); + + group('openExistingContact -', () { + test('delegates to platform', () async { + const contact = Contact(identifier: '1', displayName: 'Test'); + final result = await contactos.openExistingContact(contact); + expect(result, contact); + }); + }); + }); +} diff --git a/contactos_android/tool/tag.dart b/contactos_android/tool/tag.dart new file mode 100644 index 0000000..b2ccab2 --- /dev/null +++ b/contactos_android/tool/tag.dart @@ -0,0 +1,76 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:l/l.dart'; + +Future _runGitCommand(List args) async { + final result = await Process.run('git', args); + if (result.exitCode == 0) return; + l.e('Error running git ${args.join(" ")}: ${result.stderr}'); + exit(1); +} + +/// dart run tool/tag.dart +void main() => runZonedGuarded( + () async { + // Check if there are any uncommitted or unpushed changes + final statusResult = + await Process.run('git', ['status', '--porcelain']); + if ((statusResult.stdout as String).trim().isNotEmpty) { + l.e('There are uncommitted changes.'); + exit(1); + } + final aheadResult = await Process.run( + 'git', ['rev-list', '--count', '--left-only', '@{u}...HEAD']); + if ((aheadResult.stdout as String).trim() != '0') { + l.e('There are unpushed changes.'); + exit(1); + } + + // Fetch pubspec.yaml file to get the version + final pubspecFile = File('pubspec.yaml'); + if (!pubspecFile.existsSync()) { + l.w('File pubspec.yaml not found.'); + exit(1); + } + final pubspecContent = pubspecFile.readAsLinesSync(); + final versionLine = pubspecContent + .firstWhereOrNull((line) => line.trim().startsWith('version:')); + if (versionLine == null || versionLine.isEmpty) { + l.e('Version not found in pubspec.yaml.'); + exit(1); + } + final version = versionLine.split(':')[1].trim(); + l.i('Found version: $version'); + + // Validate version format + final versionParts = version.split('.'); + if (versionParts.length != 3 || + versionParts.any((e) => int.tryParse(e) == null)) { + l.e('Invalid version format: $version'); + exit(1); + } + + final tagName = 'v$version'; // Tag format: v1.2.3 + + // Check if the tag already exists + final tagResult = await Process.run('git', ['tag', '-l', tagName]); + if (tagResult.stdout?.toString().trim() == tagName) { + l.e('Tag $tagName already exists.'); + exit(1); + } + + // Create tag + await _runGitCommand(['tag', tagName]); + l.i('Tag $tagName created.'); + + // Push tag + await _runGitCommand(['push', 'origin', tagName]); + l.i('Tag $tagName pushed to remote repository.'); + }, + (e, st) { + l.e('Unexpected error: $e', st); + exit(1); + }, + ); diff --git a/contactos_foundation/tool/tag.dart b/contactos_foundation/tool/tag.dart new file mode 100644 index 0000000..b2ccab2 --- /dev/null +++ b/contactos_foundation/tool/tag.dart @@ -0,0 +1,76 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:l/l.dart'; + +Future _runGitCommand(List args) async { + final result = await Process.run('git', args); + if (result.exitCode == 0) return; + l.e('Error running git ${args.join(" ")}: ${result.stderr}'); + exit(1); +} + +/// dart run tool/tag.dart +void main() => runZonedGuarded( + () async { + // Check if there are any uncommitted or unpushed changes + final statusResult = + await Process.run('git', ['status', '--porcelain']); + if ((statusResult.stdout as String).trim().isNotEmpty) { + l.e('There are uncommitted changes.'); + exit(1); + } + final aheadResult = await Process.run( + 'git', ['rev-list', '--count', '--left-only', '@{u}...HEAD']); + if ((aheadResult.stdout as String).trim() != '0') { + l.e('There are unpushed changes.'); + exit(1); + } + + // Fetch pubspec.yaml file to get the version + final pubspecFile = File('pubspec.yaml'); + if (!pubspecFile.existsSync()) { + l.w('File pubspec.yaml not found.'); + exit(1); + } + final pubspecContent = pubspecFile.readAsLinesSync(); + final versionLine = pubspecContent + .firstWhereOrNull((line) => line.trim().startsWith('version:')); + if (versionLine == null || versionLine.isEmpty) { + l.e('Version not found in pubspec.yaml.'); + exit(1); + } + final version = versionLine.split(':')[1].trim(); + l.i('Found version: $version'); + + // Validate version format + final versionParts = version.split('.'); + if (versionParts.length != 3 || + versionParts.any((e) => int.tryParse(e) == null)) { + l.e('Invalid version format: $version'); + exit(1); + } + + final tagName = 'v$version'; // Tag format: v1.2.3 + + // Check if the tag already exists + final tagResult = await Process.run('git', ['tag', '-l', tagName]); + if (tagResult.stdout?.toString().trim() == tagName) { + l.e('Tag $tagName already exists.'); + exit(1); + } + + // Create tag + await _runGitCommand(['tag', tagName]); + l.i('Tag $tagName created.'); + + // Push tag + await _runGitCommand(['push', 'origin', tagName]); + l.i('Tag $tagName pushed to remote repository.'); + }, + (e, st) { + l.e('Unexpected error: $e', st); + exit(1); + }, + ); From 4424595e01495c8aff136d393d24ba5af7c6ae8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 26 Nov 2025 12:44:33 +0400 Subject: [PATCH 29/35] chore(2.0.0): update --- .../example/lib/src/screens/contacts_list_screen.dart | 7 ++++++- .../lib/src/screens/navite_contacts_picker_screen.dart | 5 +++++ contactos/pubspec.lock | 2 +- contactos/pubspec.yaml | 3 +++ contactos/test/src/contactos_legacy_test.dart | 4 ++++ 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/contactos/example/lib/src/screens/contacts_list_screen.dart b/contactos/example/lib/src/screens/contacts_list_screen.dart index cda10b1..2f78c82 100644 --- a/contactos/example/lib/src/screens/contacts_list_screen.dart +++ b/contactos/example/lib/src/screens/contacts_list_screen.dart @@ -1,3 +1,8 @@ +/* + * Author: Anton Ustinoff | + * Date: 25 November 2025 + */ + import 'dart:developer'; import 'package:collection/collection.dart'; @@ -447,7 +452,7 @@ class _AddContactScreenState extends State { class _UpdateContactsPage extends StatefulWidget { const _UpdateContactsPage({ - @required this.contact, + required this.contact, super.key, // ignore: unused_element_parameter }); diff --git a/contactos/example/lib/src/screens/navite_contacts_picker_screen.dart b/contactos/example/lib/src/screens/navite_contacts_picker_screen.dart index 825e9da..5454dbf 100644 --- a/contactos/example/lib/src/screens/navite_contacts_picker_screen.dart +++ b/contactos/example/lib/src/screens/navite_contacts_picker_screen.dart @@ -1,3 +1,8 @@ +/* + * Author: Anton Ustinoff | + * Date: 26 November 2025 + */ + import 'dart:developer'; import 'package:contactos/contactos.dart'; diff --git a/contactos/pubspec.lock b/contactos/pubspec.lock index a2519b1..2f70280 100644 --- a/contactos/pubspec.lock +++ b/contactos/pubspec.lock @@ -195,7 +195,7 @@ packages: source: hosted version: "3.1.6" plugin_platform_interface: - dependency: transitive + dependency: "direct dev" description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" diff --git a/contactos/pubspec.yaml b/contactos/pubspec.yaml index 71309b6..8fa9147 100644 --- a/contactos/pubspec.yaml +++ b/contactos/pubspec.yaml @@ -55,6 +55,9 @@ dev_dependencies: flutter_test: sdk: flutter + # Testsing + plugin_platform_interface: ^2.1.8 + # Linting flutter_lints: ^6.0.0 diff --git a/contactos/test/src/contactos_legacy_test.dart b/contactos/test/src/contactos_legacy_test.dart index 6fda829..3775024 100644 --- a/contactos/test/src/contactos_legacy_test.dart +++ b/contactos/test/src/contactos_legacy_test.dart @@ -1,3 +1,7 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'package:contactos/contactos.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; From 9dd87ce103495753a4c263fd46679c14d2027366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 4 Mar 2026 12:18:50 +0400 Subject: [PATCH 30/35] chore: update `flutter` version --- .fvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.fvmrc b/.fvmrc index 07114cc..1a01499 100644 --- a/.fvmrc +++ b/.fvmrc @@ -2,6 +2,6 @@ "flutter": "stable", "flavors": { "development": "stable", - "production": "3.29.3" + "production": "3.41.2" } } \ No newline at end of file From a23f4c6e7a22cd9e99c544a7df2fbe0849a639d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 4 Mar 2026 12:21:25 +0400 Subject: [PATCH 31/35] feat: add copilot instructions --- .github/copilot-instructions.md | 120 ++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..b47da46 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,120 @@ +# AI rules for contactos package + +This is a **Flutter package** (not an app). The package is used to access and manage the device's contacts. The codebase includes platform-specific implementations for Android and iOS, as well as a common Dart interface. The package also includes example usage and documentation. + +**Important**: Before doing anything, always try to clarify everything possible, highlight weak points, corner cases that were not taken into account, and ask a lot of clarifying questions. + + +## Lint Rules +Include the package in the `analysis_options.yaml` file. Use the following +analysis_options.yaml file as a starting point: + +```yaml +include: package:flutter_lints/flutter.yaml + +linter: + rules: + # Add additional lint rules here: + # avoid_print: false + # prefer_single_quotes: true +``` + + +## Code Generation +* **Build Runner:** If the project uses code generation, ensure that + `build_runner` is listed as a dev dependency in `pubspec.yaml`. +* **Running Build Runner:** After modifying files that require code generation, + run the build command: + ```shell + dart run build_runner build --delete-conflicting-outputs --release + ``` + + +## Testing +* **Running Tests:** To run tests, use the `run_tests` tool if it is available, + otherwise use `flutter test`. +* **Unit Tests:** Use `package:test` for unit tests. +* **Widget Tests:** Use `package:flutter_test` for widget tests. +* **Integration Tests:** Use `package:integration_test` for integration tests. +* **Assertions:** Prefer using `package:checks` for more expressive and readable + assertions over the default `matchers`. + + +### Testing Best practices +* **Convention:** Follow the Arrange-Act-Assert (or Given-When-Then) pattern. +* **Unit Tests:** Write unit tests for domain logic, data layer, and state + management. +* **Widget Tests:** Write widget tests for UI components. +* **Integration Tests:** For broader application validation, use integration + tests to verify end-to-end user flows. +* **integration_test package:** Use the `integration_test` package from the + Flutter SDK for integration tests. Add it as a `dev_dependency` in + `pubspec.yaml` by specifying `sdk: flutter`. +* **Mocks:** Prefer fakes or stubs over mocks. If mocks are absolutely + necessary, use `mockito` or `mocktail` to create mocks for dependencies. While + code generation is common for state management (e.g., with `freezed`), try to + avoid it for mocks. +* **Coverage:** Aim for high test coverage. + + +## Documentation +* **`dartdoc`:** Write `dartdoc`-style comments for all public APIs. + +### Documentation Philosophy +* **Comment wisely:** Use comments to explain why the code is written a certain + way, not what the code does. The code itself should be self-explanatory. +* **Document for the user:** Write documentation with the reader in mind. If you + had a question and found the answer, add it to the documentation where you + first looked. This ensures the documentation answers real-world questions. +* **No useless documentation:** If the documentation only restates the obvious + from the code's name, it's not helpful. Good documentation provides context + and explains what isn't immediately apparent. +* **Consistency is key:** Use consistent terminology throughout your + documentation. + +### Commenting Style +* **Use `///` for doc comments:** This allows documentation generation tools to + pick them up. +* **Start with a single-sentence summary:** The first sentence should be a + concise, user-centric summary ending with a period. +* **Separate the summary:** Add a blank line after the first sentence to create + a separate paragraph. This helps tools create better summaries. +* **Avoid redundancy:** Don't repeat information that's obvious from the code's + context, like the class name or signature. +* **Don't document both getter and setter:** For properties with both, only + document one. The documentation tool will treat them as a single field. +* **Important**: Don't delete comments, but feel free to add more if you think it would help the reader understand the code better. The goal is to make the code as clear and self-explanatory as possible, so that even someone new to Flutter could understand it. + +### Writing Style +* **Be brief:** Write concisely. +* **Avoid jargon and acronyms:** Don't use abbreviations unless they are widely + understood. +* **Use Markdown sparingly:** Avoid excessive markdown and never use HTML for + formatting. +* **Use backticks for code:** Enclose code blocks in backtick fences, and + specify the language. + +### What to Document +* **Public APIs are a priority:** Always document public APIs. +* **Consider private APIs:** It's a good idea to document private APIs as well. +* **Library-level comments are helpful:** Consider adding a doc comment at the + library level to provide a general overview. +* **Include code samples:** Where appropriate, add code samples to illustrate usage. +* **Explain parameters, return values, and exceptions:** Use prose to describe + what a function expects, what it returns, and what errors it might throw. +* **Place doc comments before annotations:** Documentation should come before + any metadata annotations. + +## Accessibility (A11Y) +Implement accessibility features to empower all users, assuming a wide variety +of users with different physical abilities, mental abilities, age groups, +education levels, and learning styles. + +* **Color Contrast:** Ensure text has a contrast ratio of at least **4.5:1** + against its background. +* **Dynamic Text Scaling:** Test your UI to ensure it remains usable when users + increase the system font size. +* **Semantic Labels:** Use the `Semantics` widget to provide clear, descriptive + labels for UI elements. +* **Screen Reader Testing:** Regularly test your app with TalkBack (Android) and + VoiceOver (iOS). \ No newline at end of file From cf1f58460bfda02274d02a2dc1cc8d886dada18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 4 Mar 2026 12:45:37 +0400 Subject: [PATCH 32/35] feat: added copilot instructions --- .github/copilot-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b47da46..0f32f3d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,4 +1,4 @@ -# AI rules for contactos package +# Contactos This is a **Flutter package** (not an app). The package is used to access and manage the device's contacts. The codebase includes platform-specific implementations for Android and iOS, as well as a common Dart interface. The package also includes example usage and documentation. From 9e2d5f2bff774a90fd55a48f95f8281cd8dc2ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 4 Mar 2026 12:45:52 +0400 Subject: [PATCH 33/35] feat: added cloud and agents files --- AGENTS.md | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 82 ++++++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..611c0d7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,129 @@ +# Contactos + +This is a **Flutter plugin** (not an app) for accessing and managing the device's contacts. It follows the [federated plugin architecture](https://flutter.dev/go/federated-plugins) with platform-specific implementations for Android and iOS, as well as a common Dart platform interface. + + +## Environment setup + +- **Flutter version**: managed via [FVM](https://fvm.app/) (see `.fvmrc`). Always prefix Flutter/Dart commands with `fvm`. + +```sh +fvm flutter pub get +``` + +To run the example app: + +```sh +cd contactos/example +fvm flutter run +``` + + +## Project Structure + +Full project tree: see `README.md` β†’ **Project Structure**. + + +## Build, test, and validate + +All commands use `make` targets. Always run `make get` first if dependencies might be stale. + +### Key commands (root level) + +```sh +make get # Get dependencies for all packages +make format # Format all packages (line length 80) +make analyze # Analyze all packages +make check # Analyze + pana for all packages +make test-unit # Run unit tests for all packages +make all # Full pipeline: format + check + test-unit +make precommit # Same as `make all` +``` + +### Per-package commands + +Each package (`contactos/`, `contactos_platform_interface/`, `contactos_android/`, `contactos_foundation/`) has its own `Makefile` with the same targets. Run from within the package directory: + +```sh +cd contactos && make all +cd contactos_platform_interface && make all +``` + +### Test structure + +Each package has a `test/` directory with unit tests. The main test file matches the package name (e.g., `contactos_test.dart`). Use `make test-unit` to run tests with coverage. + + +## Key conventions + +### Do not edit + +- `**/generated/**`, `*.g.dart`, `*.gen.dart` β€” generated files. + +### Coding rules + +- **No `print()`** β€” use `dart:developer` (`dev.log`). +- **No `dynamic`** in JSON parsing β€” use pattern matching + `switch`. Errors β†’ `FormatException`. +- **Immutability**: models are immutable, use `copyWith`, `const` constructors where possible. +- **Platform interface**: extend `ContactosPlatform`, do **not** implement it. +- **Method channels**: use `MethodChannelContactos` for platform communication. +- **Dart format**: line length **80**, enforced via `make format`. +- **Linting**: `flutter_lints` with strict casts, strict raw types, strict inference. + +### Branching and commits + +- Branch: `author/github-/` or `author//`. +- Commits: Conventional Commits β€” `(github-): `. +- PR title: same pattern. Always run `make precommit` before submitting. + + +## Configuration files + +| File | Purpose | +|---|---| +| `/pubspec.yaml` | Package dependencies and metadata | +| `/analysis_options.yaml` | Lint rules (`flutter_lints` + custom), analyzer excludes | +| `/Makefile` | Per-package build/test/format/analyze targets | +| `.fvmrc` | FVM Flutter version configuration | +| `Makefile` | Root-level orchestration targets | + + +## Agent behavior expectations + +Before doing anything: +1. **Clarify** everything unclear β€” ask clarifying questions. +2. **Highlight** weak points, corner cases that were not accounted for. +3. Read `README.md` for project overview and usage. + + +## Critical rules + +- Before substantial work, read `CLAUDE.md` and `AGENTS.md` for full conventions. +- Never edit: `**/generated/**`, `*.g.dart`, `*.gen.dart`. +- When you've made a major change and completed a task, update the patch version in the relevant `pubspec.yaml` and add a note to the corresponding `CHANGELOG.md`. + + +## Before writing code + +For trivial fixes (typos, one-line changes, simple renames), skip discussion and just do it. + +For anything non-trivial, do NOT start implementation until all open questions are resolved. First: + +1. **Challenge the approach** β€” point out flaws, missed edge cases, and risks. Be direct. +2. **Ask about unknowns** β€” if anything is ambiguous, ask. Do not guess or assume. +3. **Propose alternatives** β€” if there is a simpler or more robust way, say so and explain why. +4. **List edge cases** β€” enumerate what can break. +5. **Wait for confirmation** β€” do not write code until the user explicitly approves the plan. + +Do only what was asked. Do not refactor surrounding code, add comments to code you did not change, or introduce abstractions for hypothetical future needs. + + +## After writing code + +Do not consider a task done until verified: + +- `make format` β€” no formatting issues +- `make analyze` β€” no analyzer warnings +- `make test-unit` β€” all tests pass + +If tests or analysis fail, fix the issue before reporting completion. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cdef374 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# Contactos + +A Flutter plugin for accessing and managing the device's contacts. Federated plugin monorepo. + +## Structure + +``` +contactos/ # App-facing package (what users depend on) +contactos_platform_interface/ # Platform interface (abstract contract + types) +contactos_android/ # Android implementation +contactos_foundation/ # iOS (Darwin) implementation +``` + +- `contactos/lib/src/contactos.dart` β€” `Contactos` class, delegates to `ContactosPlatform` +- `contactos_platform_interface/lib/src/` β€” `ContactosPlatform`, `MethodChannelContactos`, `Contact`, `Contact$Field`, `Contact$PostalAddress` +- `contactos_android/lib/src/` β€” `ContactosPluginAndroid` extends `ContactosPlatform` +- `contactos_foundation/lib/src/` β€” `ContactosPluginFoundation` extends `ContactosPlatform` + +## Key Commands + +```bash +# Setup +make get # Get dependencies for all packages + +# Build & validate +make all # Full pipeline: format + check + test-unit +make precommit # Same as `make all` +make format # Format all packages (line length 80) +make analyze # Analyze all packages +make check # Analyze + pana for all packages +make test-unit # Unit tests for all packages +``` + +### Per-package + +```bash +cd contactos && make all +cd contactos_platform_interface && make all +cd contactos_android && make all +cd contactos_foundation && make all +``` + +## Conventions + +- **Commits**: Conventional Commits β€” `(github-): ` +- **Flutter version**: managed via FVM (see `.fvmrc`). Always prefix commands with `fvm` +- **Dart format**: line length **80**, enforced by `make format` +- **No `print()`** β€” use `dart:developer` (`dev.log`) +- **No `dynamic`** in JSON parsing β€” pattern matching + `switch`, errors β†’ `FormatException` +- **Immutability**: models are immutable, use `copyWith`, `const` constructors +- **Platform interface**: extend `ContactosPlatform`, do **not** implement it + +## Critical Rules + +- Before substantial work, read `CLAUDE.md` and `AGENTS.md` for full conventions. +- Never edit: `**/generated/**`, `*.g.dart`, `*.gen.dart`. +- When you've made a major change and completed a task, update the patch version in the relevant `pubspec.yaml` (e.g. `0.1.0` β†’ `0.1.1`) and add a note to the corresponding `CHANGELOG.md`. + +## Before Writing Code + +For trivial fixes (typos, one-line changes, simple renames), skip discussion and just do it. + +For anything non-trivial, do NOT start implementation until all open questions are resolved. First: + +1. **Challenge the approach** β€” point out flaws, missed edge cases, and risks. Be direct. +2. **Ask about unknowns** β€” if anything is ambiguous, ask. Do not guess or assume. +3. **Propose alternatives** β€” if there is a simpler or more robust way, say so and explain why. +4. **List edge cases** β€” enumerate what can break. +5. **Wait for confirmation** β€” do not write code until the user explicitly approves the plan. + +Do only what was asked. Do not refactor surrounding code, add comments to code you did not change, or introduce abstractions for hypothetical future needs. + +## After Writing Code + +Do not consider a task done until verified: + +- `make format` β€” no formatting issues +- `make analyze` β€” no analyzer warnings +- `make test-unit` β€” all tests pass + +If tests or analysis fail, fix the issue before reporting completion. + From 97c87bbd732180429792e993392b184dbf012bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 4 Mar 2026 12:46:21 +0400 Subject: [PATCH 34/35] chore: update --- Makefile | 84 +++++++++-------- README.md | 104 ++++++++++++++++++---- contactos/CHANGELOG.md | 4 +- contactos/Makefile | 2 +- contactos/README.md | 24 +++-- contactos/example/pubspec.lock | 20 ++--- contactos/pubspec.lock | 18 ++-- contactos/pubspec.yaml | 2 +- contactos_android/CHANGELOG.md | 2 + contactos_android/Makefile | 2 +- contactos_android/README.md | 20 ++++- contactos_android/example/pubspec.lock | 16 ++-- contactos_android/pubspec.lock | 18 ++-- contactos_foundation/CHANGELOG.md | 2 + contactos_foundation/Makefile | 2 +- contactos_foundation/README.md | 20 ++++- contactos_foundation/example/pubspec.lock | 16 ++-- contactos_foundation/pubspec.lock | 18 ++-- contactos_platform_interface/CHANGELOG.md | 2 + contactos_platform_interface/README.md | 17 +++- contactos_platform_interface/pubspec.lock | 18 ++-- 21 files changed, 267 insertions(+), 144 deletions(-) diff --git a/Makefile b/Makefile index 4819d91..00f7ea8 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,12 @@ SHELL :=/bin/bash -e -o pipefail PWD :=$(shell pwd) +# All packages in dependency order +PACKAGES := contactos_platform_interface contactos_android contactos_foundation contactos + .DEFAULT_GOAL := all .PHONY: all -all: ## build pipeline +all: ## Full pipeline: format + check + test-unit all: format check test-unit .PHONY: ci @@ -11,12 +14,12 @@ ci: ## CI build pipeline ci: all .PHONY: precommit -precommit: ## validate the branch before commit +precommit: ## Validate the branch before commit precommit: all .PHONY: help help: ## Help dialog - @echo 'Usage: make ' + @echo 'Usage: make ' @echo '' @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' @@ -29,55 +32,66 @@ version: ## Check flutter version @fvm flutter --version .PHONY: format -format: ## Format code - @fvm dart format -l 80 lib test || (echo "Β―\_(ツ)_/Β― Format code error"; exit 1) +format: ## Format all packages + @for pkg in $(PACKAGES); do \ + echo "Formatting $$pkg..."; \ + cd $(PWD)/$$pkg && fvm dart format -l 80 lib test || (echo "Β―\_(ツ)_/Β― Format $$pkg error"; exit 1); \ + done .PHONY: fix -fix: format ## Fix code - @fvm dart fix --apply lib +fix: ## Fix all packages + @for pkg in $(PACKAGES); do \ + echo "Fixing $$pkg..."; \ + cd $(PWD)/$$pkg && fvm dart fix --apply lib || (echo "Β―\_(ツ)_/Β― Fix $$pkg error"; exit 1); \ + done .PHONY: clean-cache clean-cache: ## Clean the pub cache @fvm flutter pub cache repair .PHONY: clean -clean: ## Clean flutter - @fvm flutter clean +clean: ## Clean all packages + @for pkg in $(PACKAGES); do \ + echo "Cleaning $$pkg..."; \ + cd $(PWD)/$$pkg && fvm flutter clean || true; \ + done .PHONY: get -get: ## Get dependencies - @cd contactos && fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos dependencies error"; exit 1) - @cd contactos_platform_interface && fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos_platform_interface dependencies error"; exit 2) +get: ## Get dependencies for all packages + @for pkg in $(PACKAGES); do \ + echo "Getting dependencies for $$pkg..."; \ + cd $(PWD)/$$pkg && fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get $$pkg dependencies error"; exit 1); \ + done .PHONY: analyze -analyze: get format ## Analyze code - @fvm dart analyze --fatal-infos --fatal-warnings +analyze: get ## Analyze all packages + @for pkg in $(PACKAGES); do \ + echo "Analyzing $$pkg..."; \ + cd $(PWD)/$$pkg && fvm dart analyze --fatal-infos --fatal-warnings || (echo "Β―\_(ツ)_/Β― Analyze $$pkg error"; exit 1); \ + done .PHONY: check -check: analyze ## Check the code - @dart pub global activate pana - @pana --json --no-warning --line-length 80 > log.pana.json +check: analyze ## Analyze + pana for all packages + @fvm dart pub global deactivate pana > /dev/null 2>&1 || true + @fvm dart pub global activate pana + @for pkg in $(PACKAGES); do \ + echo "Running pana for $$pkg..."; \ + cd $(PWD)/$$pkg && fvm dart pub global run pana --json > log.pana.json || (echo "Β―\_(ツ)_/Β― Pana $$pkg error"; exit 1); \ + done .PHONY: publish-check -publish-check: check ## Check the code before publish - @fvm dart pub publish --dry-run - -.PHONY: publish -publish: ## Publish package - @fvm dart pub publish --server=https://pub.dartlang.org || (echo "Β―\_(ツ)_/Β― Publish error"; exit 1) - -.PHONY: coverage -coverage: ## Runs get coverage - @lcov --summary coverage/lcov.info - -.PHONY: run-genhtml -run-genhtml: ## Runs generage coverage html - @genhtml coverage/lcov.info -o coverage/html +publish-check: ## Dry-run publish for all packages + @for pkg in $(PACKAGES); do \ + echo "Publish check $$pkg..."; \ + cd $(PWD)/$$pkg && fvm dart pub publish --dry-run || (echo "Β―\_(ツ)_/Β― Publish check $$pkg error"; exit 1); \ + done .PHONY: test-unit -test-unit: ## Runs unit tests - @fvm flutter test --coverage || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) - @genhtml coverage/lcov.info --output=coverage -o coverage/html || (echo "Β―\_(ツ)_/Β― Error while running genhtml with coverage"; exit 2) +test-unit: ## Run unit tests for all packages + @for pkg in $(PACKAGES); do \ + echo "Testing $$pkg..."; \ + cd $(PWD)/$$pkg && fvm flutter test --coverage || (echo "Β―\_(ツ)_/Β― Test $$pkg error"; exit 1); \ + done .PHONY: tag tag: ## Add a tag to the current commit @@ -96,7 +110,7 @@ tag-add: ## Add TAG. E.g: make tag-add TAG=v1.0.0 @echo "" .PHONY: tag-remove -tag-remove: ## Delete TAG. E.g: make tag-delete TAG=v1.0.0 +tag-remove: ## Delete TAG. E.g: make tag-remove TAG=v1.0.0 @if [ -z "$(TAG)" ]; then echo "Β―\_(ツ)_/Β― TAG is not set"; exit 1; fi @echo "" @echo "START REMOVING TAG: $(TAG)" diff --git a/README.md b/README.md index 22a5ee3..614aa38 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,71 @@ # Contactos Plugin for Flutter -[![pub package](https://img.shields.io/pub/v/contactos.svg)](https://pub.dartlang.org/packages/contactos) +[![pub package](https://img.shields.io/pub/v/contactos.svg)](https://pub.dev/packages/contactos) [![codecov](https://codecov.io/gh/ziqq/contactos/graph/badge.svg?token=S5CVNZKDAE)](https://codecov.io/gh/ziqq/contactos) [![style: flutter lints](https://img.shields.io/badge/style-flutter__lints-blue)](https://pub.dev/packages/flutter_lints) -## Description +## Description A Flutter plugin to access and manage the device's contacts. +This plugin uses the [federated plugin architecture](https://flutter.dev/go/federated-plugins). The main packages are: - +| Package | Description | Pub | +|---|---|---| +| [`contactos`](contactos/) | App-facing package | [![pub](https://img.shields.io/pub/v/contactos.svg)](https://pub.dev/packages/contactos) | +| [`contactos_platform_interface`](contactos_platform_interface/) | Platform interface | [![pub](https://img.shields.io/pub/v/contactos_platform_interface.svg)](https://pub.dev/packages/contactos_platform_interface) | +| [`contactos_android`](contactos_android/) | Android implementation | [![pub](https://img.shields.io/pub/v/contactos_android.svg)](https://pub.dev/packages/contactos_android) | +| [`contactos_foundation`](contactos_foundation/) | iOS implementation | [![pub](https://img.shields.io/pub/v/contactos_foundation.svg)](https://pub.dev/packages/contactos_foundation) | + + +## Package Structure + +The repository is a multi-package monorepo: + +``` +contactos/ +β”œβ”€β”€ contactos/ # App-facing package (what users depend on) +β”‚ β”œβ”€β”€ lib/ +β”‚ β”‚ β”œβ”€β”€ contactos.dart # Public API barrel file +β”‚ β”‚ └── src/ +β”‚ β”‚ β”œβ”€β”€ contactos.dart # Contactos class (delegates to platform) +β”‚ β”‚ └── contactos_legacy.dart. # Legacy API (deprecated) +β”‚ β”œβ”€β”€ example/ # Example app +β”‚ └── test/ +β”œβ”€β”€ contactos_platform_interface/ # Platform interface (abstract contract) +β”‚ β”œβ”€β”€ lib/ +β”‚ β”‚ β”œβ”€β”€ contactos_platform_interface.dart +β”‚ β”‚ └── src/ +β”‚ β”‚ β”œβ”€β”€ contactos_platform_interface.dart # ContactosPlatform abstract class +β”‚ β”‚ β”œβ”€β”€ method_channel_contactos.dart # MethodChannel implementation +β”‚ β”‚ └── types.dart # Contact, Contact$Field, Contact$PostalAddress, etc. +β”‚ └── test/ +β”œβ”€β”€ contactos_android/ # Android implementation +β”‚ β”œβ”€β”€ android/ # Native Android code (Java/Kotlin) +β”‚ β”œβ”€β”€ lib/ +β”‚ β”‚ └── src/ +β”‚ β”‚ └── contactos_android.dart # ContactosPluginAndroid +β”‚ └── test/ +β”œβ”€β”€ contactos_foundation/ # iOS (Darwin) implementation +β”‚ β”œβ”€β”€ darwin/ # Native iOS code (Swift/ObjC) +β”‚ β”œβ”€β”€ lib/ +β”‚ β”‚ └── src/ +β”‚ β”‚ └── contactos_foundation.dart # ContactosPluginFoundation +β”‚ └── test/ +β”œβ”€β”€ AGENTS.md # This file β€” agent conventions +β”œβ”€β”€ CLAUDE.md # Claude-specific instructions +β”œβ”€β”€ Makefile # Root-level make targets +└── .fvmrc # FVM Flutter version config +``` ## Installation -To use this plugin, add `contactos` as a [dependency in your `pubspec.yaml` file](https://flutter.io/platform-plugins/). +To use this plugin, add `contactos` as a [dependency in your `pubspec.yaml` file](https://flutter.dev/to/using-packages). For example: ```yaml dependencies: - contactos: ^1.0.6 + contactos: ^latest_version ``` @@ -39,7 +86,7 @@ Set the `NSContactsUsageDescription` in your `Info.plist` file. This app requires contacts access to function properly. ``` -And add PermissionGroup.contacts in your Podfile +And add PermissionGroup.contacts in your Podfile: ```Ruby target.build_configurations.each do |config| config.build_settings @@ -54,7 +101,7 @@ end **Note** -`contactos` does not handle the process of asking and checking for permissions. To check and request user permission to access contacts, try using the following plugins: [permission_handler](https://pub.dartlang.org/packages/permission_handler). +`contactos` does not handle the process of asking and checking for permissions. To check and request user permission to access contacts, try using the following plugins: [permission_handler](https://pub.dev/packages/permission_handler). If you do not request user permission or have it granted, the application will fail. For testing purposes, you can manually set the permissions for your test app in Settings for your app on the device that you are using. For Android, go to "Settings" - "Apps" - select your test app - "Permissions" - then turn "on" the slider for contacts. @@ -75,7 +122,7 @@ List contacts = await Contactos.instance.getContacts(withThumbnails: fa Uint8List avatar = await Contactos.instance.getAvatar(contact); // Get contacts matching a string. -List johns = await Contactos.instance.getContacts(query : "john"); +List johns = await Contactos.instance.getContacts(query: "john"); // Add a contact. // The contact must have a firstName / lastName to be successfully added. @@ -90,16 +137,15 @@ await Contactos.instance.deleteContact(contact); await Contactos.instance.updateContact(contact); // Usage of the native device form for creating a Contact. -// Throws a error if the Form could not be open or the Operation is canceled by the User. +// Throws an error if the form could not be opened or the operation is canceled by the user. await Contactos.instance.openContactForm(); // Usage of the native device form for editing a Contact. // The contact must have a valid identifier. -// Throws a error if the Form could not be open or the Operation is canceled by the User. +// Throws an error if the form could not be opened or the operation is canceled by the user. await Contactos.instance.openExistingContact(contact); - - ``` + **Contact Model** ```dart // Name @@ -121,12 +167,35 @@ List postalAddresses = []; Uint8List avatar; ``` -![Example](https://raw.githubusercontent.com/ziqq/contactos/refs/heads/main/.docs/example.gif "Example screenshot") +![Example](https://raw.githubusercontent.com/ziqq/contactos/refs/heads/main/.github/images/example.gif "Example screenshot") + + +## Development + +This project uses [FVM](https://fvm.app/) for Flutter version management. See `.fvmrc` for the configured version. + +```bash +# Get dependencies for all packages +make get + +# Full pipeline: format + check + test-unit +make all + +# Run before committing +make precommit +``` + +See each package's `Makefile` for per-package targets. ## Changelog -Refer to the [Changelog](https://github.com/ziqq/contactos/blob/main/CHANGELOG.md) to get all release notes. +Each package has its own changelog: + +- [contactos/CHANGELOG.md](contactos/CHANGELOG.md) +- [contactos_platform_interface/CHANGELOG.md](contactos_platform_interface/CHANGELOG.md) +- [contactos_android/CHANGELOG.md](contactos_android/CHANGELOG.md) +- [contactos_foundation/CHANGELOG.md](contactos_foundation/CHANGELOG.md) ## Maintainers @@ -143,7 +212,7 @@ Refer to the [Changelog](https://github.com/ziqq/contactos/blob/main/CHANGELOG.m Contributions are welcome! If you find a bug or want a feature, please fill an issue. -If you want to contribute code please create a pull request under the master branch. +If you want to contribute code please create a pull request. ## Funding @@ -151,10 +220,9 @@ If you want to contribute code please create a pull request under the master bra If you want to support the development of our library, there are several ways you can do it: - [Buy me a coffee](https://www.buymeacoffee.com/ziqq) -- [Support on Patreon](https://www.patreon.com/ziqq) - [Subscribe through Boosty](https://boosty.to/ziqq) -## Coverage +## Coverage - + diff --git a/contactos/CHANGELOG.md b/contactos/CHANGELOG.md index 6f3baf2..7bdf460 100644 --- a/contactos/CHANGELOG.md +++ b/contactos/CHANGELOG.md @@ -1,4 +1,6 @@ -## 2.0.0-pre.1 +# Changelog + +## 2.0.0 - **CHANGED**: Migrate to platform implementations ## 1.0.1 - 1.0.6 diff --git a/contactos/Makefile b/contactos/Makefile index e731423..d215fe2 100644 --- a/contactos/Makefile +++ b/contactos/Makefile @@ -46,7 +46,7 @@ clean: ## Clean flutter .PHONY: get get: ## Get dependencies - @fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos_platform_interface dependencies error"; exit 1) + @fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos dependencies error"; exit 1) .PHONY: analyze analyze: get format ## Analyze code diff --git a/contactos/README.md b/contactos/README.md index 75c30ff..1c16bbc 100644 --- a/contactos/README.md +++ b/contactos/README.md @@ -1,5 +1,5 @@ # Contactos Plugin for Flutter -[![pub package](https://img.shields.io/pub/v/contactos.svg)](https://pub.dartlang.org/packages/contactos) +[![pub package](https://img.shields.io/pub/v/contactos.svg)](https://pub.dev/packages/contactos) [![codecov](https://codecov.io/gh/ziqq/contactos/graph/badge.svg?token=S5CVNZKDAE)](https://codecov.io/gh/ziqq/contactos) [![style: flutter lints](https://img.shields.io/badge/style-flutter__lints-blue)](https://pub.dev/packages/flutter_lints) @@ -14,11 +14,11 @@ A Flutter plugin to access and manage the device's contacts. ## Installation -To use this plugin, add `contactos` as a [dependency in your `pubspec.yaml` file](https://flutter.io/platform-plugins/). +To use this plugin, add `contactos` as a [dependency in your `pubspec.yaml` file](https://flutter.dev/to/using-packages). For example: ```yaml dependencies: - contactos: + contactos: ^latest_version ``` @@ -54,7 +54,7 @@ end **Note** -`contactos` does not handle the process of asking and checking for permissions. To check and request user permission to access contacts, try using the following plugins: [permission_handler](https://pub.dartlang.org/packages/permission_handler). +`contactos` does not handle the process of asking and checking for permissions. To check and request user permission to access contacts, try using the following plugins: [permission_handler](https://pub.dev/packages/permission_handler). If you do not request user permission or have it granted, the application will fail. For testing purposes, you can manually set the permissions for your test app in Settings for your app on the device that you are using. For Android, go to "Settings" - "Apps" - select your test app - "Permissions" - then turn "on" the slider for contacts. @@ -75,7 +75,7 @@ List contacts = await Contactos.instance.getContacts(withThumbnails: fa Uint8List avatar = await Contactos.instance.getAvatar(contact); // Get contacts matching a string. -List johns = await Contactos.instance.getContacts(query : "john"); +List johns = await Contactos.instance.getContacts(query: "john"); // Add a contact. // The contact must have a firstName / lastName to be successfully added. @@ -90,15 +90,14 @@ await Contactos.instance.deleteContact(contact); await Contactos.instance.updateContact(contact); // Usage of the native device form for creating a Contact. -// Throws a error if the Form could not be open or the Operation is canceled by the User. +// Throws an error if the form could not be opened or the operation is canceled by the user. await Contactos.instance.openContactForm(); // Usage of the native device form for editing a Contact. // The contact must have a valid identifier. -// Throws a error if the Form could not be open or the Operation is canceled by the User. +// Throws an error if the form could not be opened or the operation is canceled by the user. await Contactos.instance.openExistingContact(contact); - ``` **Contact Model** ```dart @@ -126,7 +125,7 @@ Uint8List avatar; ## Changelog -Refer to the [Changelog](https://github.com/ziqq/contactos/blob/main/CHANGELOG.md) to get all release notes. +Refer to the [Changelog](https://github.com/ziqq/contactos/blob/main/contactos/CHANGELOG.md) to get all release notes. ## Maintainers @@ -142,7 +141,7 @@ Refer to the [Changelog](https://github.com/ziqq/contactos/blob/main/CHANGELOG.m ## Contributions Contributions are welcome! If you find a bug or want a feature, please fill an issue. -If you want to contribute code please create a pull request under the master branch. +If you want to contribute code please create a pull request. ## Funding @@ -150,10 +149,9 @@ If you want to contribute code please create a pull request under the master bra If you want to support the development of our library, there are several ways you can do it: - [Buy me a coffee](https://www.buymeacoffee.com/ziqq) -- [Support on Patreon](https://www.patreon.com/ziqq) - [Subscribe through Boosty](https://boosty.to/ziqq) -## Coverage +## Coverage - + diff --git a/contactos/example/pubspec.lock b/contactos/example/pubspec.lock index 8f6bbf7..65c6b70 100644 --- a/contactos/example/pubspec.lock +++ b/contactos/example/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -47,7 +47,7 @@ packages: path: ".." relative: true source: path - version: "2.0.0-pre.1" + version: "2.0.0" contactos_android: dependency: transitive description: @@ -162,18 +162,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -319,10 +319,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" vector_math: dependency: transitive description: @@ -356,5 +356,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=3.8.0-0 <4.0.0" + dart: ">=3.9.0-0 <4.0.0" flutter: ">=3.29.3" diff --git a/contactos/pubspec.lock b/contactos/pubspec.lock index 2f70280..2ef8d26 100644 --- a/contactos/pubspec.lock +++ b/contactos/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -158,18 +158,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -267,10 +267,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" vector_math: dependency: transitive description: @@ -296,5 +296,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=3.8.0 <4.0.0" + dart: ">=3.9.0-0 <4.0.0" flutter: ">=3.29.3" diff --git a/contactos/pubspec.yaml b/contactos/pubspec.yaml index 8fa9147..45cff4e 100644 --- a/contactos/pubspec.yaml +++ b/contactos/pubspec.yaml @@ -1,7 +1,7 @@ name: contactos description: Plugin to access and manage contacts on Android and iOS. -version: 2.0.0-pre.1 +version: 2.0.0 homepage: https://github.com/ziqq/contactos diff --git a/contactos_android/CHANGELOG.md b/contactos_android/CHANGELOG.md index 1e64072..5bb8d30 100644 --- a/contactos_android/CHANGELOG.md +++ b/contactos_android/CHANGELOG.md @@ -1,2 +1,4 @@ +# Changelog + ## 0.0.1 - **ADDED**: Initial release \ No newline at end of file diff --git a/contactos_android/Makefile b/contactos_android/Makefile index bb2b44c..9be9690 100644 --- a/contactos_android/Makefile +++ b/contactos_android/Makefile @@ -46,7 +46,7 @@ clean: ## Clean flutter .PHONY: get get: ## Get dependencies - @fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos_platform_interface dependencies error"; exit 1) + @fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos_android dependencies error"; exit 1) .PHONY: analyze analyze: get format ## Analyze code diff --git a/contactos_android/README.md b/contactos_android/README.md index 7ef9eeb..42cc88e 100644 --- a/contactos_android/README.md +++ b/contactos_android/README.md @@ -1,15 +1,27 @@ -# Contactos Android +# contactos_android -[![Don't depend on me](https://img.shields.io/badge/platform-internal-lightgrey.svg)](https://github.com/ziqq/contactos) +[![pub package](https://img.shields.io/pub/v/contactos_android.svg)](https://pub.dev/packages/contactos_android) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![style: flutter lints](https://img.shields.io/badge/style-flutter__lints-blue)](https://pub.dev/packages/flutter_lints) -This package is the Android implementation of the [`contactos`](https://github.com/ziqq/contactos/tree/main/contactos) plugin. +The Android implementation of the [`contactos`](https://pub.dev/packages/contactos) plugin. ## Usage -This package is not meant to be used directly. It is automatically included in your app when you depend on the main [`contactos`](https://github.com/ziqq/contactos/tree/main/contactos) package. +This package is not meant to be used directly. It is automatically included in your app when you depend on the main [`contactos`](https://pub.dev/packages/contactos) package. For more information on how to use the `contactos` plugin, please see the [main package's README](https://github.com/ziqq/contactos/blob/main/contactos/README.md). ## Contributing This is part of a federated plugin. Contributions to the platform-specific implementations are welcome. Please see the [main repository](https://github.com/ziqq/contactos) for more information on how to contribute. + + +## Maintainers + +[Anton Ustinoff (ziqq)](https://github.com/ziqq) + + +## License + +[MIT](https://github.com/ziqq/contactos/blob/main/LICENSE) diff --git a/contactos_android/example/pubspec.lock b/contactos_android/example/pubspec.lock index 5ae71a0..8a6ea2b 100644 --- a/contactos_android/example/pubspec.lock +++ b/contactos_android/example/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -162,18 +162,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -319,10 +319,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" vector_math: dependency: transitive description: diff --git a/contactos_android/pubspec.lock b/contactos_android/pubspec.lock index 5285f9e..4acdebf 100644 --- a/contactos_android/pubspec.lock +++ b/contactos_android/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -134,18 +134,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -243,10 +243,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" vector_math: dependency: transitive description: @@ -272,5 +272,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=3.8.0 <4.0.0" + dart: ">=3.9.0-0 <4.0.0" flutter: ">=3.29.3" diff --git a/contactos_foundation/CHANGELOG.md b/contactos_foundation/CHANGELOG.md index a471847..ef51eaa 100644 --- a/contactos_foundation/CHANGELOG.md +++ b/contactos_foundation/CHANGELOG.md @@ -1,3 +1,5 @@ +# Changelog + ## 0.0.2 - **CHANGED**: Package metadata diff --git a/contactos_foundation/Makefile b/contactos_foundation/Makefile index 0f16681..ee4c505 100644 --- a/contactos_foundation/Makefile +++ b/contactos_foundation/Makefile @@ -46,7 +46,7 @@ clean: ## Clean flutter .PHONY: get get: ## Get dependencies - @fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos_platform_interface dependencies error"; exit 1) + @fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos_foundation dependencies error"; exit 1) .PHONY: analyze analyze: get format ## Analyze code diff --git a/contactos_foundation/README.md b/contactos_foundation/README.md index cf92b57..efb636b 100644 --- a/contactos_foundation/README.md +++ b/contactos_foundation/README.md @@ -1,15 +1,27 @@ -# Contactos Foundation +# contactos_foundation -[![Don't depend on me](https://img.shields.io/badge/platform-internal-lightgrey.svg)](https://github.com/ziqq/contactos) +[![pub package](https://img.shields.io/pub/v/contactos_foundation.svg)](https://pub.dev/packages/contactos_foundation) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![style: flutter lints](https://img.shields.io/badge/style-flutter__lints-blue)](https://pub.dev/packages/flutter_lints) -This package is the Darwin (iOS and ~~macOS~~) implementation of the [`contactos`](https://github.com/ziqq/contactos/tree/main/contactos) plugin. +The iOS (Darwin) implementation of the [`contactos`](https://pub.dev/packages/contactos) plugin. ## Usage -This package is not meant to be used directly. It is automatically included in your app when you depend on the main [`contactos`](https://github.com/ziqq/contactos/tree/main/contactos) package. +This package is not meant to be used directly. It is automatically included in your app when you depend on the main [`contactos`](https://pub.dev/packages/contactos) package. For more information on how to use the `contactos` plugin, please see the [main package's README](https://github.com/ziqq/contactos/blob/main/contactos/README.md). ## Contributing This is part of a federated plugin. Contributions to the platform-specific implementations are welcome. Please see the [main repository](https://github.com/ziqq/contactos) for more information on how to contribute. + + +## Maintainers + +[Anton Ustinoff (ziqq)](https://github.com/ziqq) + + +## License + +[MIT](https://github.com/ziqq/contactos/blob/main/LICENSE) diff --git a/contactos_foundation/example/pubspec.lock b/contactos_foundation/example/pubspec.lock index ad961d7..fb39450 100644 --- a/contactos_foundation/example/pubspec.lock +++ b/contactos_foundation/example/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -146,18 +146,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -303,10 +303,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" vector_math: dependency: transitive description: diff --git a/contactos_foundation/pubspec.lock b/contactos_foundation/pubspec.lock index 5285f9e..4acdebf 100644 --- a/contactos_foundation/pubspec.lock +++ b/contactos_foundation/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -134,18 +134,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -243,10 +243,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" vector_math: dependency: transitive description: @@ -272,5 +272,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=3.8.0 <4.0.0" + dart: ">=3.9.0-0 <4.0.0" flutter: ">=3.29.3" diff --git a/contactos_platform_interface/CHANGELOG.md b/contactos_platform_interface/CHANGELOG.md index b700c2a..edff57b 100644 --- a/contactos_platform_interface/CHANGELOG.md +++ b/contactos_platform_interface/CHANGELOG.md @@ -1,3 +1,5 @@ +# Changelog + ## 1.0.1 - **ADDED**: `LICENSE` file to the package - **ADDED**: Tests diff --git a/contactos_platform_interface/README.md b/contactos_platform_interface/README.md index b5b42f4..e94a35e 100644 --- a/contactos_platform_interface/README.md +++ b/contactos_platform_interface/README.md @@ -1,8 +1,8 @@ # contactos_platform_interface [![pub package](https://img.shields.io/pub/v/contactos_platform_interface.svg)](https://pub.dev/packages/contactos_platform_interface) -[![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) -[![style: very_good_analysis](https://img.shields.io/badge/style-very_good_analysis-B22C11.svg)](https://pub.dev/packages/very_good_analysis) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![style: flutter lints](https://img.shields.io/badge/style-flutter__lints-blue)](https://pub.dev/packages/flutter_lints) A common platform interface for the [`contactos`](https://pub.dev/packages/contactos) plugin. @@ -37,4 +37,15 @@ class ContactosWindows extends ContactosPlatform { } // ... implement other methods -} \ No newline at end of file +} +``` + + +## Maintainers + +[Anton Ustinoff (ziqq)](https://github.com/ziqq) + + +## License + +[MIT](https://github.com/ziqq/contactos/blob/main/LICENSE) \ No newline at end of file diff --git a/contactos_platform_interface/pubspec.lock b/contactos_platform_interface/pubspec.lock index 83e7d69..7b98765 100644 --- a/contactos_platform_interface/pubspec.lock +++ b/contactos_platform_interface/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -111,18 +111,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: "direct main" description: @@ -196,10 +196,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" vector_math: dependency: transitive description: @@ -217,5 +217,5 @@ packages: source: hosted version: "15.0.0" sdks: - dart: ">=3.8.0 <4.0.0" + dart: ">=3.9.0-0 <4.0.0" flutter: ">=3.29.3" From 4a10f8876c6d155a67048783ac099a94aec4ed67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=98n=20Ustin=C3=98ff?= Date: Wed, 4 Mar 2026 13:00:07 +0400 Subject: [PATCH 35/35] chore: update `workflow` --- .github/workflows/checkout.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checkout.yml b/.github/workflows/checkout.yml index af3d328..b9c8816 100644 --- a/.github/workflows/checkout.yml +++ b/.github/workflows/checkout.yml @@ -73,7 +73,7 @@ jobs: - name: Format check working-directory: ${{ matrix.path }} - run: dart format --set-exit-if-changed -l 80 . + run: dart format -l 80 lib test - name: Analyze working-directory: ${{ matrix.path }}