Initial commit: LDAP Contacts Android App
Features: - LDAP contact management (CRUD) - Search by name, phone, company - Call/SMS integration - Android contacts sync for caller ID - Material Design 3 UI LDAP Structure: - uid-based DN for flexible cn modification - Attributes: cn, displayName, mobile, mail, o, ou, title
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Gradle
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Android Studio
|
||||||
|
*.iml
|
||||||
|
.idea/
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# APK files
|
||||||
|
*.apk
|
||||||
|
*.aab
|
||||||
|
|
||||||
|
# Signing
|
||||||
|
*.jks
|
||||||
|
*.keystore
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
121
README.md
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
# LDAPContacts (외부 주소록)
|
||||||
|
|
||||||
|
LDAP 서버와 연동하여 연락처를 관리하는 Android 앱입니다.
|
||||||
|
|
||||||
|
## 주요 기능
|
||||||
|
|
||||||
|
- **LDAP 연락처 조회**: LDAP 서버에서 연락처 목록 조회
|
||||||
|
- **검색**: 이름, 전화번호, 회사명으로 검색
|
||||||
|
- **연락처 추가/수정/삭제**: LDAP 서버에 연락처 CRUD
|
||||||
|
- **전화/문자**: 연락처에서 바로 전화 걸기, 문자 보내기
|
||||||
|
- **Android 연락처 동기화**: LDAP 연락처를 Android 기본 연락처에 동기화 (발신자 표시용)
|
||||||
|
|
||||||
|
## 기술 스택
|
||||||
|
|
||||||
|
- **Language**: Kotlin
|
||||||
|
- **UI**: Android View Binding, Material Design 3
|
||||||
|
- **LDAP**: UnboundID LDAP SDK
|
||||||
|
- **Async**: Kotlin Coroutines
|
||||||
|
|
||||||
|
## LDAP 구조
|
||||||
|
|
||||||
|
### DN (Distinguished Name) 형식
|
||||||
|
```
|
||||||
|
uid={uuid},ou=ioresponse,ou=users,dc=ioresponse,dc=net
|
||||||
|
```
|
||||||
|
|
||||||
|
uid 기반 DN을 사용하여 cn(이름) 수정이 자유롭습니다.
|
||||||
|
|
||||||
|
### 사용하는 LDAP 속성
|
||||||
|
|
||||||
|
| 속성 | 설명 | 앱 필드 |
|
||||||
|
|------|------|---------|
|
||||||
|
| uid | 고유 식별자 (UUID) | - |
|
||||||
|
| cn | 이름 | 이름 |
|
||||||
|
| displayName | 별명/닉네임 | 별명 |
|
||||||
|
| telephoneNumber | 전화번호 | 전화번호 |
|
||||||
|
| mobile | 휴대폰번호 | 전화번호 (우선) |
|
||||||
|
| mail | 이메일 | 이메일 |
|
||||||
|
| o | 회사/조직 | 회사 |
|
||||||
|
| ou | 부서 | 부서 |
|
||||||
|
| title | 직함 | 직함 |
|
||||||
|
|
||||||
|
### 이름 표시 로직
|
||||||
|
- `displayName`이 있으면 displayName 표시
|
||||||
|
- 없으면 `cn` 표시
|
||||||
|
|
||||||
|
## 주요 소스 파일
|
||||||
|
|
||||||
|
```
|
||||||
|
app/src/main/java/net/ioresponse/ldapcontacts/
|
||||||
|
├── MainActivity.kt # 메인 화면 (연락처 목록)
|
||||||
|
├── ContactDetailActivity.kt # 연락처 상세 화면
|
||||||
|
├── EditContactActivity.kt # 연락처 추가/수정 화면
|
||||||
|
├── SettingsActivity.kt # LDAP 서버 설정
|
||||||
|
├── Contact.kt # 연락처 데이터 모델
|
||||||
|
├── ContactAdapter.kt # RecyclerView 어댑터
|
||||||
|
├── LdapManager.kt # LDAP 통신 관리
|
||||||
|
└── ContactSyncManager.kt # Android 연락처 동기화
|
||||||
|
```
|
||||||
|
|
||||||
|
## LdapManager 주요 함수
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// 전체 연락처 조회
|
||||||
|
suspend fun getContacts(): Result<List<Contact>>
|
||||||
|
|
||||||
|
// 연락처 검색 (이름, 전화번호, 회사명)
|
||||||
|
suspend fun searchContacts(query: String): Result<List<Contact>>
|
||||||
|
|
||||||
|
// DN으로 연락처 조회
|
||||||
|
suspend fun getContactByDn(dn: String): Contact?
|
||||||
|
|
||||||
|
// 연락처 추가
|
||||||
|
suspend fun addContact(contact: Contact): Result<Unit>
|
||||||
|
|
||||||
|
// 연락처 수정
|
||||||
|
suspend fun updateContact(contact: Contact): Result<Unit>
|
||||||
|
|
||||||
|
// 연락처 삭제
|
||||||
|
suspend fun deleteContact(contact: Contact): Result<Unit>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 연락처 동기화 로직
|
||||||
|
|
||||||
|
`ContactSyncManager.kt`에서 Android 기본 연락처에 동기화:
|
||||||
|
|
||||||
|
1. **기존 연락처 전체 삭제** (CALLER_IS_SYNCADAPTER 사용)
|
||||||
|
2. **휴지통 비우기 시도**
|
||||||
|
3. **LDAP 연락처 추가**
|
||||||
|
|
||||||
|
> 삼성 기기에서는 휴지통 자동 비우기가 동작하지 않아 사용자가 수동으로 비워야 합니다.
|
||||||
|
|
||||||
|
## 빌드 방법
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debug APK 빌드
|
||||||
|
./gradlew assembleDebug
|
||||||
|
|
||||||
|
# APK 위치
|
||||||
|
app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
## 설정
|
||||||
|
|
||||||
|
앱 실행 후 설정 화면에서 LDAP 서버 정보 입력:
|
||||||
|
|
||||||
|
- **서버**: LDAP 서버 주소
|
||||||
|
- **포트**: LDAP 포트 (기본 389)
|
||||||
|
- **Bind DN**: 인증용 DN (예: cn=Directory Manager)
|
||||||
|
- **비밀번호**: Bind DN 비밀번호
|
||||||
|
- **Base DN**: 검색 기준 DN
|
||||||
|
|
||||||
|
## 권한
|
||||||
|
|
||||||
|
- `INTERNET`: LDAP 서버 연결
|
||||||
|
- `READ_CONTACTS`, `WRITE_CONTACTS`: Android 연락처 동기화
|
||||||
|
- `CALL_PHONE`: 전화 걸기
|
||||||
|
|
||||||
|
## 라이선스
|
||||||
|
|
||||||
|
Private - ioresponse.net
|
||||||
62
app/build.gradle
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
plugins {
|
||||||
|
id 'com.android.application'
|
||||||
|
id 'org.jetbrains.kotlin.android'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'net.ioresponse.ldapcontacts'
|
||||||
|
compileSdk 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "net.ioresponse.ldapcontacts"
|
||||||
|
minSdk 26
|
||||||
|
targetSdk 34
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '17'
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources {
|
||||||
|
excludes += ['META-INF/DEPENDENCIES', 'META-INF/LICENSE', 'META-INF/LICENSE.txt', 'META-INF/NOTICE', 'META-INF/NOTICE.txt']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||||
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
|
|
||||||
|
// LDAP
|
||||||
|
implementation 'com.unboundid:unboundid-ldapsdk:6.0.11'
|
||||||
|
|
||||||
|
// Coroutines
|
||||||
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
||||||
|
}
|
||||||
3
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# UnboundID LDAP SDK
|
||||||
|
-keep class com.unboundid.** { *; }
|
||||||
|
-dontwarn com.unboundid.**
|
||||||
43
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||||
|
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.LDAPContacts"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
tools:targetApi="31">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ContactDetailActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".EditContactActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".SettingsActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
44
app/src/main/java/net/ioresponse/ldapcontacts/Contact.kt
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package net.ioresponse.ldapcontacts
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class Contact(
|
||||||
|
var dn: String = "",
|
||||||
|
var cn: String = "",
|
||||||
|
var displayName: String = "",
|
||||||
|
var telephoneNumber: String = "",
|
||||||
|
var mobile: String = "",
|
||||||
|
var mail: String = "",
|
||||||
|
var organization: String = "",
|
||||||
|
var department: String = "",
|
||||||
|
var title: String = ""
|
||||||
|
) : Serializable {
|
||||||
|
|
||||||
|
val name: String
|
||||||
|
get() = displayName.ifEmpty { cn }
|
||||||
|
|
||||||
|
val phone: String
|
||||||
|
get() = mobile.ifEmpty { telephoneNumber }
|
||||||
|
|
||||||
|
val initials: String
|
||||||
|
get() {
|
||||||
|
val n = name
|
||||||
|
return if (n.isNotEmpty()) n.first().toString() else "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromLdapEntry(dn: String, attributes: Map<String, String>): Contact {
|
||||||
|
return Contact(
|
||||||
|
dn = dn,
|
||||||
|
cn = attributes["cn"] ?: "",
|
||||||
|
displayName = attributes["displayName"] ?: "",
|
||||||
|
telephoneNumber = attributes["telephoneNumber"] ?: "",
|
||||||
|
mobile = attributes["mobile"] ?: "",
|
||||||
|
mail = attributes["mail"] ?: "",
|
||||||
|
organization = attributes["o"] ?: "",
|
||||||
|
department = attributes["ou"] ?: attributes["departmentNumber"] ?: "",
|
||||||
|
title = attributes["title"] ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
package net.ioresponse.ldapcontacts
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
class ContactAdapter(
|
||||||
|
private var contacts: List<Contact>,
|
||||||
|
private val onItemClick: (Contact) -> Unit,
|
||||||
|
private val onCallClick: (Contact) -> Unit
|
||||||
|
) : RecyclerView.Adapter<ContactAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
private val colors = listOf(
|
||||||
|
Color.parseColor("#F44336"),
|
||||||
|
Color.parseColor("#E91E63"),
|
||||||
|
Color.parseColor("#9C27B0"),
|
||||||
|
Color.parseColor("#673AB7"),
|
||||||
|
Color.parseColor("#3F51B5"),
|
||||||
|
Color.parseColor("#2196F3"),
|
||||||
|
Color.parseColor("#03A9F4"),
|
||||||
|
Color.parseColor("#00BCD4"),
|
||||||
|
Color.parseColor("#009688"),
|
||||||
|
Color.parseColor("#4CAF50"),
|
||||||
|
Color.parseColor("#8BC34A"),
|
||||||
|
Color.parseColor("#FF9800"),
|
||||||
|
Color.parseColor("#FF5722")
|
||||||
|
)
|
||||||
|
|
||||||
|
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
val initialsText: TextView = view.findViewById(R.id.initialsText)
|
||||||
|
val nameText: TextView = view.findViewById(R.id.nameText)
|
||||||
|
val phoneText: TextView = view.findViewById(R.id.phoneText)
|
||||||
|
val orgText: TextView = view.findViewById(R.id.orgText)
|
||||||
|
val callButton: ImageButton = view.findViewById(R.id.callButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_contact, parent, false)
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val contact = contacts[position]
|
||||||
|
|
||||||
|
holder.initialsText.text = contact.initials
|
||||||
|
holder.initialsText.background.setTint(colors[position % colors.size])
|
||||||
|
|
||||||
|
// 이름 + 직급 표시
|
||||||
|
val nameWithTitle = buildString {
|
||||||
|
append(contact.name)
|
||||||
|
if (contact.title.isNotEmpty() && contact.title != "-") {
|
||||||
|
append(" ")
|
||||||
|
append(contact.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.nameText.text = nameWithTitle
|
||||||
|
holder.phoneText.text = contact.phone.ifEmpty { "-" }
|
||||||
|
|
||||||
|
// 회사 - 부서 형식으로 표시
|
||||||
|
val orgDept = buildString {
|
||||||
|
if (contact.organization.isNotEmpty() && contact.organization != "-") {
|
||||||
|
append(contact.organization)
|
||||||
|
}
|
||||||
|
if (contact.department.isNotEmpty() && contact.department != "-") {
|
||||||
|
if (isNotEmpty()) append(" - ")
|
||||||
|
append(contact.department)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (orgDept.isNotEmpty()) {
|
||||||
|
holder.orgText.visibility = View.VISIBLE
|
||||||
|
holder.orgText.text = orgDept
|
||||||
|
} else {
|
||||||
|
holder.orgText.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.itemView.setOnClickListener { onItemClick(contact) }
|
||||||
|
|
||||||
|
if (contact.phone.isNotEmpty()) {
|
||||||
|
holder.callButton.visibility = View.VISIBLE
|
||||||
|
holder.callButton.setOnClickListener { onCallClick(contact) }
|
||||||
|
} else {
|
||||||
|
holder.callButton.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = contacts.size
|
||||||
|
|
||||||
|
fun updateContacts(newContacts: List<Contact>) {
|
||||||
|
contacts = newContacts
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,140 @@
|
|||||||
|
package net.ioresponse.ldapcontacts
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import net.ioresponse.ldapcontacts.databinding.ActivityContactDetailBinding
|
||||||
|
|
||||||
|
class ContactDetailActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityContactDetailBinding
|
||||||
|
private lateinit var ldapManager: LdapManager
|
||||||
|
private var contact: Contact? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityContactDetailBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
ldapManager = LdapManager(this)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
contact = intent.getSerializableExtra("contact") as? Contact
|
||||||
|
|
||||||
|
setupToolbar()
|
||||||
|
displayContact()
|
||||||
|
setupButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupToolbar() {
|
||||||
|
setSupportActionBar(binding.toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
binding.toolbar.setNavigationOnClickListener { finish() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayContact() {
|
||||||
|
contact?.let { c ->
|
||||||
|
supportActionBar?.title = c.name
|
||||||
|
|
||||||
|
binding.initialsText.text = c.initials
|
||||||
|
binding.nameText.text = c.name
|
||||||
|
binding.phoneText.text = c.phone.ifEmpty { "-" }
|
||||||
|
binding.emailText.text = c.mail.ifEmpty { "-" }
|
||||||
|
binding.orgText.text = c.organization.ifEmpty { "-" }
|
||||||
|
binding.deptText.text = c.department.ifEmpty { "-" }
|
||||||
|
binding.titleText.text = c.title.ifEmpty { "-" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupButtons() {
|
||||||
|
binding.callButton.setOnClickListener {
|
||||||
|
contact?.phone?.let { phone ->
|
||||||
|
if (phone.isNotEmpty()) makeCall(phone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.messageButton.setOnClickListener {
|
||||||
|
contact?.phone?.let { phone ->
|
||||||
|
if (phone.isNotEmpty()) {
|
||||||
|
val intent = Intent(Intent.ACTION_SENDTO)
|
||||||
|
intent.data = Uri.parse("smsto:$phone")
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.editButton.setOnClickListener {
|
||||||
|
val intent = Intent(this, EditContactActivity::class.java)
|
||||||
|
intent.putExtra("contact", contact)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.deleteButton.setOnClickListener {
|
||||||
|
showDeleteConfirmation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeleteConfirmation() {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.delete_contact)
|
||||||
|
.setMessage(R.string.confirm_delete)
|
||||||
|
.setPositiveButton(R.string.delete) { _, _ ->
|
||||||
|
deleteContact()
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteContact() {
|
||||||
|
contact?.let { c ->
|
||||||
|
lifecycleScope.launch {
|
||||||
|
ldapManager.deleteContact(c).fold(
|
||||||
|
onSuccess = {
|
||||||
|
Toast.makeText(this@ContactDetailActivity, "삭제되었습니다", Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
Toast.makeText(this@ContactDetailActivity, "삭제 실패: ${error.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeCall(phoneNumber: String) {
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CALL_PHONE), 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val intent = Intent(Intent.ACTION_CALL)
|
||||||
|
intent.data = Uri.parse("tel:$phoneNumber")
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
// Reload contact in case it was edited
|
||||||
|
reloadContact()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reloadContact() {
|
||||||
|
contact?.let { c ->
|
||||||
|
lifecycleScope.launch {
|
||||||
|
ldapManager.getContactByDn(c.dn)?.let { updated ->
|
||||||
|
contact = updated
|
||||||
|
displayContact()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,174 @@
|
|||||||
|
package net.ioresponse.ldapcontacts
|
||||||
|
|
||||||
|
import android.content.ContentProviderOperation
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.ContactsContract
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class ContactSyncManager(private val context: Context) {
|
||||||
|
|
||||||
|
suspend fun syncContacts(ldapContacts: List<Contact>): Result<SyncResult> = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val contentResolver = context.contentResolver
|
||||||
|
|
||||||
|
// 1. 전체 연락처 삭제
|
||||||
|
val deletedCount = deleteAllContacts(contentResolver)
|
||||||
|
|
||||||
|
// 2. 휴지통 비우기
|
||||||
|
emptyTrash(contentResolver)
|
||||||
|
|
||||||
|
// 3. LDAP 연락처 추가
|
||||||
|
var addedCount = 0
|
||||||
|
for (contact in ldapContacts) {
|
||||||
|
if (addContact(contentResolver, contact)) {
|
||||||
|
addedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result.success(SyncResult(addedCount, deletedCount))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteAllContacts(contentResolver: ContentResolver): Int {
|
||||||
|
var totalDeleted = 0
|
||||||
|
|
||||||
|
// 1. 모든 RawContact ID 조회
|
||||||
|
val rawContactIds = mutableListOf<Long>()
|
||||||
|
contentResolver.query(
|
||||||
|
ContactsContract.RawContacts.CONTENT_URI,
|
||||||
|
arrayOf(ContactsContract.RawContacts._ID),
|
||||||
|
null, null, null
|
||||||
|
)?.use { cursor ->
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
rawContactIds.add(cursor.getLong(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 각 RawContact를 CALLER_IS_SYNCADAPTER로 개별 삭제 (영구 삭제)
|
||||||
|
for (rawContactId in rawContactIds) {
|
||||||
|
try {
|
||||||
|
val uri = ContactsContract.RawContacts.CONTENT_URI.buildUpon()
|
||||||
|
.appendPath(rawContactId.toString())
|
||||||
|
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
|
||||||
|
.build()
|
||||||
|
val deleted = contentResolver.delete(uri, null, null)
|
||||||
|
totalDeleted += deleted
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalDeleted
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emptyTrash(contentResolver: ContentResolver) {
|
||||||
|
// 휴지통에 있는 연락처 (DELETED=1) 영구 삭제
|
||||||
|
try {
|
||||||
|
val deletedIds = mutableListOf<Long>()
|
||||||
|
contentResolver.query(
|
||||||
|
ContactsContract.RawContacts.CONTENT_URI,
|
||||||
|
arrayOf(ContactsContract.RawContacts._ID),
|
||||||
|
"${ContactsContract.RawContacts.DELETED} = 1",
|
||||||
|
null, null
|
||||||
|
)?.use { cursor ->
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
deletedIds.add(cursor.getLong(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (id in deletedIds) {
|
||||||
|
val uri = ContactsContract.RawContacts.CONTENT_URI.buildUpon()
|
||||||
|
.appendPath(id.toString())
|
||||||
|
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
|
||||||
|
.build()
|
||||||
|
contentResolver.delete(uri, null, null)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addContact(contentResolver: ContentResolver, contact: Contact): Boolean {
|
||||||
|
try {
|
||||||
|
val ops = ArrayList<ContentProviderOperation>()
|
||||||
|
|
||||||
|
// RawContact 생성
|
||||||
|
ops.add(
|
||||||
|
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
|
||||||
|
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
|
||||||
|
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
// 이름
|
||||||
|
val displayName = buildString {
|
||||||
|
append(contact.name)
|
||||||
|
if (contact.title.isNotEmpty() && contact.title != "-") {
|
||||||
|
append(" ")
|
||||||
|
append(contact.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ops.add(
|
||||||
|
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||||
|
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
|
||||||
|
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
|
||||||
|
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
// 전화번호
|
||||||
|
if (contact.phone.isNotEmpty()) {
|
||||||
|
ops.add(
|
||||||
|
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||||
|
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
|
||||||
|
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
|
||||||
|
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, contact.phone)
|
||||||
|
.withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이메일
|
||||||
|
if (contact.mail.isNotEmpty() && contact.mail != "-") {
|
||||||
|
ops.add(
|
||||||
|
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||||
|
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
|
||||||
|
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
|
||||||
|
.withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, contact.mail)
|
||||||
|
.withValue(ContactsContract.CommonDataKinds.Email.TYPE, ContactsContract.CommonDataKinds.Email.TYPE_WORK)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 회사/부서/직급
|
||||||
|
val company = if (contact.organization.isNotEmpty() && contact.organization != "-") contact.organization else ""
|
||||||
|
val department = if (contact.department.isNotEmpty() && contact.department != "-") contact.department else ""
|
||||||
|
val title = if (contact.title.isNotEmpty() && contact.title != "-") contact.title else ""
|
||||||
|
|
||||||
|
if (company.isNotEmpty() || department.isNotEmpty() || title.isNotEmpty()) {
|
||||||
|
ops.add(
|
||||||
|
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||||
|
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
|
||||||
|
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE)
|
||||||
|
.withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, company)
|
||||||
|
.withValue(ContactsContract.CommonDataKinds.Organization.DEPARTMENT, department)
|
||||||
|
.withValue(ContactsContract.CommonDataKinds.Organization.TITLE, title)
|
||||||
|
.withValue(ContactsContract.CommonDataKinds.Organization.TYPE, ContactsContract.CommonDataKinds.Organization.TYPE_WORK)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SyncResult(val added: Int, val deleted: Int)
|
||||||
|
}
|
||||||
@ -0,0 +1,131 @@
|
|||||||
|
package net.ioresponse.ldapcontacts
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import net.ioresponse.ldapcontacts.databinding.ActivityEditContactBinding
|
||||||
|
|
||||||
|
class EditContactActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityEditContactBinding
|
||||||
|
private lateinit var ldapManager: LdapManager
|
||||||
|
private var existingContact: Contact? = null
|
||||||
|
private var isEditMode = false
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityEditContactBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
ldapManager = LdapManager(this)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
existingContact = intent.getSerializableExtra("contact") as? Contact
|
||||||
|
isEditMode = existingContact != null
|
||||||
|
|
||||||
|
setupToolbar()
|
||||||
|
populateFields()
|
||||||
|
setupSaveButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupToolbar() {
|
||||||
|
setSupportActionBar(binding.toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.title = if (isEditMode) getString(R.string.edit_contact) else getString(R.string.add_contact)
|
||||||
|
binding.toolbar.setNavigationOnClickListener { finish() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateFields() {
|
||||||
|
existingContact?.let { contact ->
|
||||||
|
binding.nameEditText.setText(contact.cn)
|
||||||
|
binding.nicknameEditText.setText(contact.displayName)
|
||||||
|
binding.phoneEditText.setText(contact.phone)
|
||||||
|
binding.emailEditText.setText(contact.mail)
|
||||||
|
binding.companyEditText.setText(contact.organization)
|
||||||
|
binding.departmentEditText.setText(contact.department)
|
||||||
|
binding.titleEditText.setText(contact.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSaveButton() {
|
||||||
|
binding.saveButton.setOnClickListener {
|
||||||
|
val name = binding.nameEditText.text?.toString()?.trim() ?: ""
|
||||||
|
val nickname = binding.nicknameEditText.text?.toString()?.trim() ?: ""
|
||||||
|
val phone = binding.phoneEditText.text?.toString()?.trim() ?: ""
|
||||||
|
val email = binding.emailEditText.text?.toString()?.trim() ?: ""
|
||||||
|
val company = binding.companyEditText.text?.toString()?.trim() ?: ""
|
||||||
|
val department = binding.departmentEditText.text?.toString()?.trim() ?: ""
|
||||||
|
val title = binding.titleEditText.text?.toString()?.trim() ?: ""
|
||||||
|
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
binding.nameEditText.error = "이름을 입력하세요"
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
val contact = if (isEditMode) {
|
||||||
|
existingContact!!.copy(
|
||||||
|
cn = name,
|
||||||
|
displayName = nickname,
|
||||||
|
mobile = phone,
|
||||||
|
telephoneNumber = phone,
|
||||||
|
mail = email,
|
||||||
|
organization = company,
|
||||||
|
department = department,
|
||||||
|
title = title
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Contact(
|
||||||
|
cn = name,
|
||||||
|
displayName = nickname,
|
||||||
|
mobile = phone,
|
||||||
|
telephoneNumber = phone,
|
||||||
|
mail = email,
|
||||||
|
organization = company,
|
||||||
|
department = department,
|
||||||
|
title = title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveContact(contact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveContact(contact: Contact) {
|
||||||
|
showLoading(true)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val result = if (isEditMode) {
|
||||||
|
ldapManager.updateContact(contact)
|
||||||
|
} else {
|
||||||
|
ldapManager.addContact(contact)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
onSuccess = {
|
||||||
|
Toast.makeText(
|
||||||
|
this@EditContactActivity,
|
||||||
|
if (isEditMode) "수정되었습니다" else "추가되었습니다",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
showLoading(false)
|
||||||
|
Toast.makeText(
|
||||||
|
this@EditContactActivity,
|
||||||
|
"저장 실패: ${error.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoading(show: Boolean) {
|
||||||
|
binding.progressBar.visibility = if (show) View.VISIBLE else View.GONE
|
||||||
|
binding.saveButton.isEnabled = !show
|
||||||
|
}
|
||||||
|
}
|
||||||
263
app/src/main/java/net/ioresponse/ldapcontacts/LdapManager.kt
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
package net.ioresponse.ldapcontacts
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.unboundid.ldap.sdk.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.UUID
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
class LdapManager(private val context: Context) {
|
||||||
|
|
||||||
|
// Quoted-Printable 디코딩
|
||||||
|
private fun decodeQuotedPrintable(input: String): String {
|
||||||
|
if (!input.contains("=")) return input
|
||||||
|
|
||||||
|
try {
|
||||||
|
// soft line break 제거 및 끝의 불완전한 = 제거
|
||||||
|
var cleaned = input.replace("=\r\n", "").replace("=\n", "").replace("=\r", "")
|
||||||
|
// 끝에 = 또는 =X 형태로 끝나면 제거 (불완전한 인코딩)
|
||||||
|
cleaned = cleaned.replace(Regex("=[0-9A-Fa-f]?$"), "")
|
||||||
|
|
||||||
|
val baos = ByteArrayOutputStream()
|
||||||
|
var i = 0
|
||||||
|
while (i < cleaned.length) {
|
||||||
|
val c = cleaned[i]
|
||||||
|
if (c == '=' && i + 2 < cleaned.length) {
|
||||||
|
val hex = cleaned.substring(i + 1, i + 3)
|
||||||
|
if (hex.matches(Regex("[0-9A-Fa-f]{2}"))) {
|
||||||
|
baos.write(hex.toInt(16))
|
||||||
|
i += 3
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
baos.write(c.code)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
val result = String(baos.toByteArray(), Charsets.UTF_8)
|
||||||
|
// 세미콜론 뒤의 회사명만 추출 (FINNQ;부서명 형태)
|
||||||
|
return if (result.contains(";")) {
|
||||||
|
result.substringAfter(";")
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val prefs = context.getSharedPreferences("ldap_settings", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
var server: String
|
||||||
|
get() = prefs.getString("server", "49.247.139.155") ?: "49.247.139.155"
|
||||||
|
set(value) = prefs.edit().putString("server", value).apply()
|
||||||
|
|
||||||
|
var port: Int
|
||||||
|
get() = prefs.getInt("port", 389)
|
||||||
|
set(value) = prefs.edit().putInt("port", value).apply()
|
||||||
|
|
||||||
|
var bindDn: String
|
||||||
|
get() = prefs.getString("bindDn", "cn=Directory Manager") ?: "cn=Directory Manager"
|
||||||
|
set(value) = prefs.edit().putString("bindDn", value).apply()
|
||||||
|
|
||||||
|
var password: String
|
||||||
|
get() = prefs.getString("password", "Qkdrnwoddl11@") ?: ""
|
||||||
|
set(value) = prefs.edit().putString("password", value).apply()
|
||||||
|
|
||||||
|
var baseDn: String
|
||||||
|
get() = prefs.getString("baseDn", "ou=ioresponse,ou=users,dc=ioresponse,dc=net") ?: ""
|
||||||
|
set(value) = prefs.edit().putString("baseDn", value).apply()
|
||||||
|
|
||||||
|
private fun getConnection(): LDAPConnection {
|
||||||
|
val connection = LDAPConnection()
|
||||||
|
connection.connect(server, port, 10000)
|
||||||
|
connection.bind(bindDn, password)
|
||||||
|
return connection
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getContacts(): Result<List<Contact>> = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val connection = getConnection()
|
||||||
|
val contacts = mutableListOf<Contact>()
|
||||||
|
|
||||||
|
val searchRequest = SearchRequest(
|
||||||
|
baseDn,
|
||||||
|
SearchScope.SUB,
|
||||||
|
"(objectClass=inetOrgPerson)",
|
||||||
|
"cn", "displayName", "telephoneNumber", "mobile", "mail", "o", "ou", "departmentNumber", "title"
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = connection.search(searchRequest)
|
||||||
|
|
||||||
|
for (entry in result.searchEntries) {
|
||||||
|
val attrs = mutableMapOf<String, String>()
|
||||||
|
for (attr in entry.attributes) {
|
||||||
|
attrs[attr.name] = decodeQuotedPrintable(attr.value ?: "")
|
||||||
|
}
|
||||||
|
contacts.add(Contact.fromLdapEntry(entry.dn, attrs))
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
// Sort by Korean name (가나다 순)
|
||||||
|
contacts.sortBy { it.name }
|
||||||
|
|
||||||
|
Result.success(contacts)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun searchContacts(query: String): Result<List<Contact>> = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val connection = getConnection()
|
||||||
|
val contacts = mutableListOf<Contact>()
|
||||||
|
|
||||||
|
val filter = Filter.createORFilter(
|
||||||
|
Filter.createSubstringFilter("cn", query, null, null),
|
||||||
|
Filter.createSubstringFilter("displayName", query, null, null),
|
||||||
|
Filter.createSubstringFilter("telephoneNumber", null, arrayOf(query), null),
|
||||||
|
Filter.createSubstringFilter("mobile", null, arrayOf(query), null),
|
||||||
|
Filter.createSubstringFilter("o", query, null, null)
|
||||||
|
)
|
||||||
|
|
||||||
|
val searchRequest = SearchRequest(
|
||||||
|
baseDn,
|
||||||
|
SearchScope.SUB,
|
||||||
|
filter,
|
||||||
|
"cn", "displayName", "telephoneNumber", "mobile", "mail", "o", "ou", "departmentNumber", "title"
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = connection.search(searchRequest)
|
||||||
|
|
||||||
|
for (entry in result.searchEntries) {
|
||||||
|
val attrs = mutableMapOf<String, String>()
|
||||||
|
for (attr in entry.attributes) {
|
||||||
|
attrs[attr.name] = decodeQuotedPrintable(attr.value ?: "")
|
||||||
|
}
|
||||||
|
contacts.add(Contact.fromLdapEntry(entry.dn, attrs))
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.close()
|
||||||
|
contacts.sortBy { it.name }
|
||||||
|
|
||||||
|
Result.success(contacts)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getContactByDn(dn: String): Contact? = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val connection = getConnection()
|
||||||
|
val entry = connection.getEntry(dn, "cn", "displayName", "telephoneNumber", "mobile", "mail", "o", "ou", "departmentNumber", "title")
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
entry?.let {
|
||||||
|
val attrs = mutableMapOf<String, String>()
|
||||||
|
for (attr in it.attributes) {
|
||||||
|
attrs[attr.name] = decodeQuotedPrintable(attr.value ?: "")
|
||||||
|
}
|
||||||
|
Contact.fromLdapEntry(it.dn, attrs)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun addContact(contact: Contact): Result<Unit> = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val connection = getConnection()
|
||||||
|
|
||||||
|
val uid = UUID.randomUUID().toString().substring(0, 8)
|
||||||
|
val realName = contact.cn.ifEmpty { contact.displayName }
|
||||||
|
val dn = "uid=$uid,$baseDn"
|
||||||
|
|
||||||
|
val attributes = mutableListOf(
|
||||||
|
Attribute("objectClass", "top", "person", "organizationalPerson", "inetOrgPerson"),
|
||||||
|
Attribute("uid", uid),
|
||||||
|
Attribute("cn", realName),
|
||||||
|
Attribute("sn", realName.firstOrNull()?.toString() ?: "Unknown"),
|
||||||
|
Attribute("displayName", contact.displayName.ifEmpty { realName })
|
||||||
|
)
|
||||||
|
|
||||||
|
if (contact.telephoneNumber.isNotEmpty()) {
|
||||||
|
attributes.add(Attribute("telephoneNumber", contact.telephoneNumber))
|
||||||
|
}
|
||||||
|
if (contact.mobile.isNotEmpty()) {
|
||||||
|
attributes.add(Attribute("mobile", contact.mobile))
|
||||||
|
}
|
||||||
|
if (contact.mail.isNotEmpty()) {
|
||||||
|
attributes.add(Attribute("mail", contact.mail))
|
||||||
|
}
|
||||||
|
if (contact.organization.isNotEmpty()) {
|
||||||
|
attributes.add(Attribute("o", contact.organization))
|
||||||
|
}
|
||||||
|
if (contact.department.isNotEmpty()) {
|
||||||
|
attributes.add(Attribute("ou", contact.department))
|
||||||
|
}
|
||||||
|
if (contact.title.isNotEmpty()) {
|
||||||
|
attributes.add(Attribute("title", contact.title))
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.add(dn, attributes)
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
Result.success(Unit)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateContact(contact: Contact): Result<Unit> = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val connection = getConnection()
|
||||||
|
|
||||||
|
val modifications = mutableListOf<Modification>()
|
||||||
|
|
||||||
|
// uid 기반 DN이므로 cn 수정이 자유롭게 가능
|
||||||
|
if (contact.cn.isNotEmpty()) {
|
||||||
|
modifications.add(Modification(ModificationType.REPLACE, "cn", contact.cn))
|
||||||
|
modifications.add(Modification(ModificationType.REPLACE, "sn", contact.cn.firstOrNull()?.toString() ?: "Unknown"))
|
||||||
|
}
|
||||||
|
modifications.add(Modification(ModificationType.REPLACE, "displayName", contact.displayName.ifEmpty { contact.cn }))
|
||||||
|
|
||||||
|
if (contact.telephoneNumber.isNotEmpty()) {
|
||||||
|
modifications.add(Modification(ModificationType.REPLACE, "telephoneNumber", contact.telephoneNumber))
|
||||||
|
}
|
||||||
|
if (contact.mobile.isNotEmpty()) {
|
||||||
|
modifications.add(Modification(ModificationType.REPLACE, "mobile", contact.mobile))
|
||||||
|
}
|
||||||
|
if (contact.mail.isNotEmpty()) {
|
||||||
|
modifications.add(Modification(ModificationType.REPLACE, "mail", contact.mail))
|
||||||
|
}
|
||||||
|
if (contact.organization.isNotEmpty()) {
|
||||||
|
modifications.add(Modification(ModificationType.REPLACE, "o", contact.organization))
|
||||||
|
}
|
||||||
|
if (contact.department.isNotEmpty()) {
|
||||||
|
modifications.add(Modification(ModificationType.REPLACE, "ou", contact.department))
|
||||||
|
}
|
||||||
|
if (contact.title.isNotEmpty()) {
|
||||||
|
modifications.add(Modification(ModificationType.REPLACE, "title", contact.title))
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.modify(contact.dn, modifications)
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
Result.success(Unit)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteContact(contact: Contact): Result<Unit> = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val connection = getConnection()
|
||||||
|
connection.delete(contact.dn)
|
||||||
|
connection.close()
|
||||||
|
Result.success(Unit)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
241
app/src/main/java/net/ioresponse/ldapcontacts/MainActivity.kt
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
package net.ioresponse.ldapcontacts
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import net.ioresponse.ldapcontacts.databinding.ActivityMainBinding
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
private lateinit var ldapManager: LdapManager
|
||||||
|
private lateinit var syncManager: ContactSyncManager
|
||||||
|
private lateinit var adapter: ContactAdapter
|
||||||
|
private var allContacts = listOf<Contact>()
|
||||||
|
private var searchJob: Job? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PERMISSION_REQUEST_CONTACTS = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolbar)
|
||||||
|
|
||||||
|
ldapManager = LdapManager(this)
|
||||||
|
syncManager = ContactSyncManager(this)
|
||||||
|
|
||||||
|
setupRecyclerView()
|
||||||
|
setupSearch()
|
||||||
|
setupSwipeRefresh()
|
||||||
|
setupFab()
|
||||||
|
|
||||||
|
loadContacts()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
adapter = ContactAdapter(
|
||||||
|
emptyList(),
|
||||||
|
onItemClick = { contact ->
|
||||||
|
val intent = Intent(this, ContactDetailActivity::class.java)
|
||||||
|
intent.putExtra("contact", contact)
|
||||||
|
startActivity(intent)
|
||||||
|
},
|
||||||
|
onCallClick = { contact ->
|
||||||
|
makeCall(contact.phone)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
binding.contactsRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
binding.contactsRecyclerView.adapter = adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSearch() {
|
||||||
|
binding.searchEditText.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
searchJob?.cancel()
|
||||||
|
searchJob = lifecycleScope.launch {
|
||||||
|
delay(300)
|
||||||
|
val query = s?.toString()?.trim() ?: ""
|
||||||
|
if (query.isEmpty()) {
|
||||||
|
adapter.updateContacts(allContacts)
|
||||||
|
} else {
|
||||||
|
searchContacts(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSwipeRefresh() {
|
||||||
|
binding.swipeRefresh.setOnRefreshListener {
|
||||||
|
binding.searchEditText.text?.clear()
|
||||||
|
loadContacts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupFab() {
|
||||||
|
binding.fabAdd.setOnClickListener {
|
||||||
|
val intent = Intent(this, EditContactActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadContacts() {
|
||||||
|
showLoading(true)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
ldapManager.getContacts().fold(
|
||||||
|
onSuccess = { contacts ->
|
||||||
|
allContacts = contacts
|
||||||
|
adapter.updateContacts(contacts)
|
||||||
|
showLoading(false)
|
||||||
|
showEmpty(contacts.isEmpty())
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
showLoading(false)
|
||||||
|
showEmpty(true)
|
||||||
|
Toast.makeText(this@MainActivity, "연결 실패: ${error.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchContacts(query: String) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
ldapManager.searchContacts(query).fold(
|
||||||
|
onSuccess = { contacts ->
|
||||||
|
adapter.updateContacts(contacts)
|
||||||
|
showEmpty(contacts.isEmpty())
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
Toast.makeText(this@MainActivity, "검색 실패: ${error.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeCall(phoneNumber: String) {
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CALL_PHONE), 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val intent = Intent(Intent.ACTION_CALL)
|
||||||
|
intent.data = Uri.parse("tel:$phoneNumber")
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoading(show: Boolean) {
|
||||||
|
binding.swipeRefresh.isRefreshing = false
|
||||||
|
binding.progressBar.visibility = if (show) View.VISIBLE else View.GONE
|
||||||
|
binding.contactsRecyclerView.visibility = if (show) View.GONE else View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEmpty(show: Boolean) {
|
||||||
|
binding.emptyText.visibility = if (show) View.VISIBLE else View.GONE
|
||||||
|
binding.contactsRecyclerView.visibility = if (show) View.GONE else View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
loadContacts()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.main_menu, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
R.id.action_sync -> {
|
||||||
|
requestContactsPermissionAndSync()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.action_settings -> {
|
||||||
|
startActivity(Intent(this, SettingsActivity::class.java))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestContactsPermissionAndSync() {
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_CONTACTS)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
this,
|
||||||
|
arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS),
|
||||||
|
PERMISSION_REQUEST_CONTACTS
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
syncToAndroidContacts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun syncToAndroidContacts() {
|
||||||
|
if (allContacts.isEmpty()) {
|
||||||
|
Toast.makeText(this, "동기화할 연락처가 없습니다", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading(true)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
syncManager.syncContacts(allContacts).fold(
|
||||||
|
onSuccess = { result ->
|
||||||
|
showLoading(false)
|
||||||
|
Toast.makeText(
|
||||||
|
this@MainActivity,
|
||||||
|
"${result.added}개 동기화 완료! 휴지통을 비워주세요.",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
showLoading(false)
|
||||||
|
Toast.makeText(
|
||||||
|
this@MainActivity,
|
||||||
|
"동기화 실패: ${error.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(
|
||||||
|
requestCode: Int,
|
||||||
|
permissions: Array<out String>,
|
||||||
|
grantResults: IntArray
|
||||||
|
) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
when (requestCode) {
|
||||||
|
PERMISSION_REQUEST_CONTACTS -> {
|
||||||
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
syncToAndroidContacts()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "연락처 권한이 필요합니다", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
package net.ioresponse.ldapcontacts
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import net.ioresponse.ldapcontacts.databinding.ActivitySettingsBinding
|
||||||
|
|
||||||
|
class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivitySettingsBinding
|
||||||
|
private lateinit var ldapManager: LdapManager
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
ldapManager = LdapManager(this)
|
||||||
|
|
||||||
|
setupToolbar()
|
||||||
|
loadSettings()
|
||||||
|
setupButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupToolbar() {
|
||||||
|
setSupportActionBar(binding.toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
binding.toolbar.setNavigationOnClickListener { finish() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadSettings() {
|
||||||
|
binding.serverEditText.setText(ldapManager.server)
|
||||||
|
binding.portEditText.setText(ldapManager.port.toString())
|
||||||
|
binding.bindDnEditText.setText(ldapManager.bindDn)
|
||||||
|
binding.passwordEditText.setText(ldapManager.password)
|
||||||
|
binding.baseDnEditText.setText(ldapManager.baseDn)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupButtons() {
|
||||||
|
binding.saveButton.setOnClickListener {
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.testButton.setOnClickListener {
|
||||||
|
testConnection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveSettings() {
|
||||||
|
ldapManager.server = binding.serverEditText.text?.toString() ?: ""
|
||||||
|
ldapManager.port = binding.portEditText.text?.toString()?.toIntOrNull() ?: 389
|
||||||
|
ldapManager.bindDn = binding.bindDnEditText.text?.toString() ?: ""
|
||||||
|
ldapManager.password = binding.passwordEditText.text?.toString() ?: ""
|
||||||
|
ldapManager.baseDn = binding.baseDnEditText.text?.toString() ?: ""
|
||||||
|
|
||||||
|
Toast.makeText(this, "설정이 저장되었습니다", Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testConnection() {
|
||||||
|
// Temporarily save settings for test
|
||||||
|
ldapManager.server = binding.serverEditText.text?.toString() ?: ""
|
||||||
|
ldapManager.port = binding.portEditText.text?.toString()?.toIntOrNull() ?: 389
|
||||||
|
ldapManager.bindDn = binding.bindDnEditText.text?.toString() ?: ""
|
||||||
|
ldapManager.password = binding.passwordEditText.text?.toString() ?: ""
|
||||||
|
ldapManager.baseDn = binding.baseDnEditText.text?.toString() ?: ""
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
ldapManager.getContacts().fold(
|
||||||
|
onSuccess = { contacts ->
|
||||||
|
Toast.makeText(
|
||||||
|
this@SettingsActivity,
|
||||||
|
"연결 성공! ${contacts.size}개의 연락처를 찾았습니다.",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
Toast.makeText(
|
||||||
|
this@SettingsActivity,
|
||||||
|
"연결 실패: ${error.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
app/src/main/res/drawable/circle_background.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<solid android:color="@color/primary" />
|
||||||
|
</shape>
|
||||||
224
app/src/main/res/layout/activity_contact_detail.xml
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/background">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/primary"
|
||||||
|
app:navigationIcon="@android:drawable/ic_menu_close_clear_cancel"
|
||||||
|
app:titleTextColor="@color/white" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/initialsText"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:background="@drawable/circle_background"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="40sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/nameText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/callButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@string/call"
|
||||||
|
app:icon="@android:drawable/sym_action_call" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/messageButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/message"
|
||||||
|
app:icon="@android:drawable/sym_action_email"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
app:cardCornerRadius="12dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/phone"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/phoneText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
android:background="@color/divider" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/email"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/emailText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
android:background="@color/divider" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/company"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/orgText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
android:background="@color/divider" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/department"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/deptText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
android:background="@color/divider" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/title"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/titleText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/editButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@string/edit_contact"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/deleteButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/delete"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
app:backgroundTint="#F44336" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
155
app/src/main/res/layout/activity_edit_contact.xml
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/background">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/primary"
|
||||||
|
app:navigationIcon="@android:drawable/ic_menu_close_clear_cancel"
|
||||||
|
app:titleTextColor="@color/white" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/name"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/nameEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPersonName" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/nickname"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/nicknameEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPersonName" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/phone"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/phoneEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="phone" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/email"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/emailEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textEmailAddress" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/company"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/companyEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/department"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/departmentEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/title"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/titleEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/saveButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="@string/save" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
89
app/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/background">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/primary"
|
||||||
|
app:title="@string/app_name"
|
||||||
|
app:titleTextColor="@color/white" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||||
|
app:boxBackgroundColor="@color/white">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/searchEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/search_hint"
|
||||||
|
android:inputType="text"
|
||||||
|
android:imeOptions="actionSearch"
|
||||||
|
android:drawableStart="@android:drawable/ic_menu_search"
|
||||||
|
android:drawablePadding="8dp" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/swipeRefresh"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/contactsRecyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="80dp" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/emptyText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="@string/no_contacts"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/fabAdd"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:contentDescription="@string/add_contact"
|
||||||
|
android:src="@android:drawable/ic_input_add"
|
||||||
|
app:backgroundTint="@color/primary" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
128
app/src/main/res/layout/activity_settings.xml
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/background">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/primary"
|
||||||
|
app:navigationIcon="@android:drawable/ic_menu_close_clear_cancel"
|
||||||
|
app:title="@string/settings"
|
||||||
|
app:titleTextColor="@color/white" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/ldap_server"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/serverEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/ldap_port"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/portEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/ldap_bind_dn"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/bindDnEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/ldap_password"
|
||||||
|
app:endIconMode="password_toggle"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/passwordEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPassword" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/ldap_base_dn"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/baseDnEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/saveButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="@string/save" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/testButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="연결 테스트"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
71
app/src/main/res/layout/item_contact.xml
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
android:layout_marginVertical="4dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/initialsText"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@drawable/circle_background"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/nameText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/phoneText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/orgText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/callButton"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/call"
|
||||||
|
android:src="@android:drawable/sym_action_call" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
17
app/src/main/res/menu/main_menu.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_sync"
|
||||||
|
android:icon="@android:drawable/stat_notify_sync"
|
||||||
|
android:title="@string/sync_contacts"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_settings"
|
||||||
|
android:icon="@android:drawable/ic_menu_preferences"
|
||||||
|
android:title="@string/settings"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
18
app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
<color name="primary">#2196F3</color>
|
||||||
|
<color name="primary_dark">#1976D2</color>
|
||||||
|
<color name="accent">#FF5722</color>
|
||||||
|
<color name="background">#F5F5F5</color>
|
||||||
|
<color name="card_background">#FFFFFF</color>
|
||||||
|
<color name="text_primary">#212121</color>
|
||||||
|
<color name="text_secondary">#757575</color>
|
||||||
|
<color name="divider">#BDBDBD</color>
|
||||||
|
</resources>
|
||||||
31
app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">외부 주소록</string>
|
||||||
|
<string name="search_hint">이름 또는 전화번호 검색</string>
|
||||||
|
<string name="add_contact">연락처 추가</string>
|
||||||
|
<string name="edit_contact">연락처 수정</string>
|
||||||
|
<string name="delete_contact">연락처 삭제</string>
|
||||||
|
<string name="name">이름</string>
|
||||||
|
<string name="nickname">별명</string>
|
||||||
|
<string name="phone">전화번호</string>
|
||||||
|
<string name="email">이메일</string>
|
||||||
|
<string name="company">회사</string>
|
||||||
|
<string name="department">부서</string>
|
||||||
|
<string name="title">직함</string>
|
||||||
|
<string name="save">저장</string>
|
||||||
|
<string name="cancel">취소</string>
|
||||||
|
<string name="delete">삭제</string>
|
||||||
|
<string name="confirm_delete">정말 삭제하시겠습니까?</string>
|
||||||
|
<string name="loading">불러오는 중...</string>
|
||||||
|
<string name="no_contacts">연락처가 없습니다</string>
|
||||||
|
<string name="connection_error">서버 연결 실패</string>
|
||||||
|
<string name="settings">설정</string>
|
||||||
|
<string name="ldap_server">LDAP 서버</string>
|
||||||
|
<string name="ldap_port">포트</string>
|
||||||
|
<string name="ldap_bind_dn">Bind DN</string>
|
||||||
|
<string name="ldap_password">비밀번호</string>
|
||||||
|
<string name="ldap_base_dn">Base DN</string>
|
||||||
|
<string name="call">전화걸기</string>
|
||||||
|
<string name="message">문자보내기</string>
|
||||||
|
<string name="sync_contacts">연락처 동기화</string>
|
||||||
|
</resources>
|
||||||
9
app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.LDAPContacts" parent="Theme.Material3.Light.NoActionBar">
|
||||||
|
<item name="colorPrimary">@color/primary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/primary_dark</item>
|
||||||
|
<item name="colorAccent">@color/accent</item>
|
||||||
|
<item name="android:statusBarColor">@color/primary_dark</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
4
build.gradle
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
plugins {
|
||||||
|
id 'com.android.application' version '8.1.0' apply false
|
||||||
|
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
|
||||||
|
}
|
||||||
4
gradle.properties
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
android.useAndroidX=true
|
||||||
|
kotlin.code.style=official
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
249
gradlew
vendored
Executable file
@ -0,0 +1,249 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
92
gradlew.bat
vendored
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
18
settings.gradle
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "LDAPContacts"
|
||||||
|
include ':app'
|
||||||