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 -Get it on Google Play -Get it on F-Droid -App image -App image -App image +Get it on Google Play +Get it on F-Droid + +
+App image +App image +App image +
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