diff --git a/CHANGELOG.md b/CHANGELOG.md index f9854e29..3c6a8c2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,57 @@ Changelog ========== +Version 4.2.2 *(2018-08-13)* +---------------------------- + + * Added an optional Nickname field + * Improved searching and sorting UTF8 characters + * Fixed updating Notes and Organization fields + +Version 4.2.1 *(2018-08-05)* +---------------------------- + + * Added some stability and light theme UX fixes + +Version 4.2.0 *(2018-08-04)* +---------------------------- + + * Added a Recent Calls tab + * Allow customizing which tabs are visible + * Added an optional call confirmation dialog + * Fixed some glitches related to company contacts + * Some other performance and stability improvements + +Version 4.1.0 *(2018-07-16)* +---------------------------- + + * Fixed a couple issues related to importing contacts from .vcf files + * Couple other UX and stability improvements + +Version 4.0.5 *(2018-07-05)* +---------------------------- + + * Make duplicate contact filtering more agressive + * Couple UX and stability improvements + +Version 4.0.4 *(2018-06-19)* +---------------------------- + + * Make "Try filtering out duplicate contacts" more agressive + * Ignore hidden contact fields, do not wipe them + * Prefer the contacts Mobile number at sending batch SMS + * Added a couple stability improvements + +Version 4.0.3 *(2018-05-13)* +---------------------------- + + * Show a couple additional contact sources + +Version 4.0.2 *(2018-05-12)* +---------------------------- + + * Make sure all relevant contact sources are visible + Version 4.0.1 *(2018-05-09)* ---------------------------- diff --git a/app/build.gradle b/app/build.gradle index ca15f081..dd3e3290 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,15 +3,15 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { - compileSdkVersion 27 - buildToolsVersion "27.0.3" + compileSdkVersion 28 + buildToolsVersion "28.0.2" defaultConfig { applicationId "com.simplemobiletools.contacts" minSdkVersion 16 - targetSdkVersion 27 - versionCode 21 - versionName "4.0.1" + targetSdkVersion 28 + versionCode 29 + versionName "4.2.2" setProperty("archivesBaseName", "contacts") } @@ -45,9 +45,11 @@ ext { } dependencies { - implementation 'com.simplemobiletools:commons:4.0.0' + implementation 'com.simplemobiletools:commons:4.6.15' implementation 'joda-time:joda-time:2.9.9' implementation 'com.facebook.stetho:stetho:1.5.0' + implementation 'com.android.support.constraint:constraint-layout:1.1.2' + compile 'com.googlecode.ez-vcard:ez-vcard:0.10.4' debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion" releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index e69de29b..50dfb901 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -0,0 +1,5 @@ +# ez-vcard +-keep,includedescriptorclasses class ezvcard.property.** { *; } +-keep enum ezvcard.VCardVersion { *; } +-dontwarn ezvcard.io.json.** +-dontwarn freemarker.** diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6d7fec68..4422660d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,8 @@ + + - - - - + android:theme="@style/SplashTheme"/> @@ -191,19 +189,6 @@ android:resource="@xml/provider_paths"/> - - - - - - - - + + + + + + + saveContact() R.id.share -> shareContact() @@ -232,7 +233,11 @@ class EditContactActivity : ContactActivity() { Intent().apply { action = Intent.ACTION_EDIT data = getContactPublicUri(contact!!) - startActivity(this) + if (resolveActivity(packageManager) != null) { + startActivity(this) + } else { + toast(R.string.no_app_found) + } } } @@ -264,6 +269,7 @@ class EditContactActivity : ContactActivity() { } private fun setupFieldVisibility() { + val showFields = config.showContactFields if (showFields and (SHOW_PREFIX_FIELD or SHOW_FIRST_NAME_FIELD or SHOW_MIDDLE_NAME_FIELD or SHOW_SURNAME_FIELD or SHOW_SUFFIX_FIELD) == 0) { contact_name_image.beInvisible() } @@ -273,6 +279,7 @@ class EditContactActivity : ContactActivity() { contact_middle_name.beVisibleIf(showFields and SHOW_MIDDLE_NAME_FIELD != 0) contact_surname.beVisibleIf(showFields and SHOW_SURNAME_FIELD != 0) contact_suffix.beVisibleIf(showFields and SHOW_SUFFIX_FIELD != 0) + contact_nickname.beVisibleIf(showFields and SHOW_NICKNAME_FIELD != 0) contact_source.beVisibleIf(showFields and SHOW_CONTACT_SOURCE_FIELD != 0) contact_source_image.beVisibleIf(showFields and SHOW_CONTACT_SOURCE_FIELD != 0) @@ -340,112 +347,99 @@ class EditContactActivity : ContactActivity() { contact_middle_name.setText(middleName) contact_surname.setText(surname) contact_suffix.setText(suffix) + contact_nickname.setText(nickname) } } private fun setupPhoneNumbers() { - if (showFields and SHOW_PHONE_NUMBERS_FIELD != 0) { - contact!!.phoneNumbers.forEachIndexed { index, number -> - var numberHolder = contact_numbers_holder.getChildAt(index) - if (numberHolder == null) { - numberHolder = layoutInflater.inflate(R.layout.item_edit_phone_number, contact_numbers_holder, false) - contact_numbers_holder.addView(numberHolder) - } + contact!!.phoneNumbers.forEachIndexed { index, number -> + var numberHolder = contact_numbers_holder.getChildAt(index) + if (numberHolder == null) { + numberHolder = layoutInflater.inflate(R.layout.item_edit_phone_number, contact_numbers_holder, false) + contact_numbers_holder.addView(numberHolder) + } - numberHolder!!.apply { - contact_number.setText(number.value) - setupPhoneNumberTypePicker(contact_number_type, number.type) - } + numberHolder!!.apply { + contact_number.setText(number.value) + setupPhoneNumberTypePicker(contact_number_type, number.type) } } } private fun setupEmails() { - if (showFields and SHOW_EMAILS_FIELD != 0) { - contact!!.emails.forEachIndexed { index, email -> - var emailHolder = contact_emails_holder.getChildAt(index) - if (emailHolder == null) { - emailHolder = layoutInflater.inflate(R.layout.item_edit_email, contact_emails_holder, false) - contact_emails_holder.addView(emailHolder) - } + contact!!.emails.forEachIndexed { index, email -> + var emailHolder = contact_emails_holder.getChildAt(index) + if (emailHolder == null) { + emailHolder = layoutInflater.inflate(R.layout.item_edit_email, contact_emails_holder, false) + contact_emails_holder.addView(emailHolder) + } - emailHolder!!.apply { - contact_email.setText(email.value) - setupEmailTypePicker(contact_email_type, email.type) - } + emailHolder!!.apply { + contact_email.setText(email.value) + setupEmailTypePicker(contact_email_type, email.type) } } } private fun setupAddresses() { - if (showFields and SHOW_ADDRESSES_FIELD != 0) { - contact!!.addresses.forEachIndexed { index, address -> - var addressHolder = contact_addresses_holder.getChildAt(index) - if (addressHolder == null) { - addressHolder = layoutInflater.inflate(R.layout.item_edit_address, contact_addresses_holder, false) - contact_addresses_holder.addView(addressHolder) - } + contact!!.addresses.forEachIndexed { index, address -> + var addressHolder = contact_addresses_holder.getChildAt(index) + if (addressHolder == null) { + addressHolder = layoutInflater.inflate(R.layout.item_edit_address, contact_addresses_holder, false) + contact_addresses_holder.addView(addressHolder) + } - addressHolder!!.apply { - contact_address.setText(address.value) - setupAddressTypePicker(contact_address_type, address.type) - } + addressHolder!!.apply { + contact_address.setText(address.value) + setupAddressTypePicker(contact_address_type, address.type) } } } private fun setupNotes() { - if (showFields and SHOW_NOTES_FIELD != 0) { - contact_notes.setText(contact!!.notes) - } + contact_notes.setText(contact!!.notes) } private fun setupOrganization() { - if (showFields and SHOW_ORGANIZATION_FIELD != 0) { - contact_organization_company.setText(contact!!.organization.company) - contact_organization_job_position.setText(contact!!.organization.jobPosition) - } + contact_organization_company.setText(contact!!.organization.company) + contact_organization_job_position.setText(contact!!.organization.jobPosition) } private fun setupWebsites() { - if (showFields and SHOW_WEBSITES_FIELD != 0) { - contact!!.websites.forEachIndexed { index, website -> - var websitesHolder = contact_websites_holder.getChildAt(index) - if (websitesHolder == null) { - websitesHolder = layoutInflater.inflate(R.layout.item_edit_website, contact_websites_holder, false) - contact_websites_holder.addView(websitesHolder) - } - - websitesHolder!!.contact_website.setText(website) + contact!!.websites.forEachIndexed { index, website -> + var websitesHolder = contact_websites_holder.getChildAt(index) + if (websitesHolder == null) { + websitesHolder = layoutInflater.inflate(R.layout.item_edit_website, contact_websites_holder, false) + contact_websites_holder.addView(websitesHolder) } + + websitesHolder!!.contact_website.setText(website) } } private fun setupEvents() { - if (showFields and SHOW_EVENTS_FIELD != 0) { - contact!!.events.forEachIndexed { index, event -> - var eventHolder = contact_events_holder.getChildAt(index) - if (eventHolder == null) { - eventHolder = layoutInflater.inflate(R.layout.item_event, contact_events_holder, false) - contact_events_holder.addView(eventHolder) + contact!!.events.forEachIndexed { index, event -> + var eventHolder = contact_events_holder.getChildAt(index) + if (eventHolder == null) { + eventHolder = layoutInflater.inflate(R.layout.item_event, contact_events_holder, false) + contact_events_holder.addView(eventHolder) + } + + (eventHolder as ViewGroup).apply { + val contactEvent = contact_event.apply { + event.value.getDateTimeFromDateString(this) + tag = event.value + alpha = 1f } - (eventHolder as ViewGroup).apply { - val contactEvent = contact_event.apply { - getDateTime(event.value, this) - tag = event.value - alpha = 1f - } + setupEventTypePicker(this, event.type) - setupEventTypePicker(this, event.type) - - contact_event_remove.apply { - beVisible() - applyColorFilter(getAdjustedPrimaryColor()) - background.applyColorFilter(config.textColor) - setOnClickListener { - resetContactEvent(contactEvent, this) - } + contact_event_remove.apply { + beVisible() + applyColorFilter(getAdjustedPrimaryColor()) + background.applyColorFilter(config.textColor) + setOnClickListener { + resetContactEvent(contactEvent, this) } } } @@ -453,52 +447,50 @@ class EditContactActivity : ContactActivity() { } private fun setupGroups() { - if (showFields and SHOW_GROUPS_FIELD != 0) { - contact_groups_holder.removeAllViews() - val groups = contact!!.groups - groups.forEachIndexed { index, group -> - var groupHolder = contact_groups_holder.getChildAt(index) - if (groupHolder == null) { - groupHolder = layoutInflater.inflate(R.layout.item_edit_group, contact_groups_holder, false) - contact_groups_holder.addView(groupHolder) + contact_groups_holder.removeAllViews() + val groups = contact!!.groups + groups.forEachIndexed { index, group -> + var groupHolder = contact_groups_holder.getChildAt(index) + if (groupHolder == null) { + groupHolder = layoutInflater.inflate(R.layout.item_edit_group, contact_groups_holder, false) + contact_groups_holder.addView(groupHolder) + } + + (groupHolder as ViewGroup).apply { + contact_group.apply { + text = group.title + setTextColor(config.textColor) + tag = group.id + alpha = 1f } - (groupHolder as ViewGroup).apply { - contact_group.apply { - text = group.title - setTextColor(config.textColor) - tag = group.id - alpha = 1f - } + setOnClickListener { + showSelectGroupsDialog() + } + contact_group_remove.apply { + beVisible() + applyColorFilter(getAdjustedPrimaryColor()) + background.applyColorFilter(config.textColor) setOnClickListener { - showSelectGroupsDialog() - } - - contact_group_remove.apply { - beVisible() - applyColorFilter(getAdjustedPrimaryColor()) - background.applyColorFilter(config.textColor) - setOnClickListener { - removeGroup(group.id) - } + removeGroup(group.id) } } } + } - if (groups.isEmpty()) { - layoutInflater.inflate(R.layout.item_edit_group, contact_groups_holder, false).apply { - contact_group.apply { - alpha = 0.5f - text = getString(R.string.no_groups) - setTextColor(config.textColor) - } + if (groups.isEmpty()) { + layoutInflater.inflate(R.layout.item_edit_group, contact_groups_holder, false).apply { + contact_group.apply { + alpha = 0.5f + text = getString(R.string.no_groups) + setTextColor(config.textColor) + } - contact_groups_holder.addView(this) - contact_group_remove.beGone() - setOnClickListener { - showSelectGroupsDialog() - } + contact_groups_holder.addView(this) + contact_group_remove.beGone() + setOnClickListener { + showSelectGroupsDialog() } } } @@ -513,8 +505,8 @@ class EditContactActivity : ContactActivity() { supportActionBar?.title = resources.getString(R.string.new_contact) originalContactSource = if (hasContactPermissions()) config.lastUsedContactSource else SMT_PRIVATE val organization = Organization("", "") - contact = Contact(0, "", "", "", "", "", "", ArrayList(), ArrayList(), ArrayList(), ArrayList(), originalContactSource, 0, 0, "", null, "", - ArrayList(), organization, ArrayList()) + contact = Contact(0, "", "", "", "", "", "", "", ArrayList(), ArrayList(), ArrayList(), ArrayList(), originalContactSource, 0, 0, "", + null, "", ArrayList(), organization, ArrayList()) contact_source.text = getPublicContactSource(contact!!.source) } @@ -603,7 +595,7 @@ class EditContactActivity : ContactActivity() { } } - val date = getDateTime(eventField.tag?.toString() ?: "") + val date = (eventField.tag?.toString() ?: "").getDateTimeFromDateString() DatePickerDialog(this, getDialogTheme(), setDateListener, date.year, date.monthOfYear - 1, date.dayOfMonth).show() } @@ -713,7 +705,7 @@ class EditContactActivity : ContactActivity() { } private fun saveContact() { - if (isSaving || contact == null) { + if (isSaving) { return } @@ -725,6 +717,7 @@ class EditContactActivity : ContactActivity() { middleName = contact_middle_name.value surname = contact_surname.value suffix = contact_suffix.value + nickname = contact_nickname.value photoUri = currentContactPhotoPath phoneNumbers = getFilledPhoneNumbers() emails = getFilledEmails() diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/GroupContactsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/GroupContactsActivity.kt index c81d309b..3716007b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/GroupContactsActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/GroupContactsActivity.kt @@ -3,12 +3,17 @@ package com.simplemobiletools.contacts.activities import android.os.Bundle import android.view.Menu import android.view.MenuItem -import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.extensions.beVisibleIf +import com.simplemobiletools.commons.extensions.getAdjustedPrimaryColor +import com.simplemobiletools.commons.extensions.underlineText +import com.simplemobiletools.commons.extensions.updateTextColors import com.simplemobiletools.contacts.R import com.simplemobiletools.contacts.adapters.ContactsAdapter import com.simplemobiletools.contacts.dialogs.SelectContactsDialog import com.simplemobiletools.contacts.extensions.* -import com.simplemobiletools.contacts.helpers.* +import com.simplemobiletools.contacts.helpers.ContactsHelper +import com.simplemobiletools.contacts.helpers.GROUP +import com.simplemobiletools.contacts.helpers.LOCATION_GROUP_CONTACTS import com.simplemobiletools.contacts.interfaces.RefreshContactsListener import com.simplemobiletools.contacts.interfaces.RemoveFromGroupListener import com.simplemobiletools.contacts.models.Contact @@ -102,24 +107,13 @@ class GroupContactsActivity : SimpleActivity(), RemoveFromGroupListener, Refresh val currAdapter = group_contacts_list.adapter if (currAdapter == null) { ContactsAdapter(this, contacts, this, LOCATION_GROUP_CONTACTS, this, group_contacts_list, group_contacts_fastscroller) { - when (config.onContactClick) { - ON_CLICK_CALL_CONTACT -> { - val contact = it as Contact - if (contact.phoneNumbers.isNotEmpty()) { - tryStartCall(it) - } else { - toast(R.string.no_phone_number_found) - } - } - ON_CLICK_VIEW_CONTACT -> viewContact(it as Contact) - ON_CLICK_EDIT_CONTACT -> editContact(it as Contact) - } + contactClicked(it as Contact) }.apply { addVerticalDividers(true) group_contacts_list.adapter = this } - group_contacts_fastscroller.setScrollTo(0) + group_contacts_fastscroller.setScrollToY(0) group_contacts_fastscroller.setViews(group_contacts_list) { val item = (group_contacts_list.adapter as ContactsAdapter).contactItems.getOrNull(it) group_contacts_fastscroller.updateBubbleText(item?.getBubbleText() ?: "") 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 90f0f9e4..19c92a75 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/MainActivity.kt @@ -1,15 +1,13 @@ package com.simplemobiletools.contacts.activities -import android.Manifest import android.app.SearchManager import android.content.Context import android.content.Intent -import android.content.pm.PackageManager import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable import android.net.Uri import android.os.Bundle -import android.support.v4.app.ActivityCompat -import android.support.v4.content.ContextCompat +import android.os.Handler import android.support.v4.view.MenuItemCompat import android.support.v4.view.ViewPager import android.support.v7.widget.SearchView @@ -30,6 +28,7 @@ import com.simplemobiletools.contacts.dialogs.ImportContactsDialog import com.simplemobiletools.contacts.extensions.config import com.simplemobiletools.contacts.extensions.dbHelper import com.simplemobiletools.contacts.extensions.getTempFile +import com.simplemobiletools.contacts.fragments.MyViewPagerFragment import com.simplemobiletools.contacts.helpers.* import com.simplemobiletools.contacts.interfaces.RefreshContactsListener import com.simplemobiletools.contacts.models.Contact @@ -37,6 +36,7 @@ import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.fragment_contacts.* import kotlinx.android.synthetic.main.fragment_favorites.* import kotlinx.android.synthetic.main.fragment_groups.* +import kotlinx.android.synthetic.main.fragment_recents.* import java.io.FileOutputStream class MainActivity : SimpleActivity(), RefreshContactsListener { @@ -45,6 +45,7 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { private var werePermissionsHandled = false private var isFirstResume = true private var isGettingContacts = false + private var handledShowTabs = 0 private var storedTextColor = 0 private var storedBackgroundColor = 0 @@ -53,6 +54,7 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { private var storedShowPhoneNumbers = false private var storedStartNameWithSurname = false private var storedFilterDuplicates = true + private var storedShowTabs = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -63,26 +65,36 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { // just get a reference to the database to make sure it is created properly dbHelper + handlePermission(PERMISSION_READ_CALL_LOG) { + if (it) { + handlePermission(PERMISSION_WRITE_CALL_LOG) { + checkContactPermissions() + } + } else { + checkContactPermissions() + } + } + + storeStateVariables() + checkWhatsNewDialog() + } + + private fun checkContactPermissions() { handlePermission(PERMISSION_READ_CONTACTS) { werePermissionsHandled = true if (it) { handlePermission(PERMISSION_WRITE_CONTACTS) { // workaround for upgrading from version 3.x to 4.x as we added a new permission from an already granted permissions group - val hasGetAccountsPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.GET_ACCOUNTS) == PackageManager.PERMISSION_GRANTED - if (!hasGetAccountsPermission) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.GET_ACCOUNTS), 34) + handlePermission(PERMISSION_GET_ACCOUNTS) { + storeLocalAccountData() + initFragments() } - - storeLocalAccountData() - initFragments() } } else { storeLocalAccountData() initFragments() } } - storeStateVariables() - checkWhatsNewDialog() } override fun onResume() { @@ -92,6 +104,12 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { return } + if (storedShowTabs != config.showTabs) { + config.lastUsedViewPagerPage = 0 + System.exit(0) + return + } + val configShowContactThumbnails = config.showContactThumbnails if (storedShowContactThumbnails != configShowContactThumbnails) { getAllFragments().forEach { @@ -138,10 +156,10 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { initFragments() } else { refreshContacts(ALL_TABS_MASK) - } - getAllFragments().forEach { - it?.onActivityResume() + getAllFragments().forEach { + it?.onActivityResume() + } } } isFirstResume = false @@ -159,11 +177,12 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu, menu) - val currentPage = viewpager?.currentItem + val currentFragment = getCurrentFragment() + menu.apply { - findItem(R.id.search).isVisible = currentPage != LOCATION_GROUPS_TAB - findItem(R.id.sort).isVisible = currentPage != LOCATION_GROUPS_TAB - findItem(R.id.filter).isVisible = currentPage != LOCATION_GROUPS_TAB + findItem(R.id.search).isVisible = currentFragment != groups_fragment && currentFragment != recents_fragment + findItem(R.id.sort).isVisible = currentFragment != groups_fragment && currentFragment != recents_fragment + findItem(R.id.filter).isVisible = currentFragment != groups_fragment } setupSearch(menu) return true @@ -191,6 +210,7 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { storedShowPhoneNumbers = showPhoneNumbers storedStartNameWithSurname = startNameWithSurname storedFilterDuplicates = filterDuplicates + storedShowTabs = showTabs } } @@ -200,7 +220,7 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { (searchMenuItem!!.actionView as SearchView).apply { setSearchableInfo(searchManager.getSearchableInfo(componentName)) isSubmitButtonEnabled = false - queryHint = getString(if (viewpager.currentItem == 0) R.string.search_contacts else R.string.search_favorites) + queryHint = getString(if (getCurrentFragment() == contacts_fragment) R.string.search_contacts else R.string.search_favorites) setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String) = false @@ -228,13 +248,30 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { }) } - private fun getCurrentFragment() = when (viewpager.currentItem) { - 0 -> contacts_fragment - 1 -> favorites_fragment - else -> groups_fragment + private fun getCurrentFragment(): MyViewPagerFragment? { + val showTabs = config.showTabs + val fragments = arrayListOf() + if (showTabs and CONTACTS_TAB_MASK != 0) { + fragments.add(contacts_fragment) + } + + if (showTabs and FAVORITES_TAB_MASK != 0) { + fragments.add(favorites_fragment) + } + + if (showTabs and RECENTS_TAB_MASK != 0) { + fragments.add(recents_fragment) + } + + if (showTabs and GROUPS_TAB_MASK != 0) { + fragments.add(groups_fragment) + } + + return fragments[viewpager.currentItem] } private fun setupTabColors() { + handledShowTabs = config.showTabs val lastUsedPage = config.lastUsedViewPagerPage main_tabs_holder.apply { background = ColorDrawable(config.backgroundColor) @@ -250,10 +287,10 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { private fun storeLocalAccountData() { if (config.localAccountType == "-1") { - ContactsHelper(this).getContactSources { + ContactsHelper(this).getContactSources { sources -> var localAccountType = "" var localAccountName = "" - it.forEach { + sources.forEach { if (localAccountTypes.contains(it.type)) { localAccountType = it.type localAccountName = it.name @@ -266,11 +303,10 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { } } - private fun getInactiveTabIndexes(activeIndex: Int) = arrayListOf(0, 1, 2).filter { it != activeIndex } + private fun getInactiveTabIndexes(activeIndex: Int) = arrayListOf(0, 1, 2, 3).filter { it != activeIndex } private fun initFragments() { - refreshContacts(ALL_TABS_MASK) - viewpager.offscreenPageLimit = 2 + viewpager.offscreenPageLimit = 3 viewpager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { override fun onPageScrollStateChanged(state: Int) { if (isSearchOpen) { @@ -291,11 +327,19 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { } }) + viewpager.onGlobalLayout { + refreshContacts(ALL_TABS_MASK) + } + main_tabs_holder.onTabSelectionChanged( tabUnselectedAction = { it.icon?.applyColorFilter(config.textColor) }, tabSelectedAction = { + if (isSearchOpen) { + getCurrentFragment()?.onSearchQueryChanged("") + searchMenuItem?.collapseActionView() + } viewpager.currentItem = it.position it.icon?.applyColorFilter(getAdjustedPrimaryColor()) } @@ -305,6 +349,37 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { tryImportContactsFromFile(intent.data) intent.data = null } + + main_tabs_holder.removeAllTabs() + var skippedTabs = 0 + tabsList.forEachIndexed { index, value -> + if (config.showTabs and value == 0) { + skippedTabs++ + } else { + main_tabs_holder.addTab(main_tabs_holder.newTab().setIcon(getTabIcon(index)), index - skippedTabs, config.lastUsedViewPagerPage == index - skippedTabs) + } + } + + // selecting the proper tab sometimes glitches, add an extra selector to make sure we have it right + main_tabs_holder.onGlobalLayout { + Handler().postDelayed({ + main_tabs_holder.getTabAt(config.lastUsedViewPagerPage)?.select() + invalidateOptionsMenu() + }, 100L) + } + + main_tabs_holder.beVisibleIf(skippedTabs < 3) + } + + private fun getTabIcon(position: Int): Drawable { + val drawableId = when (position) { + LOCATION_CONTACTS_TAB -> R.drawable.ic_person + LOCATION_FAVORITES_TAB -> R.drawable.ic_star_on + LOCATION_RECENTS_TAB -> R.drawable.ic_clock + else -> R.drawable.ic_group + } + + return resources.getColoredDrawableWithColor(drawableId, config.textColor) } private fun showSortingDialog() { @@ -354,10 +429,14 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { return } - val inputStream = contentResolver.openInputStream(uri) - val out = FileOutputStream(tempFile) - inputStream.copyTo(out) - showImportContactsDialog(tempFile.absolutePath) + try { + val inputStream = contentResolver.openInputStream(uri) + val out = FileOutputStream(tempFile) + inputStream.copyTo(out) + showImportContactsDialog(tempFile.absolutePath) + } catch (e: Exception) { + showErrorToast(e) + } } else -> toast(R.string.invalid_file_format) } @@ -375,13 +454,13 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { FilePickerDialog(this, pickFile = false, showFAB = true) { ExportContactsDialog(this, it) { file, contactSources -> Thread { - ContactsHelper(this).getContacts { - val contacts = it.filter { contactSources.contains(it.source) } + ContactsHelper(this).getContacts { allContacts -> + val contacts = allContacts.filter { contactSources.contains(it.source) } if (contacts.isEmpty()) { toast(R.string.no_entries_for_exporting) } else { - VcfExporter().exportContacts(this, file, contacts as ArrayList, true) { - toast(when (it) { + VcfExporter().exportContacts(this, file, contacts as ArrayList, true) { result -> + toast(when (result) { VcfExporter.ExportResult.EXPORT_OK -> R.string.exporting_successful VcfExporter.ExportResult.EXPORT_PARTIAL -> R.string.exporting_some_entries_failed else -> R.string.exporting_failed @@ -395,32 +474,33 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { } private fun launchAbout() { + val licenses = LICENSE_MULTISELECT or LICENSE_JODA or LICENSE_GLIDE or LICENSE_GSON or LICENSE_STETHO + val faqItems = arrayListOf( FAQItem(R.string.faq_1_title, R.string.faq_1_text), FAQItem(R.string.faq_2_title_commons, R.string.faq_2_text_commons) ) - startAboutActivity(R.string.app_name, LICENSE_MULTISELECT or LICENSE_JODA or LICENSE_GLIDE or LICENSE_GSON or LICENSE_STETHO, - BuildConfig.VERSION_NAME, faqItems) + startAboutActivity(R.string.app_name, licenses, BuildConfig.VERSION_NAME, faqItems, true) } override fun refreshContacts(refreshTabsMask: Int) { if (isActivityDestroyed() || isGettingContacts) { return } - isGettingContacts = true + + if (viewpager.adapter == null) { + viewpager.adapter = ViewPagerAdapter(this) + viewpager.currentItem = config.lastUsedViewPagerPage + } + ContactsHelper(this).getContacts { isGettingContacts = false if (isActivityDestroyed()) { return@getContacts } - if (viewpager.adapter == null) { - viewpager.adapter = ViewPagerAdapter(this, it) - viewpager.currentItem = config.lastUsedViewPagerPage - } - if (refreshTabsMask and CONTACTS_TAB_MASK != 0) { contacts_fragment?.refreshContacts(it) } @@ -429,6 +509,10 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { favorites_fragment?.refreshContacts(it) } + if (refreshTabsMask and RECENTS_TAB_MASK != 0) { + recents_fragment?.refreshContacts(it) + } + if (refreshTabsMask and GROUPS_TAB_MASK != 0) { if (refreshTabsMask == GROUPS_TAB_MASK) { groups_fragment.skipHashComparing = true @@ -436,15 +520,25 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { groups_fragment?.refreshContacts(it) } } + + if (refreshTabsMask and RECENTS_TAB_MASK != 0) { + ContactsHelper(this).getRecents { + runOnUiThread { + recents_fragment?.updateRecentCalls(it) + } + } + } } - private fun getAllFragments() = arrayListOf(contacts_fragment, favorites_fragment, groups_fragment) + private fun getAllFragments() = arrayListOf(contacts_fragment, favorites_fragment, recents_fragment, groups_fragment) private fun checkWhatsNewDialog() { arrayListOf().apply { add(Release(10, R.string.release_10)) add(Release(11, R.string.release_11)) add(Release(16, R.string.release_16)) + add(Release(27, R.string.release_27)) + add(Release(29, R.string.release_29)) checkWhatsNew(this, BuildConfig.VERSION_CODE) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/SettingsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/SettingsActivity.kt index 1cf3bb24..37811085 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/SettingsActivity.kt @@ -3,10 +3,13 @@ package com.simplemobiletools.contacts.activities import android.os.Bundle import com.simplemobiletools.commons.dialogs.RadioGroupDialog import com.simplemobiletools.commons.extensions.beVisibleIf +import com.simplemobiletools.commons.extensions.isThankYouInstalled +import com.simplemobiletools.commons.extensions.launchPurchaseThankYouIntent import com.simplemobiletools.commons.extensions.updateTextColors import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.contacts.R import com.simplemobiletools.contacts.dialogs.ManageVisibleFieldsDialog +import com.simplemobiletools.contacts.dialogs.ManageVisibleTabsDialog import com.simplemobiletools.contacts.extensions.config import com.simplemobiletools.contacts.helpers.ON_CLICK_CALL_CONTACT import com.simplemobiletools.contacts.helpers.ON_CLICK_EDIT_CONTACT @@ -23,8 +26,10 @@ class SettingsActivity : SimpleActivity() { override fun onResume() { super.onResume() + setupPurchaseThankYou() setupCustomizeColors() setupManageShownContactFields() + setupManageShownTabs() setupUseEnglish() setupAvoidWhatsNew() setupShowInfoBubble() @@ -32,10 +37,18 @@ class SettingsActivity : SimpleActivity() { setupShowPhoneNumbers() setupStartNameWithSurname() setupFilterDuplicates() + setupShowCallConfirmation() setupOnContactClick() updateTextColors(settings_holder) } + private fun setupPurchaseThankYou() { + settings_purchase_thank_you_holder.beVisibleIf(config.appRunCount > 10 && !isThankYouInstalled()) + settings_purchase_thank_you_holder.setOnClickListener { + launchPurchaseThankYouIntent() + } + } + private fun setupCustomizeColors() { settings_customize_colors_holder.setOnClickListener { startCustomizationActivity() @@ -48,6 +61,12 @@ class SettingsActivity : SimpleActivity() { } } + private fun setupManageShownTabs() { + settings_manage_tabs_holder.setOnClickListener { + ManageVisibleTabsDialog(this) + } + } + private fun setupUseEnglish() { settings_use_english_holder.beVisibleIf(config.wasUseEnglishToggled || Locale.getDefault().language != "en") settings_use_english.isChecked = config.useEnglish @@ -126,4 +145,12 @@ class SettingsActivity : SimpleActivity() { ON_CLICK_VIEW_CONTACT -> R.string.view_contact else -> R.string.edit_contact }) + + private fun setupShowCallConfirmation() { + settings_show_call_confirmation.isChecked = config.showCallConfirmation + settings_show_call_confirmation_holder.setOnClickListener { + settings_show_call_confirmation.toggle() + config.showCallConfirmation = settings_show_call_confirmation.isChecked + } + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/ViewContactActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/ViewContactActivity.kt index a0cd2ade..3b41f777 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/ViewContactActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/ViewContactActivity.kt @@ -23,6 +23,7 @@ import kotlinx.android.synthetic.main.item_website.view.* class ViewContactActivity : ContactActivity() { private var isViewIntent = false + private var showFields = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -57,6 +58,10 @@ class ViewContactActivity : ContactActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (contact == null) { + return true + } + when (item.itemId) { R.id.edit -> editContact(contact!!) R.id.share -> shareContact() @@ -160,7 +165,11 @@ class ViewContactActivity : ContactActivity() { Intent().apply { action = ContactsContract.QuickContact.ACTION_QUICK_CONTACT data = getContactPublicUri(contact!!) - startActivity(this) + if (resolveActivity(packageManager) != null) { + startActivity(this) + } else { + toast(R.string.no_app_found) + } } } @@ -193,7 +202,11 @@ class ViewContactActivity : ContactActivity() { contact_suffix.text = suffix contact_suffix.beVisibleIf(suffix.isNotEmpty() && showFields and SHOW_SUFFIX_FIELD != 0) - if (contact_prefix.isGone() && contact_first_name.isGone() && contact_middle_name.isGone() && contact_surname.isGone() && contact_suffix.isGone()) { + contact_nickname.text = nickname + contact_nickname.beVisibleIf(nickname.isNotEmpty() && showFields and SHOW_NICKNAME_FIELD != 0) + + if (contact_prefix.isGone() && contact_first_name.isGone() && contact_middle_name.isGone() && contact_surname.isGone() && contact_suffix.isGone() + && contact_nickname.isGone()) { contact_name_image.beInvisible() (contact_photo.layoutParams as RelativeLayout.LayoutParams).bottomMargin = resources.getDimension(R.dimen.medium_margin).toInt() } @@ -280,7 +293,7 @@ class ViewContactActivity : ContactActivity() { layoutInflater.inflate(R.layout.item_event, contact_events_holder, false).apply { contact_events_holder.addView(this) contact_event.alpha = 1f - getDateTime(it.value, contact_event) + it.value.getDateTimeFromDateString(contact_event) contact_event_type.setText(getEventTextId(it.type)) contact_event_remove.beGone() } 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 fd9db502..39bd3220 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/ContactsAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/ContactsAdapter.kt @@ -70,10 +70,10 @@ class ContactsAdapter(activity: SimpleActivity, var contactItems: ArrayList + val view = holder.bindView(contact, true, true) { itemView, layoutPosition -> setupView(itemView, contact) } bindViewHolder(holder, position, view) @@ -145,7 +147,10 @@ class ContactsAdapter(activity: SimpleActivity, var contactItems: ArrayList() selectedPositions.sortedDescending().forEach { - contactsToRemove.add(contactItems[it]) + val contact = contactItems.getOrNull(it) + if (contact != null) { + contactsToRemove.add(contact) + } } contactItems.removeAll(contactsToRemove) @@ -155,7 +160,7 @@ class ContactsAdapter(activity: SimpleActivity, var contactItems: ArrayList, val override fun prepareActionMode(menu: Menu) { menu.apply { - findItem(R.id.cab_edit).isVisible = isOneItemSelected() + findItem(R.id.cab_rename).isVisible = isOneItemSelected() } } - override fun prepareItemSelection(view: View) {} + override fun prepareItemSelection(viewHolder: ViewHolder) {} - override fun markItemSelection(select: Boolean, view: View?) { - view?.group_frame?.isSelected = select + override fun markViewHolderSelection(select: Boolean, viewHolder: ViewHolder?) { + viewHolder?.itemView?.group_frame?.isSelected = select } override fun actionItemPressed(id: Int) { @@ -53,7 +53,7 @@ class GroupsAdapter(activity: SimpleActivity, var groups: ArrayList, val } when (id) { - R.id.cab_edit -> editGroup() + R.id.cab_rename -> renameGroup() R.id.cab_select_all -> selectAll() R.id.cab_delete -> askConfirmDelete() } @@ -61,11 +61,13 @@ class GroupsAdapter(activity: SimpleActivity, var groups: ArrayList, val override fun getSelectableItemCount() = groups.size + override fun getIsItemSelectable(position: Int) = true + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_group, parent) override fun onBindViewHolder(holder: MyRecyclerViewAdapter.ViewHolder, position: Int) { val group = groups[position] - val view = holder.bindView(group, true) { itemView, layoutPosition -> + val view = holder.bindView(group, true, true) { itemView, layoutPosition -> setupView(itemView, group) } bindViewHolder(holder, position, view) @@ -80,7 +82,7 @@ class GroupsAdapter(activity: SimpleActivity, var groups: ArrayList, val fastScroller?.measureRecyclerView() } - private fun editGroup() { + private fun renameGroup() { RenameGroupDialog(activity, groups[selectedPositions.first()]) { finishActMode() refreshListener?.refreshContacts(GROUPS_TAB_MASK) @@ -89,11 +91,11 @@ class GroupsAdapter(activity: SimpleActivity, var groups: ArrayList, val private fun askConfirmDelete() { ConfirmationDialog(activity) { - deleteContacts() + deleteGroups() } } - private fun deleteContacts() { + private fun deleteGroups() { if (selectedPositions.isEmpty()) { return } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/RecentCallsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/RecentCallsAdapter.kt new file mode 100644 index 00000000..e54d8d2b --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/RecentCallsAdapter.kt @@ -0,0 +1,119 @@ +package com.simplemobiletools.contacts.adapters + +import android.view.Menu +import android.view.View +import android.view.ViewGroup +import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter +import com.simplemobiletools.commons.dialogs.ConfirmationDialog +import com.simplemobiletools.commons.extensions.beVisibleIf +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.helpers.ContactsHelper +import com.simplemobiletools.contacts.helpers.RECENTS_TAB_MASK +import com.simplemobiletools.contacts.interfaces.RefreshContactsListener +import com.simplemobiletools.contacts.models.RecentCall +import kotlinx.android.synthetic.main.item_recent_call.view.* +import java.util.* + +class RecentCallsAdapter(activity: SimpleActivity, var recentCalls: ArrayList, val refreshListener: RefreshContactsListener?, recyclerView: MyRecyclerView, + fastScroller: FastScroller, itemClick: (Any) -> Unit) : MyRecyclerViewAdapter(activity, recyclerView, fastScroller, itemClick) { + private val showPhoneNumbers = activity.config.showPhoneNumbers + + init { + setupDragListener(true) + } + + override fun getActionMenuId() = R.menu.cab_recent_calls + + override fun prepareActionMode(menu: Menu) {} + + override fun prepareItemSelection(viewHolder: ViewHolder) {} + + override fun markViewHolderSelection(select: Boolean, viewHolder: ViewHolder?) { + viewHolder?.itemView?.recent_call_frame?.isSelected = select + } + + override fun actionItemPressed(id: Int) { + if (selectedPositions.isEmpty()) { + return + } + + when (id) { + R.id.cab_select_all -> selectAll() + R.id.cab_delete -> askConfirmDelete() + } + } + + override fun getSelectableItemCount() = recentCalls.size + + override fun getIsItemSelectable(position: Int) = true + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_recent_call, parent) + + override fun onBindViewHolder(holder: MyRecyclerViewAdapter.ViewHolder, position: Int) { + val recentCall = recentCalls[position] + val view = holder.bindView(recentCall, true, true) { itemView, layoutPosition -> + setupView(itemView, recentCall) + } + bindViewHolder(holder, position, view) + } + + override fun getItemCount() = recentCalls.size + + fun updateItems(newItems: ArrayList) { + recentCalls = newItems + notifyDataSetChanged() + finishActMode() + fastScroller?.measureRecyclerView() + } + + private fun askConfirmDelete() { + ConfirmationDialog(activity) { + deleteRecentCalls() + } + } + + private fun deleteRecentCalls() { + if (selectedPositions.isEmpty()) { + return + } + + val callsToRemove = ArrayList() + selectedPositions.sortedDescending().forEach { + val call = recentCalls[it] + callsToRemove.add(call) + } + ContactsHelper(activity).removeRecentCalls(callsToRemove.map { it.id } as ArrayList) + recentCalls.removeAll(callsToRemove) + + if (recentCalls.isEmpty()) { + refreshListener?.refreshContacts(RECENTS_TAB_MASK) + finishActMode() + } else { + removeSelectedItems() + } + } + + private fun setupView(view: View, recentCall: RecentCall) { + view.apply { + recent_call_name.apply { + text = recentCall.name ?: recentCall.number + setTextColor(textColor) + } + + recent_call_number.apply { + beVisibleIf(showPhoneNumbers && recentCall.name != null) + text = recentCall.number + setTextColor(textColor) + } + + recent_call_date_time.apply { + text = recentCall.dateTime + setTextColor(textColor) + } + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/ViewPagerAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/ViewPagerAdapter.kt index beb17f42..c78ffaef 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/ViewPagerAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/ViewPagerAdapter.kt @@ -5,19 +5,22 @@ import android.view.View import android.view.ViewGroup import com.simplemobiletools.contacts.R import com.simplemobiletools.contacts.activities.MainActivity +import com.simplemobiletools.contacts.extensions.config import com.simplemobiletools.contacts.fragments.MyViewPagerFragment -import com.simplemobiletools.contacts.models.Contact +import com.simplemobiletools.contacts.helpers.* -class ViewPagerAdapter(val activity: MainActivity, val contacts: ArrayList) : PagerAdapter() { +class ViewPagerAdapter(val activity: MainActivity) : PagerAdapter() { + private val showTabs = activity.config.showTabs override fun instantiateItem(container: ViewGroup, position: Int): Any { val layout = getFragment(position) val view = activity.layoutInflater.inflate(layout, container, false) container.addView(view) + (view as MyViewPagerFragment).apply { setupFragment(activity) - refreshContacts(contacts) } + return view } @@ -25,12 +28,28 @@ class ViewPagerAdapter(val activity: MainActivity, val contacts: ArrayList R.layout.fragment_contacts - 1 -> R.layout.fragment_favorites - else -> R.layout.fragment_groups + private fun getFragment(position: Int): Int { + val fragments = arrayListOf() + if (showTabs and CONTACTS_TAB_MASK != 0) { + fragments.add(R.layout.fragment_contacts) + } + + if (showTabs and FAVORITES_TAB_MASK != 0) { + fragments.add(R.layout.fragment_favorites) + } + + if (showTabs and RECENTS_TAB_MASK != 0) { + fragments.add(R.layout.fragment_recents) + } + + if (showTabs and GROUPS_TAB_MASK != 0) { + fragments.add(R.layout.fragment_groups) + } + + return fragments[position] } } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/CallConfirmationDialog.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/CallConfirmationDialog.kt new file mode 100644 index 00000000..40f3ef89 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/CallConfirmationDialog.kt @@ -0,0 +1,33 @@ +package com.simplemobiletools.contacts.dialogs + +import android.support.v7.app.AlertDialog +import android.view.animation.AnimationUtils +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.extensions.applyColorFilter +import com.simplemobiletools.commons.extensions.setupDialogStuff +import com.simplemobiletools.contacts.R +import com.simplemobiletools.contacts.extensions.config +import com.simplemobiletools.contacts.models.Contact +import kotlinx.android.synthetic.main.dialog_call_confirmation.view.* + +class CallConfirmationDialog(val activity: BaseSimpleActivity, val contact: Contact, private val callback: () -> Unit) { + private var view = activity.layoutInflater.inflate(R.layout.dialog_call_confirmation, null) + + init { + view.call_confirm_phone.applyColorFilter(activity.config.textColor) + AlertDialog.Builder(activity) + .setNegativeButton(R.string.cancel, null) + .create().apply { + val title = String.format(activity.getString(R.string.call_person), contact.getFullName()) + activity.setupDialogStuff(view, this, titleText = title) { + view.call_confirm_phone.apply { + startAnimation(AnimationUtils.loadAnimation(activity, R.anim.pulsing_animation)) + setOnClickListener { + callback.invoke() + dismiss() + } + } + } + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/ManageVisibleFieldsDialog.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/ManageVisibleFieldsDialog.kt index cb0b8424..9560f4b2 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/ManageVisibleFieldsDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/ManageVisibleFieldsDialog.kt @@ -19,6 +19,7 @@ class ManageVisibleFieldsDialog(val activity: BaseSimpleActivity) { put(SHOW_MIDDLE_NAME_FIELD, R.id.manage_visible_fields_middle_name) put(SHOW_SURNAME_FIELD, R.id.manage_visible_fields_surname) put(SHOW_SUFFIX_FIELD, R.id.manage_visible_fields_suffix) + put(SHOW_NICKNAME_FIELD, R.id.manage_visible_fields_nickname) put(SHOW_PHONE_NUMBERS_FIELD, R.id.manage_visible_fields_phone_numbers) put(SHOW_EMAILS_FIELD, R.id.manage_visible_fields_emails) put(SHOW_ADDRESSES_FIELD, R.id.manage_visible_fields_addresses) @@ -36,7 +37,7 @@ class ManageVisibleFieldsDialog(val activity: BaseSimpleActivity) { } AlertDialog.Builder(activity) - .setPositiveButton(R.string.ok, { dialog, which -> dialogConfirmed() }) + .setPositiveButton(R.string.ok) { dialog, which -> dialogConfirmed() } .setNegativeButton(R.string.cancel, null) .create().apply { activity.setupDialogStuff(view, this) diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/ManageVisibleTabsDialog.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/ManageVisibleTabsDialog.kt new file mode 100644 index 00000000..241c62cf --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/ManageVisibleTabsDialog.kt @@ -0,0 +1,50 @@ +package com.simplemobiletools.contacts.dialogs + +import android.support.v7.app.AlertDialog +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.extensions.setupDialogStuff +import com.simplemobiletools.commons.views.MyAppCompatCheckbox +import com.simplemobiletools.contacts.R +import com.simplemobiletools.contacts.extensions.config +import com.simplemobiletools.contacts.helpers.* + +class ManageVisibleTabsDialog(val activity: BaseSimpleActivity) { + private var view = activity.layoutInflater.inflate(R.layout.dialog_manage_visible_tabs, null) + private val tabs = LinkedHashMap() + + init { + tabs.apply { + put(CONTACTS_TAB_MASK, R.id.manage_visible_tabs_contacts) + put(FAVORITES_TAB_MASK, R.id.manage_visible_tabs_favorites) + put(RECENTS_TAB_MASK, R.id.manage_visible_tabs_recents) + put(GROUPS_TAB_MASK, R.id.manage_visible_tabs_groups) + } + + val showTabs = activity.config.showTabs + for ((key, value) in tabs) { + view.findViewById(value).isChecked = showTabs and key != 0 + } + + AlertDialog.Builder(activity) + .setPositiveButton(R.string.ok) { dialog, which -> dialogConfirmed() } + .setNegativeButton(R.string.cancel, null) + .create().apply { + activity.setupDialogStuff(view, this) + } + } + + private fun dialogConfirmed() { + var result = 0 + for ((key, value) in tabs) { + if (view.findViewById(value).isChecked) { + result += key + } + } + + if (result == 0) { + result = ALL_TABS_MASK + } + + activity.config.showTabs = result + } +} 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 7044edc6..a63eb96d 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/Activity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/Activity.kt @@ -14,9 +14,8 @@ import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.contacts.BuildConfig import com.simplemobiletools.contacts.R import com.simplemobiletools.contacts.activities.SimpleActivity -import com.simplemobiletools.contacts.helpers.ContactsHelper -import com.simplemobiletools.contacts.helpers.SMT_PRIVATE -import com.simplemobiletools.contacts.helpers.VcfExporter +import com.simplemobiletools.contacts.dialogs.CallConfirmationDialog +import com.simplemobiletools.contacts.helpers.* import com.simplemobiletools.contacts.models.Contact import com.simplemobiletools.contacts.models.ContactSource import java.io.File @@ -36,6 +35,16 @@ fun SimpleActivity.startCallIntent(recipient: String) { } fun SimpleActivity.tryStartCall(contact: Contact) { + if (config.showCallConfirmation) { + CallConfirmationDialog(this, contact) { + startCall(contact) + } + } else { + startCall(contact) + } +} + +fun SimpleActivity.startCall(contact: Contact) { val numbers = contact.phoneNumbers if (numbers.size == 1) { startCallIntent(numbers.first().value) @@ -105,10 +114,10 @@ fun BaseSimpleActivity.shareContacts(contacts: ArrayList) { fun BaseSimpleActivity.sendSMSToContacts(contacts: ArrayList) { val numbers = StringBuilder() contacts.forEach { - it.phoneNumbers.forEach { - if (it.value.isNotEmpty()) { - numbers.append("${it.value};") - } + val number = it.phoneNumbers.firstOrNull { it.type == ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE } + ?: it.phoneNumbers.firstOrNull() + if (number != null) { + numbers.append("${number.value};") } val uriString = "smsto:${numbers.toString().trimEnd(';')}" @@ -191,3 +200,17 @@ fun Activity.getVisibleContactSources(): ArrayList { sourceNames.removeAll(config.ignoredContactSources) return sourceNames } + +fun SimpleActivity.contactClicked(contact: Contact) { + when (config.onContactClick) { + ON_CLICK_CALL_CONTACT -> { + if (contact.phoneNumbers.isNotEmpty()) { + tryStartCall(contact) + } else { + toast(R.string.no_phone_number_found) + } + } + ON_CLICK_VIEW_CONTACT -> viewContact(contact) + ON_CLICK_EDIT_CONTACT -> editContact(contact) + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/String.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/String.kt new file mode 100644 index 00000000..6134b1ad --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/String.kt @@ -0,0 +1,34 @@ +package com.simplemobiletools.contacts.extensions + +import android.widget.TextView +import com.simplemobiletools.commons.helpers.getDateFormats +import org.joda.time.DateTime +import org.joda.time.format.DateTimeFormat +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* + +fun String.getDateTimeFromDateString(viewToUpdate: TextView? = null): DateTime { + val dateFormats = getDateFormats() + var date = DateTime() + for (format in dateFormats) { + try { + date = DateTime.parse(this, DateTimeFormat.forPattern(format)) + + val formatter = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()) + var localPattern = (formatter as SimpleDateFormat).toLocalizedPattern() + + val hasYear = format.contains("y") + if (!hasYear) { + localPattern = localPattern.replace("y", "").trim() + date = date.withYear(DateTime().year) + } + + val formattedString = date.toString(localPattern) + viewToUpdate?.text = formattedString + break + } catch (ignored: Exception) { + } + } + return date +} 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 853b1292..7afecd04 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/MyViewPagerFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/MyViewPagerFragment.kt @@ -15,7 +15,10 @@ import com.simplemobiletools.contacts.activities.MainActivity import com.simplemobiletools.contacts.activities.SimpleActivity import com.simplemobiletools.contacts.adapters.ContactsAdapter import com.simplemobiletools.contacts.adapters.GroupsAdapter -import com.simplemobiletools.contacts.extensions.* +import com.simplemobiletools.contacts.adapters.RecentCallsAdapter +import com.simplemobiletools.contacts.extensions.config +import com.simplemobiletools.contacts.extensions.contactClicked +import com.simplemobiletools.contacts.extensions.getVisibleContactSources import com.simplemobiletools.contacts.helpers.* import com.simplemobiletools.contacts.models.Contact import com.simplemobiletools.contacts.models.Group @@ -47,21 +50,28 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) fragment_placeholder_2.underlineText() updateViewStuff() - if (this is FavoritesFragment) { - fragment_placeholder.text = activity.getString(R.string.no_favorites) - fragment_placeholder_2.text = activity.getString(R.string.add_favorites) - } else if (this is GroupsFragment) { - fragment_placeholder.text = activity.getString(R.string.no_group_created) - fragment_placeholder_2.text = activity.getString(R.string.create_group) + when { + this is FavoritesFragment -> { + fragment_placeholder.text = activity.getString(R.string.no_favorites) + fragment_placeholder_2.text = activity.getString(R.string.add_favorites) + } + this is GroupsFragment -> { + fragment_placeholder.text = activity.getString(R.string.no_group_created) + fragment_placeholder_2.text = activity.getString(R.string.create_group) + } + this is RecentsFragment -> { + fragment_fab.beGone() + fragment_placeholder_2.text = activity.getString(R.string.request_the_required_permissions) + } } } } fun textColorChanged(color: Int) { - if (this is GroupsFragment) { - (fragment_list.adapter as GroupsAdapter).updateTextColor(color) - } else { - (fragment_list.adapter as ContactsAdapter).apply { + when { + this is GroupsFragment -> (fragment_list.adapter as GroupsAdapter).updateTextColor(color) + this is RecentsFragment -> (fragment_list.adapter as RecentCallsAdapter).updateTextColor(color) + else -> (fragment_list.adapter as ContactsAdapter).apply { updateTextColor(color) initDrawables() } @@ -77,7 +87,7 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) } fun startNameWithSurnameChanged(startNameWithSurname: Boolean) { - if (this !is GroupsFragment) { + if (this !is GroupsFragment && this !is RecentsFragment) { (fragment_list.adapter as ContactsAdapter).apply { config.sorting = if (startNameWithSurname) SORT_BY_SURNAME else SORT_BY_FIRST_NAME this@MyViewPagerFragment.activity!!.refreshContacts(CONTACTS_TAB_MASK or FAVORITES_TAB_MASK) @@ -86,6 +96,13 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) } fun refreshContacts(contacts: ArrayList) { + if ((config.showTabs and CONTACTS_TAB_MASK == 0 && this is ContactsFragment) || + (config.showTabs and FAVORITES_TAB_MASK == 0 && this is FavoritesFragment) || + (config.showTabs and RECENTS_TAB_MASK == 0 && this is RecentsFragment) || + (config.showTabs and GROUPS_TAB_MASK == 0 && this is GroupsFragment)) { + return + } + if (config.lastUsedContactSource.isEmpty()) { val grouped = contacts.groupBy { it.source }.maxWith(compareBy { it.value.size }) config.lastUsedContactSource = grouped?.key ?: "" @@ -99,6 +116,7 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) val filtered = when { this is GroupsFragment -> contacts this is FavoritesFragment -> contacts.filter { it.starred == 1 } as ArrayList + this is RecentsFragment -> ArrayList() else -> { val contactSources = activity!!.getVisibleContactSources() contacts.filter { contactSources.contains(it.source) } as ArrayList @@ -117,9 +135,13 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) private fun setupContacts(contacts: ArrayList) { if (this is GroupsFragment) { setupGroupsAdapter(contacts) - } else { + } else if (this !is RecentsFragment) { setupContactsFavoritesAdapter(contacts) } + + if (this is ContactsFragment || this is FavoritesFragment) { + contactsIgnoringSearch = (fragment_list?.adapter as? ContactsAdapter)?.contactItems ?: ArrayList() + } } private fun setupGroupsAdapter(contacts: ArrayList) { @@ -150,7 +172,7 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) fragment_list.adapter = this } - fragment_fastscroller.setScrollTo(0) + fragment_fastscroller.setScrollToY(0) fragment_fastscroller.setViews(fragment_list) { val item = (fragment_list.adapter as GroupsAdapter).groups.getOrNull(it) fragment_fastscroller.updateBubbleText(item?.getBubbleText() ?: "") @@ -164,33 +186,19 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) } private fun setupContactsFavoritesAdapter(contacts: ArrayList) { - fragment_placeholder_2.beVisibleIf(contacts.isEmpty()) - fragment_placeholder.beVisibleIf(contacts.isEmpty()) - fragment_list.beVisibleIf(contacts.isNotEmpty()) - + setupViewVisibility(contacts) val currAdapter = fragment_list.adapter if (currAdapter == null || forceListRedraw) { forceListRedraw = false val location = if (this is FavoritesFragment) LOCATION_FAVORITES_TAB else LOCATION_CONTACTS_TAB ContactsAdapter(activity as SimpleActivity, contacts, activity, location, null, fragment_list, fragment_fastscroller) { - when (config.onContactClick) { - ON_CLICK_CALL_CONTACT -> { - val contact = it as Contact - if (contact.phoneNumbers.isNotEmpty()) { - (activity as SimpleActivity).tryStartCall(it) - } else { - activity!!.toast(R.string.no_phone_number_found) - } - } - ON_CLICK_VIEW_CONTACT -> context!!.viewContact(it as Contact) - ON_CLICK_EDIT_CONTACT -> context!!.editContact(it as Contact) - } + activity?.contactClicked(it as Contact) }.apply { addVerticalDividers(true) fragment_list.adapter = this } - fragment_fastscroller.setScrollTo(0) + fragment_fastscroller.setScrollToY(0) fragment_fastscroller.setViews(fragment_list) { val item = (fragment_list.adapter as ContactsAdapter).contactItems.getOrNull(it) fragment_fastscroller.updateBubbleText(item?.getBubbleText() ?: "") @@ -211,7 +219,7 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) showContactThumbnails = showThumbnails notifyDataSetChanged() } - } else { + } else if (this !is RecentsFragment) { (fragment_list.adapter as? ContactsAdapter)?.apply { showContactThumbnails = showThumbnails notifyDataSetChanged() @@ -228,38 +236,44 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) } fun onSearchQueryChanged(text: String) { + val shouldNormalize = text.normalizeString() == text (fragment_list.adapter as? ContactsAdapter)?.apply { val filtered = contactsIgnoringSearch.filter { - it.getFullName().contains(text, true) || + getProperText(it.getFullName(), shouldNormalize).contains(text, true) || + getProperText(it.nickname, shouldNormalize).contains(text, true) || it.phoneNumbers.any { it.value.contains(text, true) } || it.emails.any { it.value.contains(text, true) } || - it.addresses.any { it.value.contains(text, true) } || - it.notes.contains(text, true) || - it.organization.company.contains(text, true) || - it.organization.jobPosition.contains(text, true) || + it.addresses.any { getProperText(it.value, shouldNormalize).contains(text, true) } || + getProperText(it.notes, shouldNormalize).contains(text, true) || + getProperText(it.organization.company, shouldNormalize).contains(text, true) || + getProperText(it.organization.jobPosition, shouldNormalize).contains(text, true) || it.websites.any { it.contains(text, true) } } as ArrayList Contact.sorting = config.sorting Contact.startWithSurname = config.startNameWithSurname filtered.sort() - filtered.sortBy { !it.getFullName().startsWith(text, true) } + filtered.sortBy { !getProperText(it.getFullName(), shouldNormalize).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, text) + updateItems(filtered, text.normalizeString()) } } + private fun getProperText(text: String, shouldNormalize: Boolean) = if (shouldNormalize) text.normalizeString() else text + fun onSearchOpened() { contactsIgnoringSearch = (fragment_list?.adapter as? ContactsAdapter)?.contactItems ?: ArrayList() } fun onSearchClosed() { (fragment_list.adapter as? ContactsAdapter)?.updateItems(contactsIgnoringSearch) + setupViewVisibility(contactsIgnoringSearch) + if (this is FavoritesFragment) { fragment_placeholder.text = activity?.getString(R.string.no_favorites) } @@ -272,6 +286,12 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) fragment_placeholder_2.setTextColor(context.getAdjustedPrimaryColor()) } + private fun setupViewVisibility(contacts: ArrayList) { + fragment_placeholder_2.beVisibleIf(contacts.isEmpty()) + fragment_placeholder.beVisibleIf(contacts.isEmpty()) + fragment_list.beVisibleIf(contacts.isNotEmpty()) + } + abstract fun fabClicked() abstract fun placeholderClicked() diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/RecentsFragment.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/RecentsFragment.kt new file mode 100644 index 00000000..c1319997 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/RecentsFragment.kt @@ -0,0 +1,79 @@ +package com.simplemobiletools.contacts.fragments + +import android.content.Context +import android.content.Intent +import android.util.AttributeSet +import com.simplemobiletools.commons.extensions.beVisibleIf +import com.simplemobiletools.commons.extensions.hasPermission +import com.simplemobiletools.commons.extensions.isActivityDestroyed +import com.simplemobiletools.commons.helpers.PERMISSION_READ_CALL_LOG +import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_CALL_LOG +import com.simplemobiletools.contacts.activities.EditContactActivity +import com.simplemobiletools.contacts.adapters.RecentCallsAdapter +import com.simplemobiletools.contacts.extensions.contactClicked +import com.simplemobiletools.contacts.helpers.IS_FROM_SIMPLE_CONTACTS +import com.simplemobiletools.contacts.helpers.KEY_PHONE +import com.simplemobiletools.contacts.helpers.PHONE_NUMBER_PATTERN +import com.simplemobiletools.contacts.helpers.RECENTS_TAB_MASK +import com.simplemobiletools.contacts.models.Contact +import com.simplemobiletools.contacts.models.RecentCall +import kotlinx.android.synthetic.main.fragment_layout.view.* + +class RecentsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet) { + override fun fabClicked() {} + + override fun placeholderClicked() { + activity!!.handlePermission(PERMISSION_WRITE_CALL_LOG) { + if (it) { + activity!!.handlePermission(PERMISSION_READ_CALL_LOG) { + activity?.refreshContacts(RECENTS_TAB_MASK) + } + } + } + } + + fun updateRecentCalls(recentCalls: ArrayList) { + if (activity == null || activity!!.isActivityDestroyed()) { + return + } + + fragment_placeholder.beVisibleIf(recentCalls.isEmpty()) + fragment_placeholder_2.beVisibleIf(recentCalls.isEmpty() && !activity!!.hasPermission(PERMISSION_WRITE_CALL_LOG)) + fragment_list.beVisibleIf(recentCalls.isNotEmpty()) + + val currAdapter = fragment_list.adapter + if (currAdapter == null) { + RecentCallsAdapter(activity!!, recentCalls, activity, fragment_list, fragment_fastscroller) { + val recentCall = (it as RecentCall).number.replace(PHONE_NUMBER_PATTERN.toRegex(), "") + var selectedContact: Contact? = null + for (contact in allContacts) { + if (contact.phoneNumbers.any { it.value.replace(PHONE_NUMBER_PATTERN.toRegex(), "") == recentCall }) { + selectedContact = contact + break + } + } + + if (selectedContact != null) { + activity?.contactClicked(selectedContact) + } else { + Intent(context, EditContactActivity::class.java).apply { + action = Intent.ACTION_INSERT + putExtra(KEY_PHONE, recentCall) + putExtra(IS_FROM_SIMPLE_CONTACTS, true) + context.startActivity(this) + } + } + }.apply { + addVerticalDividers(true) + fragment_list.adapter = this + } + + fragment_fastscroller.setViews(fragment_list) { + val item = (fragment_list.adapter as RecentCallsAdapter).recentCalls.getOrNull(it) + fragment_fastscroller.updateBubbleText(item?.name ?: item?.number ?: "") + } + } else { + (currAdapter as RecentCallsAdapter).updateItems(recentCalls) + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Config.kt index de1bb860..2370665b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Config.kt @@ -48,4 +48,12 @@ class Config(context: Context) : BaseConfig(context) { var filterDuplicates: Boolean get() = prefs.getBoolean(FILTER_DUPLICATES, true) set(filterDuplicates) = prefs.edit().putBoolean(FILTER_DUPLICATES, filterDuplicates).apply() + + var showTabs: Int + get() = prefs.getInt(SHOW_TABS, ALL_TABS_MASK) + set(showTabs) = prefs.edit().putInt(SHOW_TABS, showTabs).apply() + + var showCallConfirmation: Boolean + get() = prefs.getBoolean(SHOW_CALL_CONFIRMATION, false) + set(showCallConfirmation) = prefs.edit().putBoolean(SHOW_CALL_CONFIRMATION, showCallConfirmation).apply() } 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 5409d2dd..0f56d22b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Constants.kt @@ -12,23 +12,39 @@ const val LOCAL_ACCOUNT_NAME = "local_account_name" const val LOCAL_ACCOUNT_TYPE = "local_account_type" const val ON_CONTACT_CLICK = "on_contact_click" const val SHOW_CONTACT_FIELDS = "show_contact_fields" +const val SHOW_TABS = "show_tabs" const val FILTER_DUPLICATES = "filter_duplicates" +const val SHOW_CALL_CONFIRMATION = "show_call_confirmation" const val CONTACT_ID = "contact_id" const val SMT_PRIVATE = "smt_private" // used at the contact source of local contacts hidden from other apps const val IS_PRIVATE = "is_private" const val GROUP = "group" const val FIRST_GROUP_ID = 10000 +const val PHONE_NUMBER_PATTERN = "\\D+" +const val IS_FROM_SIMPLE_CONTACTS = "is_from_simple_contacts" + +// extras used at third party intents +const val KEY_PHONE = "phone" +const val KEY_NAME = "name" const val LOCATION_CONTACTS_TAB = 0 const val LOCATION_FAVORITES_TAB = 1 -const val LOCATION_GROUPS_TAB = 2 -const val LOCATION_GROUP_CONTACTS = 3 +const val LOCATION_RECENTS_TAB = 2 +const val LOCATION_GROUPS_TAB = 3 +const val LOCATION_GROUP_CONTACTS = 4 const val CONTACTS_TAB_MASK = 1 const val FAVORITES_TAB_MASK = 2 -const val GROUPS_TAB_MASK = 4 -const val ALL_TABS_MASK = 7 +const val RECENTS_TAB_MASK = 4 +const val GROUPS_TAB_MASK = 8 +const val ALL_TABS_MASK = 15 + +val tabsList = arrayListOf(CONTACTS_TAB_MASK, + FAVORITES_TAB_MASK, + RECENTS_TAB_MASK, + GROUPS_TAB_MASK +) // contact photo changes const val PHOTO_ADDED = 1 @@ -36,25 +52,6 @@ const val PHOTO_REMOVED = 2 const val PHOTO_CHANGED = 3 const val PHOTO_UNCHANGED = 4 -// export/import -const val BEGIN_VCARD = "BEGIN:VCARD" -const val END_VCARD = "END:VCARD" -const val N = "N" -const val TEL = "TEL" -const val BDAY = "BDAY:" -const val ANNIVERSARY = "ANNIVERSARY:" -const val PHOTO = "PHOTO" -const val EMAIL = "EMAIL" -const val ADR = "ADR" -const val NOTE = "NOTE:" -const val ORG = "ORG:" -const val TITLE = "TITLE:" -const val URL = "URL:" -const val ENCODING = "ENCODING" -const val BASE64 = "BASE64" -const val JPEG = "JPEG" -const val VERSION_2_1 = "VERSION:2.1" - // phone number/email types const val CELL = "CELL" const val WORK = "WORK" @@ -66,7 +63,6 @@ const val WORK_FAX = "WORK;FAX" const val HOME_FAX = "HOME;FAX" const val PAGER = "PAGER" const val MOBILE = "MOBILE" -const val VOICE = "VOICE" const val ON_CLICK_CALL_CONTACT = 1 const val ON_CLICK_VIEW_CONTACT = 2 @@ -87,6 +83,7 @@ const val SHOW_ORGANIZATION_FIELD = 1024 const val SHOW_GROUPS_FIELD = 2048 const val SHOW_CONTACT_SOURCE_FIELD = 4096 const val SHOW_WEBSITES_FIELD = 8192 +const val SHOW_NICKNAME_FIELD = 16384 const val DEFAULT_EMAIL_TYPE = CommonDataKinds.Email.TYPE_HOME const val DEFAULT_PHONE_NUMBER_TYPE = CommonDataKinds.Phone.TYPE_MOBILE 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 e52181fa..17d26c16 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/ContactsHelper.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/ContactsHelper.kt @@ -1,26 +1,29 @@ package com.simplemobiletools.contacts.helpers +import android.accounts.Account import android.accounts.AccountManager +import android.annotation.SuppressLint import android.app.Activity import android.content.* import android.database.Cursor import android.graphics.Bitmap import android.net.Uri +import android.provider.CallLog import android.provider.ContactsContract import android.provider.ContactsContract.CommonDataKinds +import android.provider.ContactsContract.CommonDataKinds.Nickname import android.provider.ContactsContract.CommonDataKinds.Note import android.provider.MediaStore import android.text.TextUtils import android.util.SparseArray import com.simplemobiletools.commons.extensions.* -import com.simplemobiletools.commons.helpers.SORT_BY_FIRST_NAME -import com.simplemobiletools.commons.helpers.SORT_BY_MIDDLE_NAME -import com.simplemobiletools.commons.helpers.SORT_BY_SURNAME -import com.simplemobiletools.commons.helpers.SORT_DESCENDING +import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.contacts.R import com.simplemobiletools.contacts.extensions.* import com.simplemobiletools.contacts.models.* import com.simplemobiletools.contacts.overloads.times +import java.text.SimpleDateFormat +import java.util.* class ContactsHelper(val activity: Activity) { private val BATCH_SIZE = 100 @@ -61,6 +64,33 @@ class ContactsHelper(val activity: Activity) { }.start() } + private fun getContentResolverAccounts(): HashSet { + val uri = ContactsContract.Data.CONTENT_URI + val projection = arrayOf( + ContactsContract.RawContacts.ACCOUNT_NAME, + ContactsContract.RawContacts.ACCOUNT_TYPE + ) + + val sources = HashSet() + var cursor: Cursor? = null + try { + cursor = activity.contentResolver.query(uri, projection, null, null, null) + if (cursor?.moveToFirst() == true) { + do { + val name = cursor.getStringValue(ContactsContract.RawContacts.ACCOUNT_NAME) ?: "" + val type = cursor.getStringValue(ContactsContract.RawContacts.ACCOUNT_TYPE) ?: "" + val source = ContactSource(name, type) + sources.add(source) + } while (cursor.moveToNext()) + } + } catch (e: Exception) { + } finally { + cursor?.close() + } + + return sources + } + private fun getDeviceContacts(contacts: SparseArray) { if (!activity.hasContactPermissions()) { return @@ -83,6 +113,7 @@ class ContactsHelper(val activity: Activity) { val middleName = cursor.getStringValue(CommonDataKinds.StructuredName.MIDDLE_NAME) ?: "" val surname = cursor.getStringValue(CommonDataKinds.StructuredName.FAMILY_NAME) ?: "" val suffix = cursor.getStringValue(CommonDataKinds.StructuredName.SUFFIX) ?: "" + val nickname = "" val photoUri = cursor.getStringValue(CommonDataKinds.StructuredName.PHOTO_URI) ?: "" val number = ArrayList() // proper value is obtained below val emails = ArrayList() @@ -96,8 +127,8 @@ class ContactsHelper(val activity: Activity) { val groups = ArrayList() val organization = Organization("", "") val websites = ArrayList() - val contact = Contact(id, prefix, firstName, middleName, surname, suffix, photoUri, number, emails, addresses, events, - accountName, starred, contactId, thumbnailUri, null, notes, groups, organization, websites) + val contact = Contact(id, prefix, firstName, middleName, surname, suffix, nickname, photoUri, number, emails, addresses, + events, accountName, starred, contactId, thumbnailUri, null, notes, groups, organization, websites) contacts.put(id, contact) } while (cursor.moveToNext()) @@ -115,6 +146,13 @@ class ContactsHelper(val activity: Activity) { contacts[key]?.phoneNumbers = phoneNumbers.valueAt(i) } + val nicknames = getNicknames() + size = nicknames.size() + for (i in 0 until size) { + val key = nicknames.keyAt(i) + contacts[key]?.nickname = nicknames.valueAt(i) + } + val emails = getEmails() size = emails.size() for (i in 0 until size) { @@ -196,6 +234,36 @@ class ContactsHelper(val activity: Activity) { return phoneNumbers } + private fun getNicknames(contactId: Int? = null): SparseArray { + val nicknames = SparseArray() + val uri = ContactsContract.Data.CONTENT_URI + val projection = arrayOf( + ContactsContract.Data.RAW_CONTACT_ID, + Nickname.NAME + ) + + val selection = getSourcesSelection(true, contactId != null) + val selectionArgs = getSourcesSelectionArgs(Nickname.CONTENT_ITEM_TYPE, contactId) + + var cursor: Cursor? = null + try { + cursor = activity.contentResolver.query(uri, projection, selection, selectionArgs, null) + if (cursor?.moveToFirst() == true) { + do { + val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID) + val nickname = cursor.getStringValue(Nickname.NAME) ?: continue + nicknames.put(id, nickname) + } while (cursor.moveToNext()) + } + } catch (e: Exception) { + activity.showErrorToast(e) + } finally { + cursor?.close() + } + + return nicknames + } + private fun getEmails(contactId: Int? = null): SparseArray> { val emails = SparseArray>() val uri = CommonDataKinds.Email.CONTENT_URI @@ -251,7 +319,7 @@ class ContactsHelper(val activity: Activity) { if (cursor?.moveToFirst() == true) { do { val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID) - val address = cursor.getStringValue(CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS) ?: "" + val address = cursor.getStringValue(CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS) ?: continue val type = cursor.getIntValue(CommonDataKinds.StructuredPostal.TYPE) if (addresses[id] == null) { @@ -324,7 +392,7 @@ class ContactsHelper(val activity: Activity) { if (cursor?.moveToFirst() == true) { do { val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID) - val note = cursor.getStringValue(CommonDataKinds.Note.NOTE) ?: continue + val note = cursor.getStringValue(Note.NOTE) ?: continue notes.put(id, note) } while (cursor.moveToNext()) } @@ -355,8 +423,12 @@ class ContactsHelper(val activity: Activity) { if (cursor?.moveToFirst() == true) { do { val id = cursor.getIntValue(ContactsContract.Data.RAW_CONTACT_ID) - val company = cursor.getStringValue(CommonDataKinds.Organization.COMPANY) ?: continue - val title = cursor.getStringValue(CommonDataKinds.Organization.TITLE) ?: continue + val company = cursor.getStringValue(CommonDataKinds.Organization.COMPANY) ?: "" + val title = cursor.getStringValue(CommonDataKinds.Organization.TITLE) ?: "" + if (company.isEmpty() && title.isEmpty()) { + continue + } + val organization = Organization(company, title) organizations.put(id, organization) } while (cursor.moveToNext()) @@ -621,6 +693,7 @@ class ContactsHelper(val activity: Activity) { val middleName = cursor.getStringValue(CommonDataKinds.StructuredName.MIDDLE_NAME) ?: "" val surname = cursor.getStringValue(CommonDataKinds.StructuredName.FAMILY_NAME) ?: "" val suffix = cursor.getStringValue(CommonDataKinds.StructuredName.SUFFIX) ?: "" + val nickname = getNicknames(id)[id] ?: "" val photoUri = cursor.getStringValue(CommonDataKinds.Phone.PHOTO_URI) ?: "" val number = getPhoneNumbers(id)[id] ?: ArrayList() val emails = getEmails(id)[id] ?: ArrayList() @@ -634,8 +707,8 @@ class ContactsHelper(val activity: Activity) { val thumbnailUri = cursor.getStringValue(CommonDataKinds.StructuredName.PHOTO_THUMBNAIL_URI) ?: "" val organization = getOrganizations(id)[id] ?: Organization("", "") val websites = getWebsites(id)[id] ?: ArrayList() - return Contact(id, prefix, firstName, middleName, surname, suffix, photoUri, number, emails, addresses, events, accountName, - starred, contactId, thumbnailUri, null, notes, groups, organization, websites) + return Contact(id, prefix, firstName, middleName, surname, suffix, nickname, photoUri, number, emails, addresses, events, + accountName, starred, contactId, thumbnailUri, null, notes, groups, organization, websites) } } finally { cursor?.close() @@ -662,14 +735,19 @@ class ContactsHelper(val activity: Activity) { return sources } - val accountManager = AccountManager.get(activity) - accountManager.accounts.filter { it.name.contains("@") || localAccountTypes.contains(it.type) }.forEach { + val accounts = AccountManager.get(activity).accounts + accounts.forEach { if (ContentResolver.getIsSyncable(it, ContactsContract.AUTHORITY) == 1) { val contactSource = ContactSource(it.name, it.type) sources.add(contactSource) } } + val contentResolverAccounts = getContentResolverAccounts().filter { + it.name.isNotEmpty() && it.type.isNotEmpty() && !accounts.contains(Account(it.name, it.type)) + } + sources.addAll(contentResolverAccounts) + if (sources.isEmpty() && activity.config.localAccountName.isEmpty() && activity.config.localAccountType.isEmpty()) { sources.add(ContactSource("", "")) } @@ -747,6 +825,22 @@ class ContactsHelper(val activity: Activity) { operations.add(build()) } + // delete nickname + ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI).apply { + val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ? " + val selectionArgs = arrayOf(contact.id.toString(), Nickname.CONTENT_ITEM_TYPE) + withSelection(selection, selectionArgs) + operations.add(build()) + } + + // add nickname + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { + withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.id) + withValue(ContactsContract.Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE) + withValue(Nickname.NAME, contact.nickname) + operations.add(build()) + } + // delete phone numbers ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI).apply { val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ? " @@ -823,20 +917,34 @@ class ContactsHelper(val activity: Activity) { } } - // notes - ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI).apply { - val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ?" + // delete notes + ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI).apply { + val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ? " val selectionArgs = arrayOf(contact.id.toString(), Note.CONTENT_ITEM_TYPE) withSelection(selection, selectionArgs) + operations.add(build()) + } + + // add notes + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { + withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.id) + withValue(ContactsContract.Data.MIMETYPE, Note.CONTENT_ITEM_TYPE) withValue(Note.NOTE, contact.notes) operations.add(build()) } - // organization - ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI).apply { - val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ?" + // delete organization + ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI).apply { + val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ? " val selectionArgs = arrayOf(contact.id.toString(), CommonDataKinds.Organization.CONTENT_ITEM_TYPE) withSelection(selection, selectionArgs) + operations.add(build()) + } + + // add organization + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { + withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.id) + withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Organization.CONTENT_ITEM_TYPE) withValue(CommonDataKinds.Organization.COMPANY, contact.organization.company) withValue(CommonDataKinds.Organization.TYPE, DEFAULT_ORGANIZATION_TYPE) withValue(CommonDataKinds.Organization.TITLE, contact.organization.jobPosition) @@ -960,7 +1068,12 @@ class ContactsHelper(val activity: Activity) { operations.clear() } } - activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + + try { + activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + } catch (e: Exception) { + activity.showErrorToast(e) + } } fun removeContactsFromGroup(contacts: ArrayList, groupId: Long) { @@ -1006,6 +1119,14 @@ class ContactsHelper(val activity: Activity) { operations.add(build()) } + // nickname + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { + withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + withValue(ContactsContract.Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE) + withValue(Nickname.NAME, contact.nickname) + operations.add(build()) + } + // phone numbers contact.phoneNumbers.forEach { ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { @@ -1262,7 +1383,92 @@ class ContactsHelper(val activity: Activity) { } } - activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + if (activity.hasPermission(PERMISSION_WRITE_CONTACTS)) { + activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + } + } catch (e: Exception) { + activity.showErrorToast(e) + } + }.start() + } + + @SuppressLint("MissingPermission") + fun getRecents(callback: (ArrayList) -> Unit) { + Thread { + val calls = ArrayList() + if (!activity.hasPermission(PERMISSION_WRITE_CALL_LOG) || !activity.hasPermission(PERMISSION_READ_CALL_LOG)) { + callback(calls) + return@Thread + } + + val uri = CallLog.Calls.CONTENT_URI + val projection = arrayOf( + CallLog.Calls._ID, + CallLog.Calls.NUMBER, + CallLog.Calls.DATE, + CallLog.Calls.CACHED_NAME + ) + + val sorting = "${CallLog.Calls._ID} DESC LIMIT 100" + val currentDate = Date(System.currentTimeMillis()) + val currentYear = SimpleDateFormat("yyyy", Locale.getDefault()).format(currentDate) + val todayDate = SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(currentDate) + val yesterdayDate = SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(Date(System.currentTimeMillis() - DAY_SECONDS * 1000)) + val yesterday = activity.getString(R.string.yesterday) + var prevNumber = "" + + var cursor: Cursor? = null + try { + cursor = activity.contentResolver.query(uri, projection, null, null, sorting) + if (cursor?.moveToFirst() == true) { + do { + val id = cursor.getIntValue(CallLog.Calls._ID) + val number = cursor.getStringValue(CallLog.Calls.NUMBER) + val date = cursor.getLongValue(CallLog.Calls.DATE) + val name = cursor.getStringValue(CallLog.Calls.CACHED_NAME) + if (number == prevNumber) { + continue + } + + var formattedDate = SimpleDateFormat("dd MMM yyyy, HH:mm", Locale.getDefault()).format(Date(date)) + val datePart = formattedDate.substring(0, 11) + when { + datePart == todayDate -> formattedDate = formattedDate.substring(12) + datePart == yesterdayDate -> formattedDate = yesterday + formattedDate.substring(11) + formattedDate.substring(7, 11) == currentYear -> formattedDate = formattedDate.substring(0, 6) + formattedDate.substring(11) + } + + prevNumber = number + val recentCall = RecentCall(id, number, formattedDate, name) + calls.add(recentCall) + } while (cursor.moveToNext()) + } + } finally { + cursor?.close() + } + callback(calls) + }.start() + } + + fun removeRecentCalls(ids: ArrayList) { + Thread { + try { + val operations = ArrayList() + val selection = "${CallLog.Calls._ID} = ?" + ids.forEach { + ContentProviderOperation.newDelete(CallLog.Calls.CONTENT_URI).apply { + val selectionArgs = arrayOf(it.toString()) + withSelection(selection, selectionArgs) + operations.add(build()) + } + + if (operations.size % BATCH_SIZE == 0) { + activity.contentResolver.applyBatch(CallLog.AUTHORITY, operations) + operations.clear() + } + } + + activity.contentResolver.applyBatch(CallLog.AUTHORITY, operations) } catch (e: Exception) { activity.showErrorToast(e) } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/DBHelper.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/DBHelper.kt index 63690935..860b3698 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/DBHelper.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/DBHelper.kt @@ -28,6 +28,7 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont private val COL_MIDDLE_NAME = "middle_name" private val COL_SURNAME = "surname" private val COL_SUFFIX = "suffix" + private val COL_NICKNAME = "nickname" private val COL_PHOTO = "photo" private val COL_PHONE_NUMBERS = "phone_numbers" private val COL_EMAILS = "emails" @@ -48,9 +49,9 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont private val mDb = writableDatabase companion object { - private const val DB_VERSION = 5 const val DB_NAME = "contacts.db" - var dbInstance: DBHelper? = null + private const val DB_VERSION = 6 + private var dbInstance: DBHelper? = null var gson = Gson() fun newInstance(context: Context): DBHelper { @@ -65,7 +66,7 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont db.execSQL("CREATE TABLE $CONTACTS_TABLE_NAME ($COL_ID INTEGER PRIMARY KEY AUTOINCREMENT, $COL_FIRST_NAME TEXT, $COL_MIDDLE_NAME TEXT, " + "$COL_SURNAME TEXT, $COL_PHOTO BLOB, $COL_PHONE_NUMBERS TEXT, $COL_EMAILS TEXT, $COL_EVENTS TEXT, $COL_STARRED INTEGER, " + "$COL_ADDRESSES TEXT, $COL_NOTES TEXT, $COL_GROUPS TEXT, $COL_PREFIX TEXT, $COL_SUFFIX TEXT, $COL_COMPANY TEXT, $COL_JOB_POSITION TEXT," + - "$COL_WEBSITES TEXT)") + "$COL_WEBSITES TEXT, $COL_NICKNAME TEXT)") // start autoincrement ID from FIRST_CONTACT_ID to avoid conflicts db.execSQL("REPLACE INTO sqlite_sequence (name, seq) VALUES ('$CONTACTS_TABLE_NAME', $FIRST_CONTACT_ID)") @@ -94,6 +95,10 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont if (oldVersion < 5) { db.execSQL("ALTER TABLE $CONTACTS_TABLE_NAME ADD COLUMN $COL_WEBSITES TEXT DEFAULT ''") } + + if (oldVersion < 6) { + db.execSQL("ALTER TABLE $CONTACTS_TABLE_NAME ADD COLUMN $COL_NICKNAME TEXT DEFAULT ''") + } } private fun createGroupsTable(db: SQLiteDatabase) { @@ -135,6 +140,7 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont put(COL_MIDDLE_NAME, contact.middleName) put(COL_SURNAME, contact.surname) put(COL_SUFFIX, contact.suffix) + put(COL_NICKNAME, contact.nickname) put(COL_PHONE_NUMBERS, gson.toJson(contact.phoneNumbers)) put(COL_EMAILS, gson.toJson(contact.emails)) put(COL_ADDRESSES, gson.toJson(contact.addresses)) @@ -252,8 +258,8 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont fun getContacts(activity: Activity, selection: String? = null, selectionArgs: Array? = null): ArrayList { val storedGroups = ContactsHelper(activity).getStoredGroups() val contacts = ArrayList() - val projection = arrayOf(COL_ID, COL_PREFIX, COL_FIRST_NAME, COL_MIDDLE_NAME, COL_SURNAME, COL_SUFFIX, COL_PHONE_NUMBERS, COL_EMAILS, - COL_EVENTS, COL_STARRED, COL_PHOTO, COL_ADDRESSES, COL_NOTES, COL_GROUPS, COL_COMPANY, COL_JOB_POSITION, COL_WEBSITES) + val projection = arrayOf(COL_ID, COL_PREFIX, COL_FIRST_NAME, COL_MIDDLE_NAME, COL_SURNAME, COL_SUFFIX, COL_NICKNAME, COL_PHONE_NUMBERS, + COL_EMAILS, COL_EVENTS, COL_STARRED, COL_PHOTO, COL_ADDRESSES, COL_NOTES, COL_GROUPS, COL_COMPANY, COL_JOB_POSITION, COL_WEBSITES) val phoneNumbersToken = object : TypeToken>() {}.type val emailsToken = object : TypeToken>() {}.type @@ -271,6 +277,7 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont val middleName = cursor.getStringValue(COL_MIDDLE_NAME) val surname = cursor.getStringValue(COL_SURNAME) val suffix = cursor.getStringValue(COL_SUFFIX) + val nickname = cursor.getStringValue(COL_NICKNAME) val phoneNumbersJson = cursor.getStringValue(COL_PHONE_NUMBERS) val phoneNumbers = if (phoneNumbersJson == "[]") ArrayList() else gson.fromJson>(phoneNumbersJson, phoneNumbersToken) @@ -290,7 +297,11 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont val photoByteArray = cursor.getBlobValue(COL_PHOTO) ?: null val photo = if (photoByteArray?.isNotEmpty() == true) { - BitmapFactory.decodeByteArray(photoByteArray, 0, photoByteArray.size) + try { + BitmapFactory.decodeByteArray(photoByteArray, 0, photoByteArray.size) + } catch (e: OutOfMemoryError) { + null + } } else { null } @@ -311,8 +322,8 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont val websites = if (websitesJson == "[]") ArrayList() else gson.fromJson>(websitesJson, websitesToken) ?: ArrayList(1) - val contact = Contact(id, prefix, firstName, middleName, surname, suffix, "", phoneNumbers, emails, addresses, events, - SMT_PRIVATE, starred, id, "", photo, notes, groups, organization, websites) + val contact = Contact(id, prefix, firstName, middleName, surname, suffix, nickname, "", phoneNumbers, emails, addresses, + events, SMT_PRIVATE, starred, id, "", photo, notes, groups, organization, websites) contacts.add(contact) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfExporter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfExporter.kt index 88f077f3..4832c2ac 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfExporter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfExporter.kt @@ -1,22 +1,30 @@ 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.* +import com.simplemobiletools.commons.extensions.getFileOutputStream +import com.simplemobiletools.commons.extensions.showErrorToast +import com.simplemobiletools.commons.extensions.toFileDirItem +import com.simplemobiletools.commons.extensions.toast import com.simplemobiletools.contacts.R +import com.simplemobiletools.contacts.extensions.getByteArray +import com.simplemobiletools.contacts.extensions.getDateTimeFromDateString import com.simplemobiletools.contacts.helpers.VcfExporter.ExportResult.EXPORT_FAIL import com.simplemobiletools.contacts.models.Contact -import java.io.BufferedWriter -import java.io.ByteArrayOutputStream +import ezvcard.Ezvcard +import ezvcard.VCard +import ezvcard.parameter.AddressType +import ezvcard.parameter.EmailType +import ezvcard.parameter.ImageType +import ezvcard.parameter.TelephoneType +import ezvcard.property.* +import ezvcard.util.PartialDate import java.io.File +import java.util.* class VcfExporter { - private val ENCODED_PHOTO_LINE_LENGTH = 74 - enum class ExportResult { EXPORT_FAIL, EXPORT_OK, EXPORT_PARTIAL } @@ -36,61 +44,93 @@ class VcfExporter { activity.toast(R.string.exporting) } - it.bufferedWriter().use { out -> - for (contact in contacts) { - out.writeLn(BEGIN_VCARD) - out.writeLn(VERSION_2_1) - out.writeLn("$N${getNames(contact)}") + val cards = ArrayList() + for (contact in contacts) { + val card = VCard() + StructuredName().apply { + prefixes.add(contact.prefix) + given = contact.firstName + additionalNames.add(contact.middleName) + family = contact.surname + suffixes.add(contact.suffix) + card.structuredName = this + } - contact.phoneNumbers.forEach { - out.writeLn("$TEL;${getPhoneNumberLabel(it.type)}:${it.value}") - } + if (contact.nickname.isNotEmpty()) { + card.setNickname(contact.nickname) + } - contact.emails.forEach { - val type = getEmailTypeLabel(it.type) - val delimiterType = if (type.isEmpty()) "" else ";$type" - out.writeLn("$EMAIL$delimiterType:${it.value}") - } + contact.phoneNumbers.forEach { + val phoneNumber = Telephone(it.value) + phoneNumber.types.add(TelephoneType.find(getPhoneNumberLabel(it.type))) + card.addTelephoneNumber(phoneNumber) + } - contact.addresses.forEach { - val type = getAddressTypeLabel(it.type) - val delimiterType = if (type.isEmpty()) "" else ";$type" - out.writeLn("$ADR$delimiterType:;;${it.value.replace("\n", "\\n")};;;;") - } + contact.emails.forEach { + val email = Email(it.value) + email.types.add(EmailType.find(getEmailTypeLabel(it.type))) + card.addEmail(email) + } - contact.events.forEach { - if (it.type == CommonDataKinds.Event.TYPE_BIRTHDAY) { - out.writeLn("$BDAY${it.value}") + contact.events.forEach { + if (it.type == CommonDataKinds.Event.TYPE_BIRTHDAY || it.type == CommonDataKinds.Event.TYPE_ANNIVERSARY) { + val dateTime = it.value.getDateTimeFromDateString() + if (it.value.startsWith("--")) { + val partialDate = PartialDate.builder().year(null).month(dateTime.monthOfYear - 1).date(dateTime.dayOfMonth).build() + if (it.type == CommonDataKinds.Event.TYPE_BIRTHDAY) { + card.birthdays.add(Birthday(partialDate)) + } else { + card.anniversaries.add(Anniversary(partialDate)) + } + } else { + Calendar.getInstance().apply { + clear() + set(Calendar.YEAR, dateTime.year) + set(Calendar.MONTH, dateTime.monthOfYear - 1) + set(Calendar.DAY_OF_MONTH, dateTime.dayOfMonth) + if (it.type == CommonDataKinds.Event.TYPE_BIRTHDAY) { + card.birthdays.add(Birthday(time)) + } else { + card.anniversaries.add(Anniversary(time)) + } + } } } - - if (contact.notes.isNotEmpty()) { - out.writeLn("$NOTE${contact.notes.replace("\n", "\\n")}") - } - - if (!contact.organization.isEmpty()) { - out.writeLn("$ORG${contact.organization.company.replace("\n", "\\n")}") - out.writeLn("$TITLE${contact.organization.jobPosition.replace("\n", "\\n")}") - } - - contact.websites.forEach { - out.writeLn("$URL$it") - } - - if (contact.thumbnailUri.isNotEmpty()) { - val bitmap = MediaStore.Images.Media.getBitmap(activity.contentResolver, Uri.parse(contact.thumbnailUri)) - addBitmap(bitmap, out) - } - - if (contact.photo != null) { - addBitmap(contact.photo!!, out) - } - - out.writeLn(END_VCARD) - contactsExported++ } + + contact.addresses.forEach { + val address = Address() + address.streetAddress = it.value + address.types.add(AddressType.find(getAddressTypeLabel(it.type))) + card.addAddress(address) + } + + if (contact.notes.isNotEmpty()) { + card.addNote(contact.notes) + } + + if (!contact.organization.isEmpty()) { + val organization = Organization() + organization.values.add(contact.organization.company) + card.organization = organization + card.titles.add(Title(contact.organization.jobPosition)) + } + + contact.websites.forEach { + card.addUrl(it) + } + + if (contact.thumbnailUri.isNotEmpty()) { + val photoByteArray = MediaStore.Images.Media.getBitmap(activity.contentResolver, Uri.parse(contact.thumbnailUri)).getByteArray() + val photo = Photo(photoByteArray, ImageType.JPEG) + card.addPhoto(photo) + } + + cards.add(card) + contactsExported++ } + Ezvcard.write(cards).go(file) } catch (e: Exception) { activity.showErrorToast(e) } @@ -103,71 +143,24 @@ class VcfExporter { } } - private fun addBitmap(bitmap: Bitmap, out: BufferedWriter) { - val firstLine = "$PHOTO;$ENCODING=$BASE64;$JPEG:" - 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("") - } - - private fun getNames(contact: Contact): String { - var result = "" - var firstName = contact.firstName - var surName = contact.surname - var middleName = contact.middleName - var prefix = contact.prefix - var suffix = contact.suffix - - if (QuotedPrintable.urlEncode(firstName) != firstName - || QuotedPrintable.urlEncode(surName) != surName - || QuotedPrintable.urlEncode(middleName) != middleName - || QuotedPrintable.urlEncode(prefix) != prefix - || QuotedPrintable.urlEncode(suffix) != suffix) { - firstName = QuotedPrintable.encode(firstName) - surName = QuotedPrintable.encode(surName) - middleName = QuotedPrintable.encode(middleName) - prefix = QuotedPrintable.encode(prefix) - suffix = QuotedPrintable.encode(suffix) - result += ";CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE" - } - - return "$result:$surName;$firstName;$middleName;$prefix;$suffix" - } - 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 + else -> HOME } private fun getEmailTypeLabel(type: Int) = when (type) { - CommonDataKinds.Email.TYPE_HOME -> HOME CommonDataKinds.Email.TYPE_WORK -> WORK CommonDataKinds.Email.TYPE_MOBILE -> MOBILE - else -> "" + else -> HOME } private fun getAddressTypeLabel(type: Int) = when (type) { - CommonDataKinds.StructuredPostal.TYPE_HOME -> HOME CommonDataKinds.StructuredPostal.TYPE_WORK -> WORK - else -> "" + else -> HOME } } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfImporter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfImporter.kt index 85561636..d247b0ba 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfImporter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfImporter.kt @@ -3,7 +3,6 @@ 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 @@ -11,40 +10,19 @@ import com.simplemobiletools.contacts.extensions.getCachePhoto import com.simplemobiletools.contacts.extensions.getCachePhotoUri import com.simplemobiletools.contacts.helpers.VcfImporter.ImportResult.* import com.simplemobiletools.contacts.models.* +import ezvcard.Ezvcard +import org.joda.time.DateTime +import org.joda.time.format.DateTimeFormat import java.io.File import java.io.FileOutputStream +import java.util.* class VcfImporter(val activity: SimpleActivity) { enum class ImportResult { IMPORT_FAIL, IMPORT_OK, IMPORT_PARTIAL } - private var curPrefix = "" - private var curFirstName = "" - private var curMiddleName = "" - private var curSurname = "" - private var curSuffix = "" - private var curPhotoUri = "" - private var curNotes = "" - private var curCompany = "" - private var curJobPosition = "" - private var curPhoneNumbers = ArrayList() - private var curEmails = ArrayList() - private var curEvents = ArrayList() - private var curAddresses = ArrayList
() - private var curGroups = ArrayList() - private var curWebsites = ArrayList() - - private var isGettingPhoto = false - private var currentPhotoString = StringBuilder() - private var currentPhotoCompressionFormat = Bitmap.CompressFormat.JPEG - - private var isGettingName = false - private var currentNameIsANSI = false - private var currentNameString = StringBuilder() - - private var isGettingNotes = false - private var currentNotesSB = StringBuilder() + private val PATTERN = "EEE MMM dd HH:mm:ss 'GMT'ZZ YYYY" private var contactsImported = 0 private var contactsFailed = 0 @@ -57,45 +35,70 @@ class VcfImporter(val activity: SimpleActivity) { activity.assets.open(path) } - inputStream.bufferedReader().use { - while (true) { - val line = it.readLine() ?: break - if (line.trim().isEmpty()) { - if (isGettingPhoto) { - savePhoto() - isGettingPhoto = false - } - continue - } else if (line.startsWith('\t') && isGettingName) { - currentNameString.append(line.trimStart('\t')) - isGettingName = false - parseNames() - } else if (isGettingNotes) { - if (line.startsWith(' ')) { - currentNotesSB.append(line.substring(1)) - } else { - curNotes = currentNotesSB.toString().replace("\\n", "\n").replace("\\,", ",") - isGettingNotes = false - } - } + val ezContacts = Ezvcard.parse(inputStream).all() + for (ezContact in ezContacts) { + val structuredName = ezContact.structuredName + val prefix = structuredName?.prefixes?.firstOrNull() ?: "" + val firstName = structuredName?.given ?: "" + val middleName = structuredName?.additionalNames?.firstOrNull() ?: "" + val surname = structuredName?.family ?: "" + val suffix = structuredName?.suffixes?.firstOrNull() ?: "" + val nickname = ezContact.nickname?.values?.firstOrNull() ?: "" + val photoUri = "" - when { - line.toUpperCase() == BEGIN_VCARD -> resetValues() - line.toUpperCase().startsWith(NOTE) -> addNotes(line.substring(NOTE.length)) - line.toUpperCase().startsWith(N) -> addNames(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(ADR) -> addAddress(line.substring(ADR.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().startsWith(ORG) -> addCompany(line.substring(ORG.length)) - line.toUpperCase().startsWith(TITLE) -> addJobPosition(line.substring(TITLE.length)) - line.toUpperCase().startsWith(URL) -> addWebsite(line.substring(URL.length)) - line.toUpperCase() == END_VCARD -> saveContact(targetContactSource) - isGettingPhoto -> currentPhotoString.append(line.trim()) + val phoneNumbers = ArrayList() + ezContact.telephoneNumbers.forEach { + val type = getPhoneNumberTypeId(it.types.firstOrNull()?.value ?: MOBILE) + val number = it.text + phoneNumbers.add(PhoneNumber(number, type)) + } + + val emails = ArrayList() + ezContact.emails.forEach { + val type = getEmailTypeId(it.types.firstOrNull()?.value ?: HOME) + val email = it.value + emails.add(Email(email, type)) + } + + val addresses = ArrayList
() + ezContact.addresses.forEach { + val type = getAddressTypeId(it.types.firstOrNull()?.value ?: HOME) + val address = it.streetAddress + if (address?.isNotEmpty() == true) { + addresses.add(Address(address, type)) } } + + val events = ArrayList() + ezContact.birthdays.forEach { + val event = Event(formatDateToDayCode(it.date), CommonDataKinds.Event.TYPE_BIRTHDAY) + events.add(event) + } + + ezContact.anniversaries.forEach { + val event = Event(formatDateToDayCode(it.date), CommonDataKinds.Event.TYPE_ANNIVERSARY) + events.add(event) + } + + val starred = 0 + val contactId = 0 + val notes = ezContact.notes.firstOrNull()?.value ?: "" + val groups = ArrayList() + val company = ezContact.organization?.values?.firstOrNull() ?: "" + val jobPosition = ezContact.titles?.firstOrNull()?.value ?: "" + val organization = Organization(company, jobPosition) + val websites = ezContact.urls.map { it.value } as ArrayList + + val photoData = ezContact.photos.firstOrNull()?.data + val photo = null + val thumbnailUri = savePhoto(photoData) + + val contact = Contact(0, prefix, firstName, middleName, surname, suffix, nickname, photoUri, phoneNumbers, emails, addresses, events, + targetContactSource, starred, contactId, thumbnailUri, photo, notes, groups, organization, websites) + + if (ContactsHelper(activity).insertContact(contact)) { + contactsImported++ + } } } catch (e: Exception) { activity.showErrorToast(e, Toast.LENGTH_LONG) @@ -109,188 +112,51 @@ class VcfImporter(val activity: SimpleActivity) { } } - private fun addNames(names: String) { - val parts = names.split(":") - currentNameIsANSI = parts.first().toUpperCase().contains("QUOTED-PRINTABLE") - currentNameString.append(parts[1].trimEnd('=')) - if (!isGettingName && currentNameIsANSI && names.endsWith('=')) { - isGettingName = true - } else { - parseNames() - } + private fun formatDateToDayCode(date: Date): String { + val dateTime = DateTime.parse(date.toString(), DateTimeFormat.forPattern(PATTERN)) + return dateTime.toString("yyyy-MM-dd") } - private fun parseNames() { - val nameParts = currentNameString.split(";") - curSurname = if (currentNameIsANSI) QuotedPrintable.decode(nameParts[0]) else nameParts[0] - curFirstName = if (currentNameIsANSI) QuotedPrintable.decode(nameParts[1]) else nameParts[1] - if (nameParts.size > 2) { - curMiddleName = if (currentNameIsANSI) QuotedPrintable.decode(nameParts[2]) else nameParts[2] - curPrefix = if (currentNameIsANSI) QuotedPrintable.decode(nameParts[3]) else nameParts[3] - curSuffix = if (currentNameIsANSI) QuotedPrintable.decode(nameParts[4]) else nameParts[4] - } - } - - 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) { + private fun getPhoneNumberTypeId(type: String) = when (type.toUpperCase()) { 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 + FAX -> CommonDataKinds.Phone.TYPE_FAX_WORK 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) { + private fun getEmailTypeId(type: String) = when (type.toUpperCase()) { HOME -> CommonDataKinds.Email.TYPE_HOME WORK -> CommonDataKinds.Email.TYPE_WORK MOBILE -> CommonDataKinds.Email.TYPE_MOBILE else -> CommonDataKinds.Email.TYPE_OTHER } - private fun addAddress(address: String) { - val addressParts = address.trimStart(';').split(":") - var rawType = addressParts[0] - if (rawType.contains('=')) { - rawType = rawType.split('=').last() - } - val type = getAddressTypeId(rawType.toUpperCase()) - val addresses = addressParts[1].split(";") - if (addresses.size == 7) { - curAddresses.add(Address(addresses[2].replace("\\n", "\n"), type)) - } - } - - private fun getAddressTypeId(type: String) = when (type) { + private fun getAddressTypeId(type: String) = when (type.toUpperCase()) { HOME -> CommonDataKinds.Email.TYPE_HOME WORK -> CommonDataKinds.Email.TYPE_WORK 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 savePhoto(byteArray: ByteArray?): String { + if (byteArray == null) { + return "" } - } - 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) + val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) var fileOutputStream: FileOutputStream? = null try { fileOutputStream = FileOutputStream(file) - bitmap.compress(currentPhotoCompressionFormat, 100, fileOutputStream) + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream) } finally { fileOutputStream?.close() } - curPhotoUri = activity.getCachePhotoUri(file).toString() - } - - private fun addNotes(notes: String) { - currentNotesSB.append(notes) - isGettingNotes = true - } - - private fun addCompany(company: String) { - curCompany = company - } - - private fun addJobPosition(jobPosition: String) { - curJobPosition = jobPosition - } - - private fun addWebsite(website: String) { - curWebsites.add(website) - } - - private fun saveContact(source: String) { - val organization = Organization(curCompany, curJobPosition) - val contact = Contact(0, curPrefix, curFirstName, curMiddleName, curSurname, curSuffix, curPhotoUri, curPhoneNumbers, curEmails, curAddresses, curEvents, - source, 0, 0, "", null, curNotes, curGroups, organization, curWebsites) - if (ContactsHelper(activity).insertContact(contact)) { - contactsImported++ - } - } - - private fun resetValues() { - curPrefix = "" - curFirstName = "" - curMiddleName = "" - curSurname = "" - curSuffix = "" - curPhotoUri = "" - curNotes = "" - curCompany = "" - curJobPosition = "" - curPhoneNumbers = ArrayList() - curEmails = ArrayList() - curEvents = ArrayList() - curAddresses = ArrayList() - curGroups = ArrayList() - curWebsites = ArrayList() - - isGettingPhoto = false - currentPhotoString = StringBuilder() - currentPhotoCompressionFormat = Bitmap.CompressFormat.JPEG - - isGettingName = false - currentNameIsANSI = false - currentNameString = StringBuilder() - - isGettingNotes = false - currentNotesSB = StringBuilder() + return activity.getCachePhotoUri(file).toString() } } 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 5e16348a..6a18a265 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/models/Contact.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/models/Contact.kt @@ -1,39 +1,48 @@ package com.simplemobiletools.contacts.models import android.graphics.Bitmap +import com.simplemobiletools.commons.extensions.normalizeString import com.simplemobiletools.commons.helpers.SORT_BY_FIRST_NAME import com.simplemobiletools.commons.helpers.SORT_BY_MIDDLE_NAME import com.simplemobiletools.commons.helpers.SORT_DESCENDING +import com.simplemobiletools.contacts.helpers.PHONE_NUMBER_PATTERN -data class Contact(val id: Int, var prefix: String, var firstName: String, var middleName: String, var surname: String, var suffix: String, var photoUri: String, - var phoneNumbers: ArrayList, var emails: ArrayList, var addresses: ArrayList
, var events: ArrayList, - var source: String, var starred: Int, val contactId: Int, val thumbnailUri: String, var photo: Bitmap?, var notes: String, +data class Contact(val id: Int, var prefix: String, var firstName: String, var middleName: String, var surname: String, var suffix: String, var nickname: String, + var photoUri: String, var phoneNumbers: ArrayList, var emails: ArrayList, var addresses: ArrayList
, + var events: ArrayList, var source: String, var starred: Int, val contactId: Int, val thumbnailUri: String, var photo: Bitmap?, var notes: String, var groups: ArrayList, var organization: Organization, var websites: ArrayList) : Comparable { companion object { var sorting = 0 var startWithSurname = false - val pattern = "\\D+".toRegex() } override fun compareTo(other: Contact): Int { - val firstString: String - val secondString: String + var firstString: String + var secondString: String when { sorting and SORT_BY_FIRST_NAME != 0 -> { - firstString = firstName - secondString = other.firstName + firstString = firstName.normalizeString() + secondString = other.firstName.normalizeString() } sorting and SORT_BY_MIDDLE_NAME != 0 -> { - firstString = middleName - secondString = other.middleName + firstString = middleName.normalizeString() + secondString = other.middleName.normalizeString() } else -> { - firstString = surname - secondString = other.surname + firstString = surname.normalizeString() + secondString = other.surname.normalizeString() } } + if (firstString.isEmpty() && firstName.isEmpty() && middleName.isEmpty() && surname.isEmpty() && organization.company.isNotEmpty()) { + firstString = organization.company.normalizeString() + } + + if (secondString.isEmpty() && other.firstName.isEmpty() && other.middleName.isEmpty() && other.surname.isEmpty() && other.organization.company.isNotEmpty()) { + secondString = other.organization.company.normalizeString() + } + var result = if (firstString.firstOrNull()?.isLetter() == true && secondString.firstOrNull()?.isLetter() == false) { -1 } else if (firstString.firstOrNull()?.isLetter() == false && secondString.firstOrNull()?.isLetter() == true) { @@ -45,9 +54,9 @@ data class Contact(val id: Int, var prefix: String, var firstName: String, var m -1 } else { if (firstString.toLowerCase() == secondString.toLowerCase()) { - getFullName().compareTo(other.getFullName()) + getFullName().compareTo(other.getFullName(), true) } else { - firstString.toLowerCase().compareTo(secondString.toLowerCase()) + firstString.compareTo(secondString, true) } } } @@ -85,8 +94,13 @@ data class Contact(val id: Int, var prefix: String, var firstName: String, var m fun getHashToCompare(): Int { val newPhoneNumbers = ArrayList() - phoneNumbers.mapTo(newPhoneNumbers, { PhoneNumber(it.value.replace(pattern, ""), 0) }) - return copy(id = 0, prefix = "", firstName = getFullName().toLowerCase(), middleName = "", surname = "", suffix = "", photoUri = "", - phoneNumbers = newPhoneNumbers, source = "", starred = 0, contactId = 0, thumbnailUri = "", notes = "").hashCode() + phoneNumbers.mapTo(newPhoneNumbers) { PhoneNumber(it.value.replace(PHONE_NUMBER_PATTERN.toRegex(), ""), 0) } + + val newEmails = ArrayList() + emails.mapTo(newEmails) { Email(it.value, 0) } + + return copy(id = 0, prefix = "", firstName = getFullName().toLowerCase(), middleName = "", surname = "", suffix = "", nickname = "", photoUri = "", + phoneNumbers = newPhoneNumbers, events = ArrayList(), addresses = ArrayList(), emails = newEmails, source = "", starred = 0, + contactId = 0, thumbnailUri = "", notes = "", groups = ArrayList(), websites = ArrayList(), organization = Organization("", "")).hashCode() } } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/models/RecentCall.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/models/RecentCall.kt new file mode 100644 index 00000000..e009be76 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/models/RecentCall.kt @@ -0,0 +1,3 @@ +package com.simplemobiletools.contacts.models + +data class RecentCall(var id: Int, var number: String, var dateTime: String, var name: String?) diff --git a/app/src/main/res/anim/pulsing_animation.xml b/app/src/main/res/anim/pulsing_animation.xml new file mode 100644 index 00000000..119d4b04 --- /dev/null +++ b/app/src/main/res/anim/pulsing_animation.xml @@ -0,0 +1,14 @@ + + diff --git a/app/src/main/res/drawable-hdpi/ic_clock.png b/app/src/main/res/drawable-hdpi/ic_clock.png new file mode 100644 index 00000000..43b00e0a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_clock.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_phone_big.png b/app/src/main/res/drawable-hdpi/ic_phone_big.png new file mode 100644 index 00000000..61d59bd4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_phone_big.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_clock.png b/app/src/main/res/drawable-xhdpi/ic_clock.png new file mode 100644 index 00000000..9d64be88 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_clock.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_phone_big.png b/app/src/main/res/drawable-xhdpi/ic_phone_big.png new file mode 100644 index 00000000..3aae5d70 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_phone_big.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_clock.png b/app/src/main/res/drawable-xxhdpi/ic_clock.png new file mode 100644 index 00000000..a29eea53 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_clock.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_phone_big.png b/app/src/main/res/drawable-xxhdpi/ic_phone_big.png new file mode 100644 index 00000000..aaa1b757 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_phone_big.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_clock.png b/app/src/main/res/drawable-xxxhdpi/ic_clock.png new file mode 100644 index 00000000..bfd0dc3e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_clock.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_phone_big.png b/app/src/main/res/drawable-xxxhdpi/ic_phone_big.png new file mode 100644 index 00000000..707350b0 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_phone_big.png differ diff --git a/app/src/main/res/layout/activity_edit_contact.xml b/app/src/main/res/layout/activity_edit_contact.xml index 534ac995..15c77a7c 100644 --- a/app/src/main/res/layout/activity_edit_contact.xml +++ b/app/src/main/res/layout/activity_edit_contact.xml @@ -170,6 +170,22 @@ android:textCursorDrawable="@null" android:textSize="@dimen/bigger_text_size"/> + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 95c89e52..1c26ff25 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -14,27 +14,7 @@ app:tabIndicatorColor="@android:color/white" app:tabIndicatorHeight="2dp" app:tabMinWidth="150dp" - app:tabSelectedTextColor="@android:color/white"> - - - - - - - - + app:tabSelectedTextColor="@android:color/white"/> @@ -11,6 +12,28 @@ android:layout_height="wrap_content" android:orientation="vertical"> + + + + + + + + + + + + + android:text="@string/use_english_language" + app:switchPadding="@dimen/medium_margin"/> @@ -97,7 +143,8 @@ android:clickable="false" android:paddingLeft="@dimen/medium_margin" android:paddingStart="@dimen/medium_margin" - android:text="@string/avoid_whats_new"/> + android:text="@string/avoid_whats_new" + app:switchPadding="@dimen/medium_margin"/> @@ -120,7 +167,8 @@ android:clickable="false" android:paddingLeft="@dimen/medium_margin" android:paddingStart="@dimen/medium_margin" - android:text="@string/show_info_bubble"/> + android:text="@string/show_info_bubble" + app:switchPadding="@dimen/medium_margin"/> @@ -143,7 +191,8 @@ android:clickable="false" android:paddingLeft="@dimen/medium_margin" android:paddingStart="@dimen/medium_margin" - android:text="@string/show_contact_thumbnails"/> + android:text="@string/show_contact_thumbnails" + app:switchPadding="@dimen/medium_margin"/> @@ -166,7 +215,8 @@ android:clickable="false" android:paddingLeft="@dimen/medium_margin" android:paddingStart="@dimen/medium_margin" - android:text="@string/show_phone_numbers"/> + android:text="@string/show_phone_numbers" + app:switchPadding="@dimen/medium_margin"/> @@ -189,7 +239,8 @@ android:clickable="false" android:paddingLeft="@dimen/medium_margin" android:paddingStart="@dimen/medium_margin" - android:text="@string/start_name_with_surname"/> + android:text="@string/start_name_with_surname" + app:switchPadding="@dimen/medium_margin"/> @@ -212,7 +263,32 @@ android:clickable="false" android:paddingLeft="@dimen/medium_margin" android:paddingStart="@dimen/medium_margin" - android:text="@string/filter_duplicates"/> + android:text="@string/filter_duplicates" + app:switchPadding="@dimen/medium_margin"/> + + + + + + diff --git a/app/src/main/res/layout/activity_view_contact.xml b/app/src/main/res/layout/activity_view_contact.xml index 8d50e8cf..e471ae49 100644 --- a/app/src/main/res/layout/activity_view_contact.xml +++ b/app/src/main/res/layout/activity_view_contact.xml @@ -166,6 +166,21 @@ android:singleLine="true" android:textSize="@dimen/bigger_text_size"/> + + diff --git a/app/src/main/res/layout/dialog_call_confirmation.xml b/app/src/main/res/layout/dialog_call_confirmation.xml new file mode 100644 index 00000000..9d406f6b --- /dev/null +++ b/app/src/main/res/layout/dialog_call_confirmation.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/layout/dialog_manage_visible_fields.xml b/app/src/main/res/layout/dialog_manage_visible_fields.xml index 2b8bbf80..599fdb88 100644 --- a/app/src/main/res/layout/dialog_manage_visible_fields.xml +++ b/app/src/main/res/layout/dialog_manage_visible_fields.xml @@ -54,6 +54,14 @@ android:paddingTop="@dimen/activity_margin" android:text="@string/suffix"/> + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_recents.xml b/app/src/main/res/layout/fragment_recents.xml new file mode 100644 index 00000000..4c7e37b1 --- /dev/null +++ b/app/src/main/res/layout/fragment_recents.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/layout/item_recent_call.xml b/app/src/main/res/layout/item_recent_call.xml new file mode 100644 index 00000000..61334288 --- /dev/null +++ b/app/src/main/res/layout/item_recent_call.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/cab.xml b/app/src/main/res/menu/cab.xml index d775dbc5..ae59e0ae 100644 --- a/app/src/main/res/menu/cab.xml +++ b/app/src/main/res/menu/cab.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + diff --git a/app/src/main/res/menu/menu_view_contact.xml b/app/src/main/res/menu/menu_view_contact.xml index a4436f7f..0ed4b3ec 100644 --- a/app/src/main/res/menu/menu_view_contact.xml +++ b/app/src/main/res/menu/menu_view_contact.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amber.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amber.xml index ddd51757..dab4c0c5 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amber.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amber.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_blue.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_blue.xml index 0bb69cf2..37bf057f 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_blue.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_blue.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_blue_grey.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_blue_grey.xml index aa10bae8..3e4d0696 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_blue_grey.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_blue_grey.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_brown.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_brown.xml index 8d2fc23f..9786d7bf 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_brown.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_brown.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_cyan.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_cyan.xml index 89f7a275..afb3d0d3 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_cyan.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_cyan.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_deep_orange.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_deep_orange.xml index 0822e55f..1846b81d 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_deep_orange.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_deep_orange.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_deep_purple.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_deep_purple.xml index 0735af68..4152801a 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_deep_purple.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_deep_purple.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_green.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_green.xml index 06e60b88..e55d1092 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_green.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_green.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_grey_black.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_grey_black.xml index 4765f7ec..40d07450 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_grey_black.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_grey_black.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_indigo.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_indigo.xml index 37f5249f..601d8170 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_indigo.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_indigo.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_light_blue.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_light_blue.xml index eed925f1..01f2fead 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_light_blue.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_light_blue.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_light_green.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_light_green.xml index a051acce..d37b24c4 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_light_green.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_light_green.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_lime.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_lime.xml index d6df0e23..9fd7bc5b 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_lime.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_lime.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_pink.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_pink.xml index 4b6fc188..d2adf9a0 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_pink.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_pink.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_purple.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_purple.xml index e7e2cb97..32c838cd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_purple.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_purple.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_red.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_red.xml index 972ad3de..a6e93599 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_red.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_red.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_teal.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_teal.xml index c3020ef7..18492d28 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_teal.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_teal.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_yellow.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_yellow.xml index 0c336264..854427e0 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_yellow.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_yellow.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml new file mode 100644 index 00000000..d197f75f --- /dev/null +++ b/app/src/main/res/values-az/strings.xml @@ -0,0 +1,133 @@ + + Sadə Kontaktlar + Kontaktlar + Ünvan + Daxil edilir… + Yenilənir… + Telefon yaddaşı + Telefon yaddaşı (digər tətbiqlərə görünmür) + Şirkət + İş vəziyyəti + Vebsayt + Kontaktlara SMS göndər + Kontaktlara e-poçt göndər + Grupa SMS göndər + Grupa e-poçt göndər + %s şəxsinə zng et + Lazım olan icazələri istə + + Yeni kontakt + Redaktə et + Kontakt seç + Kontaktları seç + Ad + Orta Ad + Soyad + Nickname + + + Qruplar yoxdur + Yeni qrup yarat + Qrupdan sil + Bu qrup boşdur + Kontaktlar əlavə et + Cihazda heçbir kontakt qrupu yoxdur + Qrup yarat + Qrupa əlavə et + Hesab altında qrup yarat + + + Şəkil çək + Şəkil seç + Şəkli sil + + + Ada soyaddan başla + Telefon nömrələrini əsas ekranda göstər + Kontakt görüntülərini göstər + Kontakta toxunduqda + Kontakta zəng et + Kontakt detallarına bax + Göstərilən kontakt sahəsini idarə et + Təkrarlanmış kontaktları filtrləməyə çalış + Göstərilən nişanları idarə et + Kontaktlar + Sevimlilər + Hazırki zənglər + Zəngə başlamazdan əvvəl zəng təsdiq pəncərəsi göstər + + + E-poçt + Ev + İş + Başqa + + + Nömrə + Mobil + Əsas + İş Faksı + Ev Faksı + Zəng cihazı + Heçbir telefon nömrəsi tapılmadı + + + Ad günü + İl dönümü + + + Görünür, hələlik heçbir sevimli kontakt əlavə etməmisiniz. + Sevimlilər əlavə et + Sevimlilərə əlavə et + Sevimlilərdən sil + Kontaktı dəyişmək üçün İdarə et ekranında olmalısınız + + + Kontaktları axtar + Sevimliləri axtar + + + Kontaktları daxil et + Kontaktları xaric et + .vcf faylından kontaktları daxil et + .vcf faylından kontaktları xaric et + Kontakt kökünü nişanla + Kontakt köklərini daxil et + Fayl adı (.vcf olmadan) + + + Göstərmək üçün sahəni seç + Ön şəkilçi + Orta şəkilçi + Telefon nömrələri + E-poçtlar + Ünvanlar + Hadisələr (ad günləri, il dönümləri) + Qeydlər + Təşkilat + Vebsaytlar + Qruplar + Kontakt kökü + + + I want to change what fields are visible at contacts. Can I do it? + Yes, all you have to do is go in Settings -> Manage shown contact fields. There you can select what fields should be visible. Some of them are even disabled by default, so you might find some new ones there. + + + + A contacts app for managing your contacts without ads. + + 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 https://www.simplemobiletools.com + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 58b24d96..74ec5350 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -6,13 +6,15 @@ Aktualisiere… Gerätespeicher Gerätespeicher (nicht sichtbar für andere Apps) - Company - Job position - Website - Send SMS to contacts - Send email to contacts - Send SMS to group - Send email to group + Unternehmen + Arbeitsstelle + Webseite + Sende SMS an Kontakte + Sende E-Mail an Kontakte + Sende SMS an Gruppe + Sende E-Mail an Gruppe + %s anrufen + Benötigte Berechtigungen anfordern Neuer Kontakt Kontakt bearbeiten @@ -20,12 +22,13 @@ Kontakte auswählen Vorname Zweiter Vorname - Familienname + Nachname + Spitzname Keine Gruppen Eine neue Gruppe erstellen - Von Gruppe entfernen + Aus Gruppe entfernen Diese Gruppe ist leer Kontakte hinzufügen Es sind keine Kontaktgruppen auf diesem Gerät vorhanden @@ -34,24 +37,27 @@ Gruppe in diesem Konto erstellen - Foto machen + Foto aufnehmen Foto auswählen Foto entfernen Namen mit Nachnamen beginnen Zeige Telefonnummern im Hauptmenü - Zeige Vorschaubilder für Kontakte + Zeige Vorschaubilder der Kontakte Beim Klicken auf den Kontakt Kontakt anrufen - Kontaktdetails ansehen - Show favorites tab - Show groups tab - Manage shown contact fields - Try filtering out duplicate contacts + Kontaktdetails anzeigen + Bearbeite sichtbare Kontaktfelder + Versuche Kontaktduplikate herauszufiltern + Anzuzeigende Tabs festlegen + Kontakte + Favoriten + Anrufliste + Bestätigungsdialog zeigen, bevor ein Anruf durchgeführt wird - Email + E-Mail Privat Arbeit Sonstiges @@ -74,7 +80,7 @@ Favoriten hinzufügen Zu Favoriten hinzufügen Aus Favoriten entfernen - You must be at the Edit screen to modify a contact + Sie müssen sich im Bearbeitungsmodus befinden, um einen Kontakt zu bearbeiten Kontakte durchsuchen @@ -90,34 +96,34 @@ Dateiname (ohne .vcf) - Select fields to show - Prefix + Sichtbare Felder auswählen + Titel Suffix - Phone numbers - Emails - Addresses - Events (birthdays, anniversaries) - Notes - Organization - Websites - Groups - Contact source + Telefonnummern + E-Mails + Addressen + Termine (Geburtstage, Jahrestage) + Notizen + Organisation + Webseiten + Gruppen + Kontaktquelle - I want to change what fields are visible at contacts. Can I do it? - Yes, all you have to do is go in Settings -> Manage shown contact fields. There you can select what fields should be visible. Some of them are even disabled by default, so you might find some new ones there. + Ich möchte die sichtbaren Kontaktfelder ändern. Kann ich das machen? + Ja, alles, was Sie tun müssen ist folgendes: Gehen Sie zu Einstellungen -> Bearbeite sichtbare Kontaktfelder. Hier können die sichtbaren Felder ausgewählt werden. Einige sind standardmäßig deaktiviert, weshalb hier neue gefunden werden können. - Eine App zum Verwalten von Kontakten, ganz ohne Werbung. + Eine Kontakte-App zur Verwaltung Ihrer Kontake. Ohne Werbung. - Eine schlichte App um Kontakte aus allen Quellen zu verwalten und neue zu erstellen. Die Kontakte können nur auf deinem Gerät gespeichert werden, aber auch über Google oder andere Dienste synchronisiert werden. Deine wichtigsten Kontakte werden in einer separaten Liste angezeigt. + Eine einfache App, mit der Sie Kontakte erstellen oder von jeder Quelle verwalten können. Die Kontakte können entweder nur auf Ihrem Gerät gespeichert, oder mittels Google oder anderer Konten synchronisiert werden. Sie können Ihre Lieblingskontake in einer separaten Liste anzeigen. - Du kannst diese App auch dazu nutzen, um die Email-Adressen und Termine von Kontakten zu verwalten. Sie hat die Möglichkeit mithilfe von mehreren Parametern zu sortieren/filtern, optional auch den Familiennamen als Vornamen anzuzeigen. + Sie können es auch zur Verwaltung von Nutzer-E-Mails und Ereignisse nutzen. Es kann nach mehreren Parametern sortieren oder filtern, oder optional den Nachnamen zuerst anzeigen. - Beinhaltet keine Werbung oder unnötige Berechtigungen. Sie ist komplett Open Source, alle verwendeten Farben sind anpassbar. + Enthält keine Werbung oder unnötige Berechtigungen. Vollständig quelloffen. Die Farben sind anpassbar. - Diese App ist nur eine aus einer größeren Serie von schlichten Apps. Der Rest davon findet sich auf https://www.simplemobiletools.com + Diese App ist nur ein Teil einer größeren App-Familie. Die übrigen finden Sie unter https://www.simplemobiletools.com Δεν υπάρχουν ομάδες @@ -31,7 +34,7 @@ Δεν υπάρχουν ομάδες επαφών στη συσκευή Δημιουργία ομάδας Προσθήκη σε ομάδα - Δημιουργία ομάδας κάτω από λογαριασμό + Δημιουργία ομάδας υπο ενός λογαριασμού Λήψη φωτογραφίας @@ -39,20 +42,23 @@ Αφαίρεση φωτογραφίας - Το όνομα ξεκινά με το επώνυμο + Εμφάνιση πρώτα το επώνυμο Εμφάνιση τηλεφωνικών αριθμών στην κύρια οθόνη Εμφάνιση μικρογραφιών επαφής Στην επιλογή επαφής Κλήση επαφής Εμφάνιση λεπτομερειών επαφής - Εμφάνιση καρτέλας αγαπημένων - Εμφάνιση καρτέλας ομάδων - Manage shown contact fields - Try filtering out duplicate contacts + Διαχείριση εμφανιζόμενων πεδίων επαφής + Δοκιμάστε το φιλτράρισμα διπλών επαφών + Διαχείριση εμφανιζόμενων καρτελών + Επαφές + Αγαπημένες + Πρόσφατες Κλήσεις + Εμφάνιση διαλόγου επιβεβαίωσης πριν από την έναρξη μιας κλήσης Email - Σπίτι + Οικία Εργασία Άλλο @@ -60,8 +66,8 @@ Αριθμός Κινητό Κύριο - Φαξ εργασίας - Φαξ σπιτιού + Φαξ Εργασίας + Φαξ Οικίας Βομβητής Δεν βρέθηκε τηλεφωνικός αριθμός @@ -70,11 +76,11 @@ Επέτειος - Φαίνεται ότι δεν έχεις προσθέσει αγαπημένες επαφές ακόμα. + Φαίνεται ότι δεν έχετε προσθέσει αγαπημένες επαφές ακόμη. Προσθήκη αγαπημένων - Προσθήκη στα αγαπημένα + Προσθήκη στις αγαπημένες Αφαίρεση από τα αγαπημένα - You must be at the Edit screen to modify a contact + Πρέπει να είστε στην οθόνη "Επεξεργασία" για να τροποποιήσετε μια επαφή Αναζήτηση επαφών @@ -90,34 +96,34 @@ Όνομα αρχείου (χωρίς .vcf) - Select fields to show - Prefix - Suffix - Phone numbers + Επιλογή εμφάνισης πεδίων + Πρόθεμα + Κατάληξη + Αριθμοί Τηλεφώνων Emails - Addresses - Events (birthdays, anniversaries) - Notes - Organization - Websites - Groups - Contact source + Διευθύνσεις + Εκδηλώσεις (γενέθλια, επετείους) + Σημειώσεις + Εταιρεία + Ιστοσελίδα + Ομάδες + Προέλευση επαφής - I want to change what fields are visible at contacts. Can I do it? - Yes, all you have to do is go in Settings -> Manage shown contact fields. There you can select what fields should be visible. Some of them are even disabled by default, so you might find some new ones there. + Θέλω να αλλάξω τα πεδία που θα είναι ορατά στις επαφές. Μπορώ να το κάνω? + Ναι, το μόνο που έχετε να κάνετε είναι να μεταβείτε στις Ρυθμίσεις -> Διαχείριση εμφανιζόμενων πεδίων επαφής. Εκεί μπορείτε να επιλέξετε ποια πεδία θα πρέπει να είναι ορατά. Κάποια από αυτά είναι ακόμη και απενεργοποιημένα από προεπιλογή, επομένως ίσως βρείτε κάποια νέα εκεί. - Μια εφαρμογή επαφών για να διαχειρίζεσαι τις επαφές σου χωρίς διαφημίσεις. + Μια εφαρμογή για την διαχείρηση των επαφών σου χωρίς διαφημίσεις. Μια απλή εφαρμογή για δημιουργία και διαχείριση των επαφών σου από κάθε πηγή. Οι επαφές μπορεί να είναι αποθηκευμένες μόνο στη συσκευή σου, αλλά μπορούν να συγχρονίζονται στο Google, ή σε κάποιο άλλο λογαριασμό. Μπορείς να εμφανίσεις τις αγαπημένες σου επαφές σε ξεχωριστή λίστα. - Μπορείς να τη χρησιμοποιήσεις για τη διαχείριση των email των χρηστών και τα γεγονότα. Έχει τη δυνατότητα ταξινόμησης/φιλτραρίσματος με διάφορες παραμέτρους, προαιρετικά να εμφανίζεται το επώνυμο σαν όνομα. + Μπορείτε να τη χρησιμοποιήσετε για τη διαχείριση των email και εκδηλώσεων επίσης. Έχει τη δυνατότητα ταξινόμησης/φιλτραρίσματος με διάφορες παραμέτρους, προαιρετικά να εμφανίζεται το επώνυμο πρώτα ή το όνομα. - Δεν περιέχει διαφημίσεις ή περιττές άδειες. Είναι πλήρως ανοικτού κώδικα, παρέχει δυνατότητα προσαρμογής των χρωμάτων. + Δεν περιέχει διαφημίσεις ή περιττές άδειες. Έιναι όλη ανοιχτού κώδικα και παρέχει προσαρμόσιμα χρώματα για την εφαρμογή. - Αυτή η εφαρμογή είναι ένα μικρό κομμάτι μιας μεγαλύτερης συλλογής εφαρμογών. Μπορείς να βρεις τις υπόλοιπες στο https://www.simplemobiletools.com + Αυτή η εφαρμογή είναι μέρος μιας σειράς εφαρμογών. Μπορείτε να βρείτε τις υπόλοιπες στο https://www.simplemobiletools.com Pas de groupe @@ -45,10 +48,13 @@ Sur appui du contact Appeler le contact Voir les détails du contact - Afficher l\'onglet favoris - Afficher l\'onglet groupes Configurer l\'affichage des champs des contacts Try filtering out duplicate contacts + Manage shown tabs + Contacts + Favorites + Recent calls + Show a call confirmation dialog before initiating a call E-mail diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 08d418e7..e3b2bb3f 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -13,6 +13,8 @@ Send email to contacts Send SMS to group Send email to group + Call %s + Request the required permissions Novi kontakt Uredi kontakt @@ -21,6 +23,7 @@ Ime Srednje ime Prezime + Nickname Nema grupa @@ -45,10 +48,13 @@ Prilikom dodira kontakta Nazovi kontakt Prikaži pojedinosti o kontaktu - Prikaži karticu favorita - Prikaži karticu grupa Manage shown contact fields Try filtering out duplicate contacts + Manage shown tabs + Contacts + Favorites + Recent calls + Show a call confirmation dialog before initiating a call E-pošta diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml new file mode 100644 index 00000000..3235994a --- /dev/null +++ b/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,133 @@ + + Simple Contacts + 連絡先 + 住所 + 挿入中… + 更新中… + 内部ストレージ + 内部ストレージ (他のアプリからは表示されません) + 会社 + 役職 + ウェブサイト + 連絡先にSMSを送信 + 連絡先にメールを送信 + グループにSMSを送信 + グループにメールを送信 + Call %s + Request the required permissions + + 新しい連絡先 + 連絡先を編集 + 連絡先を選択 + 連絡先を選択 + + ミドルネーム + + Nickname + + + グループなし + 新しいグループを作成 + グループから削除 + このグループは空です + 連絡先を追加 + 連絡先グループがありません + グループを作成 + グループに追加 + アカウントの下にグループを作成 + + + 写真を撮影 + 写真を選択 + 写真を削除 + + + 姓を先に表示 + メイン画面に電話番号を表示 + 連絡先のサムネイルを表示 + 連絡先をタップ + 連絡先に発信 + 連絡先の詳細を表示 + 連絡先に表示するフィールドを管理 + 重複した連絡先を除外する + 表示するタブを管理 + 連絡先 + お気に入り + Recent calls + 発信する前に確認ダイアログを表示する + + + メール + 自宅 + 職場 + その他 + + + 番号 + 携帯 + Main + 職場FAX + 自宅FAX + ポケベル + 電話番号が見つかりません + + + 誕生日 + 記念日 + + + お気に入りの連絡先はまだありません + お気に入りを追加 + お気に入りに追加 + お気に入りから削除 + 連絡先を編集するには編集画面に切り替えてください + + + 連絡先を検索 + お気に入りを検索 + + + 連絡先をインポート + 連絡先をエクスポート + .vcfファイルから連絡先をインポート + 連絡先を.vcfファイルにエクスポート + Target contact source + Include contact sources + ファイル名 (.vcfを含めない) + + + 表示する項目を選択 + 敬称(名前の前) + 敬称(名前の後) + 電話番号 + メール + 住所 + 予定 (誕生日、記念日) + メモ + 所属 + ウェブサイト + グループ + インポート元 + + + 連絡先に表示される項目(フィールド)を変更することはできますか? + 可能です。[設定] -> [連絡先に表示するフィールドを管理] から、表示されるフィールドを選択することができます。これらの中にはデフォルトで無効になっているものもあるので、あなたは新しいものを見つけるかもしれません。 + + + + 連絡先を管理するシンプルなアプリ (広告表示なし)。 + + 連絡先を作成または管理するためのシンプルなアプリです。連絡先はお使いの端末上にのみ保存されますが、Googleや他のアカウントと同期することもできます。お気に入りの連絡先を別のリストとして表示することができます。 + + 友人のメールアドレスや予定の管理にも使用できます。これらは複数の項目で並べ替えやフィルタリングする機能があり、名前を\"姓 名\"の順に表示することもできます。 + + 広告や不要なアクセス許可は含まれていません。完全にオープンソースで、色のカスタマイズも可能です。 + + このアプリは大きな一連のアプリの一つです。 その他のアプリは http://www.simplemobiletools.com で見つけることができます。 + + + + diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 99536a44..94819d86 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -13,6 +13,8 @@ Send email to contacts Send SMS to group Send email to group + Call %s + Request the required permissions 새로운 연락처 연락처 수정 @@ -21,6 +23,7 @@ 이름 중간 이름 + Nickname No groups @@ -45,10 +48,13 @@ On contact click Call contact View contact details - Show favorites tab - Show groups tab Manage shown contact fields Try filtering out duplicate contacts + Manage shown tabs + Contacts + Favorites + Recent calls + Show a call confirmation dialog before initiating a call 이메일 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index f5ee1255..4dbd270c 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -13,6 +13,8 @@ Send email to contacts Send SMS to group Send email to group + Call %s + Request the required permissions Naujas kontaktas Redaguoti kontaktą @@ -21,6 +23,7 @@ Vardas Antras vardas Pavardė + Nickname Nėra grupių @@ -45,10 +48,13 @@ Ant kontakto paspaudimo Skambinti kontaktui Žiūrėti kontakto detales - Rodyti mėgiamiausiųjų skirtuką - Rodyti grupių skirtuką Manage shown contact fields Try filtering out duplicate contacts + Manage shown tabs + Contacts + Favorites + Recent calls + Show a call confirmation dialog before initiating a call Elektroninis paštas diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 486feb5a..f2a6d47f 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -13,6 +13,8 @@ Enviar e-mail aos contactos Enviar SMS para o grupo Enviar e-mail para o grupo + Ligar a %s + Request the required permissions Novo contacto Editar contacto @@ -21,6 +23,7 @@ Primeiro nome Segundo nome Apelido + Alcunha Não há grupos @@ -45,10 +48,13 @@ Ao tocar no contacto Ligar Ver detalhes - Mostrar favoritos - Mostrar grupos Gerir campos a exibir - Try filtering out duplicate contacts + Tentar filtrar contactos duplicados + Manage shown tabs + Contactos + Favoritos + Chamadas recentes + Mostrar diálogo para confirmar a chamada E-mail diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b107fb1e..86609349 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -13,6 +13,8 @@ Отправить письмо контактам Отправить SMS группе Отправить письмо группе + Call %s + Request the required permissions Новый контакт Редактировать контакт @@ -21,6 +23,7 @@ Имя Отчество Фамилия + Nickname Нет групп @@ -45,10 +48,13 @@ При нажатии на контакт Позвонить контакту Просмотреть подробности о контакте - Показывать вкладку избранного - Показывать вкладку групп Управление отображаемыми полями контактов Отфильтровывать повторяющиеся контакты + Manage shown tabs + Contacts + Favorites + Recent calls + Show a call confirmation dialog before initiating a call Эл. почта diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index c1f6d985..6e3f3d25 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -13,6 +13,8 @@ Poslať kontaktom email Poslať skupine SMS Poslať skupine email + Zavolať %s + Vyžiadať potrebné oprávnenia Nový kontakt Upraviť kontakt @@ -21,6 +23,7 @@ Krstné meno Stredné meno Priezvisko + Prezývka Žiadne skupiny @@ -45,10 +48,13 @@ Po kliknutí na kontakt Zavolať kontakt Zobraziť údaje kontaktu - Zobraziť okno s obľúbenými - Zobraziť okno so skupinami Spravovať zobrazené polia kontaktov Pokúsiť sa vyfiltrovať duplicitné kontakty + Spravovať zobrazené karty + Kontakty + Obľúbené + Predošlé hovory + Zobraziť pred spustením hovoru okno na jeho potvrdenie Email diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 42f606b5..c5f3e8a7 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -13,6 +13,8 @@ Skicka e-post till kontakter Skicka sms till grupp Skicka e-post till grupp + Ring %s + Begär de behörigheter som krävs Ny kontakt Redigera kontakt @@ -21,6 +23,7 @@ Förnamn Mellannamn Efternamn + Smeknamn Inga grupper @@ -45,10 +48,13 @@ Vid kontakttryckning Ring kontakt Visa kontaktuppgifter - Visa fliken Favoriter - Visa fliken Grupper Hantera visade kontaktfält - Try filtering out duplicate contacts + Försök filtrera bort dubblettkontakter + Hantera visade flikar + Kontakter + Favoriter + Senaste samtal + Visa en bekräftelsedialogruta före uppringning E-post @@ -74,7 +80,7 @@ Lägg till favoriter Lägg till i favoriter Ta bort från favoriter - You must be at the Edit screen to modify a contact + Kontakter kan bara redigeras i redigeringsvyn Sök efter kontakter diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml new file mode 100644 index 00000000..f849e887 --- /dev/null +++ b/app/src/main/res/values-tr/strings.xml @@ -0,0 +1,133 @@ + + Basit Kişiler + Kişiler + Adres + Ekleniyor… + Güncelleniyor… + Telefon belleği + Telefon belleği (diğer uygulamalar tarafından görülmez) + Şirket + İş pozisyonu + Web sitesi + Kişilere SMS gönder + Kişilere e-posta gönder + Gruba SMS gönder + Gruba e-posta gönder + Call %s + Request the required permissions + + Yeni kişi + Kişiyi düzenle + Kişi seç + Kişileri seç + Adı + Göbek adı + Soyadı + Nickname + + + Grup yok + Yeni grup oluştur + Gruptan kaldır + Bu grup boş + Kişi ekle + Cihazda kişi grubu yok + Grup oluştur + Gruba ekle + Hesap altında grup oluştur + + + Fotoğraf çek + Fotoğraf seç + Fotoğrafı kaldır + + + Soyadı ile başla + Ana ekranda telefon numaralarını göster + Kişi küçük resimlerini göster + Kişi tıklandığında + Kişiyi ara + Kişi bilgilerini göster + Görüntülenecek kişi alanlarını yönet + Çift kişileri filtrelemeyi dene + Manage shown tabs + Contacts + Favorites + Recent calls + Show a call confirmation dialog before initiating a call + + + E-posta + Ev + İş + Diğer + + + Numara + Cep + Ana + İş Faksı + Ev Faksı + Çağrı Cihazı + Telefon numarası bulunamadı + + + Doğum günü + Yıldönümü + + + Henüz hiç favori kişi eklemediniz gibi görünüyor. + Favorilerini ekle + Favorilere ekle + Favorilerden kaldır + Bir kişiyi değiştirmek için Düzen ekranında olmalısınız + + + Kişileri ara + Favorileri ara + + + Kişileri içe aktar + Kişileri dışa aktar + Kişileri bir .vcf dosyasından içe aktar + Kişileri bir .vcf dosyasına aktar + Hedef kişi kaynağı + Kişi kaynaklarını dahil et + Dosya adı (.vcf olmadan) + + + Görüntülenecek alanları seç + Önek + Sonek + Telefon numaraları + E-postalar + Adresler + Etkinlikler (doğum günleri, yıldönümleri) + Notlar + Organizasyon + Web siteleri + Gruplar + Kişi kaynağı + + + Rehberde görüntülenecek alanları değiştirmek istiyorum. Bunu yapabilir miyim? + Evet, tek yapmanız gereken Ayarlar -> Görüntülenecek kişi alanlarını yönet\'e gitmek. Orada hangi alanların görüntüleneceğini seçebilirsiniz. Bazıları varsayılan olarak devre dışı bile olsa, orada bazı yenilerini bulabilirsiniz. + + + + Kişilerinizi reklamsız yönetmek için bir kişiler uygulaması. + + Kişilerinizi herhangi bir kaynaktan oluşturmak veya yönetmek için basit bir uygulama. Kişiler yalnızca cihazınızda saklanabilir, aynı zamanda Google veya diğer hesaplarla senkronize edilebilir. Favori kişilerinizi ayrı bir listede görüntüleyebilirsiniz. + + Kullanıcı e-postalarını ve etkinliklerini yönetmek için de kullanabilirsiniz. Birden çok parametreye göre sıralama/filtreleme, isteğe bağlı olarak soyadı ilk ad olarak görüntüleme yeteneğine sahiptir. + + Reklam veya gereksiz izinler içermez. Tamamen açık kaynaktır, özelleştirilebilir renkler sağlar. + + Bu uygulama, daha büyük bir uygulama serisinden sadece bir parça. Geri kalanı http://www.simplemobiletools.com adresinde bulabilirsiniz + + + + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 79766c2a..c6f1b48b 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -8,11 +8,13 @@ 手機空間 (其他程式不可見) 公司 職位 - Website - Send SMS to contacts - Send email to contacts - Send SMS to group - Send email to group + 網站 + 發送簡訊給聯絡人 + 發送電子郵件給聯絡人 + 發送簡訊給群組 + 發送電子郵件給群組 + 打電話給 %s + 請求必要的權限 新聯絡人 編輯聯絡人 @@ -21,6 +23,7 @@ 名字 中間名 姓氏 + Nickname 沒有群組 @@ -45,10 +48,13 @@ 點擊聯絡人 打電話給聯絡人 顯示聯絡人資料 - 顯示我的最愛頁面 - 顯示群組頁面 管理顯示的聯絡人欄位 - Try filtering out duplicate contacts + 試著過濾重複的聯絡人 + 管理顯示的頁面 + 聯絡人 + 我的最愛 + 通話紀錄 + 開始通話前顯示通話確認框 電子信箱 @@ -74,7 +80,7 @@ 添加我的最愛 加入我的最愛 從我的最愛移除 - You must be at the Edit screen to modify a contact + 你必須在編輯畫面去修改聯絡人 搜尋聯絡人 @@ -99,7 +105,7 @@ 活動 (生日、紀念日) 筆記 組織 - Websites + 網站 群組 聯絡人來源 diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 613319f9..aa83af5e 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -2,6 +2,11 @@ + Added an optional Nickname field + + Allow customizing which tabs are visible\n + Added an optional call confirmation dialog + Added name prefix/suffix and contact organizations\n Added a settings item \"Manage shown contact fields\" for customizing visible contact details, with some fields disabled by default diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a3c3eede..d0c4b5f5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,6 +13,8 @@ Send email to contacts Send SMS to group Send email to group + Call %s + Request the required permissions New contact Edit contact @@ -21,6 +23,7 @@ First name Middle name Surname + Nickname No groups @@ -45,10 +48,13 @@ On contact click Call contact View contact details - Show favorites tab - Show groups tab Manage shown contact fields Try filtering out duplicate contacts + Manage shown tabs + Contacts + Favorites + Recent calls + Show a call confirmation dialog before initiating a call Email diff --git a/build.gradle b/build.gradle index 8e5373c7..1b8fb3c3 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.41' + ext.kotlin_version = '1.2.61' repositories { google() @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.1.2' + classpath 'com.android.tools.build:gradle:3.1.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/tablet-7.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/tablet-7.png new file mode 100644 index 00000000..aeb53b51 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/tablet-7.png differ diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/tablet-10.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/tablet-10.png new file mode 100644 index 00000000..e4a8b675 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/tenInchScreenshots/tablet-10.png differ