From 4795c6d69bdee1cd91da58a2e83f39938e4909b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Apr 2026 16:41:00 +0000 Subject: [PATCH 1/2] Modernize to Kotlin Multiplatform with Compose Multiplatform sample Rewrite the Android-only contacts library as a KMP library targeting Android, iOS, JVM Desktop, and WasmJs. Replace AutoValue Java models with Kotlin data classes, swap RxJava for coroutines/Flow, and migrate from Groovy build scripts to Kotlin DSL with a version catalog. Key changes: - Kotlin 2.1.10 + KMP project structure (commonMain/androidMain/iosMain/desktopMain/wasmJsMain) - Data classes replace AutoValue, KontactRepository interface with suspend + Flow API - Android: full ContentProvider implementation, iOS: CNContacts implementation - Desktop and WasmJs: stub repositories (no standard contacts API) - Compose Multiplatform sample app replacing the old Android-only sample - Removed: RxJava modules, column adapters, support libraries, Bintray publishing https://claude.ai/code/session_01JnLE37sFhikvXkDVRVek7f --- build.gradle | 27 --- build.gradle.kts | 7 + gradle.properties | 32 +--- gradle/libs.versions.toml | 24 +++ kontact-rxjava/.gitignore | 1 - kontact-rxjava/build.gradle | 70 ------- kontact-rxjava/proguard-rules.pro | 25 --- kontact-rxjava/src/main/AndroidManifest.xml | 1 - .../kontact/rxjava/RxContactUtils.kt | 20 -- kontact-rxjava2/.gitignore | 1 - kontact-rxjava2/build.gradle | 74 -------- kontact-rxjava2/proguard-rules.pro | 25 --- kontact-rxjava2/src/main/AndroidManifest.xml | 1 - .../kontact/rxjava2/RxContactUtils.kt | 18 -- kontact/build.gradle | 78 -------- kontact/build.gradle.kts | 58 ++++++ .../{main => androidMain}/AndroidManifest.xml | 9 +- .../kontact/AndroidKontactRepository.kt | 174 ++++++++++++++++++ .../hackedcube/kontact/CursorExtensions.kt | 34 ++++ .../com/hackedcube/kontact/TypeMappers.kt | 76 ++++++++ .../hackedcube/kontact/KontactRepository.kt | 12 ++ .../kotlin/com/hackedcube/kontact/Models.kt | 96 ++++++++++ .../kontact/DesktopKontactRepository.kt | 22 +++ .../kontact/IosKontactRepository.kt | 113 ++++++++++++ .../com/hackedcube/kontact/EmailAddress.java | 50 ----- .../java/com/hackedcube/kontact/Event.java | 49 ----- .../java/com/hackedcube/kontact/Kontact.java | 138 -------------- .../java/com/hackedcube/kontact/Nickname.java | 52 ------ .../com/hackedcube/kontact/PhoneNumber.java | 69 ------- .../com/hackedcube/kontact/PostalAddress.java | 76 -------- .../java/com/hackedcube/kontact/Relation.java | 60 ------ .../com/hackedcube/kontact/ContactUtils.kt | 94 ---------- .../com/hackedcube/kontact/Extensions.kt | 20 -- .../columnadapters/BooleanIntAdapter.kt | 14 -- .../columnadapters/EmailTypeIntAdapter.kt | 19 -- .../columnadapters/EventTypeIntAdapter.kt | 19 -- .../columnadapters/NicknameTypeIntAdapter.kt | 19 -- .../columnadapters/PhoneTypeIntAdapter.kt | 18 -- .../PostalAddressTypeIntAdapter.kt | 18 -- .../columnadapters/RelationTypeIntAdapter.kt | 17 -- .../kontact/WasmKontactRepository.kt | 22 +++ sample/build.gradle | 38 ---- sample/build.gradle.kts | 89 +++++++++ .../{main => androidMain}/AndroidManifest.xml | 40 ++-- .../hackedcube/kontact/sample/MainActivity.kt | 30 +++ .../res/values/colors.xml | 11 +- .../res/values/strings.xml | 6 +- .../kontact/ExampleInstrumentedTest.java | 26 --- .../com/hackedcube/kontact/sample/App.kt | 129 +++++++++++++ .../com/hackedcube/kontact/sample/Main.kt | 14 ++ .../kontact/sample/MainViewController.kt | 8 + .../kontact/sample/MainActivity.java | 86 --------- sample/src/main/res/layout/activity_main.xml | 34 ---- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3418 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 4208 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2206 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2555 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4842 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 6114 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 7718 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 10056 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 10486 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 14696 -> 0 bytes sample/src/main/res/values/styles.xml | 11 -- .../hackedcube/kontact/ExampleUnitTest.java | 17 -- .../com/hackedcube/kontact/sample/Main.kt | 12 ++ sample/src/wasmJsMain/resources/index.html | 25 +++ settings.gradle | 1 - settings.gradle.kts | 19 ++ 69 files changed, 1003 insertions(+), 1345 deletions(-) delete mode 100644 build.gradle create mode 100644 build.gradle.kts create mode 100644 gradle/libs.versions.toml delete mode 100644 kontact-rxjava/.gitignore delete mode 100644 kontact-rxjava/build.gradle delete mode 100644 kontact-rxjava/proguard-rules.pro delete mode 100644 kontact-rxjava/src/main/AndroidManifest.xml delete mode 100644 kontact-rxjava/src/main/kotlin/com/hackedcube/kontact/rxjava/RxContactUtils.kt delete mode 100644 kontact-rxjava2/.gitignore delete mode 100644 kontact-rxjava2/build.gradle delete mode 100644 kontact-rxjava2/proguard-rules.pro delete mode 100644 kontact-rxjava2/src/main/AndroidManifest.xml delete mode 100644 kontact-rxjava2/src/main/kotlin/com/hackedcube/kontact/rxjava2/RxContactUtils.kt delete mode 100644 kontact/build.gradle create mode 100644 kontact/build.gradle.kts rename kontact/src/{main => androidMain}/AndroidManifest.xml (74%) create mode 100644 kontact/src/androidMain/kotlin/com/hackedcube/kontact/AndroidKontactRepository.kt create mode 100644 kontact/src/androidMain/kotlin/com/hackedcube/kontact/CursorExtensions.kt create mode 100644 kontact/src/androidMain/kotlin/com/hackedcube/kontact/TypeMappers.kt create mode 100644 kontact/src/commonMain/kotlin/com/hackedcube/kontact/KontactRepository.kt create mode 100644 kontact/src/commonMain/kotlin/com/hackedcube/kontact/Models.kt create mode 100644 kontact/src/desktopMain/kotlin/com/hackedcube/kontact/DesktopKontactRepository.kt create mode 100644 kontact/src/iosMain/kotlin/com/hackedcube/kontact/IosKontactRepository.kt delete mode 100644 kontact/src/main/java/com/hackedcube/kontact/EmailAddress.java delete mode 100644 kontact/src/main/java/com/hackedcube/kontact/Event.java delete mode 100644 kontact/src/main/java/com/hackedcube/kontact/Kontact.java delete mode 100644 kontact/src/main/java/com/hackedcube/kontact/Nickname.java delete mode 100644 kontact/src/main/java/com/hackedcube/kontact/PhoneNumber.java delete mode 100644 kontact/src/main/java/com/hackedcube/kontact/PostalAddress.java delete mode 100644 kontact/src/main/java/com/hackedcube/kontact/Relation.java delete mode 100644 kontact/src/main/kotlin/com/hackedcube/kontact/ContactUtils.kt delete mode 100644 kontact/src/main/kotlin/com/hackedcube/kontact/Extensions.kt delete mode 100644 kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/BooleanIntAdapter.kt delete mode 100644 kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/EmailTypeIntAdapter.kt delete mode 100644 kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/EventTypeIntAdapter.kt delete mode 100644 kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/NicknameTypeIntAdapter.kt delete mode 100644 kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/PhoneTypeIntAdapter.kt delete mode 100644 kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/PostalAddressTypeIntAdapter.kt delete mode 100644 kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/RelationTypeIntAdapter.kt create mode 100644 kontact/src/wasmJsMain/kotlin/com/hackedcube/kontact/WasmKontactRepository.kt delete mode 100644 sample/build.gradle create mode 100644 sample/build.gradle.kts rename sample/src/{main => androidMain}/AndroidManifest.xml (63%) create mode 100644 sample/src/androidMain/kotlin/com/hackedcube/kontact/sample/MainActivity.kt rename sample/src/{main => androidMain}/res/values/colors.xml (78%) rename sample/src/{main => androidMain}/res/values/strings.xml (95%) delete mode 100644 sample/src/androidTest/java/com/hackedcube/kontact/ExampleInstrumentedTest.java create mode 100644 sample/src/commonMain/kotlin/com/hackedcube/kontact/sample/App.kt create mode 100644 sample/src/desktopMain/kotlin/com/hackedcube/kontact/sample/Main.kt create mode 100644 sample/src/iosMain/kotlin/com/hackedcube/kontact/sample/MainViewController.kt delete mode 100644 sample/src/main/java/com/hackedcube/kontact/sample/MainActivity.java delete mode 100644 sample/src/main/res/layout/activity_main.xml delete mode 100644 sample/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 sample/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 sample/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 sample/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 sample/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 sample/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png delete mode 100644 sample/src/main/res/values/styles.xml delete mode 100644 sample/src/test/java/com/hackedcube/kontact/ExampleUnitTest.java create mode 100644 sample/src/wasmJsMain/kotlin/com/hackedcube/kontact/sample/Main.kt create mode 100644 sample/src/wasmJsMain/resources/index.html delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts diff --git a/build.gradle b/build.gradle deleted file mode 100644 index b8a80c0..0000000 --- a/build.gradle +++ /dev/null @@ -1,27 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - ext.kotlin_version = '1.2.10' - repositories { - jcenter() - google() - } - dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.2' - classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1' - } -} - -allprojects { - repositories { - jcenter() - mavenCentral() - google() - } -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..b5cd7bf --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) apply false + alias(libs.plugins.kotlinCompose) apply false + alias(libs.plugins.composeMultiplatform) apply false + alias(libs.plugins.androidLibrary) apply false + alias(libs.plugins.androidApplication) apply false +} diff --git a/gradle.properties b/gradle.properties index 727136e..227b0dc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,23 +1,9 @@ -# Project-wide Gradle settings. - -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. - -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html - -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m - -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true - -kotlin.incremental=true - - - -version=1.0.0 +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.parallel=true +org.gradle.caching=true + +android.useAndroidX=true +android.nonTransitiveRClass=true + +kotlin.code.style=official +kotlin.mpp.androidSourceSetLayoutVersion=2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..3065aae --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,24 @@ +[versions] +kotlin = "2.1.10" +agp = "8.7.3" +coroutines = "1.9.0" +compose-multiplatform = "1.7.3" +androidx-core = "1.15.0" +androidx-activity-compose = "1.9.3" +compileSdk = "35" +minSdk = "24" +targetSdk = "35" + +[libraries] +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } +androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" } + +[plugins] +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlinCompose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } +androidApplication = { id = "com.android.application", version.ref = "agp" } diff --git a/kontact-rxjava/.gitignore b/kontact-rxjava/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/kontact-rxjava/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/kontact-rxjava/build.gradle b/kontact-rxjava/build.gradle deleted file mode 100644 index a4e84dc..0000000 --- a/kontact-rxjava/build.gradle +++ /dev/null @@ -1,70 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'maven-publish' - -android { - compileSdkVersion 26 - buildToolsVersion "26.0.2" - - defaultConfig { - minSdkVersion 16 - targetSdkVersion 26 - - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} - -dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { - exclude group: 'com.android.support', module: 'support-annotations' - }) - - compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - provided project(path: ':kontact') - testCompile 'junit:junit:4.12' - - compile 'io.reactivex:rxjava:1.2.9' -} - -def artifactVersion = properties.version - -ext { - bintrayRepo = 'Kontact' - bintrayName = 'Kontact-rxJava' - - publishedGroupId = 'com.hackedcube' - libraryName = 'Kontact' - artifact = 'kontact-rxjava' - - libraryDescription = 'Full object mapping and querying of the Android Contact API' - - siteUrl = 'https://github.com/rubixhacker/Kontact' - gitUrl = 'https://github.com/rubixhacker/Kontact.git' - - libraryVersion = artifactVersion - - developerId = 'rubixhacker' - developerName = 'Stewart Boling' - developerEmail = 'rubixhacker@gmail.com' - - licenseName = 'The Apache Software License, Version 2.0' - licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - allLicenses = ["Apache-2.0"] -} - -tasks.withType(Javadoc).all { - enabled = false -} - -apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle' -apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle' diff --git a/kontact-rxjava/proguard-rules.pro b/kontact-rxjava/proguard-rules.pro deleted file mode 100644 index c6e4461..0000000 --- a/kontact-rxjava/proguard-rules.pro +++ /dev/null @@ -1,25 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in C:\Users\rubix\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/kontact-rxjava/src/main/AndroidManifest.xml b/kontact-rxjava/src/main/AndroidManifest.xml deleted file mode 100644 index e6f756b..0000000 --- a/kontact-rxjava/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/kontact-rxjava/src/main/kotlin/com/hackedcube/kontact/rxjava/RxContactUtils.kt b/kontact-rxjava/src/main/kotlin/com/hackedcube/kontact/rxjava/RxContactUtils.kt deleted file mode 100644 index 65f3ad6..0000000 --- a/kontact-rxjava/src/main/kotlin/com/hackedcube/kontact/rxjava/RxContactUtils.kt +++ /dev/null @@ -1,20 +0,0 @@ -@file:JvmName("RxContactUtils") - -package com.hackedcube.kontact.rxjava - -import android.content.Context -import android.net.Uri -import com.hackedcube.kontact.Kontact -import com.hackedcube.kontact.getContactFromId -import com.hackedcube.kontact.queryAllContacts -import rx.Single - -fun Context.allContactsSingle(): Single> { - return Single.fromCallable { queryAllContacts() } -} - -fun Context.contactSingle(uri: Uri): Single { - return Single.fromCallable { getContactFromId(uri) } -} - - diff --git a/kontact-rxjava2/.gitignore b/kontact-rxjava2/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/kontact-rxjava2/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/kontact-rxjava2/build.gradle b/kontact-rxjava2/build.gradle deleted file mode 100644 index dd3933e..0000000 --- a/kontact-rxjava2/build.gradle +++ /dev/null @@ -1,74 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'maven-publish' - -android { - compileSdkVersion 26 - buildToolsVersion "26.0.2" - - defaultConfig { - minSdkVersion 16 - targetSdkVersion 26 - - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} - -dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { - exclude group: 'com.android.support', module: 'support-annotations' - }) - - provided project(path: ':kontact') - testCompile 'junit:junit:4.12' - - compile 'io.reactivex.rxjava2:rxjava:2.1.4' - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} - -def artifactVersion = properties.version - -ext { - bintrayRepo = 'Kontact' - bintrayName = 'Kontact-rxJava2' - - publishedGroupId = 'com.hackedcube' - libraryName = 'Kontact' - artifact = 'kontact-rxjava2' - - libraryDescription = 'Full object mapping and querying of the Android Contact API' - - siteUrl = 'https://github.com/rubixhacker/Kontact' - gitUrl = 'https://github.com/rubixhacker/Kontact.git' - - libraryVersion = artifactVersion - - developerId = 'rubixhacker' - developerName = 'Stewart Boling' - developerEmail = 'rubixhacker@gmail.com' - - licenseName = 'The Apache Software License, Version 2.0' - licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - allLicenses = ["Apache-2.0"] -} - -tasks.withType(Javadoc).all { - enabled = false -} - -apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle' -apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle' -repositories { - mavenCentral() -} diff --git a/kontact-rxjava2/proguard-rules.pro b/kontact-rxjava2/proguard-rules.pro deleted file mode 100644 index c6e4461..0000000 --- a/kontact-rxjava2/proguard-rules.pro +++ /dev/null @@ -1,25 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in C:\Users\rubix\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/kontact-rxjava2/src/main/AndroidManifest.xml b/kontact-rxjava2/src/main/AndroidManifest.xml deleted file mode 100644 index 7255a09..0000000 --- a/kontact-rxjava2/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/kontact-rxjava2/src/main/kotlin/com/hackedcube/kontact/rxjava2/RxContactUtils.kt b/kontact-rxjava2/src/main/kotlin/com/hackedcube/kontact/rxjava2/RxContactUtils.kt deleted file mode 100644 index 21d9ddc..0000000 --- a/kontact-rxjava2/src/main/kotlin/com/hackedcube/kontact/rxjava2/RxContactUtils.kt +++ /dev/null @@ -1,18 +0,0 @@ -@file:JvmName("RxContactUtils") - -package com.hackedcube.kontact.rxjava2 - -import android.content.Context -import android.net.Uri -import com.hackedcube.kontact.Kontact -import com.hackedcube.kontact.getContactFromId -import com.hackedcube.kontact.queryAllContacts -import io.reactivex.Single - -fun Context.allContacts(): Single> { - return Single.fromCallable { queryAllContacts() } -} - -fun Context.contact(uri: Uri): Single { - return Single.fromCallable { getContactFromId(uri) } -} \ No newline at end of file diff --git a/kontact/build.gradle b/kontact/build.gradle deleted file mode 100644 index 7628546..0000000 --- a/kontact/build.gradle +++ /dev/null @@ -1,78 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' - -android { - compileSdkVersion 26 - buildToolsVersion "26.0.2" - - defaultConfig { - minSdkVersion 16 - targetSdkVersion 26 - - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - buildTypes { - debug { } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} - -dependencies { - implementation 'com.android.support:support-annotations:27.0.2' - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - - //AutoValue - kapt 'com.google.auto.value:auto-value:1.5' - compileOnly 'com.jakewharton.auto.value:auto-value-annotations:1.5' - kapt 'com.gabrielittner.auto.value:auto-value-with:1.0.0' - kapt 'com.gabrielittner.auto.value:auto-value-cursor:1.1.0' - implementation 'com.gabrielittner.auto.value:auto-value-cursor-annotations:1.1.0' - - testCompile 'junit:junit:4.12' -} -repositories { - mavenCentral() -} - -def artifactVersion = properties.version - -ext { - bintrayRepo = 'Kontact' - bintrayName = 'Kontact' - - publishedGroupId = 'com.hackedcube' - libraryName = 'Kontact' - artifact = 'kontact' - - libraryDescription = 'Full object mapping and querying of the Android Contact API' - - siteUrl = 'https://github.com/rubixhacker/Kontact' - gitUrl = 'https://github.com/rubixhacker/Kontact.git' - - libraryVersion = artifactVersion - - developerId = 'rubixhacker' - developerName = 'Stewart Boling' - developerEmail = 'rubixhacker@gmail.com' - - licenseName = 'The Apache Software License, Version 2.0' - licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - allLicenses = ["Apache-2.0"] -} - -tasks.withType(Javadoc).all { - enabled = false -} - -apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle' -apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle' \ No newline at end of file diff --git a/kontact/build.gradle.kts b/kontact/build.gradle.kts new file mode 100644 index 0000000..b85a195 --- /dev/null +++ b/kontact/build.gradle.kts @@ -0,0 +1,58 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) +} + +kotlin { + androidTarget { + compilations.all { + compileTaskProvider.configure { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } + } + } + } + + jvm("desktop") + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64(), + ).forEach { + it.binaries.framework { + baseName = "Kontact" + isStatic = true + } + } + + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) + wasmJs { + browser() + } + + sourceSets { + commonMain.dependencies { + implementation(libs.kotlinx.coroutines.core) + } + androidMain.dependencies { + implementation(libs.androidx.core) + implementation(libs.kotlinx.coroutines.android) + } + } +} + +android { + namespace = "com.hackedcube.kontact" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} diff --git a/kontact/src/main/AndroidManifest.xml b/kontact/src/androidMain/AndroidManifest.xml similarity index 74% rename from kontact/src/main/AndroidManifest.xml rename to kontact/src/androidMain/AndroidManifest.xml index cd78d8c..bc3866d 100644 --- a/kontact/src/main/AndroidManifest.xml +++ b/kontact/src/androidMain/AndroidManifest.xml @@ -1,6 +1,3 @@ - - - - - + + + diff --git a/kontact/src/androidMain/kotlin/com/hackedcube/kontact/AndroidKontactRepository.kt b/kontact/src/androidMain/kotlin/com/hackedcube/kontact/AndroidKontactRepository.kt new file mode 100644 index 0000000..f127e05 --- /dev/null +++ b/kontact/src/androidMain/kotlin/com/hackedcube/kontact/AndroidKontactRepository.kt @@ -0,0 +1,174 @@ +package com.hackedcube.kontact + +import android.content.ContentResolver +import android.content.Context +import android.database.Cursor +import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds +import android.provider.ContactsContract.Contacts +import android.provider.ContactsContract.Data +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext + +class AndroidKontactRepository(private val context: Context) : KontactRepository { + + private val contentResolver: ContentResolver + get() = context.contentResolver + + override suspend fun queryAllContacts(): List = withContext(Dispatchers.IO) { + contentResolver.query(Contacts.CONTENT_URI, null, null, null, null) + ?.use { cursor -> + cursor.asSequence().map { buildKontact(it) }.toList() + } ?: emptyList() + } + + override suspend fun getContact(id: String): Kontact? = withContext(Dispatchers.IO) { + contentResolver.query( + Contacts.CONTENT_URI, + null, + "${Contacts._ID} = ?", + arrayOf(id), + null, + )?.use { cursor -> + if (cursor.moveToFirst()) buildKontact(cursor) else null + } + } + + override fun observeAllContacts(): Flow> = flow { + emit(queryAllContacts()) + }.flowOn(Dispatchers.IO) + + private fun buildKontact(cursor: Cursor): Kontact { + val id = cursor.getString(Contacts._ID) + val hasPhone = cursor.getInt(Contacts.HAS_PHONE_NUMBER) == 1 + + val base = Kontact( + id = id, + lookupKey = cursor.getString(Contacts.LOOKUP_KEY), + displayNamePrimary = cursor.getString(Contacts.DISPLAY_NAME_PRIMARY), + photoId = cursor.getLongOrNull(Contacts.PHOTO_ID), + photoUri = cursor.getStringOrNull(Contacts.PHOTO_URI), + thumbnailPhotoUri = cursor.getStringOrNull(Contacts.PHOTO_THUMBNAIL_URI), + isVisible = cursor.getInt(Contacts.IN_VISIBLE_GROUP) == 1, + hasPhoneNumber = hasPhone, + timesContacted = cursor.getInt(Contacts.TIMES_CONTACTED), + lastTimeContacted = cursor.getLong(Contacts.LAST_TIME_CONTACTED), + isStarred = cursor.getInt(Contacts.STARRED) == 1, + customRingtone = cursor.getStringOrNull(Contacts.CUSTOM_RINGTONE), + sendToVoicemail = cursor.getInt(Contacts.SEND_TO_VOICEMAIL) == 1, + ) + + val phoneNumbers = if (hasPhone) queryPhoneNumbers(id) else emptyList() + val emailAddresses = queryEmailAddresses(id) + val (events, nicknames, postalAddresses, relations) = queryAdditionalData(id) + + return base.copy( + phoneNumbers = phoneNumbers, + emailAddresses = emailAddresses, + events = events, + nicknames = nicknames, + postalAddresses = postalAddresses, + relations = relations, + ) + } + + private fun queryPhoneNumbers(contactId: String): List { + return contentResolver.query( + CommonDataKinds.Phone.CONTENT_URI, + null, + "${CommonDataKinds.Phone.CONTACT_ID} = ?", + arrayOf(contactId), + null, + )?.use { cursor -> + cursor.asSequence().map { c -> + PhoneNumber( + id = c.getString(CommonDataKinds.Phone._ID), + number = c.getString(CommonDataKinds.Phone.NUMBER), + type = c.getInt(CommonDataKinds.Phone.TYPE).toPhoneType(), + label = c.getStringOrNull(CommonDataKinds.Phone.LABEL), + ) + }.toList() + } ?: emptyList() + } + + private fun queryEmailAddresses(contactId: String): List { + return contentResolver.query( + CommonDataKinds.Email.CONTENT_URI, + null, + "${CommonDataKinds.Email.CONTACT_ID} = ?", + arrayOf(contactId), + null, + )?.use { cursor -> + cursor.asSequence().map { c -> + EmailAddress( + id = c.getString(CommonDataKinds.Email._ID), + address = c.getString(CommonDataKinds.Email.ADDRESS), + type = c.getInt(CommonDataKinds.Email.TYPE).toEmailType(), + label = c.getStringOrNull(CommonDataKinds.Email.LABEL), + ) + }.toList() + } ?: emptyList() + } + + private data class AdditionalData( + val events: List, + val nicknames: List, + val postalAddresses: List, + val relations: List, + ) + + private fun queryAdditionalData(contactId: String): AdditionalData { + val events = mutableListOf() + val nicknames = mutableListOf() + val postalAddresses = mutableListOf() + val relations = mutableListOf() + + val where = "${Data.CONTACT_ID} = ? AND ${Data.MIMETYPE} IN (?, ?, ?, ?)" + val whereParams = arrayOf( + contactId, + CommonDataKinds.Event.CONTENT_ITEM_TYPE, + CommonDataKinds.Nickname.CONTENT_ITEM_TYPE, + CommonDataKinds.Relation.CONTENT_ITEM_TYPE, + CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE, + ) + + contentResolver.query(Data.CONTENT_URI, null, where, whereParams, null)?.use { cursor -> + for (c in cursor.asSequence()) { + when (c.getString(Data.MIMETYPE)) { + CommonDataKinds.Event.CONTENT_ITEM_TYPE -> events += Event( + startDate = c.getStringOrNull(CommonDataKinds.Event.START_DATE), + type = c.getIntOrNull(CommonDataKinds.Event.TYPE)?.toEventType(), + label = c.getStringOrNull(CommonDataKinds.Event.LABEL), + ) + CommonDataKinds.Nickname.CONTENT_ITEM_TYPE -> nicknames += Nickname( + name = c.getStringOrNull(CommonDataKinds.Nickname.NAME), + type = c.getIntOrNull(CommonDataKinds.Nickname.TYPE)?.toNicknameType(), + label = c.getStringOrNull(CommonDataKinds.Nickname.LABEL), + ) + CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE -> postalAddresses += PostalAddress( + formattedAddress = c.getString(CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS), + type = c.getInt(CommonDataKinds.StructuredPostal.TYPE).toPostalAddressType(), + label = c.getStringOrNull(CommonDataKinds.StructuredPostal.LABEL), + street = c.getStringOrNull(CommonDataKinds.StructuredPostal.STREET), + pobox = c.getStringOrNull(CommonDataKinds.StructuredPostal.POBOX), + neighborhood = c.getStringOrNull(CommonDataKinds.StructuredPostal.NEIGHBORHOOD), + city = c.getStringOrNull(CommonDataKinds.StructuredPostal.CITY), + region = c.getStringOrNull(CommonDataKinds.StructuredPostal.REGION), + postcode = c.getStringOrNull(CommonDataKinds.StructuredPostal.POSTCODE), + country = c.getStringOrNull(CommonDataKinds.StructuredPostal.COUNTRY), + ) + CommonDataKinds.Relation.CONTENT_ITEM_TYPE -> relations += Relation( + name = c.getString(CommonDataKinds.Relation.NAME), + type = c.getInt(CommonDataKinds.Relation.TYPE).toRelationType(), + label = c.getStringOrNull(CommonDataKinds.Relation.LABEL), + ) + } + } + } + + return AdditionalData(events, nicknames, postalAddresses, relations) + } +} diff --git a/kontact/src/androidMain/kotlin/com/hackedcube/kontact/CursorExtensions.kt b/kontact/src/androidMain/kotlin/com/hackedcube/kontact/CursorExtensions.kt new file mode 100644 index 0000000..26a62fb --- /dev/null +++ b/kontact/src/androidMain/kotlin/com/hackedcube/kontact/CursorExtensions.kt @@ -0,0 +1,34 @@ +package com.hackedcube.kontact + +import android.database.Cursor + +internal fun Cursor.getString(columnName: String): String { + return getString(getColumnIndexOrThrow(columnName)) +} + +internal fun Cursor.getStringOrNull(columnName: String): String? { + val index = getColumnIndex(columnName) + return if (index == -1 || isNull(index)) null else getString(index) +} + +internal fun Cursor.getInt(columnName: String): Int { + return getInt(getColumnIndexOrThrow(columnName)) +} + +internal fun Cursor.getIntOrNull(columnName: String): Int? { + val index = getColumnIndex(columnName) + return if (index == -1 || isNull(index)) null else getInt(index) +} + +internal fun Cursor.getLong(columnName: String): Long { + return getLong(getColumnIndexOrThrow(columnName)) +} + +internal fun Cursor.getLongOrNull(columnName: String): Long? { + val index = getColumnIndex(columnName) + return if (index == -1 || isNull(index)) null else getLong(index) +} + +internal fun Cursor.asSequence(): Sequence = generateSequence { + if (moveToNext()) this else null +} diff --git a/kontact/src/androidMain/kotlin/com/hackedcube/kontact/TypeMappers.kt b/kontact/src/androidMain/kotlin/com/hackedcube/kontact/TypeMappers.kt new file mode 100644 index 0000000..fece03a --- /dev/null +++ b/kontact/src/androidMain/kotlin/com/hackedcube/kontact/TypeMappers.kt @@ -0,0 +1,76 @@ +package com.hackedcube.kontact + +import android.provider.ContactsContract.CommonDataKinds + +internal fun Int.toPhoneType(): PhoneNumber.Type = when (this) { + CommonDataKinds.Phone.TYPE_HOME -> PhoneNumber.Type.HOME + CommonDataKinds.Phone.TYPE_MOBILE -> PhoneNumber.Type.MOBILE + CommonDataKinds.Phone.TYPE_WORK -> PhoneNumber.Type.WORK + CommonDataKinds.Phone.TYPE_FAX_WORK -> PhoneNumber.Type.FAX_WORK + CommonDataKinds.Phone.TYPE_FAX_HOME -> PhoneNumber.Type.FAX_HOME + CommonDataKinds.Phone.TYPE_PAGER -> PhoneNumber.Type.PAGER + CommonDataKinds.Phone.TYPE_OTHER -> PhoneNumber.Type.OTHER + CommonDataKinds.Phone.TYPE_CALLBACK -> PhoneNumber.Type.CALLBACK + CommonDataKinds.Phone.TYPE_CAR -> PhoneNumber.Type.CAR + CommonDataKinds.Phone.TYPE_COMPANY_MAIN -> PhoneNumber.Type.COMPANY_MAIN + CommonDataKinds.Phone.TYPE_ISDN -> PhoneNumber.Type.ISDN + CommonDataKinds.Phone.TYPE_MAIN -> PhoneNumber.Type.MAIN + CommonDataKinds.Phone.TYPE_OTHER_FAX -> PhoneNumber.Type.OTHER_FAX + CommonDataKinds.Phone.TYPE_RADIO -> PhoneNumber.Type.RADIO + CommonDataKinds.Phone.TYPE_TELEX -> PhoneNumber.Type.TELEX + CommonDataKinds.Phone.TYPE_TTY_TDD -> PhoneNumber.Type.TTY_TDD + CommonDataKinds.Phone.TYPE_WORK_MOBILE -> PhoneNumber.Type.WORK_MOBILE + CommonDataKinds.Phone.TYPE_WORK_PAGER -> PhoneNumber.Type.WORK_PAGER + CommonDataKinds.Phone.TYPE_ASSISTANT -> PhoneNumber.Type.ASSISTANT + CommonDataKinds.Phone.TYPE_MMS -> PhoneNumber.Type.MMS + else -> PhoneNumber.Type.CUSTOM +} + +internal fun Int.toEmailType(): EmailAddress.Type = when (this) { + CommonDataKinds.Email.TYPE_HOME -> EmailAddress.Type.HOME + CommonDataKinds.Email.TYPE_WORK -> EmailAddress.Type.WORK + CommonDataKinds.Email.TYPE_OTHER -> EmailAddress.Type.OTHER + CommonDataKinds.Email.TYPE_MOBILE -> EmailAddress.Type.MOBILE + else -> EmailAddress.Type.CUSTOM +} + +internal fun Int.toPostalAddressType(): PostalAddress.Type = when (this) { + CommonDataKinds.StructuredPostal.TYPE_HOME -> PostalAddress.Type.HOME + CommonDataKinds.StructuredPostal.TYPE_WORK -> PostalAddress.Type.WORK + CommonDataKinds.StructuredPostal.TYPE_OTHER -> PostalAddress.Type.OTHER + else -> PostalAddress.Type.CUSTOM +} + +internal fun Int.toEventType(): Event.Type = when (this) { + CommonDataKinds.Event.TYPE_ANNIVERSARY -> Event.Type.ANNIVERSARY + CommonDataKinds.Event.TYPE_OTHER -> Event.Type.OTHER + CommonDataKinds.Event.TYPE_BIRTHDAY -> Event.Type.BIRTHDAY + else -> Event.Type.CUSTOM +} + +internal fun Int.toNicknameType(): Nickname.Type = when (this) { + CommonDataKinds.Nickname.TYPE_DEFAULT -> Nickname.Type.DEFAULT + CommonDataKinds.Nickname.TYPE_OTHER_NAME -> Nickname.Type.OTHER + CommonDataKinds.Nickname.TYPE_MAIDEN_NAME -> Nickname.Type.MAIDEN + CommonDataKinds.Nickname.TYPE_SHORT_NAME -> Nickname.Type.SHORT + CommonDataKinds.Nickname.TYPE_INITIALS -> Nickname.Type.INITIALS + else -> Nickname.Type.CUSTOM +} + +internal fun Int.toRelationType(): Relation.Type = when (this) { + CommonDataKinds.Relation.TYPE_ASSISTANT -> Relation.Type.ASSISTANT + CommonDataKinds.Relation.TYPE_BROTHER -> Relation.Type.BROTHER + CommonDataKinds.Relation.TYPE_CHILD -> Relation.Type.CHILD + CommonDataKinds.Relation.TYPE_DOMESTIC_PARTNER -> Relation.Type.DOMESTIC_PARTNER + CommonDataKinds.Relation.TYPE_FATHER -> Relation.Type.FATHER + CommonDataKinds.Relation.TYPE_FRIEND -> Relation.Type.FRIEND + CommonDataKinds.Relation.TYPE_MANAGER -> Relation.Type.MANAGER + CommonDataKinds.Relation.TYPE_MOTHER -> Relation.Type.MOTHER + CommonDataKinds.Relation.TYPE_PARENT -> Relation.Type.PARENT + CommonDataKinds.Relation.TYPE_PARTNER -> Relation.Type.PARTNER + CommonDataKinds.Relation.TYPE_REFERRED_BY -> Relation.Type.REFERRED_BY + CommonDataKinds.Relation.TYPE_RELATIVE -> Relation.Type.RELATIVE + CommonDataKinds.Relation.TYPE_SISTER -> Relation.Type.SISTER + CommonDataKinds.Relation.TYPE_SPOUSE -> Relation.Type.SPOUSE + else -> Relation.Type.CUSTOM +} diff --git a/kontact/src/commonMain/kotlin/com/hackedcube/kontact/KontactRepository.kt b/kontact/src/commonMain/kotlin/com/hackedcube/kontact/KontactRepository.kt new file mode 100644 index 0000000..1410237 --- /dev/null +++ b/kontact/src/commonMain/kotlin/com/hackedcube/kontact/KontactRepository.kt @@ -0,0 +1,12 @@ +package com.hackedcube.kontact + +import kotlinx.coroutines.flow.Flow + +/** + * Platform-agnostic interface for querying device contacts. + */ +interface KontactRepository { + suspend fun queryAllContacts(): List + suspend fun getContact(id: String): Kontact? + fun observeAllContacts(): Flow> +} diff --git a/kontact/src/commonMain/kotlin/com/hackedcube/kontact/Models.kt b/kontact/src/commonMain/kotlin/com/hackedcube/kontact/Models.kt new file mode 100644 index 0000000..4f7a132 --- /dev/null +++ b/kontact/src/commonMain/kotlin/com/hackedcube/kontact/Models.kt @@ -0,0 +1,96 @@ +package com.hackedcube.kontact + +data class Kontact( + val id: String, + val lookupKey: String, + val displayNamePrimary: String, + val photoId: Long? = null, + val photoUri: String? = null, + val thumbnailPhotoUri: String? = null, + val isVisible: Boolean = true, + val hasPhoneNumber: Boolean = false, + val timesContacted: Int = 0, + val lastTimeContacted: Long = 0L, + val isStarred: Boolean = false, + val customRingtone: String? = null, + val sendToVoicemail: Boolean = false, + val phoneNumbers: List = emptyList(), + val emailAddresses: List = emptyList(), + val relations: List = emptyList(), + val postalAddresses: List = emptyList(), + val nicknames: List = emptyList(), + val events: List = emptyList(), +) + +data class PhoneNumber( + val id: String, + val number: String, + val type: Type, + val label: String? = null, +) { + enum class Type { + CUSTOM, HOME, MOBILE, WORK, FAX_WORK, FAX_HOME, PAGER, OTHER, + CALLBACK, CAR, COMPANY_MAIN, ISDN, MAIN, OTHER_FAX, RADIO, + TELEX, TTY_TDD, WORK_MOBILE, WORK_PAGER, ASSISTANT, MMS, + } +} + +data class EmailAddress( + val id: String, + val address: String, + val type: Type, + val label: String? = null, +) { + enum class Type { + CUSTOM, HOME, WORK, OTHER, MOBILE, + } +} + +data class PostalAddress( + val formattedAddress: String, + val type: Type, + val label: String? = null, + val street: String? = null, + val pobox: String? = null, + val neighborhood: String? = null, + val city: String? = null, + val region: String? = null, + val postcode: String? = null, + val country: String? = null, +) { + enum class Type { + CUSTOM, HOME, WORK, OTHER, + } +} + +data class Event( + val startDate: String? = null, + val type: Type? = null, + val label: String? = null, +) { + enum class Type { + CUSTOM, ANNIVERSARY, OTHER, BIRTHDAY, + } +} + +data class Nickname( + val name: String? = null, + val type: Type? = null, + val label: String? = null, +) { + enum class Type { + CUSTOM, DEFAULT, OTHER, MAIDEN, SHORT, INITIALS, + } +} + +data class Relation( + val name: String, + val type: Type, + val label: String? = null, +) { + enum class Type { + CUSTOM, ASSISTANT, BROTHER, CHILD, DOMESTIC_PARTNER, FATHER, + FRIEND, MANAGER, MOTHER, PARENT, PARTNER, REFERRED_BY, + RELATIVE, SISTER, SPOUSE, + } +} diff --git a/kontact/src/desktopMain/kotlin/com/hackedcube/kontact/DesktopKontactRepository.kt b/kontact/src/desktopMain/kotlin/com/hackedcube/kontact/DesktopKontactRepository.kt new file mode 100644 index 0000000..34012df --- /dev/null +++ b/kontact/src/desktopMain/kotlin/com/hackedcube/kontact/DesktopKontactRepository.kt @@ -0,0 +1,22 @@ +package com.hackedcube.kontact + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +/** + * Desktop (JVM) implementation of [KontactRepository]. + * + * Desktop platforms do not have a standardised contacts API. + * This stub returns empty results; consumers can extend it to + * integrate with platform-specific stores (e.g. macOS Contacts via JNI). + */ +class DesktopKontactRepository : KontactRepository { + + override suspend fun queryAllContacts(): List = emptyList() + + override suspend fun getContact(id: String): Kontact? = null + + override fun observeAllContacts(): Flow> = flow { + emit(emptyList()) + } +} diff --git a/kontact/src/iosMain/kotlin/com/hackedcube/kontact/IosKontactRepository.kt b/kontact/src/iosMain/kotlin/com/hackedcube/kontact/IosKontactRepository.kt new file mode 100644 index 0000000..eb1bcb6 --- /dev/null +++ b/kontact/src/iosMain/kotlin/com/hackedcube/kontact/IosKontactRepository.kt @@ -0,0 +1,113 @@ +package com.hackedcube.kontact + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import platform.Contacts.CNContact +import platform.Contacts.CNContactFetchRequest +import platform.Contacts.CNContactFormatter +import platform.Contacts.CNContactFormatterStyle +import platform.Contacts.CNContactStore +import platform.Contacts.CNEntityType +import platform.Contacts.CNLabelHome +import platform.Contacts.CNLabelOther +import platform.Contacts.CNLabelPhoneNumberMobile +import platform.Contacts.CNLabelWork +import platform.Foundation.NSError + +class IosKontactRepository : KontactRepository { + + private val store = CNContactStore() + + override suspend fun queryAllContacts(): List { + val contacts = mutableListOf() + val keysToFetch = contactKeysToFetch() + val request = CNContactFetchRequest(keysToFetch = keysToFetch) + + val error: NSError? = null + store.enumerateContactsWithFetchRequest(request, error = null) { contact, _ -> + if (contact != null) { + contacts += contact.toKontact() + } + } + return contacts + } + + override suspend fun getContact(id: String): Kontact? { + val keysToFetch = contactKeysToFetch() + return try { + val contact = store.unifiedContactWithIdentifier(id, keysToFetch = keysToFetch, error = null) + contact?.toKontact() + } catch (_: Exception) { + null + } + } + + override fun observeAllContacts(): Flow> = flow { + emit(queryAllContacts()) + } + + @Suppress("UNCHECKED_CAST") + private fun contactKeysToFetch(): List = listOf( + platform.Contacts.CNContactIdentifierKey, + platform.Contacts.CNContactGivenNameKey, + platform.Contacts.CNContactFamilyNameKey, + platform.Contacts.CNContactPhoneNumbersKey, + platform.Contacts.CNContactEmailAddressesKey, + platform.Contacts.CNContactPostalAddressesKey, + platform.Contacts.CNContactDatesKey, + platform.Contacts.CNContactNicknameKey, + platform.Contacts.CNContactRelationsKey, + platform.Contacts.CNContactImageDataAvailableKey, + CNContactFormatter.descriptorForRequiredKeysForStyle(CNContactFormatterStyle.CNContactFormatterStyleFullName), + ) + + private fun CNContact.toKontact(): Kontact { + val fullName = CNContactFormatter.stringFromContact(this, CNContactFormatterStyle.CNContactFormatterStyleFullName) ?: "" + + return Kontact( + id = identifier, + lookupKey = identifier, + displayNamePrimary = fullName, + hasPhoneNumber = phoneNumbers.isNotEmpty(), + phoneNumbers = phoneNumbers.mapNotNull { labeled -> + @Suppress("UNCHECKED_CAST") + val lv = labeled as? platform.Contacts.CNLabeledValue + lv?.let { + PhoneNumber( + id = identifier, + number = (it.value as platform.Contacts.CNPhoneNumber).stringValue, + type = it.label.toPhoneType(), + label = it.label, + ) + } + }, + emailAddresses = emailAddresses.mapNotNull { labeled -> + @Suppress("UNCHECKED_CAST") + val lv = labeled as? platform.Contacts.CNLabeledValue<*> + lv?.let { + EmailAddress( + id = identifier, + address = it.value.toString(), + type = it.label.toEmailType(), + label = it.label, + ) + } + }, + ) + } + + private fun String?.toPhoneType(): PhoneNumber.Type = when (this) { + CNLabelHome -> PhoneNumber.Type.HOME + CNLabelWork -> PhoneNumber.Type.WORK + CNLabelPhoneNumberMobile -> PhoneNumber.Type.MOBILE + CNLabelOther -> PhoneNumber.Type.OTHER + else -> PhoneNumber.Type.CUSTOM + } + + private fun String?.toEmailType(): EmailAddress.Type = when (this) { + CNLabelHome -> EmailAddress.Type.HOME + CNLabelWork -> EmailAddress.Type.WORK + CNLabelOther -> EmailAddress.Type.OTHER + else -> EmailAddress.Type.CUSTOM + } +} diff --git a/kontact/src/main/java/com/hackedcube/kontact/EmailAddress.java b/kontact/src/main/java/com/hackedcube/kontact/EmailAddress.java deleted file mode 100644 index 8fe9ada..0000000 --- a/kontact/src/main/java/com/hackedcube/kontact/EmailAddress.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.hackedcube.kontact; - -import android.database.Cursor; -import android.provider.ContactsContract; -import android.support.annotation.Nullable; - -import com.gabrielittner.auto.value.cursor.ColumnAdapter; -import com.gabrielittner.auto.value.cursor.ColumnName; -import com.google.auto.value.AutoValue; -import com.hackedcube.kontact.columnadapters.EmailTypeIntAdapter; - -@AutoValue -public abstract class EmailAddress { - - @ColumnName(ContactsContract.CommonDataKinds.Email._ID) - public abstract String id(); - - @ColumnName(ContactsContract.CommonDataKinds.Email.ADDRESS) - public abstract String number(); - - @ColumnAdapter(EmailTypeIntAdapter.class) - @ColumnName(ContactsContract.CommonDataKinds.Email.TYPE) - public abstract Type type(); - - @Nullable - @ColumnName(ContactsContract.CommonDataKinds.Email.LABEL) - public abstract String label(); - - public static EmailAddress create(Cursor cursor) { - return AutoValue_EmailAddress.createFromCursor(cursor); - } - - public enum Type { - CUSTOM(ContactsContract.CommonDataKinds.Email.TYPE_CUSTOM), - HOME(ContactsContract.CommonDataKinds.Email.TYPE_HOME), - WORK(ContactsContract.CommonDataKinds.Email.TYPE_WORK), - OTHER(ContactsContract.CommonDataKinds.Email.TYPE_OTHER), - MOBILE(ContactsContract.CommonDataKinds.Email.TYPE_MOBILE); - - int typeMap; - - Type(int columnName) { - this.typeMap = columnName; - } - - public int getTypeMap() { - return typeMap; - } - } -} diff --git a/kontact/src/main/java/com/hackedcube/kontact/Event.java b/kontact/src/main/java/com/hackedcube/kontact/Event.java deleted file mode 100644 index b780563..0000000 --- a/kontact/src/main/java/com/hackedcube/kontact/Event.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.hackedcube.kontact; - -import android.database.Cursor; -import android.provider.ContactsContract; -import android.support.annotation.Nullable; - -import com.gabrielittner.auto.value.cursor.ColumnAdapter; -import com.gabrielittner.auto.value.cursor.ColumnName; -import com.google.auto.value.AutoValue; -import com.hackedcube.kontact.columnadapters.EventTypeIntAdapter; - -@AutoValue -public abstract class Event { - - @Nullable - @ColumnName(ContactsContract.CommonDataKinds.Event.START_DATE) - public abstract String startDate(); - - @Nullable - @ColumnAdapter(EventTypeIntAdapter.class) - @ColumnName(ContactsContract.CommonDataKinds.Event.TYPE) - public abstract Event.Type type(); - - @Nullable - @ColumnName(ContactsContract.CommonDataKinds.Event.LABEL) - public abstract String label(); - - public static Event create(Cursor cursor) { - return AutoValue_Event.createFromCursor(cursor); - } - - public enum Type { - CUSTOM(ContactsContract.CommonDataKinds.Event.TYPE_CUSTOM), - ANNIVERSARY(ContactsContract.CommonDataKinds.Event.TYPE_ANNIVERSARY), - OTHER(ContactsContract.CommonDataKinds.Event.TYPE_OTHER), - BIRTHDAY(ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY); - - int typeMap; - - Type(int columnName) { - this.typeMap = columnName; - } - - public int getTypeMap() { - return typeMap; - } - } - -} diff --git a/kontact/src/main/java/com/hackedcube/kontact/Kontact.java b/kontact/src/main/java/com/hackedcube/kontact/Kontact.java deleted file mode 100644 index f89a81c..0000000 --- a/kontact/src/main/java/com/hackedcube/kontact/Kontact.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.hackedcube.kontact; - -import android.database.Cursor; -import android.provider.ContactsContract; -import android.support.annotation.Nullable; - -import com.gabrielittner.auto.value.cursor.ColumnAdapter; -import com.gabrielittner.auto.value.cursor.ColumnName; -import com.google.auto.value.AutoValue; -import com.hackedcube.kontact.columnadapters.BooleanIntAdapter; - -import java.util.List; - -@AutoValue -public abstract class Kontact { - - @ColumnName(ContactsContract.Contacts._ID) - public abstract String id(); - - @ColumnName(ContactsContract.Contacts.LOOKUP_KEY) - public abstract String lookupKey(); - - @ColumnName(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY) - public abstract String displayNamePrimary(); - - @Nullable - @ColumnName(ContactsContract.Contacts.PHOTO_ID) - public abstract Long photoId(); - - @Nullable - @ColumnName(ContactsContract.Contacts.PHOTO_URI) - public abstract Long photoUri(); - - @Nullable - @ColumnName(ContactsContract.Contacts.PHOTO_THUMBNAIL_URI) - public abstract Long thumbnailPhotoUri(); - - @ColumnAdapter(BooleanIntAdapter.class) - @ColumnName(ContactsContract.Contacts.IN_VISIBLE_GROUP) - public abstract Boolean isVisible(); - - @ColumnAdapter(BooleanIntAdapter.class) - @ColumnName(ContactsContract.Contacts.HAS_PHONE_NUMBER) - public abstract Boolean hasPhoneNumber(); - - @ColumnName(ContactsContract.Contacts.TIMES_CONTACTED) - public abstract Integer timesContacted(); - - @ColumnName(ContactsContract.Contacts.LAST_TIME_CONTACTED) - public abstract Long lastTimeContacted(); - - @ColumnAdapter(BooleanIntAdapter.class) - @ColumnName(ContactsContract.Contacts.STARRED) - public abstract Boolean isStarred(); - - @Nullable - @ColumnName(ContactsContract.Contacts.CUSTOM_RINGTONE) - public abstract String customRingtone(); - - @ColumnAdapter(BooleanIntAdapter.class) - @ColumnName(ContactsContract.Contacts.SEND_TO_VOICEMAIL) - public abstract Boolean sendToVoicemail(); - - @Nullable - public abstract List phoneNumbers(); - - @Nullable - public abstract List emailAddresses(); - - @Nullable - public abstract List relations(); - - @Nullable - public abstract List postalAddresses(); - - @Nullable - public abstract List nicknames(); - - @Nullable - public abstract List events(); - - public static Kontact create(Cursor cursor) { - return AutoValue_Kontact.createFromCursor(cursor); - } - - public static Builder builder() { - return new AutoValue_Kontact.Builder(); - } - - public abstract Builder toBuilder(); - - public abstract Kontact withPhoneNumbers(List phoneNumbers); - - public abstract Kontact withEmailAddresses(List emailAddresses); - - @AutoValue.Builder - public abstract static class Builder { - public abstract Builder id(String id); - - public abstract Builder lookupKey(String lookupKey); - - public abstract Builder displayNamePrimary(String displayNamePrimary); - - public abstract Builder photoId(Long photoId); - - public abstract Builder photoUri(Long photoUri); - - public abstract Builder thumbnailPhotoUri(Long thumbnailPhotoUri); - - public abstract Builder isVisible(Boolean isVisible); - - public abstract Builder hasPhoneNumber(Boolean hasPhoneNumber); - - public abstract Builder timesContacted(Integer timesContacted); - - public abstract Builder lastTimeContacted(Long lastTimeContacted); - - public abstract Builder isStarred(Boolean isStarred); - - public abstract Builder customRingtone(String customRingtone); - - public abstract Builder sendToVoicemail(Boolean sendToVoicemail); - - public abstract Builder phoneNumbers(List phoneNumbers); - - public abstract Builder emailAddresses(List emailAddresses); - - public abstract Builder relations(List relations); - - public abstract Builder postalAddresses(List postalAddresses); - - public abstract Builder nicknames(List nicknames); - - public abstract Builder events(List events); - - public abstract Kontact build(); - } -} diff --git a/kontact/src/main/java/com/hackedcube/kontact/Nickname.java b/kontact/src/main/java/com/hackedcube/kontact/Nickname.java deleted file mode 100644 index be15699..0000000 --- a/kontact/src/main/java/com/hackedcube/kontact/Nickname.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.hackedcube.kontact; - -import android.database.Cursor; -import android.provider.ContactsContract; -import android.support.annotation.Nullable; - -import com.gabrielittner.auto.value.cursor.ColumnAdapter; -import com.gabrielittner.auto.value.cursor.ColumnName; -import com.google.auto.value.AutoValue; -import com.hackedcube.kontact.columnadapters.NicknameTypeIntAdapter; -import com.hackedcube.kontact.columnadapters.RelationTypeIntAdapter; - -@AutoValue -public abstract class Nickname { - - @Nullable - @ColumnName(ContactsContract.CommonDataKinds.Nickname.NAME) - public abstract String name(); - - @Nullable - @ColumnAdapter(NicknameTypeIntAdapter.class) - @ColumnName(ContactsContract.CommonDataKinds.Nickname.TYPE) - public abstract Nickname.Type type(); - - @Nullable - @ColumnName(ContactsContract.CommonDataKinds.Nickname.LABEL) - public abstract String label(); - - public static Nickname create(Cursor cursor) { - return AutoValue_Nickname.createFromCursor(cursor); - } - - public enum Type { - CUSTOM(ContactsContract.CommonDataKinds.Nickname.TYPE_CUSTOM), - DEFAULT(ContactsContract.CommonDataKinds.Nickname.TYPE_DEFAULT), - OTHER(ContactsContract.CommonDataKinds.Nickname.TYPE_OTHER_NAME), - MAIDEN(ContactsContract.CommonDataKinds.Nickname.TYPE_MAIDEN_NAME), - SHORT(ContactsContract.CommonDataKinds.Nickname.TYPE_SHORT_NAME), - INITIALS(ContactsContract.CommonDataKinds.Nickname.TYPE_INITIALS); - - int typeMap; - - Type(int columnName) { - this.typeMap = columnName; - } - - public int getTypeMap() { - return typeMap; - } - } - -} diff --git a/kontact/src/main/java/com/hackedcube/kontact/PhoneNumber.java b/kontact/src/main/java/com/hackedcube/kontact/PhoneNumber.java deleted file mode 100644 index 924687e..0000000 --- a/kontact/src/main/java/com/hackedcube/kontact/PhoneNumber.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.hackedcube.kontact; - - -import android.database.Cursor; -import android.provider.ContactsContract; -import android.support.annotation.Nullable; - -import com.gabrielittner.auto.value.cursor.ColumnAdapter; -import com.gabrielittner.auto.value.cursor.ColumnName; -import com.google.auto.value.AutoValue; -import com.hackedcube.kontact.columnadapters.PhoneTypeIntAdapter; - -@AutoValue -public abstract class PhoneNumber { - - - @ColumnName(ContactsContract.CommonDataKinds.Phone._ID) - public abstract String id(); - - @ColumnName(ContactsContract.CommonDataKinds.Phone.NUMBER) - public abstract String number(); - - @ColumnAdapter(PhoneTypeIntAdapter.class) - @ColumnName(ContactsContract.CommonDataKinds.Phone.TYPE) - public abstract Type type(); - - @Nullable - @ColumnName(ContactsContract.CommonDataKinds.Phone.LABEL) - public abstract String label(); - - public static PhoneNumber create(Cursor cursor) { - return AutoValue_PhoneNumber.createFromCursor(cursor); - } - - public enum Type { - CUSTOM(ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM), - HOME(ContactsContract.CommonDataKinds.Phone.TYPE_HOME), - MOBILE(ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE), - WORK(ContactsContract.CommonDataKinds.Phone.TYPE_WORK), - FAX_WORK(ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK), - FAX_HOME(ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME), - PAGER(ContactsContract.CommonDataKinds.Phone.TYPE_PAGER), - OTHER(ContactsContract.CommonDataKinds.Phone.TYPE_OTHER), - CALLBACK(ContactsContract.CommonDataKinds.Phone.TYPE_CALLBACK), - CAR(ContactsContract.CommonDataKinds.Phone.TYPE_CAR), - COMPANY_MAIN(ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN), - ISDN(ContactsContract.CommonDataKinds.Phone.TYPE_ISDN), - MAIN(ContactsContract.CommonDataKinds.Phone.TYPE_MAIN), - OTHER_FAX(ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX), - RADIO(ContactsContract.CommonDataKinds.Phone.TYPE_RADIO), - TELEX(ContactsContract.CommonDataKinds.Phone.TYPE_TELEX), - TTY_TDD(ContactsContract.CommonDataKinds.Phone.TYPE_TTY_TDD), - WORK_MOBILE(ContactsContract.CommonDataKinds.Phone.TYPE_WORK_MOBILE), - WORK_PAGER(ContactsContract.CommonDataKinds.Phone.TYPE_WORK_PAGER), - ASSISTANT(ContactsContract.CommonDataKinds.Phone.TYPE_ASSISTANT), - MMS(ContactsContract.CommonDataKinds.Phone.TYPE_MMS); - - int typeMap; - - Type(int columnName) { - this.typeMap = columnName; - } - - public int getTypeMap() { - return typeMap; - } - } - -} diff --git a/kontact/src/main/java/com/hackedcube/kontact/PostalAddress.java b/kontact/src/main/java/com/hackedcube/kontact/PostalAddress.java deleted file mode 100644 index b01ba57..0000000 --- a/kontact/src/main/java/com/hackedcube/kontact/PostalAddress.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.hackedcube.kontact; - -import android.database.Cursor; -import android.provider.ContactsContract; -import android.support.annotation.Nullable; - -import com.gabrielittner.auto.value.cursor.ColumnAdapter; -import com.gabrielittner.auto.value.cursor.ColumnName; -import com.google.auto.value.AutoValue; -import com.hackedcube.kontact.columnadapters.PostalAddressTypeIntAdapter; - -@AutoValue -public abstract class PostalAddress { - - - @ColumnName(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS) - public abstract String formattedAddress(); - - @ColumnAdapter(PostalAddressTypeIntAdapter.class) - @ColumnName(ContactsContract.CommonDataKinds.StructuredPostal.TYPE) - public abstract PostalAddress.Type type(); - - @Nullable - @ColumnName(ContactsContract.CommonDataKinds.StructuredPostal.LABEL) - public abstract String label(); - - @Nullable - @ColumnName(ContactsContract.CommonDataKinds.StructuredPostal.STREET) - public abstract String street(); - - @Nullable - @ColumnName(ContactsContract.CommonDataKinds.StructuredPostal.POBOX) - public abstract String pobox(); - - @Nullable - @ColumnName(ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD) - public abstract String neighborhood(); - - @Nullable - @ColumnName(ContactsContract.CommonDataKinds.StructuredPostal.CITY) - public abstract String city(); - - @Nullable - @ColumnName(ContactsContract.CommonDataKinds.StructuredPostal.REGION) - public abstract String region(); - - @Nullable - @ColumnName(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE) - public abstract String postcode(); - - @Nullable - @ColumnName(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY) - public abstract String country(); - - public static PostalAddress create(Cursor cursor) { - return AutoValue_PostalAddress.createFromCursor(cursor); - } - - public enum Type { - CUSTOM(ContactsContract.CommonDataKinds.StructuredPostal.TYPE_CUSTOM), - HOME(ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME), - WORK(ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK), - OTHER(ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER); - - int typeMap; - - Type(int columnName) { - this.typeMap = columnName; - } - - public int getTypeMap() { - return typeMap; - } - } - -} diff --git a/kontact/src/main/java/com/hackedcube/kontact/Relation.java b/kontact/src/main/java/com/hackedcube/kontact/Relation.java deleted file mode 100644 index 239e61e..0000000 --- a/kontact/src/main/java/com/hackedcube/kontact/Relation.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.hackedcube.kontact; - -import android.database.Cursor; -import android.provider.ContactsContract; -import android.support.annotation.Nullable; - -import com.gabrielittner.auto.value.cursor.ColumnAdapter; -import com.gabrielittner.auto.value.cursor.ColumnName; -import com.google.auto.value.AutoValue; -import com.hackedcube.kontact.columnadapters.RelationTypeIntAdapter; - -@AutoValue -public abstract class Relation { - - @ColumnName(ContactsContract.CommonDataKinds.Relation.NAME) - public abstract String name(); - - @ColumnAdapter(RelationTypeIntAdapter.class) - @ColumnName(ContactsContract.CommonDataKinds.Relation.TYPE) - public abstract Relation.Type type(); - - @Nullable - @ColumnName(ContactsContract.CommonDataKinds.Relation.LABEL) - public abstract String label(); - - public static Relation create(Cursor cursor) { - return AutoValue_Relation.createFromCursor(cursor); - } - - - public enum Type { - CUSTOM(ContactsContract.CommonDataKinds.Relation.TYPE_CUSTOM), - ASSISTANT(ContactsContract.CommonDataKinds.Relation.TYPE_ASSISTANT), - BROTHER(ContactsContract.CommonDataKinds.Relation.TYPE_BROTHER), - CHILD(ContactsContract.CommonDataKinds.Relation.TYPE_CHILD), - DOMESTIC_PARTNER(ContactsContract.CommonDataKinds.Relation.TYPE_DOMESTIC_PARTNER), - FATHER(ContactsContract.CommonDataKinds.Relation.TYPE_FATHER), - FRIEND(ContactsContract.CommonDataKinds.Relation.TYPE_FRIEND), - MANAGER(ContactsContract.CommonDataKinds.Relation.TYPE_MANAGER), - MOTHER(ContactsContract.CommonDataKinds.Relation.TYPE_MOTHER), - PARENT(ContactsContract.CommonDataKinds.Relation.TYPE_PARENT), - PARTNER(ContactsContract.CommonDataKinds.Relation.TYPE_PARTNER), - REFERRED_BY(ContactsContract.CommonDataKinds.Relation.TYPE_REFERRED_BY), - RELATIVE(ContactsContract.CommonDataKinds.Relation.TYPE_RELATIVE), - SISTER(ContactsContract.CommonDataKinds.Relation.TYPE_SISTER), - SPOUSE(ContactsContract.CommonDataKinds.Relation.TYPE_SPOUSE); - - int typeMap; - - Type(int columnName) { - this.typeMap = columnName; - } - - public int getTypeMap() { - return typeMap; - } - } - - -} diff --git a/kontact/src/main/kotlin/com/hackedcube/kontact/ContactUtils.kt b/kontact/src/main/kotlin/com/hackedcube/kontact/ContactUtils.kt deleted file mode 100644 index 83fe17a..0000000 --- a/kontact/src/main/kotlin/com/hackedcube/kontact/ContactUtils.kt +++ /dev/null @@ -1,94 +0,0 @@ -@file:JvmName("ContactUtils") - -package com.hackedcube.kontact - -import android.content.Context -import android.database.Cursor -import android.net.Uri -import android.provider.ContactsContract - - -fun Context.queryAllContacts(): List { - contentResolver.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null).use { - return generateSequence { if (it.moveToNext()) it else null } - .map { kontactFromCursor(this, it) } - .toList() - } -} - -fun Context.getContactFromId(uri: Uri): Kontact? { - contentResolver.query(uri, null, null, null, null).use { cursorContact -> - cursorContact.moveToFirst() - return kontactFromCursor(this, cursorContact) - } -} - -private fun kontactFromCursor(context: Context, cursor: Cursor): Kontact { - var kontact = Kontact.create(cursor) - - // Fetch Phone Numbers - if (kontact.hasPhoneNumber()) { - context.contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?", arrayOf(kontact.id()), null).use { phoneCursor -> - val phoneNumbers = phoneCursor.toSequence() - .map { PhoneNumber.create(it) } - .toList() - - kontact = kontact.withPhoneNumbers(phoneNumbers) - } - } - - // Fetch Email addresses - context.contentResolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, null, ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = ?", arrayOf(kontact.id()), null).use { emailCursor -> - val emailAddresses = emailCursor.toSequence() - .map { EmailAddress.create(it) } - .toList() - - kontact = kontact.withEmailAddresses(emailAddresses) - } - - val select = arrayOf(ContactsContract.Data.MIMETYPE, "data1", "data2", "data3", "data4", - "data5", "data6", "data7", "data8", "data9", "data10", "data11", "data12", "data13", - "data14", "data15") - - // Fetch additional info - val where = "${ContactsContract.Data.CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} IN (?, ?, ?, ?)" - - val whereParams = arrayOf( - kontact.id(), - ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE, - ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE, - ContactsContract.CommonDataKinds.Relation.CONTENT_ITEM_TYPE, - ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE - ) - - context.contentResolver.query(ContactsContract.Data.CONTENT_URI, select, where, whereParams, null).use { dataCursor -> - - val data = dataCursor.toSequence() - .map { - val columnType = it.getString(it.getColumnIndex(ContactsContract.Data.MIMETYPE)) - - when(columnType) { - ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE -> columnType to Event.create(it) - ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE -> columnType to Nickname.create(it) - ContactsContract.CommonDataKinds.Relation.CONTENT_ITEM_TYPE -> columnType to Relation.create(it) - ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE -> columnType to PostalAddress.create(it) - else -> columnType to null - } - } - .groupBy({it.first}, {it.second}) - - - - - kontact = kontact.toBuilder() - .events(data[ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE] as MutableList?) - .nicknames(data[ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE] as MutableList?) - .postalAddresses(data[ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE] as MutableList?) - .relations(data[ContactsContract.CommonDataKinds.Relation.CONTENT_ITEM_TYPE] as MutableList?) - .build() - } - - - - return kontact -} \ No newline at end of file diff --git a/kontact/src/main/kotlin/com/hackedcube/kontact/Extensions.kt b/kontact/src/main/kotlin/com/hackedcube/kontact/Extensions.kt deleted file mode 100644 index 9060d70..0000000 --- a/kontact/src/main/kotlin/com/hackedcube/kontact/Extensions.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.hackedcube.kontact - -import android.database.Cursor - -fun Boolean.toFlag(): Int { - val flag = when (this) { - true -> 1 - false -> 0 - } - - return flag -} - -fun Cursor.toSequence(): Sequence { - return if (this.moveToNext()) { - generateSequence(this, { if (this.moveToNext()) this else null }) - } else { - emptySequence() - } -} diff --git a/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/BooleanIntAdapter.kt b/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/BooleanIntAdapter.kt deleted file mode 100644 index a44dd92..0000000 --- a/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/BooleanIntAdapter.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.hackedcube.kontact.columnadapters - -import android.content.ContentValues -import android.database.Cursor -import com.gabrielittner.auto.value.cursor.ColumnTypeAdapter -import com.hackedcube.kontact.toFlag - -class BooleanIntAdapter : ColumnTypeAdapter { - override fun fromCursor(cursor: Cursor, columnName: String) = cursor.getInt(cursor.getColumnIndex(columnName)) == 1 - - override fun toContentValues(contentValues: ContentValues, columnName: String, value: Boolean) { - contentValues.put(columnName, value.toFlag()) - } -} \ No newline at end of file diff --git a/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/EmailTypeIntAdapter.kt b/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/EmailTypeIntAdapter.kt deleted file mode 100644 index 1b9e2e0..0000000 --- a/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/EmailTypeIntAdapter.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.hackedcube.kontact.columnadapters - -import android.content.ContentValues -import android.database.Cursor -import com.gabrielittner.auto.value.cursor.ColumnTypeAdapter -import com.hackedcube.kontact.EmailAddress -import com.hackedcube.kontact.PhoneNumber - -class EmailTypeIntAdapter : ColumnTypeAdapter { - - override fun fromCursor(cursor: Cursor, columnName: String): EmailAddress.Type { - val typeInt = cursor.getInt(cursor.getColumnIndex(columnName)) - return EmailAddress.Type.values().first { it.typeMap == typeInt } - } - - override fun toContentValues(contentValues: ContentValues, columnName: String, value: EmailAddress.Type) { - contentValues.put(columnName, value.typeMap) - } -} \ No newline at end of file diff --git a/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/EventTypeIntAdapter.kt b/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/EventTypeIntAdapter.kt deleted file mode 100644 index 9467256..0000000 --- a/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/EventTypeIntAdapter.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.hackedcube.kontact.columnadapters - -import android.content.ContentValues -import android.database.Cursor -import com.gabrielittner.auto.value.cursor.ColumnTypeAdapter -import com.hackedcube.kontact.Event - - -class EventTypeIntAdapter : ColumnTypeAdapter { - - override fun fromCursor(cursor: Cursor, columnName: String): Event.Type { - val typeInt = cursor.getInt(cursor.getColumnIndex(columnName)) - return Event.Type.values().first { it.typeMap == typeInt } - } - - override fun toContentValues(contentValues: ContentValues, columnName: String, value: Event.Type) { - contentValues.put(columnName, value.typeMap) - } -} \ No newline at end of file diff --git a/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/NicknameTypeIntAdapter.kt b/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/NicknameTypeIntAdapter.kt deleted file mode 100644 index 9b42a6f..0000000 --- a/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/NicknameTypeIntAdapter.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.hackedcube.kontact.columnadapters - -import android.content.ContentValues -import android.database.Cursor -import com.gabrielittner.auto.value.cursor.ColumnTypeAdapter -import com.hackedcube.kontact.Nickname - - -class NicknameTypeIntAdapter : ColumnTypeAdapter { - - override fun fromCursor(cursor: Cursor, columnName: String): Nickname.Type { - val typeInt = cursor.getInt(cursor.getColumnIndex(columnName)) - return Nickname.Type.values().first { it.typeMap == typeInt } - } - - override fun toContentValues(contentValues: ContentValues, columnName: String, value: Nickname.Type) { - contentValues.put(columnName, value.typeMap) - } -} \ No newline at end of file diff --git a/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/PhoneTypeIntAdapter.kt b/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/PhoneTypeIntAdapter.kt deleted file mode 100644 index 2e07fd2..0000000 --- a/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/PhoneTypeIntAdapter.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.hackedcube.kontact.columnadapters - -import android.content.ContentValues -import android.database.Cursor -import com.gabrielittner.auto.value.cursor.ColumnTypeAdapter -import com.hackedcube.kontact.PhoneNumber - -class PhoneTypeIntAdapter : ColumnTypeAdapter { - - override fun fromCursor(cursor: Cursor, columnName: String): PhoneNumber.Type { - val typeInt = cursor.getInt(cursor.getColumnIndex(columnName)) - return PhoneNumber.Type.values().first { it.typeMap == typeInt } - } - - override fun toContentValues(contentValues: ContentValues, columnName: String, value: PhoneNumber.Type) { - contentValues.put(columnName, value.typeMap) - } -} \ No newline at end of file diff --git a/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/PostalAddressTypeIntAdapter.kt b/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/PostalAddressTypeIntAdapter.kt deleted file mode 100644 index eeeaa64..0000000 --- a/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/PostalAddressTypeIntAdapter.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.hackedcube.kontact.columnadapters - -import android.content.ContentValues -import android.database.Cursor -import com.gabrielittner.auto.value.cursor.ColumnTypeAdapter -import com.hackedcube.kontact.PostalAddress - -class PostalAddressTypeIntAdapter : ColumnTypeAdapter { - - override fun fromCursor(cursor: Cursor, columnName: String): PostalAddress.Type { - val typeInt = cursor.getInt(cursor.getColumnIndex(columnName)) - return PostalAddress.Type.values().first { it.typeMap == typeInt } - } - - override fun toContentValues(contentValues: ContentValues, columnName: String, value: PostalAddress.Type) { - contentValues.put(columnName, value.typeMap) - } -} diff --git a/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/RelationTypeIntAdapter.kt b/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/RelationTypeIntAdapter.kt deleted file mode 100644 index aa26a30..0000000 --- a/kontact/src/main/kotlin/com/hackedcube/kontact/columnadapters/RelationTypeIntAdapter.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.hackedcube.kontact.columnadapters - -import android.content.ContentValues -import android.database.Cursor -import com.gabrielittner.auto.value.cursor.ColumnTypeAdapter -import com.hackedcube.kontact.Relation - -class RelationTypeIntAdapter : ColumnTypeAdapter { - override fun fromCursor(cursor: Cursor, columnName: String): Relation.Type { - val typeInt = cursor.getInt(cursor.getColumnIndex(columnName)) - return Relation.Type.values().first { it.typeMap == typeInt } - } - - override fun toContentValues(contentValues: ContentValues, columnName: String, value: Relation.Type) { - contentValues.put(columnName, value.typeMap) - } -} \ No newline at end of file diff --git a/kontact/src/wasmJsMain/kotlin/com/hackedcube/kontact/WasmKontactRepository.kt b/kontact/src/wasmJsMain/kotlin/com/hackedcube/kontact/WasmKontactRepository.kt new file mode 100644 index 0000000..ddfdbd4 --- /dev/null +++ b/kontact/src/wasmJsMain/kotlin/com/hackedcube/kontact/WasmKontactRepository.kt @@ -0,0 +1,22 @@ +package com.hackedcube.kontact + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +/** + * WasmJs (browser) implementation of [KontactRepository]. + * + * The Contact Picker API is experimental and only available in + * Chromium-based browsers. This stub returns empty results; + * consumers can extend it to integrate with the browser API. + */ +class WasmKontactRepository : KontactRepository { + + override suspend fun queryAllContacts(): List = emptyList() + + override suspend fun getContact(id: String): Kontact? = null + + override fun observeAllContacts(): Flow> = flow { + emit(emptyList()) + } +} diff --git a/sample/build.gradle b/sample/build.gradle deleted file mode 100644 index f9bd0d4..0000000 --- a/sample/build.gradle +++ /dev/null @@ -1,38 +0,0 @@ -apply plugin: 'com.android.application' - -android { - compileSdkVersion 26 - buildToolsVersion "26.0.2" - defaultConfig { - applicationId "com.hackedcube.kontact.sample" - minSdkVersion 16 - targetSdkVersion 26 - versionCode 1 - versionName properties.version - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} - -dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { - exclude group: 'com.android.support', module: 'support-annotations' - }) - - compile project(path: ':kontact') - compile 'com.android.support:appcompat-v7:26.1.0' - compile 'com.android.support.constraint:constraint-layout:1.0.2' - compile 'com.karumi:dexter:4.2.0' - testCompile 'junit:junit:4.12' -} diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts new file mode 100644 index 0000000..b424f8e --- /dev/null +++ b/sample/build.gradle.kts @@ -0,0 +1,89 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidApplication) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.kotlinCompose) +} + +kotlin { + androidTarget { + compilations.all { + compileTaskProvider.configure { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } + } + } + } + + jvm("desktop") + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64(), + ).forEach { target -> + target.binaries.framework { + baseName = "sample" + isStatic = true + } + } + + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + + sourceSets { + commonMain.dependencies { + implementation(project(":kontact")) + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.components.resources) + implementation(libs.kotlinx.coroutines.core) + } + androidMain.dependencies { + implementation(libs.androidx.activity.compose) + implementation(libs.kotlinx.coroutines.android) + } + val desktopMain by getting { + dependencies { + implementation(compose.desktop.currentOs) + implementation(libs.kotlinx.coroutines.swing) + } + } + } +} + +android { + namespace = "com.hackedcube.kontact.sample" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + applicationId = "com.hackedcube.kontact.sample" + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + versionCode = 1 + versionName = "2.0.0" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +compose.desktop { + application { + mainClass = "com.hackedcube.kontact.sample.MainKt" + } +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/androidMain/AndroidManifest.xml similarity index 63% rename from sample/src/main/AndroidManifest.xml rename to sample/src/androidMain/AndroidManifest.xml index 94bfed2..a75b576 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/androidMain/AndroidManifest.xml @@ -1,21 +1,19 @@ - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + diff --git a/sample/src/androidMain/kotlin/com/hackedcube/kontact/sample/MainActivity.kt b/sample/src/androidMain/kotlin/com/hackedcube/kontact/sample/MainActivity.kt new file mode 100644 index 0000000..7891b5c --- /dev/null +++ b/sample/src/androidMain/kotlin/com/hackedcube/kontact/sample/MainActivity.kt @@ -0,0 +1,30 @@ +package com.hackedcube.kontact.sample + +import android.Manifest +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts +import com.hackedcube.kontact.AndroidKontactRepository + +class MainActivity : ComponentActivity() { + + private val permissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + if (!granted) { + Toast.makeText(this, "Contact permission required", Toast.LENGTH_SHORT).show() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + permissionLauncher.launch(Manifest.permission.READ_CONTACTS) + + val repository = AndroidKontactRepository(this) + setContent { + App(repository) + } + } +} diff --git a/sample/src/main/res/values/colors.xml b/sample/src/androidMain/res/values/colors.xml similarity index 78% rename from sample/src/main/res/values/colors.xml rename to sample/src/androidMain/res/values/colors.xml index 2a12c47..6e10013 100644 --- a/sample/src/main/res/values/colors.xml +++ b/sample/src/androidMain/res/values/colors.xml @@ -1,6 +1,5 @@ - - - #3F51B5 - #303F9F - #FF4081 - + + #3F51B5 + #303F9F + #FF4081 + diff --git a/sample/src/main/res/values/strings.xml b/sample/src/androidMain/res/values/strings.xml similarity index 95% rename from sample/src/main/res/values/strings.xml rename to sample/src/androidMain/res/values/strings.xml index 341077a..1842251 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/androidMain/res/values/strings.xml @@ -1,3 +1,3 @@ - - Kontact - + + Kontact + diff --git a/sample/src/androidTest/java/com/hackedcube/kontact/ExampleInstrumentedTest.java b/sample/src/androidTest/java/com/hackedcube/kontact/ExampleInstrumentedTest.java deleted file mode 100644 index a16de70..0000000 --- a/sample/src/androidTest/java/com/hackedcube/kontact/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.hackedcube.kontact; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumentation test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() throws Exception { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("com.hackedcube.kontact", appContext.getPackageName()); - } -} diff --git a/sample/src/commonMain/kotlin/com/hackedcube/kontact/sample/App.kt b/sample/src/commonMain/kotlin/com/hackedcube/kontact/sample/App.kt new file mode 100644 index 0000000..323021c --- /dev/null +++ b/sample/src/commonMain/kotlin/com/hackedcube/kontact/sample/App.kt @@ -0,0 +1,129 @@ +package com.hackedcube.kontact.sample + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.hackedcube.kontact.Kontact +import com.hackedcube.kontact.KontactRepository +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun App(repository: KontactRepository) { + MaterialTheme { + var contacts by remember { mutableStateOf>(emptyList()) } + var loading by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + Scaffold( + topBar = { + TopAppBar(title = { Text("Kontact") }) + }, + ) { padding -> + Column( + modifier = Modifier.fillMaxSize().padding(padding).padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Button( + onClick = { + loading = true + scope.launch { + contacts = repository.queryAllContacts() + loading = false + } + }, + enabled = !loading, + ) { + Text(if (loading) "Loading..." else "Import All Contacts") + } + + Spacer(Modifier.height(8.dp)) + + Text( + text = "${contacts.size} contacts loaded", + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(Modifier.height(16.dp)) + + LazyColumn( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(vertical = 4.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(contacts, key = { it.id }) { contact -> + ContactCard(contact) + } + } + } + } + } +} + +@Composable +private fun ContactCard(contact: Kontact) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = contact.displayNamePrimary, + style = MaterialTheme.typography.titleMedium, + ) + + if (contact.phoneNumbers.isNotEmpty()) { + Spacer(Modifier.height(4.dp)) + contact.phoneNumbers.forEach { phone -> + Row { + Text( + text = "${phone.type}: ", + style = MaterialTheme.typography.labelSmall, + ) + Text( + text = phone.number, + style = MaterialTheme.typography.bodySmall, + ) + } + } + } + + if (contact.emailAddresses.isNotEmpty()) { + Spacer(Modifier.height(4.dp)) + contact.emailAddresses.forEach { email -> + Row { + Text( + text = "${email.type}: ", + style = MaterialTheme.typography.labelSmall, + ) + Text( + text = email.address, + style = MaterialTheme.typography.bodySmall, + ) + } + } + } + } + } +} diff --git a/sample/src/desktopMain/kotlin/com/hackedcube/kontact/sample/Main.kt b/sample/src/desktopMain/kotlin/com/hackedcube/kontact/sample/Main.kt new file mode 100644 index 0000000..e7de265 --- /dev/null +++ b/sample/src/desktopMain/kotlin/com/hackedcube/kontact/sample/Main.kt @@ -0,0 +1,14 @@ +package com.hackedcube.kontact.sample + +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import com.hackedcube.kontact.DesktopKontactRepository + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Kontact", + ) { + App(DesktopKontactRepository()) + } +} diff --git a/sample/src/iosMain/kotlin/com/hackedcube/kontact/sample/MainViewController.kt b/sample/src/iosMain/kotlin/com/hackedcube/kontact/sample/MainViewController.kt new file mode 100644 index 0000000..a66ff7c --- /dev/null +++ b/sample/src/iosMain/kotlin/com/hackedcube/kontact/sample/MainViewController.kt @@ -0,0 +1,8 @@ +package com.hackedcube.kontact.sample + +import androidx.compose.ui.window.ComposeUIViewController +import com.hackedcube.kontact.IosKontactRepository + +fun MainViewController() = ComposeUIViewController { + App(IosKontactRepository()) +} diff --git a/sample/src/main/java/com/hackedcube/kontact/sample/MainActivity.java b/sample/src/main/java/com/hackedcube/kontact/sample/MainActivity.java deleted file mode 100644 index a2b9d0b..0000000 --- a/sample/src/main/java/com/hackedcube/kontact/sample/MainActivity.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.hackedcube.kontact.sample; - -import android.Manifest; -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.provider.ContactsContract; -import android.util.Log; -import android.view.View; -import android.widget.Button; - -import com.hackedcube.kontact.ContactUtils; -import com.hackedcube.kontact.Kontact; -import com.karumi.dexter.Dexter; -import com.karumi.dexter.PermissionToken; -import com.karumi.dexter.listener.PermissionDeniedResponse; -import com.karumi.dexter.listener.PermissionGrantedResponse; -import com.karumi.dexter.listener.PermissionRequest; -import com.karumi.dexter.listener.single.PermissionListener; - -import java.util.List; - -public class MainActivity extends Activity { - - private final int PICK_CONTACT = 1; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - - Dexter.withActivity(this) - .withPermission(Manifest.permission.READ_CONTACTS) - .withListener(new PermissionListener() { - @Override - public void onPermissionGranted(PermissionGrantedResponse response) { - // NO OP - } - - @Override - public void onPermissionDenied(PermissionDeniedResponse response) { - // NO-OP - } - - @Override - public void onPermissionRationaleShouldBeShown(PermissionRequest permission, PermissionToken token) { - token.continuePermissionRequest(); - } - }) - .check(); - - - Button buttonImportContact = (Button) findViewById(R.id.buttonImport); - - buttonImportContact.setOnClickListener(v -> { - Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI); - startActivityForResult(intent, PICK_CONTACT); - }); - - - Button buttonImportAllContact = (Button) findViewById(R.id.buttonImportAllContacts); - - - buttonImportAllContact.setOnClickListener(v -> { - List kontacts = ContactUtils.queryAllContacts(this); - Log.d("Kontacts", "Kontacts Count: " + kontacts); - }); - - - - } - - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (resultCode == Activity.RESULT_OK && requestCode == PICK_CONTACT) { - Kontact kontact = ContactUtils.getContactFromId(this, data.getData()); - - Log.d("Kontact Import", "Kontact: " + kontact.toString()); - } - - } -} diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml deleted file mode 100644 index c3ff86e..0000000 --- a/sample/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - -