diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9312e027..857219bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,23 @@
Changelog
==========
+Version 3.1.2 *(2018-01-23)*
+----------------------------
+
+ * Properly handle vcf files exported from Thunderbird
+ * Misc smaller improvements
+
+Version 3.1.1 *(2018-01-22)*
+----------------------------
+
+ * An f-droid build test version
+
+Version 3.1.0 *(2018-01-16)*
+----------------------------
+
+ * Added contact import/export functions
+ * Allow sharing contacts
+
Version 3.0.3 *(2018-01-03)*
----------------------------
diff --git a/README.md b/README.md
index ae02be1f..5e9e5da6 100644
--- a/README.md
+++ b/README.md
@@ -8,16 +8,19 @@ You can use it for managing user emails and events too. It has the ability to so
Contains no ads or unnecessary permissions. It is fully opensource, provides customizable colors.
This app is just one piece of a bigger series of apps. You can find the rest of them at http://www.simplemobiletools.com
-
-
-
-
-
+
+
+
+
License
-------
- Copyright 2017 SimpleMobileTools
+ Copyright 2017-present SimpleMobileTools
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/app/build.gradle b/app/build.gradle
index 818d9a14..9caf6647 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -4,13 +4,14 @@ apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 27
+ buildToolsVersion "27.0.3"
defaultConfig {
applicationId "com.simplemobiletools.contacts"
minSdkVersion 16
targetSdkVersion 27
- versionCode 4
- versionName "3.0.3"
+ versionCode 7
+ versionName "3.1.2"
setProperty("archivesBaseName", "contacts")
}
@@ -41,11 +42,11 @@ ext {
}
dependencies {
- implementation 'com.simplemobiletools:commons:3.5.7'
+ implementation 'com.simplemobiletools:commons:3.8.2'
implementation 'joda-time:joda-time:2.9.9'
- //debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion"
- //releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion"
+ debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion"
+ releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion"
}
Properties props = new Properties()
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8060eb4f..6175cbd1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
+
+
+
+
+
+
+
+
+
+
+
saveContact()
+ R.id.delete -> deleteContact()
+ R.id.share -> shareContact()
+ else -> return super.onOptionsItemSelected(item)
+ }
+ return true
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
+ super.onActivityResult(requestCode, resultCode, resultData)
+ if (resultCode == RESULT_OK) {
+ when (requestCode) {
+ INTENT_TAKE_PHOTO, INTENT_CHOOSE_PHOTO -> startCropPhotoIntent(lastPhotoIntentUri!!)
+ INTENT_CROP_PHOTO -> updateContactPhoto(lastPhotoIntentUri.toString())
+ }
+ }
+ }
+
private fun initContact() {
var contactId = intent.getIntExtra(CONTACT_ID, 0)
val action = intent.action
@@ -179,33 +202,6 @@ class ContactActivity : SimpleActivity() {
invalidateOptionsMenu()
}
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_contact, menu)
- if (wasActivityInitialized) {
- menu.findItem(R.id.delete).isVisible = contact?.id != 0
- }
- return true
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.save -> saveContact()
- R.id.delete -> deleteContact()
- else -> return super.onOptionsItemSelected(item)
- }
- return true
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
- super.onActivityResult(requestCode, resultCode, resultData)
- if (resultCode == RESULT_OK) {
- when (requestCode) {
- INTENT_TAKE_PHOTO, INTENT_CHOOSE_PHOTO -> startCropPhotoIntent(lastPhotoIntentUri!!)
- INTENT_CROP_PHOTO -> updateContactPhoto(lastPhotoIntentUri.toString())
- }
- }
- }
-
private fun startCropPhotoIntent(uri: Uri) {
lastPhotoIntentUri = getCachePhotoUri()
Intent("com.android.camera.action.CROP").apply {
@@ -306,9 +302,17 @@ class ContactActivity : SimpleActivity() {
private fun setupNewContact() {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
supportActionBar?.title = resources.getString(R.string.new_contact)
- contact = Contact(0, "", "", "", "", ArrayList(), ArrayList(), ArrayList(), "", 0, 0)
+ contact = Contact(0, "", "", "", "", ArrayList(), ArrayList(), ArrayList(), "", 0, 0, "")
contact_source.text = config.lastUsedContactSource
- contact_source.setOnClickListener { showAccountSourcePicker() }
+ contact_source.setOnClickListener {
+ showContactSourcePicker(contact_source.value) {
+ contact_source.text = it
+ }
+ }
+ }
+
+ private fun shareContact() {
+ shareContacts(arrayListOf(contact!!))
}
private fun showPhotoPlaceholder() {
@@ -453,14 +457,14 @@ class ContactActivity : SimpleActivity() {
private fun showNumberTypePicker(numberTypeField: TextView) {
val items = arrayListOf(
- RadioItem(ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, getString(R.string.mobile)),
- RadioItem(ContactsContract.CommonDataKinds.Phone.TYPE_HOME, getString(R.string.home)),
- RadioItem(ContactsContract.CommonDataKinds.Phone.TYPE_WORK, getString(R.string.work)),
- RadioItem(ContactsContract.CommonDataKinds.Phone.TYPE_MAIN, getString(R.string.main_number)),
- RadioItem(ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK, getString(R.string.work_fax)),
- RadioItem(ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME, getString(R.string.home_fax)),
- RadioItem(ContactsContract.CommonDataKinds.Phone.TYPE_PAGER, getString(R.string.pager)),
- RadioItem(ContactsContract.CommonDataKinds.Phone.TYPE_OTHER, getString(R.string.other))
+ RadioItem(CommonDataKinds.Phone.TYPE_MOBILE, getString(R.string.mobile)),
+ RadioItem(CommonDataKinds.Phone.TYPE_HOME, getString(R.string.home)),
+ RadioItem(CommonDataKinds.Phone.TYPE_WORK, getString(R.string.work)),
+ RadioItem(CommonDataKinds.Phone.TYPE_MAIN, getString(R.string.main_number)),
+ RadioItem(CommonDataKinds.Phone.TYPE_FAX_WORK, getString(R.string.work_fax)),
+ RadioItem(CommonDataKinds.Phone.TYPE_FAX_HOME, getString(R.string.home_fax)),
+ RadioItem(CommonDataKinds.Phone.TYPE_PAGER, getString(R.string.pager)),
+ RadioItem(CommonDataKinds.Phone.TYPE_OTHER, getString(R.string.other))
)
val currentNumberTypeId = getPhoneNumberTypeId(numberTypeField.value)
@@ -471,10 +475,10 @@ class ContactActivity : SimpleActivity() {
private fun showEmailTypePicker(emailTypeField: TextView) {
val items = arrayListOf(
- RadioItem(ContactsContract.CommonDataKinds.Email.TYPE_HOME, getString(R.string.home)),
- RadioItem(ContactsContract.CommonDataKinds.Email.TYPE_WORK, getString(R.string.work)),
- RadioItem(ContactsContract.CommonDataKinds.Email.TYPE_MOBILE, getString(R.string.mobile)),
- RadioItem(ContactsContract.CommonDataKinds.Email.TYPE_OTHER, getString(R.string.other))
+ RadioItem(CommonDataKinds.Email.TYPE_HOME, getString(R.string.home)),
+ RadioItem(CommonDataKinds.Email.TYPE_WORK, getString(R.string.work)),
+ RadioItem(CommonDataKinds.Email.TYPE_MOBILE, getString(R.string.mobile)),
+ RadioItem(CommonDataKinds.Email.TYPE_OTHER, getString(R.string.other))
)
val currentEmailTypeId = getEmailTypeId(emailTypeField.value)
@@ -485,9 +489,9 @@ class ContactActivity : SimpleActivity() {
private fun showEventTypePicker(eventTypeField: TextView) {
val items = arrayListOf(
- RadioItem(ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY, getString(R.string.birthday)),
- RadioItem(ContactsContract.CommonDataKinds.Event.TYPE_ANNIVERSARY, getString(R.string.anniversary)),
- RadioItem(ContactsContract.CommonDataKinds.Event.TYPE_OTHER, getString(R.string.other))
+ RadioItem(CommonDataKinds.Event.TYPE_BIRTHDAY, getString(R.string.birthday)),
+ RadioItem(CommonDataKinds.Event.TYPE_ANNIVERSARY, getString(R.string.anniversary)),
+ RadioItem(CommonDataKinds.Event.TYPE_OTHER, getString(R.string.other))
)
val currentEventTypeId = getEventTypeId(eventTypeField.value)
@@ -579,6 +583,7 @@ class ContactActivity : SimpleActivity() {
private fun insertNewContact() {
isSaving = true
+ toast(R.string.inserting)
if (ContactsHelper(this@ContactActivity).insertContact(contact!!)) {
finish()
} else {
@@ -643,27 +648,6 @@ class ContactActivity : SimpleActivity() {
}
}
- private fun showAccountSourcePicker() {
- ContactsHelper(this).getContactSources {
- val items = ArrayList()
- val sources = it
- val currentSource = contact_source.value
- var currentSourceIndex = -1
- sources.forEachIndexed { index, account ->
- items.add(RadioItem(index, account))
- if (account == currentSource) {
- currentSourceIndex = index
- }
- }
-
- runOnUiThread {
- RadioGroupDialog(this, items, currentSourceIndex) {
- contact_source.text = sources[it as Int]
- }
- }
- }
- }
-
private fun toggleFavorite() {
val isStarred = isContactStarred()
contact_toggle_favorite.apply {
@@ -757,62 +741,51 @@ class ContactActivity : SimpleActivity() {
}
}
- private fun getCachePhotoUri(): Uri {
- val imagesFolder = File(cacheDir, "my_cache")
- if (!imagesFolder.exists()) {
- imagesFolder.mkdirs()
- }
-
- val file = File(imagesFolder, "Photo_${System.currentTimeMillis()}.jpg")
- file.createNewFile()
- return FileProvider.getUriForFile(this, "${BuildConfig.APPLICATION_ID}.provider", file)
- }
-
private fun getPhoneNumberTextId(type: Int) = when (type) {
- ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE -> R.string.mobile
- ContactsContract.CommonDataKinds.Phone.TYPE_HOME -> R.string.home
- ContactsContract.CommonDataKinds.Phone.TYPE_WORK -> R.string.work
- ContactsContract.CommonDataKinds.Phone.TYPE_MAIN -> R.string.main_number
- ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK -> R.string.work_fax
- ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME -> R.string.home_fax
- ContactsContract.CommonDataKinds.Phone.TYPE_PAGER -> R.string.pager
+ CommonDataKinds.Phone.TYPE_MOBILE -> R.string.mobile
+ CommonDataKinds.Phone.TYPE_HOME -> R.string.home
+ CommonDataKinds.Phone.TYPE_WORK -> R.string.work
+ CommonDataKinds.Phone.TYPE_MAIN -> R.string.main_number
+ CommonDataKinds.Phone.TYPE_FAX_WORK -> R.string.work_fax
+ CommonDataKinds.Phone.TYPE_FAX_HOME -> R.string.home_fax
+ CommonDataKinds.Phone.TYPE_PAGER -> R.string.pager
else -> R.string.other
}
private fun getPhoneNumberTypeId(value: String) = when (value) {
- getString(R.string.mobile) -> ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE
- getString(R.string.home) -> ContactsContract.CommonDataKinds.Phone.TYPE_HOME
- getString(R.string.work) -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK
- getString(R.string.main_number) -> ContactsContract.CommonDataKinds.Phone.TYPE_MAIN
- getString(R.string.work_fax) -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK
- getString(R.string.home_fax) -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME
- getString(R.string.pager) -> ContactsContract.CommonDataKinds.Phone.TYPE_PAGER
- else -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER
+ getString(R.string.mobile) -> CommonDataKinds.Phone.TYPE_MOBILE
+ getString(R.string.home) -> CommonDataKinds.Phone.TYPE_HOME
+ getString(R.string.work) -> CommonDataKinds.Phone.TYPE_WORK
+ getString(R.string.main_number) -> CommonDataKinds.Phone.TYPE_MAIN
+ getString(R.string.work_fax) -> CommonDataKinds.Phone.TYPE_FAX_WORK
+ getString(R.string.home_fax) -> CommonDataKinds.Phone.TYPE_FAX_HOME
+ getString(R.string.pager) -> CommonDataKinds.Phone.TYPE_PAGER
+ else -> CommonDataKinds.Phone.TYPE_OTHER
}
private fun getEmailTextId(type: Int) = when (type) {
- ContactsContract.CommonDataKinds.Email.TYPE_HOME -> R.string.home
- ContactsContract.CommonDataKinds.Email.TYPE_WORK -> R.string.work
- ContactsContract.CommonDataKinds.Email.TYPE_MOBILE -> R.string.mobile
+ CommonDataKinds.Email.TYPE_HOME -> R.string.home
+ CommonDataKinds.Email.TYPE_WORK -> R.string.work
+ CommonDataKinds.Email.TYPE_MOBILE -> R.string.mobile
else -> R.string.other
}
private fun getEmailTypeId(value: String) = when (value) {
- getString(R.string.home) -> ContactsContract.CommonDataKinds.Email.TYPE_HOME
- getString(R.string.work) -> ContactsContract.CommonDataKinds.Email.TYPE_WORK
- getString(R.string.mobile) -> ContactsContract.CommonDataKinds.Email.TYPE_MOBILE
- else -> ContactsContract.CommonDataKinds.Email.TYPE_OTHER
+ getString(R.string.home) -> CommonDataKinds.Email.TYPE_HOME
+ getString(R.string.work) -> CommonDataKinds.Email.TYPE_WORK
+ getString(R.string.mobile) -> CommonDataKinds.Email.TYPE_MOBILE
+ else -> CommonDataKinds.Email.TYPE_OTHER
}
private fun getEventTextId(type: Int) = when (type) {
- ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY -> R.string.birthday
- ContactsContract.CommonDataKinds.Event.TYPE_ANNIVERSARY -> R.string.anniversary
+ CommonDataKinds.Event.TYPE_BIRTHDAY -> R.string.birthday
+ CommonDataKinds.Event.TYPE_ANNIVERSARY -> R.string.anniversary
else -> R.string.other
}
private fun getEventTypeId(value: String) = when (value) {
- getString(R.string.birthday) -> ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY
- getString(R.string.anniversary) -> ContactsContract.CommonDataKinds.Event.TYPE_ANNIVERSARY
- else -> ContactsContract.CommonDataKinds.Event.TYPE_OTHER
+ getString(R.string.birthday) -> CommonDataKinds.Event.TYPE_BIRTHDAY
+ getString(R.string.anniversary) -> CommonDataKinds.Event.TYPE_ANNIVERSARY
+ else -> CommonDataKinds.Event.TYPE_OTHER
}
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/MainActivity.kt
index 43846846..730f4324 100644
--- a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/MainActivity.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/MainActivity.kt
@@ -4,25 +4,34 @@ import android.app.SearchManager
import android.content.Context
import android.content.Intent
import android.graphics.drawable.ColorDrawable
+import android.net.Uri
import android.os.Bundle
import android.support.v4.view.MenuItemCompat
import android.support.v4.view.ViewPager
import android.support.v7.widget.SearchView
import android.view.Menu
import android.view.MenuItem
+import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.contacts.BuildConfig
import com.simplemobiletools.contacts.R
import com.simplemobiletools.contacts.adapters.ViewPagerAdapter
import com.simplemobiletools.contacts.dialogs.ChangeSortingDialog
+import com.simplemobiletools.contacts.dialogs.ExportContactsDialog
import com.simplemobiletools.contacts.dialogs.FilterContactSourcesDialog
+import com.simplemobiletools.contacts.dialogs.ImportContactsDialog
import com.simplemobiletools.contacts.extensions.config
+import com.simplemobiletools.contacts.extensions.getTempFile
import com.simplemobiletools.contacts.extensions.onTabSelectionChanged
+import com.simplemobiletools.contacts.helpers.ContactsHelper
+import com.simplemobiletools.contacts.helpers.VcfExporter
import com.simplemobiletools.contacts.interfaces.RefreshContactsListener
+import com.simplemobiletools.contacts.models.Contact
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_contacts.*
import kotlinx.android.synthetic.main.fragment_favorites.*
+import java.io.FileOutputStream
class MainActivity : SimpleActivity(), RefreshContactsListener {
private var isFirstResume = true
@@ -102,10 +111,10 @@ class MainActivity : SimpleActivity(), RefreshContactsListener {
if (viewpager.adapter == null) {
initFragments()
}
- contacts_fragment.initContacts()
- contacts_fragment.onActivityResume()
- favorites_fragment.initContacts()
- favorites_fragment.onActivityResume()
+ contacts_fragment?.initContacts()
+ contacts_fragment?.onActivityResume()
+ favorites_fragment?.initContacts()
+ favorites_fragment?.onActivityResume()
}
isFirstResume = false
}
@@ -130,6 +139,8 @@ class MainActivity : SimpleActivity(), RefreshContactsListener {
when (item.itemId) {
R.id.sort -> showSortingDialog()
R.id.filter -> showFilterDialog()
+ R.id.import_contacts -> tryImportContacts()
+ R.id.export_contacts -> tryExportContacts()
R.id.settings -> startActivity(Intent(applicationContext, SettingsActivity::class.java))
R.id.about -> launchAbout()
else -> return super.onOptionsItemSelected(item)
@@ -156,9 +167,7 @@ class MainActivity : SimpleActivity(), RefreshContactsListener {
isSubmitButtonEnabled = false
queryHint = getString(if (viewpager.currentItem == 0) R.string.search_contacts else R.string.search_favorites)
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(query: String): Boolean {
- return false
- }
+ override fun onQueryTextSubmit(query: String) = false
override fun onQueryTextChange(newText: String): Boolean {
if (isSearchOpen) {
@@ -232,6 +241,10 @@ class MainActivity : SimpleActivity(), RefreshContactsListener {
it.icon?.applyColorFilter(getAdjustedPrimaryColor())
}
)
+
+ if (intent?.action == Intent.ACTION_VIEW && intent.data != null) {
+ tryImportContactsFromFile(intent.data)
+ }
}
private fun showSortingDialog() {
@@ -243,10 +256,86 @@ class MainActivity : SimpleActivity(), RefreshContactsListener {
fun showFilterDialog() {
FilterContactSourcesDialog(this) {
+ contacts_fragment.forceListRedraw = true
contacts_fragment.initContacts()
}
}
+ private fun tryImportContacts() {
+ handlePermission(PERMISSION_READ_STORAGE) {
+ if (it) {
+ importContacts()
+ }
+ }
+ }
+
+ private fun importContacts() {
+ FilePickerDialog(this) {
+ showImportContactsDialog(it)
+ }
+ }
+
+ private fun showImportContactsDialog(path: String) {
+ ImportContactsDialog(this, path) {
+ if (it) {
+ runOnUiThread {
+ refreshContacts()
+ }
+ }
+ }
+ }
+
+ private fun tryImportContactsFromFile(uri: Uri) {
+ when {
+ uri.scheme == "file" -> showImportContactsDialog(uri.path)
+ uri.scheme == "content" -> {
+ val tempFile = getTempFile()
+ if (tempFile == null) {
+ toast(R.string.unknown_error_occurred)
+ return
+ }
+
+ val inputStream = contentResolver.openInputStream(uri)
+ val out = FileOutputStream(tempFile)
+ inputStream.copyTo(out)
+ showImportContactsDialog(tempFile.absolutePath)
+ }
+ else -> toast(R.string.invalid_file_format)
+ }
+ }
+
+ private fun tryExportContacts() {
+ handlePermission(PERMISSION_WRITE_STORAGE) {
+ if (it) {
+ exportContacts()
+ }
+ }
+ }
+
+ private fun exportContacts() {
+ FilePickerDialog(this, pickFile = false, showFAB = true) {
+ ExportContactsDialog(this, it) { file, contactSources ->
+ Thread {
+ ContactsHelper(this).getContacts {
+ val contacts = it.filter { contactSources.contains(it.source) }
+ if (contacts.isEmpty()) {
+ toast(R.string.no_entries_for_exporting)
+ } else {
+ toast(R.string.exporting)
+ VcfExporter().exportContacts(this, file, contacts as ArrayList) {
+ toast(when (it) {
+ VcfExporter.ExportResult.EXPORT_OK -> R.string.exporting_successful
+ VcfExporter.ExportResult.EXPORT_PARTIAL -> R.string.exporting_some_entries_failed
+ else -> R.string.exporting_failed
+ })
+ }
+ }
+ }
+ }.start()
+ }
+ }
+ }
+
private fun launchAbout() {
startAboutActivity(R.string.app_name, LICENSE_KOTLIN or LICENSE_MULTISELECT or LICENSE_JODA or LICENSE_GLIDE, BuildConfig.VERSION_NAME)
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/ContactsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/ContactsAdapter.kt
index 71b2a2df..5f176f22 100644
--- a/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/ContactsAdapter.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/ContactsAdapter.kt
@@ -13,11 +13,13 @@ import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.extensions.getColoredDrawableWithColor
import com.simplemobiletools.commons.extensions.isActivityDestroyed
+import com.simplemobiletools.commons.views.FastScroller
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.contacts.R
import com.simplemobiletools.contacts.activities.SimpleActivity
import com.simplemobiletools.contacts.extensions.config
import com.simplemobiletools.contacts.extensions.openContact
+import com.simplemobiletools.contacts.extensions.shareContacts
import com.simplemobiletools.contacts.helpers.ContactsHelper
import com.simplemobiletools.contacts.interfaces.RefreshContactsListener
import com.simplemobiletools.contacts.models.Contact
@@ -25,8 +27,8 @@ import kotlinx.android.synthetic.main.item_contact_with_number.view.*
import java.util.*
class ContactsAdapter(activity: SimpleActivity, var contactItems: MutableList, val listener: RefreshContactsListener?,
- val isFavoritesFragment: Boolean, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit) :
- MyRecyclerViewAdapter(activity, recyclerView, itemClick) {
+ val isFavoritesFragment: Boolean, recyclerView: MyRecyclerView, fastScroller: FastScroller, itemClick: (Any) -> Unit) :
+ MyRecyclerViewAdapter(activity, recyclerView, fastScroller, itemClick) {
lateinit private var contactDrawable: Drawable
var config = activity.config
@@ -61,9 +63,10 @@ class ContactsAdapter(activity: SimpleActivity, var contactItems: MutableList editContact()
R.id.cab_select_all -> selectAll()
- R.id.cab_delete -> askConfirmDelete()
R.id.cab_add_to_favorites -> addToFavorites()
+ R.id.cab_share -> shareContacts()
R.id.cab_remove -> removeFavorites()
+ R.id.cab_delete -> askConfirmDelete()
}
}
@@ -160,10 +163,23 @@ class ContactsAdapter(activity: SimpleActivity, var contactItems: MutableList()
+ selectedPositions.forEach {
+ contacts.add(contactItems[it])
+ }
+
+ activity.shareContacts(contacts)
+ }
+
override fun onViewRecycled(holder: ViewHolder?) {
super.onViewRecycled(holder)
if (!activity.isActivityDestroyed()) {
- Glide.with(activity).clear(holder?.itemView?.contact_tmb)
+ Glide.with(activity).clear(holder?.itemView?.contact_tmb!!)
}
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/SelectContactsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/SelectContactsAdapter.kt
index 419a8d75..f3ec600a 100644
--- a/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/SelectContactsAdapter.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/SelectContactsAdapter.kt
@@ -124,6 +124,6 @@ class SelectContactsAdapter(val activity: SimpleActivity, val contacts: List) -> Unit) {
+
+ init {
+ val view = (activity.layoutInflater.inflate(R.layout.dialog_export_contacts, null) as ViewGroup).apply {
+ export_contacts_folder.text = activity.humanizePath(path)
+ export_contacts_filename.setText("contacts_${System.currentTimeMillis() / 1000}")
+
+ ContactsHelper(activity).getContactSources {
+ activity.runOnUiThread {
+ export_contacts_list.adapter = FilterContactSourcesAdapter(activity, it, activity.config.displayContactSources)
+ }
+ }
+ }
+
+ AlertDialog.Builder(activity)
+ .setPositiveButton(R.string.ok, null)
+ .setNegativeButton(R.string.cancel, null)
+ .create().apply {
+ activity.setupDialogStuff(view, this, R.string.export_contacts) {
+ getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
+ val filename = view.export_contacts_filename.value
+ when {
+ filename.isEmpty() -> activity.toast(R.string.empty_name)
+ filename.isAValidFilename() -> {
+ val file = File(path, "$filename.vcf")
+ if (file.exists()) {
+ activity.toast(R.string.name_taken)
+ return@setOnClickListener
+ }
+
+ val contactSources = (view.export_contacts_list.adapter as FilterContactSourcesAdapter).getSelectedItemsSet()
+ callback(file, contactSources)
+ dismiss()
+ }
+ else -> activity.toast(R.string.invalid_name)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/ImportContactsDialog.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/ImportContactsDialog.kt
new file mode 100644
index 00000000..fde2a6fa
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/ImportContactsDialog.kt
@@ -0,0 +1,52 @@
+package com.simplemobiletools.contacts.dialogs
+
+import android.support.v7.app.AlertDialog
+import android.view.ViewGroup
+import com.simplemobiletools.commons.extensions.setupDialogStuff
+import com.simplemobiletools.commons.extensions.toast
+import com.simplemobiletools.commons.extensions.value
+import com.simplemobiletools.contacts.R
+import com.simplemobiletools.contacts.activities.SimpleActivity
+import com.simplemobiletools.contacts.extensions.config
+import com.simplemobiletools.contacts.extensions.showContactSourcePicker
+import com.simplemobiletools.contacts.helpers.VcfImporter
+import com.simplemobiletools.contacts.helpers.VcfImporter.ImportResult.IMPORT_FAIL
+import kotlinx.android.synthetic.main.dialog_import_contacts.view.*
+
+class ImportContactsDialog(val activity: SimpleActivity, val path: String, val callback: (refreshView: Boolean) -> Unit) {
+ init {
+ val view = (activity.layoutInflater.inflate(R.layout.dialog_import_contacts, null) as ViewGroup).apply {
+ import_contacts_title.text = activity.config.lastUsedContactSource
+ import_contacts_title.setOnClickListener {
+ activity.showContactSourcePicker(import_contacts_title.value) {
+ import_contacts_title.text = it
+ }
+ }
+ }
+
+ AlertDialog.Builder(activity)
+ .setPositiveButton(R.string.ok, null)
+ .setNegativeButton(R.string.cancel, null)
+ .create().apply {
+ activity.setupDialogStuff(view, this, R.string.import_contacts) {
+ getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
+ activity.toast(R.string.importing)
+ Thread {
+ val result = VcfImporter(activity).importContacts(path, view.import_contacts_title.value)
+ handleParseResult(result)
+ dismiss()
+ }.start()
+ }
+ }
+ }
+ }
+
+ private fun handleParseResult(result: VcfImporter.ImportResult) {
+ activity.toast(when (result) {
+ VcfImporter.ImportResult.IMPORT_OK -> R.string.importing_successful
+ VcfImporter.ImportResult.IMPORT_PARTIAL -> R.string.importing_some_entries_failed
+ else -> R.string.importing_failed
+ })
+ callback(result != IMPORT_FAIL)
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/Activity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/Activity.kt
index 997b899d..0f1eeb83 100644
--- a/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/Activity.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/Activity.kt
@@ -3,12 +3,19 @@ package com.simplemobiletools.contacts.extensions
import android.content.Intent
import android.net.Uri
import com.simplemobiletools.commons.R
+import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.dialogs.RadioGroupDialog
+import com.simplemobiletools.commons.extensions.getFilePublicUri
+import com.simplemobiletools.commons.extensions.shareUri
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.commons.helpers.PERMISSION_CALL_PHONE
import com.simplemobiletools.commons.models.RadioItem
+import com.simplemobiletools.contacts.BuildConfig
import com.simplemobiletools.contacts.activities.SimpleActivity
+import com.simplemobiletools.contacts.helpers.ContactsHelper
+import com.simplemobiletools.contacts.helpers.VcfExporter
import com.simplemobiletools.contacts.models.Contact
+import java.io.File
fun SimpleActivity.startCallIntent(recipient: String) {
handlePermission(PERMISSION_CALL_PHONE) {
@@ -40,3 +47,50 @@ fun SimpleActivity.tryStartCall(contact: Contact) {
}
}
}
+
+fun SimpleActivity.showContactSourcePicker(currentSource: String, callback: (newSource: String) -> Unit) {
+ ContactsHelper(this).getContactSources {
+ val items = ArrayList()
+ val sources = it
+ var currentSourceIndex = -1
+ sources.forEachIndexed { index, account ->
+ items.add(RadioItem(index, account))
+ if (account == currentSource) {
+ currentSourceIndex = index
+ }
+ }
+
+ runOnUiThread {
+ RadioGroupDialog(this, items, currentSourceIndex) {
+ callback(sources[it as Int])
+ }
+ }
+ }
+}
+
+fun BaseSimpleActivity.shareContacts(contacts: ArrayList) {
+ val file = getTempFile()
+ if (file == null) {
+ toast(R.string.unknown_error_occurred)
+ return
+ }
+
+ VcfExporter().exportContacts(this, file, contacts) {
+ if (it == VcfExporter.ExportResult.EXPORT_OK) {
+ val uri = getFilePublicUri(file, BuildConfig.APPLICATION_ID)
+ shareUri(uri, BuildConfig.APPLICATION_ID)
+ }
+ }
+}
+
+fun BaseSimpleActivity.getTempFile(): File? {
+ val folder = File(cacheDir, "contacts")
+ if (!folder.exists()) {
+ if (!folder.mkdir()) {
+ toast(R.string.unknown_error_occurred)
+ return null
+ }
+ }
+
+ return File(folder, "contacts.vcf")
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/Context.kt
index c738891a..c8e5c5bd 100644
--- a/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/Context.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/Context.kt
@@ -7,14 +7,17 @@ import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.provider.ContactsContract
+import android.support.v4.content.FileProvider
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.getIntValue
import com.simplemobiletools.commons.extensions.isLollipopPlus
import com.simplemobiletools.commons.extensions.toast
+import com.simplemobiletools.contacts.BuildConfig
import com.simplemobiletools.contacts.activities.ContactActivity
import com.simplemobiletools.contacts.helpers.CONTACT_ID
import com.simplemobiletools.contacts.helpers.Config
import com.simplemobiletools.contacts.models.Contact
+import java.io.File
val Context.config: Config get() = Config.newInstance(applicationContext)
@@ -94,3 +97,16 @@ fun lookupContactUri(lookup: String, context: Context): Uri {
val lookupUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookup)
return ContactsContract.Contacts.lookupContact(context.contentResolver, lookupUri)
}
+
+fun Context.getCachePhoto(): File {
+ val imagesFolder = File(cacheDir, "my_cache")
+ if (!imagesFolder.exists()) {
+ imagesFolder.mkdirs()
+ }
+
+ val file = File(imagesFolder, "Photo_${System.currentTimeMillis()}.jpg")
+ file.createNewFile()
+ return file
+}
+
+fun Context.getCachePhotoUri(file: File = getCachePhoto()) = FileProvider.getUriForFile(this, "${BuildConfig.APPLICATION_ID}.provider", file)
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/MyViewPagerFragment.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/MyViewPagerFragment.kt
index 2b979922..150cfd3e 100644
--- a/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/MyViewPagerFragment.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/MyViewPagerFragment.kt
@@ -26,6 +26,8 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet)
private var contactsIgnoringSearch = ArrayList()
lateinit private var config: Config
+ var forceListRedraw = false
+
fun setupFragment(activity: MainActivity) {
config = activity.config
if (this.activity == null) {
@@ -114,8 +116,9 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet)
fragment_list.beVisibleIf(contacts.isNotEmpty())
val currAdapter = fragment_list.adapter
- if (currAdapter == null) {
- ContactsAdapter(activity as SimpleActivity, contacts, activity, this is FavoritesFragment, fragment_list) {
+ if (currAdapter == null || forceListRedraw) {
+ forceListRedraw = false
+ ContactsAdapter(activity as SimpleActivity, contacts, activity, this is FavoritesFragment, fragment_list, fragment_fastscroller) {
if (config.callContact) {
val contact = it as Contact
if (contact.phoneNumbers.isNotEmpty()) {
@@ -132,6 +135,7 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet)
fragment_list.adapter = this
}
+ fragment_fastscroller.setScrollTo(0)
fragment_fastscroller.setViews(fragment_list) {
val item = (fragment_list.adapter as ContactsAdapter).contactItems.getOrNull(it)
fragment_fastscroller.updateBubbleText(item?.getBubbleText() ?: "")
@@ -165,16 +169,24 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet)
filtered.sort()
filtered.sortBy { !it.getFullName(startNameWithSurname).startsWith(text, true) }
+ if (filtered.isEmpty() && this@MyViewPagerFragment is FavoritesFragment) {
+ fragment_placeholder.text = activity.getString(R.string.no_items_found)
+ }
+
+ fragment_placeholder.beVisibleIf(filtered.isEmpty())
updateItems(filtered)
}
}
fun onSearchOpened() {
- contactsIgnoringSearch = (fragment_list.adapter as ContactsAdapter).contactItems as ArrayList
+ contactsIgnoringSearch = (fragment_list.adapter as? ContactsAdapter)?.contactItems as ArrayList
}
fun onSearchClosed() {
(fragment_list.adapter as ContactsAdapter).updateItems(contactsIgnoringSearch)
+ if (this is FavoritesFragment) {
+ fragment_placeholder.text = activity?.getString(R.string.no_favorites)
+ }
}
private fun updateViewStuff() {
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Constants.kt
index eff937cf..bd29cb23 100644
--- a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Constants.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Constants.kt
@@ -1,5 +1,7 @@
package com.simplemobiletools.contacts.helpers
+import android.provider.ContactsContract.CommonDataKinds
+
// shared prefs
val CALL_CONTACT_ON_CLICK = "call_contact_on_click"
val SHOW_PHONE_NUMBERS = "show_phone_numbers"
@@ -15,3 +17,35 @@ val PHOTO_ADDED = 1
val PHOTO_REMOVED = 2
val PHOTO_CHANGED = 3
val PHOTO_UNCHANGED = 4
+
+// default contact values
+val DEFAULT_EMAIL_TYPE = CommonDataKinds.Email.TYPE_HOME
+val DEFAULT_PHONE_NUMBER_TYPE = CommonDataKinds.Phone.TYPE_MOBILE
+val DEFAULT_EVENT_TYPE = CommonDataKinds.Event.TYPE_BIRTHDAY
+
+// export/import
+val BEGIN_VCARD = "BEGIN:VCARD"
+val END_VCARD = "END:VCARD"
+val N = "N:"
+val TEL = "TEL"
+val BDAY = "BDAY:"
+val ANNIVERSARY = "ANNIVERSARY:"
+val PHOTO = "PHOTO"
+val EMAIL = "EMAIL"
+val ENCODING = "ENCODING"
+val BASE64 = "BASE64"
+val JPEG = "JPEG"
+val VERSION_2_1 = "VERSION:2.1"
+
+// phone number/email types
+val CELL = "CELL"
+val WORK = "WORK"
+val HOME = "HOME"
+val PREF = "PREF"
+val MAIN = "MAIN"
+val FAX = "FAX"
+val WORK_FAX = "WORK;FAX"
+val HOME_FAX = "HOME;FAX"
+val PAGER = "PAGER"
+val MOBILE = "MOBILE"
+val VOICE = "VOICE"
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/ContactsHelper.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/ContactsHelper.kt
index 99d2b29c..6827551e 100644
--- a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/ContactsHelper.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/ContactsHelper.kt
@@ -8,6 +8,7 @@ import android.database.Cursor
import android.graphics.Bitmap
import android.net.Uri
import android.provider.ContactsContract
+import android.provider.ContactsContract.CommonDataKinds
import android.provider.MediaStore
import android.util.SparseArray
import com.simplemobiletools.commons.activities.BaseSimpleActivity
@@ -35,7 +36,7 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
val uri = ContactsContract.Data.CONTENT_URI
val projection = getContactProjection()
val selection = "${ContactsContract.Data.MIMETYPE} = ?"
- val selectionArgs = arrayOf(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+ val selectionArgs = arrayOf(CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
val sortOrder = getSortString()
var cursor: Cursor? = null
@@ -44,17 +45,18 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
if (cursor?.moveToFirst() == true) {
do {
val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID)
- val firstName = cursor.getStringValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME) ?: ""
- val middleName = cursor.getStringValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME) ?: ""
- val surname = cursor.getStringValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME) ?: ""
- val photoUri = cursor.getStringValue(ContactsContract.CommonDataKinds.StructuredName.PHOTO_URI) ?: ""
+ val firstName = cursor.getStringValue(CommonDataKinds.StructuredName.GIVEN_NAME) ?: ""
+ val middleName = cursor.getStringValue(CommonDataKinds.StructuredName.MIDDLE_NAME) ?: ""
+ val surname = cursor.getStringValue(CommonDataKinds.StructuredName.FAMILY_NAME) ?: ""
+ val photoUri = cursor.getStringValue(CommonDataKinds.StructuredName.PHOTO_URI) ?: ""
val number = ArrayList() // proper value is obtained below
val emails = ArrayList()
val events = ArrayList()
- val accountName = cursor.getStringValue(ContactsContract.RawContacts.ACCOUNT_NAME)
- val starred = cursor.getIntValue(ContactsContract.CommonDataKinds.StructuredName.STARRED)
+ val accountName = cursor.getStringValue(ContactsContract.RawContacts.ACCOUNT_NAME) ?: ""
+ val starred = cursor.getIntValue(CommonDataKinds.StructuredName.STARRED)
val contactId = cursor.getIntValue(ContactsContract.Data.CONTACT_ID)
- val contact = Contact(id, firstName, middleName, surname, photoUri, number, emails, events, accountName, starred, contactId)
+ val thumbnailUri = cursor.getStringValue(CommonDataKinds.StructuredName.PHOTO_THUMBNAIL_URI) ?: ""
+ val contact = Contact(id, firstName, middleName, surname, photoUri, number, emails, events, accountName, starred, contactId, thumbnailUri)
contacts.put(id, contact)
} while (cursor.moveToNext())
}
@@ -88,11 +90,11 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
private fun getPhoneNumbers(contactId: Int? = null): SparseArray> {
val phoneNumbers = SparseArray>()
- val uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI
+ val uri = CommonDataKinds.Phone.CONTENT_URI
val projection = arrayOf(
ContactsContract.Data.RAW_CONTACT_ID,
- ContactsContract.CommonDataKinds.Phone.NUMBER,
- ContactsContract.CommonDataKinds.Phone.TYPE
+ CommonDataKinds.Phone.NUMBER,
+ CommonDataKinds.Phone.TYPE
)
val selection = if (contactId == null) null else "${ContactsContract.Data.RAW_CONTACT_ID} = ?"
@@ -104,16 +106,19 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
if (cursor?.moveToFirst() == true) {
do {
val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID)
- val number = cursor.getStringValue(ContactsContract.CommonDataKinds.Phone.NUMBER)
- val type = cursor.getIntValue(ContactsContract.CommonDataKinds.Phone.TYPE)
+ val number = cursor.getStringValue(CommonDataKinds.Phone.NUMBER) ?: continue
+ val type = cursor.getIntValue(CommonDataKinds.Phone.TYPE)
if (phoneNumbers[id] == null) {
phoneNumbers.put(id, ArrayList())
}
- phoneNumbers[id].add(PhoneNumber(number, type))
+ val phoneNumber = PhoneNumber(number, type)
+ phoneNumbers[id].add(phoneNumber)
} while (cursor.moveToNext())
}
+ } catch (e: Exception) {
+ activity.showErrorToast(e)
} finally {
cursor?.close()
}
@@ -122,11 +127,11 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
private fun getEmails(contactId: Int? = null): SparseArray> {
val emails = SparseArray>()
- val uri = ContactsContract.CommonDataKinds.Email.CONTENT_URI
+ val uri = CommonDataKinds.Email.CONTENT_URI
val projection = arrayOf(
ContactsContract.Data.RAW_CONTACT_ID,
- ContactsContract.CommonDataKinds.Email.DATA,
- ContactsContract.CommonDataKinds.Email.TYPE
+ CommonDataKinds.Email.DATA,
+ CommonDataKinds.Email.TYPE
)
val selection = if (contactId == null) null else "${ContactsContract.Data.RAW_CONTACT_ID} = ?"
@@ -138,8 +143,8 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
if (cursor?.moveToFirst() == true) {
do {
val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID)
- val email = cursor.getStringValue(ContactsContract.CommonDataKinds.Email.DATA)
- val type = cursor.getIntValue(ContactsContract.CommonDataKinds.Email.TYPE)
+ val email = cursor.getStringValue(CommonDataKinds.Email.DATA) ?: continue
+ val type = cursor.getIntValue(CommonDataKinds.Email.TYPE)
if (emails[id] == null) {
emails.put(id, ArrayList())
@@ -148,6 +153,9 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
emails[id]!!.add(Email(email, type))
} while (cursor.moveToNext())
}
+
+ } catch (e: Exception) {
+ activity.showErrorToast(e)
} finally {
cursor?.close()
}
@@ -159,18 +167,18 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
val events = SparseArray>()
val uri = ContactsContract.Data.CONTENT_URI
val projection = arrayOf(
- ContactsContract.CommonDataKinds.Event.START_DATE,
- ContactsContract.CommonDataKinds.Event.TYPE
+ CommonDataKinds.Event.START_DATE,
+ CommonDataKinds.Event.TYPE
)
val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ?"
- val selectionArgs = arrayOf(contactId.toString(), ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE)
+ val selectionArgs = arrayOf(contactId.toString(), CommonDataKinds.Event.CONTENT_ITEM_TYPE)
var cursor: Cursor? = null
try {
cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null)
if (cursor?.moveToFirst() == true) {
do {
- val startDate = cursor.getStringValue(ContactsContract.CommonDataKinds.Event.START_DATE)
- val type = cursor.getIntValue(ContactsContract.CommonDataKinds.Event.TYPE)
+ val startDate = cursor.getStringValue(CommonDataKinds.Event.START_DATE)
+ val type = cursor.getIntValue(CommonDataKinds.Event.TYPE)
if (events[contactId] == null) {
events.put(contactId, ArrayList())
@@ -196,22 +204,23 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
val uri = ContactsContract.Data.CONTENT_URI
val projection = getContactProjection()
val selection = "${ContactsContract.Data.MIMETYPE} = ? AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?"
- val selectionArgs = arrayOf(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, id.toString())
+ val selectionArgs = arrayOf(CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, id.toString())
var cursor: Cursor? = null
try {
cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null)
if (cursor?.moveToFirst() == true) {
- val firstName = cursor.getStringValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME) ?: ""
- val middleName = cursor.getStringValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME) ?: ""
- val surname = cursor.getStringValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME) ?: ""
- val photoUri = cursor.getStringValue(ContactsContract.CommonDataKinds.Phone.PHOTO_URI) ?: ""
+ val firstName = cursor.getStringValue(CommonDataKinds.StructuredName.GIVEN_NAME) ?: ""
+ val middleName = cursor.getStringValue(CommonDataKinds.StructuredName.MIDDLE_NAME) ?: ""
+ val surname = cursor.getStringValue(CommonDataKinds.StructuredName.FAMILY_NAME) ?: ""
+ val photoUri = cursor.getStringValue(CommonDataKinds.Phone.PHOTO_URI) ?: ""
val number = getPhoneNumbers(id)[id] ?: ArrayList()
val emails = getEmails(id)[id] ?: ArrayList()
val events = getEvents(id)[id] ?: ArrayList()
- val accountName = cursor.getStringValue(ContactsContract.RawContacts.ACCOUNT_NAME)
- val starred = cursor.getIntValue(ContactsContract.CommonDataKinds.StructuredName.STARRED)
+ val accountName = cursor.getStringValue(ContactsContract.RawContacts.ACCOUNT_NAME) ?: ""
+ val starred = cursor.getIntValue(CommonDataKinds.StructuredName.STARRED)
val contactId = cursor.getIntValue(ContactsContract.Data.CONTACT_ID)
- return Contact(id, firstName, middleName, surname, photoUri, number, emails, events, accountName, starred, contactId)
+ val thumbnailUri = cursor.getStringValue(CommonDataKinds.StructuredName.PHOTO_THUMBNAIL_URI) ?: ""
+ return Contact(id, firstName, middleName, surname, photoUri, number, emails, events, accountName, starred, contactId, thumbnailUri)
}
} finally {
cursor?.close()
@@ -264,21 +273,22 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
private fun getContactProjection() = arrayOf(
ContactsContract.Data.CONTACT_ID,
ContactsContract.Data.RAW_CONTACT_ID,
- ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME,
- ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME,
- ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME,
- ContactsContract.CommonDataKinds.StructuredName.PHOTO_URI,
- ContactsContract.CommonDataKinds.StructuredName.STARRED,
+ CommonDataKinds.StructuredName.GIVEN_NAME,
+ CommonDataKinds.StructuredName.MIDDLE_NAME,
+ CommonDataKinds.StructuredName.FAMILY_NAME,
+ CommonDataKinds.StructuredName.PHOTO_URI,
+ CommonDataKinds.StructuredName.PHOTO_THUMBNAIL_URI,
+ CommonDataKinds.StructuredName.STARRED,
ContactsContract.RawContacts.ACCOUNT_NAME
)
private fun getSortString(): String {
val sorting = activity.config.sorting
var sort = when {
- sorting and SORT_BY_FIRST_NAME != 0 -> "${ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME} COLLATE NOCASE"
- sorting and SORT_BY_MIDDLE_NAME != 0 -> "${ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME} COLLATE NOCASE"
- sorting and SORT_BY_SURNAME != 0 -> "${ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME} COLLATE NOCASE"
- else -> ContactsContract.CommonDataKinds.Phone.NUMBER
+ sorting and SORT_BY_FIRST_NAME != 0 -> "${CommonDataKinds.StructuredName.GIVEN_NAME} COLLATE NOCASE"
+ sorting and SORT_BY_MIDDLE_NAME != 0 -> "${CommonDataKinds.StructuredName.MIDDLE_NAME} COLLATE NOCASE"
+ sorting and SORT_BY_SURNAME != 0 -> "${CommonDataKinds.StructuredName.FAMILY_NAME} COLLATE NOCASE"
+ else -> CommonDataKinds.Phone.NUMBER
}
if (sorting and SORT_DESCENDING != 0) {
@@ -294,18 +304,18 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
val operations = ArrayList()
ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI).apply {
val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ?"
- val selectionArgs = arrayOf(contact.id.toString(), ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+ val selectionArgs = arrayOf(contact.id.toString(), CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
withSelection(selection, selectionArgs)
- withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, contact.firstName)
- withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, contact.middleName)
- withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, contact.surname)
+ withValue(CommonDataKinds.StructuredName.GIVEN_NAME, contact.firstName)
+ withValue(CommonDataKinds.StructuredName.MIDDLE_NAME, contact.middleName)
+ withValue(CommonDataKinds.StructuredName.FAMILY_NAME, contact.surname)
operations.add(build())
}
// delete phone numbers
ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI).apply {
val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ? "
- val selectionArgs = arrayOf(contact.id.toString(), ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ val selectionArgs = arrayOf(contact.id.toString(), CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
withSelection(selection, selectionArgs)
operations.add(build())
}
@@ -314,9 +324,9 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
contact.phoneNumbers.forEach {
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply {
withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.id)
- withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
- withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, it.value)
- withValue(ContactsContract.CommonDataKinds.Phone.TYPE, it.type)
+ withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ withValue(CommonDataKinds.Phone.NUMBER, it.value)
+ withValue(CommonDataKinds.Phone.TYPE, it.type)
operations.add(build())
}
}
@@ -324,7 +334,7 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
// delete emails
ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI).apply {
val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ? "
- val selectionArgs = arrayOf(contact.id.toString(), ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
+ val selectionArgs = arrayOf(contact.id.toString(), CommonDataKinds.Email.CONTENT_ITEM_TYPE)
withSelection(selection, selectionArgs)
operations.add(build())
}
@@ -333,9 +343,9 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
contact.emails.forEach {
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply {
withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.id)
- withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
- withValue(ContactsContract.CommonDataKinds.Email.DATA, it.value)
- withValue(ContactsContract.CommonDataKinds.Email.TYPE, it.type)
+ withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE)
+ withValue(CommonDataKinds.Email.DATA, it.value)
+ withValue(CommonDataKinds.Email.TYPE, it.type)
operations.add(build())
}
}
@@ -343,7 +353,7 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
// delete events
ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI).apply {
val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ? "
- val selectionArgs = arrayOf(contact.id.toString(), ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE)
+ val selectionArgs = arrayOf(contact.id.toString(), CommonDataKinds.Event.CONTENT_ITEM_TYPE)
withSelection(selection, selectionArgs)
operations.add(build())
}
@@ -352,9 +362,9 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
contact.events.forEach {
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply {
withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.id)
- withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE)
- withValue(ContactsContract.CommonDataKinds.Event.START_DATE, it.value)
- withValue(ContactsContract.CommonDataKinds.Event.TYPE, it.type)
+ withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Event.CONTENT_ITEM_TYPE)
+ withValue(CommonDataKinds.Event.START_DATE, it.value)
+ withValue(CommonDataKinds.Event.TYPE, it.type)
operations.add(build())
}
}
@@ -398,8 +408,8 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply {
withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.id)
- withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
- withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, scaledSizePhotoData)
+ withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
+ withValue(CommonDataKinds.Photo.PHOTO, scaledSizePhotoData)
operations.add(build())
}
@@ -411,7 +421,7 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
private fun removePhoto(contact: Contact, operations: ArrayList): ArrayList {
ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI).apply {
val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ?"
- val selectionArgs = arrayOf(contact.id.toString(), ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
+ val selectionArgs = arrayOf(contact.id.toString(), CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
withSelection(selection, selectionArgs)
operations.add(build())
}
@@ -421,7 +431,6 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
fun insertContact(contact: Contact): Boolean {
return try {
- activity.toast(R.string.inserting)
val operations = ArrayList()
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI).apply {
withValue(ContactsContract.RawContacts.ACCOUNT_NAME, contact.source)
@@ -432,10 +441,10 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
// names
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply {
withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
- withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
- withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, contact.firstName)
- withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, contact.middleName)
- withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, contact.surname)
+ withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+ withValue(CommonDataKinds.StructuredName.GIVEN_NAME, contact.firstName)
+ withValue(CommonDataKinds.StructuredName.MIDDLE_NAME, contact.middleName)
+ withValue(CommonDataKinds.StructuredName.FAMILY_NAME, contact.surname)
operations.add(build())
}
@@ -443,9 +452,9 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
contact.phoneNumbers.forEach {
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply {
withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
- withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
- withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, it.value)
- withValue(ContactsContract.CommonDataKinds.Phone.TYPE, it.type)
+ withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ withValue(CommonDataKinds.Phone.NUMBER, it.value)
+ withValue(CommonDataKinds.Phone.TYPE, it.type)
operations.add(build())
}
}
@@ -454,9 +463,9 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
contact.emails.forEach {
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply {
withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
- withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
- withValue(ContactsContract.CommonDataKinds.Email.DATA, it.value)
- withValue(ContactsContract.CommonDataKinds.Email.TYPE, it.type)
+ withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE)
+ withValue(CommonDataKinds.Email.DATA, it.value)
+ withValue(CommonDataKinds.Email.TYPE, it.type)
operations.add(build())
}
}
@@ -465,9 +474,9 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
contact.events.forEach {
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply {
withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
- withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE)
- withValue(ContactsContract.CommonDataKinds.Event.START_DATE, it.value)
- withValue(ContactsContract.CommonDataKinds.Event.TYPE, it.type)
+ withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Event.CONTENT_ITEM_TYPE)
+ withValue(CommonDataKinds.Event.START_DATE, it.value)
+ withValue(CommonDataKinds.Event.TYPE, it.type)
operations.add(build())
}
}
@@ -482,15 +491,15 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
val thumbnailSize = getThumbnailSize()
val scaledPhoto = Bitmap.createScaledBitmap(bitmap, thumbnailSize, thumbnailSize, false)
scaledSizePhotoData = bitmapToByteArray(scaledPhoto)
- scaledPhoto.recycle()
fullSizePhotoData = bitmapToByteArray(bitmap)
+ scaledPhoto.recycle()
bitmap.recycle()
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply {
withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
- withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
- withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, scaledSizePhotoData)
+ withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
+ withValue(CommonDataKinds.Photo.PHOTO, scaledSizePhotoData)
operations.add(build())
}
}
@@ -540,7 +549,7 @@ class ContactsHelper(val activity: BaseSimpleActivity) {
val uri = ContactsContract.Data.CONTENT_URI
val projection = arrayOf(ContactsContract.Data.CONTACT_ID, ContactsContract.Data.LOOKUP_KEY)
val selection = "${ContactsContract.Data.MIMETYPE} = ? AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?"
- val selectionArgs = arrayOf(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, contactId)
+ val selectionArgs = arrayOf(CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, contactId)
var cursor: Cursor? = null
try {
cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null)
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfExporter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfExporter.kt
new file mode 100644
index 00000000..b45bd0af
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfExporter.kt
@@ -0,0 +1,106 @@
+package com.simplemobiletools.contacts.helpers
+
+import android.graphics.Bitmap
+import android.net.Uri
+import android.provider.ContactsContract.CommonDataKinds
+import android.provider.MediaStore
+import android.util.Base64
+import com.simplemobiletools.commons.activities.BaseSimpleActivity
+import com.simplemobiletools.commons.extensions.getFileOutputStream
+import com.simplemobiletools.commons.extensions.writeLn
+import com.simplemobiletools.contacts.helpers.VcfExporter.ExportResult.*
+import com.simplemobiletools.contacts.models.Contact
+import java.io.ByteArrayOutputStream
+import java.io.File
+
+class VcfExporter {
+ private val ENCODED_PHOTO_LINE_LENGTH = 74
+
+ enum class ExportResult {
+ EXPORT_FAIL, EXPORT_OK, EXPORT_PARTIAL
+ }
+
+ private var contactsExported = 0
+ private var contactsFailed = 0
+
+ fun exportContacts(activity: BaseSimpleActivity, file: File, contacts: ArrayList, callback: (result: ExportResult) -> Unit) {
+ activity.getFileOutputStream(file) {
+ if (it == null) {
+ callback(EXPORT_FAIL)
+ return@getFileOutputStream
+ }
+
+ it.bufferedWriter().use { out ->
+ for (contact in contacts) {
+ out.writeLn(BEGIN_VCARD)
+ out.writeLn(VERSION_2_1)
+ out.writeLn("$N${contact.surname};${contact.firstName};${contact.middleName};;")
+
+ contact.phoneNumbers.forEach {
+ out.writeLn("$TEL;${getPhoneNumberLabel(it.type)}:${it.value}")
+ }
+
+ contact.emails.forEach {
+ val type = getEmailTypeLabel(it.type)
+ val delimiterType = if (type.isEmpty()) "" else ";$type"
+ out.writeLn("$EMAIL$delimiterType:${it.value}")
+ }
+
+ contact.events.forEach {
+ if (it.type == CommonDataKinds.Event.TYPE_BIRTHDAY) {
+ out.writeLn("$BDAY${it.value}")
+ }
+ }
+
+ if (contact.thumbnailUri.isNotEmpty()) {
+ val firstLine = "$PHOTO;$ENCODING=$BASE64;$JPEG:"
+ val bitmap = MediaStore.Images.Media.getBitmap(activity.contentResolver, Uri.parse(contact.thumbnailUri))
+ val byteArrayOutputStream = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 85, byteArrayOutputStream)
+ bitmap.recycle()
+ val byteArray = byteArrayOutputStream.toByteArray()
+ val encoded = Base64.encodeToString(byteArray, Base64.NO_WRAP)
+
+ val encodedFirstLineSection = encoded.substring(0, ENCODED_PHOTO_LINE_LENGTH - firstLine.length)
+ out.writeLn(firstLine + encodedFirstLineSection)
+ var curStartIndex = encodedFirstLineSection.length
+ do {
+ val part = encoded.substring(curStartIndex, Math.min(curStartIndex + ENCODED_PHOTO_LINE_LENGTH - 1, encoded.length))
+ out.writeLn(" $part")
+ curStartIndex += ENCODED_PHOTO_LINE_LENGTH - 1
+ } while (curStartIndex < encoded.length)
+
+ out.writeLn("")
+ }
+
+ out.writeLn(END_VCARD)
+ contactsExported++
+ }
+ }
+ }
+
+ callback(when {
+ contactsExported == 0 -> EXPORT_FAIL
+ contactsFailed > 0 -> EXPORT_PARTIAL
+ else -> EXPORT_OK
+ })
+ }
+
+ private fun getPhoneNumberLabel(type: Int) = when (type) {
+ CommonDataKinds.Phone.TYPE_MOBILE -> CELL
+ CommonDataKinds.Phone.TYPE_HOME -> HOME
+ CommonDataKinds.Phone.TYPE_WORK -> WORK
+ CommonDataKinds.Phone.TYPE_MAIN -> PREF
+ CommonDataKinds.Phone.TYPE_FAX_WORK -> WORK_FAX
+ CommonDataKinds.Phone.TYPE_FAX_HOME -> HOME_FAX
+ CommonDataKinds.Phone.TYPE_PAGER -> PAGER
+ else -> VOICE
+ }
+
+ private fun getEmailTypeLabel(type: Int) = when (type) {
+ CommonDataKinds.Email.TYPE_HOME -> HOME
+ CommonDataKinds.Email.TYPE_WORK -> WORK
+ CommonDataKinds.Email.TYPE_MOBILE -> MOBILE
+ else -> ""
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfImporter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfImporter.kt
new file mode 100644
index 00000000..d42f7fbd
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfImporter.kt
@@ -0,0 +1,202 @@
+package com.simplemobiletools.contacts.helpers
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.provider.ContactsContract.CommonDataKinds
+import android.util.Base64
+import android.widget.Toast
+import com.simplemobiletools.commons.extensions.showErrorToast
+import com.simplemobiletools.contacts.activities.SimpleActivity
+import com.simplemobiletools.contacts.extensions.getCachePhoto
+import com.simplemobiletools.contacts.extensions.getCachePhotoUri
+import com.simplemobiletools.contacts.helpers.VcfImporter.ImportResult.*
+import com.simplemobiletools.contacts.models.Contact
+import com.simplemobiletools.contacts.models.Email
+import com.simplemobiletools.contacts.models.Event
+import com.simplemobiletools.contacts.models.PhoneNumber
+import java.io.File
+import java.io.FileOutputStream
+
+class VcfImporter(val activity: SimpleActivity) {
+ enum class ImportResult {
+ IMPORT_FAIL, IMPORT_OK, IMPORT_PARTIAL
+ }
+
+ private var curFirstName = ""
+ private var curMiddleName = ""
+ private var curSurname = ""
+ private var curPhotoUri = ""
+ private var curPhoneNumbers = ArrayList()
+ private var curEmails = ArrayList()
+ private var curEvents = ArrayList()
+
+ private var isGettingPhoto = false
+ private var currentPhotoString = StringBuilder()
+ private var currentPhotoCompressionFormat = Bitmap.CompressFormat.JPEG
+
+ private var contactsImported = 0
+ private var contactsFailed = 0
+
+ fun importContacts(path: String, targetContactSource: String): ImportResult {
+ try {
+ val inputStream = if (path.contains("/")) {
+ File(path).inputStream()
+ } else {
+ activity.assets.open(path)
+ }
+
+ inputStream.bufferedReader().use {
+ while (true) {
+ val line = it.readLine() ?: break
+ if (line.trim().isEmpty()) {
+ if (isGettingPhoto) {
+ savePhoto()
+ isGettingPhoto = false
+ }
+ continue
+ }
+
+ when {
+ line.toUpperCase() == BEGIN_VCARD -> resetValues()
+ line.toUpperCase().startsWith(N) -> parseNames(line.substring(N.length))
+ line.toUpperCase().startsWith(TEL) -> addPhoneNumber(line.substring(TEL.length))
+ line.toUpperCase().startsWith(EMAIL) -> addEmail(line.substring(EMAIL.length))
+ line.toUpperCase().startsWith(BDAY) -> addBirthday(line.substring(BDAY.length))
+ line.toUpperCase().startsWith(ANNIVERSARY) -> addAnniversary(line.substring(ANNIVERSARY.length))
+ line.toUpperCase().startsWith(PHOTO) -> addPhoto(line.substring(PHOTO.length))
+ line.toUpperCase() == END_VCARD -> saveContact(targetContactSource)
+ isGettingPhoto -> currentPhotoString.append(line.trim())
+ }
+ }
+ }
+ } catch (e: Exception) {
+ activity.showErrorToast(e, Toast.LENGTH_LONG)
+ contactsFailed++
+ }
+
+ return when {
+ contactsImported == 0 -> IMPORT_FAIL
+ contactsFailed > 0 -> IMPORT_PARTIAL
+ else -> IMPORT_OK
+ }
+ }
+
+ private fun parseNames(names: String) {
+ val nameParts = names.split(";")
+ curSurname = nameParts[0]
+ curFirstName = nameParts[1]
+ if (nameParts.size > 2) {
+ curMiddleName = nameParts[2]
+ }
+ }
+
+ private fun addPhoneNumber(phoneNumber: String) {
+ val phoneParts = phoneNumber.trimStart(';').split(":")
+ var rawType = phoneParts[0]
+ var subType = ""
+ if (rawType.contains('=')) {
+ val types = rawType.split('=')
+ if (types.any { it.contains(';') }) {
+ subType = types[1].split(';')[0]
+ }
+ rawType = types.last()
+ }
+
+ val type = getPhoneNumberTypeId(rawType.toUpperCase(), subType)
+ val value = phoneParts[1]
+ curPhoneNumbers.add(PhoneNumber(value, type))
+ }
+
+ private fun getPhoneNumberTypeId(type: String, subType: String) = when (type) {
+ CELL -> CommonDataKinds.Phone.TYPE_MOBILE
+ HOME -> CommonDataKinds.Phone.TYPE_HOME
+ WORK -> CommonDataKinds.Phone.TYPE_WORK
+ PREF, MAIN -> CommonDataKinds.Phone.TYPE_MAIN
+ WORK_FAX -> CommonDataKinds.Phone.TYPE_FAX_WORK
+ HOME_FAX -> CommonDataKinds.Phone.TYPE_FAX_HOME
+ FAX -> if (subType == WORK) CommonDataKinds.Phone.TYPE_FAX_WORK else CommonDataKinds.Phone.TYPE_FAX_HOME
+ PAGER -> CommonDataKinds.Phone.TYPE_PAGER
+ else -> CommonDataKinds.Phone.TYPE_OTHER
+ }
+
+ private fun addEmail(email: String) {
+ val emailParts = email.trimStart(';').split(":")
+ var rawType = emailParts[0]
+ if (rawType.contains('=')) {
+ rawType = rawType.split('=').last()
+ }
+ val type = getEmailTypeId(rawType.toUpperCase())
+ val value = emailParts[1]
+ curEmails.add(Email(value, type))
+ }
+
+ private fun getEmailTypeId(type: String) = when (type) {
+ HOME -> CommonDataKinds.Email.TYPE_HOME
+ WORK -> CommonDataKinds.Email.TYPE_WORK
+ MOBILE -> CommonDataKinds.Email.TYPE_MOBILE
+ else -> CommonDataKinds.Email.TYPE_OTHER
+ }
+
+ private fun addBirthday(birthday: String) {
+ curEvents.add(Event(birthday, CommonDataKinds.Event.TYPE_BIRTHDAY))
+ }
+
+ private fun addAnniversary(anniversary: String) {
+ curEvents.add(Event(anniversary, CommonDataKinds.Event.TYPE_ANNIVERSARY))
+ }
+
+ private fun addPhoto(photo: String) {
+ val photoParts = photo.trimStart(';').split(';')
+ if (photoParts.size == 2) {
+ val typeParts = photoParts[1].split(':')
+ currentPhotoCompressionFormat = getPhotoCompressionFormat(typeParts[0])
+ val encoding = photoParts[0].split('=').last()
+ if (encoding == BASE64) {
+ isGettingPhoto = true
+ currentPhotoString.append(typeParts[1].trim())
+ }
+ }
+ }
+
+ private fun getPhotoCompressionFormat(type: String) = when (type.toLowerCase()) {
+ "png" -> Bitmap.CompressFormat.PNG
+ "webp" -> Bitmap.CompressFormat.WEBP
+ else -> Bitmap.CompressFormat.JPEG
+ }
+
+ private fun savePhoto() {
+ val file = activity.getCachePhoto()
+ val imageAsBytes = Base64.decode(currentPhotoString.toString().toByteArray(), Base64.DEFAULT)
+ val bitmap = BitmapFactory.decodeByteArray(imageAsBytes, 0, imageAsBytes.size)
+ var fileOutputStream: FileOutputStream? = null
+ try {
+ fileOutputStream = FileOutputStream(file)
+ bitmap.compress(currentPhotoCompressionFormat, 100, fileOutputStream)
+ } finally {
+ fileOutputStream?.close()
+ }
+
+ curPhotoUri = activity.getCachePhotoUri(file).toString()
+ }
+
+ private fun saveContact(source: String) {
+ val contact = Contact(0, curFirstName, curMiddleName, curSurname, curPhotoUri, curPhoneNumbers, curEmails, curEvents, source, 0, 0, "")
+ if (ContactsHelper(activity).insertContact(contact)) {
+ contactsImported++
+ }
+ }
+
+ private fun resetValues() {
+ curFirstName = ""
+ curMiddleName = ""
+ curSurname = ""
+ curPhotoUri = ""
+ curPhoneNumbers = ArrayList()
+ curEmails = ArrayList()
+ curEvents = ArrayList()
+
+ isGettingPhoto = false
+ currentPhotoString = StringBuilder()
+ currentPhotoCompressionFormat = Bitmap.CompressFormat.JPEG
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/models/Contact.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/models/Contact.kt
index 44e3ab64..95131fe6 100644
--- a/app/src/main/kotlin/com/simplemobiletools/contacts/models/Contact.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/models/Contact.kt
@@ -6,7 +6,7 @@ import com.simplemobiletools.commons.helpers.SORT_DESCENDING
data class Contact(val id: Int, var firstName: String, var middleName: String, var surname: String, var photoUri: String,
var phoneNumbers: ArrayList, var emails: ArrayList, var events: ArrayList, var source: String,
- var starred: Int, val contactId: Int) : Comparable {
+ var starred: Int, val contactId: Int, val thumbnailUri: String) : Comparable {
companion object {
var sorting: Int = 0
}
diff --git a/app/src/main/res/layout/dialog_export_contacts.xml b/app/src/main/res/layout/dialog_export_contacts.xml
new file mode 100644
index 00000000..0f053e06
--- /dev/null
+++ b/app/src/main/res/layout/dialog_export_contacts.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_import_contacts.xml b/app/src/main/res/layout/dialog_import_contacts.xml
new file mode 100644
index 00000000..98ef049f
--- /dev/null
+++ b/app/src/main/res/layout/dialog_import_contacts.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/cab.xml b/app/src/main/res/menu/cab.xml
index 50c583c0..c727d9bd 100644
--- a/app/src/main/res/menu/cab.xml
+++ b/app/src/main/res/menu/cab.xml
@@ -16,6 +16,11 @@
android:icon="@drawable/ic_star_on"
android:title="@string/add_to_favorites"
app:showAsAction="ifRoom"/>
+
+
+
+
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 4149bc48..8809a0f8 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -21,8 +21,9 @@
Kontakt bei Klick anrufen
- Namen mit Vornamen beginnen
+ Namen mit Nachnamen beginnen
Zeige Telefonnummern im Hauptmenü
+ Show contact avatars
Email
@@ -53,6 +54,15 @@
Kontakte durchsuchen
Favoriten durchsuchen
+
+ Kontakte importieren
+ Kontakte exportieren
+ Kontakte aus .vcf Datei importieren
+ Kontakte in .vcf Datei exportieren
+ Zielkontaktquelle
+ Kontaktquellen einschließen
+ Dateiname (ohne .vcf)
+
Eine App zum Verwalten von Kontakten, ganz ohne Werbung.
diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml
index 2ef130a7..bde780d0 100644
--- a/app/src/main/res/values-ko-rKR/strings.xml
+++ b/app/src/main/res/values-ko-rKR/strings.xml
@@ -3,7 +3,7 @@
연락처
주소
등록중…
- 갱신중…
+ 수정중…
연락처가 포함된 계정을 선택하세요.
새로운 연락처
@@ -23,6 +23,7 @@
클릭으로 전화걸기
성을 먼저 표시
메인 스크린에 전화번호 표시
+ Show contact avatars
이메일
@@ -53,6 +54,15 @@
연락처 검색
자주쓰는 연락처 검색
+
+ 연락처 가져오기
+ 연락처 내보내기
+ .vcf file에서 가져오기
+ .vcf file로 내보내기
+ 내보내기 대상
+ 가져오기 대상
+ 파일이름 (.vcf 확장자 생략)
+
광고가 없는 연락처 관리 애플리케이션입니다.
@@ -60,7 +70,7 @@
연락처를 생성하고 관리하는 간단한 앱입니다. 연락처는 기본적으로 기기에만 저장되며 Google 또는 다른 계정을 통해 동기화 할 수도 있습니다. 즐겨 찾는 연락처는 별도의 목록에 표시 할 수 있습니다.
다양한 조건으로 정렬 및 필터링이 가능하며 성과 이름을 표시하는 옵션도 제공합니다. 또한 애플리케이션을 이용해 사용자 이메일 및 이벤트 관리를 할 수도 있습니다.
-
+
광고가 포함되어 있거나, 불필요한 권한을 요청하지 않습니다. 이 앱의 모든 소스는 오픈소스이며, 사용자가 직접 애플리케이션의 컬러를 설정 할 수 있습니다.
이 앱은 다양한 시리즈의 모바일앱 중 하나입니다. 나머지는 http://www.simplemobiletools.com 에서 찾아 보실 수 있습니다.
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 0cf93b1f..e88a3341 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -23,6 +23,7 @@
Call contact on click
Start name with surname
Show phone numbers on the main screen
+ Show contact avatars
E-mail
@@ -53,6 +54,15 @@
Search contacts
Search favorites
+
+ Import contacts
+ Export contacts
+ Import contacts from a .vcf file
+ Export contacts to a .vcf file
+ Target contact source
+ Include contact sources
+ Filename (without .vcf)
+
A contacts app for managing your contacts without ads.
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index af1107af..9e6936ff 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -23,9 +23,10 @@
Вызывать контакт при нажатии
Отображать сначала фамилии
Отображать номера телефонов на главном экране
+ Show contact avatars
- Эл.почта
+ Эл. почта
Домашний
Рабочий
Другой
@@ -53,6 +54,15 @@
Поиск контактов
Поиск избранных
+
+ Импортировать контакты
+ Экспортировать контакты
+ Импортировать контакты из .vcf файла
+ Экспортировать контакты в .vcf файл
+ Назначенный к обновлению источник контактов
+ Включить источники контактов
+ Имя файла (без .vcf)
+
Приложение для управления контактами без рекламы.
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index aecf75dc..0d76b488 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -23,6 +23,7 @@
Zavolať kontakt po kliknutí
Začať meno priezviskom
Zobraziť telefónne čísla na hlavnej obrazovke
+ Zobraziť obrázky kontaktov
Email
@@ -53,6 +54,15 @@
Hľadať v kontaktoch
Hľadať medzi obľúbenými
+
+ Importovať kontakty
+ Exportovať kontakty
+ Importovať kontakty z .vcf súboru
+ Exportovať kontakty do .vcf súboru
+ Cieľový zdroj kontaktov
+ Zahrnúť zdroje kontaktov
+ Názov súboru (bez .vcf)
+
Aplikácia pre správu vašich kontaktov bez reklám.
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
new file mode 100644
index 00000000..1d4f9c6a
--- /dev/null
+++ b/app/src/main/res/values-sv/strings.xml
@@ -0,0 +1,83 @@
+
+ Simple Contacts
+ Kontakter
+ Adress
+ Lägger till…
+ Uppdaterar…
+ Du måste välja vilket konto kontakten ska tillhöra
+
+ Ny kontakt
+ Redigera kontakt
+ Välj kontakt
+ Välj kontakter
+ Förnamn
+ Mellannamn
+ Efternamn
+
+
+ Ta foto
+ Välj foto
+ Ta bort foto
+
+
+ Ring kontakter när jag trycker på dem
+ Visa efternamn först
+ Visa telefonnummer i huvudvyn
+ Show contact avatars
+
+
+ E-post
+ Hem
+ Arbete
+ Annat
+
+
+ Nummer
+ Mobil
+ Primärt nummer
+ Arbetsfax
+ Hemfax
+ Personsökare
+ Inget telefonnummer hittades
+
+
+ Födelsedag
+ Årsdag
+
+
+ Det verkar som att du inte har lagt till några favoritkontakter ännu.
+ Lägg till favoriter
+ Lägg till i favoriter
+ Ta bort från favoriter
+
+
+ Sök efter kontakter
+ Sök efter favoriter
+
+
+ Importera kontakter
+ Exportera kontakter
+ Importera kontakter från en .vcf-fil
+ Exportera kontakter till en .vcf-fil
+ Målkontaktkälla
+ Inkludera kontaktkällor
+ Filnamn (utan .vcf)
+
+
+
+ En app för att hantera dina kontakter utan reklam.
+
+ En enkel app för att skapa och hantera kontakter från olika källor. Kontakterna kan lagras bara på din enhet, men kan också synkroniseras med ditt Google-konto eller andra konton. Du kan visa dina favoritkontakter i en separat lista.
+
+ Du kan också använda den för att hantera dina kontakters e-postadresser och händelser. Den kan sortera och filtrera efter flera parametrar. Den kan också visa efternamn först.
+
+ Innehåller ingen reklam eller onödiga behörigheter. Den har helt öppen källkod och anpassningsbara färger.
+
+ Denna app är bara en del av en större serie appar. Du hittar resten av dem på http://www.simplemobiletools.com
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 0ebcd872..d781ec5f 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,6 +1,4 @@
-
- @color/theme_dark_text_color
- @color/theme_dark_background_color
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6ff54cca..4966b2d5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -23,6 +23,7 @@
Call contact on click
Start name with surname
Show phone numbers on the main screen
+ Show contact avatars
Email
@@ -53,6 +54,15 @@
Search contacts
Search favorites
+
+ Import contacts
+ Export contacts
+ Import contacts from a .vcf file
+ Export contacts to a .vcf file
+ Target contact source
+ Include contact sources
+ Filename (without .vcf)
+
A contacts app for managing your contacts without ads.
diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml
index 90100f76..c91794e3 100644
--- a/app/src/main/res/xml/provider_paths.xml
+++ b/app/src/main/res/xml/provider_paths.xml
@@ -3,4 +3,5 @@
+
diff --git a/build.gradle b/build.gradle
index 9d3da9d8..97726280 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '1.2.10'
+ ext.kotlin_version = '1.2.20'
repositories {
google()
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
new file mode 100644
index 00000000..30b18821
--- /dev/null
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -0,0 +1,7 @@
+A simple app for creating or managing your contacts from any source. The contacts can be stored on your device only, but also synchronized via Google, or other accounts. You can display your favorite contacts on a separate list.
+
+You can use it for managing user emails and events too. It has the ability to sort/filter by multiple parameters, optionally display surname as the first name.
+
+Contains no ads or unnecessary permissions. It is fully opensource, provides customizable colors.
+
+This app is just one piece of a bigger series of apps. You can find the rest of them at http://www.simplemobiletools.com
diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png
new file mode 100644
index 00000000..bb47ee95
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png
new file mode 100644
index 00000000..692e44ce
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/app.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/app.png
new file mode 100644
index 00000000..5fdfe132
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/app.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/app_2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/app_2.png
new file mode 100644
index 00000000..ca454ba6
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/app_2.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/app_3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/app_3.png
new file mode 100644
index 00000000..6d51e66d
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/app_3.png differ
diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt
new file mode 100644
index 00000000..754859ae
--- /dev/null
+++ b/fastlane/metadata/android/en-US/short_description.txt
@@ -0,0 +1 @@
+A contacts app for managing your contacts without ads.
diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt
new file mode 100644
index 00000000..d83108ca
--- /dev/null
+++ b/fastlane/metadata/android/en-US/title.txt
@@ -0,0 +1 @@
+Simple Contacts
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index a693299b..7d73f3c2 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip
diff --git a/screenshots/app.png b/screenshots/app.png
deleted file mode 100644
index 98de0809..00000000
Binary files a/screenshots/app.png and /dev/null differ
diff --git a/screenshots/app_2.png b/screenshots/app_2.png
deleted file mode 100644
index 6d7c50cb..00000000
Binary files a/screenshots/app_2.png and /dev/null differ
diff --git a/screenshots/app_3.png b/screenshots/app_3.png
deleted file mode 100644
index 4c6d025c..00000000
Binary files a/screenshots/app_3.png and /dev/null differ