diff --git a/CHANGELOG.md b/CHANGELOG.md index 43d342aa..5cc45df2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Changelog ========== +Version 3.4.0 *(2018-03-21)* +---------------------------- + + * Added groups + * Make phone numbers, emails and addresses clickable on the view screen + * Many smaller improvements and bugfixes + Version 3.3.3 *(2018-03-04)* ---------------------------- diff --git a/app/build.gradle b/app/build.gradle index c6cb783b..abf62b29 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "com.simplemobiletools.contacts" minSdkVersion 16 targetSdkVersion 27 - versionCode 14 - versionName "3.3.3" + versionCode 15 + versionName "3.4.0" setProperty("archivesBaseName", "contacts") } @@ -45,7 +45,7 @@ ext { } dependencies { - implementation 'com.simplemobiletools:commons:3.16.11' + implementation 'com.simplemobiletools:commons:3.16.12' implementation 'joda-time:joda-time:2.9.9' implementation 'com.facebook.stetho:stetho:1.5.0' implementation 'com.google.code.gson:gson:2.8.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c3692df2..cdb40add 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -74,6 +74,10 @@ android:label="@string/settings" android:parentActivityName=".activities.MainActivity"/> + + var groupHolder = contact_groups_holder.getChildAt(index) if (groupHolder == null) { - groupHolder = layoutInflater.inflate(R.layout.item_group, contact_groups_holder, false) + groupHolder = layoutInflater.inflate(R.layout.item_edit_group, contact_groups_holder, false) contact_groups_holder.addView(groupHolder) } @@ -359,7 +359,7 @@ class EditContactActivity : ContactActivity() { } if (groups.isEmpty()) { - layoutInflater.inflate(R.layout.item_group, contact_groups_holder, false).apply { + layoutInflater.inflate(R.layout.item_edit_group, contact_groups_holder, false).apply { contact_group.apply { alpha = 0.5f text = getString(R.string.no_groups) diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/GroupContactsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/GroupContactsActivity.kt new file mode 100644 index 00000000..03136965 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/GroupContactsActivity.kt @@ -0,0 +1,118 @@ +package com.simplemobiletools.contacts.activities + +import android.os.Bundle +import com.simplemobiletools.commons.extensions.* +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.interfaces.RefreshContactsListener +import com.simplemobiletools.contacts.interfaces.RemoveFromGroupListener +import com.simplemobiletools.contacts.models.Contact +import com.simplemobiletools.contacts.models.Group +import kotlinx.android.synthetic.main.activity_group_contacts.* + +class GroupContactsActivity : SimpleActivity(), RemoveFromGroupListener, RefreshContactsListener { + private var allContacts = ArrayList() + private var groupContacts = ArrayList() + private var wasInit = false + lateinit var group: Group + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_group_contacts) + updateTextColors(group_contacts_coordinator) + + group = intent.extras.getSerializable(GROUP) as Group + supportActionBar?.title = group.title + + group_contacts_fab.setOnClickListener { + if (wasInit) { + fabClicked() + } + } + + group_contacts_placeholder_2.setOnClickListener { + fabClicked() + } + + group_contacts_placeholder_2.underlineText() + group_contacts_placeholder_2.setTextColor(getAdjustedPrimaryColor()) + } + + override fun onResume() { + super.onResume() + refreshContacts() + } + + private fun fabClicked() { + SelectContactsDialog(this, allContacts, groupContacts) { addedContacts, removedContacts -> + Thread { + addContactsToGroup(addedContacts, group.id) + removeContactsFromGroup(removedContacts, group.id) + refreshContacts() + }.start() + } + } + + private fun refreshContacts() { + ContactsHelper(this).getContacts { + wasInit = true + allContacts = it + + groupContacts = it.filter { it.groups.map { it.id }.contains(group.id) } as ArrayList + group_contacts_placeholder_2.beVisibleIf(groupContacts.isEmpty()) + group_contacts_placeholder.beVisibleIf(groupContacts.isEmpty()) + group_contacts_list.beVisibleIf(groupContacts.isNotEmpty()) + + Contact.sorting = config.sorting + groupContacts.sort() + + updateContacts(groupContacts) + } + } + + private fun updateContacts(contacts: ArrayList) { + 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) + } + }.apply { + setupDragListener(true) + addVerticalDividers(true) + group_contacts_list.adapter = this + } + + group_contacts_fastscroller.setScrollTo(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() ?: "") + } + } else { + (currAdapter as ContactsAdapter).updateItems(contacts) + } + } + + override fun refreshContacts(refreshTabsMask: Int) { + refreshContacts() + } + + override fun removeFromGroup(contacts: ArrayList) { + ContactsHelper(this).removeContactsFromGroup(contacts, group.id) + if (groupContacts.size == 0) { + refreshContacts() + } + } +} 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 27a0cadd..3997c5b0 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/MainActivity.kt @@ -26,9 +26,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.ContactsHelper -import com.simplemobiletools.contacts.helpers.VcfExporter +import com.simplemobiletools.contacts.helpers.* import com.simplemobiletools.contacts.interfaces.RefreshContactsListener import com.simplemobiletools.contacts.models.Contact import kotlinx.android.synthetic.main.activity_main.* @@ -93,8 +91,9 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { val configShowContactThumbnails = config.showContactThumbnails if (storedShowContactThumbnails != configShowContactThumbnails) { - contacts_fragment?.showContactThumbnailsChanged(configShowContactThumbnails) - favorites_fragment?.showContactThumbnailsChanged(configShowContactThumbnails) + getAllFragments().forEach { + it?.showContactThumbnailsChanged(configShowContactThumbnails) + } } val configTextColor = config.textColor @@ -102,8 +101,9 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { getInactiveTabIndexes(viewpager.currentItem).forEach { main_tabs_holder.getTabAt(it)?.icon?.applyColorFilter(configTextColor) } - contacts_fragment?.textColorChanged(configTextColor) - favorites_fragment?.textColorChanged(configTextColor) + getAllFragments().forEach { + it?.textColorChanged(configTextColor) + } } val configBackgroundColor = config.backgroundColor @@ -115,8 +115,9 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { if (storedPrimaryColor != configPrimaryColor) { main_tabs_holder.setSelectedTabIndicatorColor(getAdjustedPrimaryColor()) main_tabs_holder.getTabAt(viewpager.currentItem)?.icon?.applyColorFilter(getAdjustedPrimaryColor()) - contacts_fragment?.primaryColorChanged(configPrimaryColor) - favorites_fragment?.primaryColorChanged(configPrimaryColor) + getAllFragments().forEach { + it?.primaryColorChanged() + } } val configStartNameWithSurname = config.startNameWithSurname @@ -130,9 +131,10 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { initFragments() } - contacts_fragment?.onActivityResume() - favorites_fragment?.onActivityResume() - refreshContacts(true, true) + getAllFragments().forEach { + it?.onActivityResume() + } + refreshContacts(ALL_TABS_MASK) } if (hasPermission(PERMISSION_WRITE_CONTACTS)) { @@ -152,6 +154,12 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu, menu) + val currentPage = viewpager?.currentItem + 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 + } setupSearch(menu) return true } @@ -193,7 +201,7 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { override fun onQueryTextChange(newText: String): Boolean { if (isSearchOpen) { - (getCurrentFragment() as? MyViewPagerFragment)?.onSearchQueryChanged(newText) + getCurrentFragment()?.onSearchQueryChanged(newText) } return true } @@ -202,13 +210,13 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { MenuItemCompat.setOnActionExpandListener(searchMenuItem, object : MenuItemCompat.OnActionExpandListener { override fun onMenuItemActionExpand(item: MenuItem?): Boolean { - (getCurrentFragment() as? MyViewPagerFragment)?.onSearchOpened() + getCurrentFragment()?.onSearchOpened() isSearchOpen = true return true } override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { - (getCurrentFragment() as? MyViewPagerFragment)?.onSearchClosed() + getCurrentFragment()?.onSearchClosed() isSearchOpen = false return true } @@ -266,12 +274,12 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { private fun getInactiveTabIndexes(activeIndex: Int) = arrayListOf(0, 1, 2).filter { it != activeIndex } private fun initFragments() { - refreshContacts(true, true) + refreshContacts(ALL_TABS_MASK) viewpager.offscreenPageLimit = 2 viewpager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { override fun onPageScrollStateChanged(state: Int) { if (isSearchOpen) { - (getCurrentFragment() as? MyViewPagerFragment)?.onSearchQueryChanged("") + getCurrentFragment()?.onSearchQueryChanged("") searchMenuItem?.collapseActionView() } } @@ -281,12 +289,12 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { override fun onPageSelected(position: Int) { main_tabs_holder.getTabAt(position)?.select() - contacts_fragment?.finishActMode() - favorites_fragment?.finishActMode() + getAllFragments().forEach { + it?.finishActMode() + } invalidateOptionsMenu() } }) - viewpager.currentItem = config.lastUsedViewPagerPage main_tabs_holder.onTabSelectionChanged( tabUnselectedAction = { @@ -305,14 +313,14 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { private fun showSortingDialog() { ChangeSortingDialog(this) { - refreshContacts(true, true) + refreshContacts(CONTACTS_TAB_MASK or FAVORITES_TAB_MASK) } } fun showFilterDialog() { FilterContactSourcesDialog(this) { contacts_fragment?.forceListRedraw = true - refreshContacts(true, false) + refreshContacts(CONTACTS_TAB_MASK) } } @@ -334,7 +342,7 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { ImportContactsDialog(this, path) { if (it) { runOnUiThread { - refreshContacts(true, true) + refreshContacts(ALL_TABS_MASK) } } } @@ -397,7 +405,7 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { BuildConfig.VERSION_NAME, faqItems) } - override fun refreshContacts(refreshContactsTab: Boolean, refreshFavoritesTab: Boolean) { + override fun refreshContacts(refreshTabsMask: Int) { if (isActivityDestroyed()) { return } @@ -409,21 +417,27 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { if (viewpager.adapter == null) { viewpager.adapter = ViewPagerAdapter(this, it) + viewpager.currentItem = config.lastUsedViewPagerPage } - if (refreshContactsTab) { + if (refreshTabsMask and CONTACTS_TAB_MASK != 0) { contacts_fragment?.refreshContacts(it) } - if (refreshFavoritesTab) { + if (refreshTabsMask and FAVORITES_TAB_MASK != 0) { favorites_fragment?.refreshContacts(it) } + + if (refreshTabsMask and GROUPS_TAB_MASK != 0) { + if (refreshTabsMask == GROUPS_TAB_MASK) { + groups_fragment.skipHashComparing = true + } + groups_fragment?.refreshContacts(it) + } } } - override fun refreshFavorites() { - refreshContacts(false, true) - } + private fun getAllFragments() = arrayListOf(contacts_fragment, favorites_fragment, groups_fragment) private fun checkWhatsNewDialog() { arrayListOf().apply { diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/SelectContactActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/SelectContactActivity.kt index b7a10501..601f6e85 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/SelectContactActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/SelectContactActivity.kt @@ -23,7 +23,7 @@ import com.simplemobiletools.contacts.models.Contact import kotlinx.android.synthetic.main.layout_select_contact.* class SelectContactActivity : SimpleActivity() { - private var isGetEmailIntent = false + private var specialMimeType: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -34,7 +34,11 @@ class SelectContactActivity : SimpleActivity() { if (it) { handlePermission(PERMISSION_WRITE_CONTACTS) { if (it) { - isGetEmailIntent = intent.data == ContactsContract.CommonDataKinds.Email.CONTENT_URI + specialMimeType = when (intent.data) { + ContactsContract.CommonDataKinds.Email.CONTENT_URI -> ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE + ContactsContract.CommonDataKinds.Phone.CONTENT_URI -> ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + else -> null + } initContacts() } else { toast(R.string.no_contacts_permission) @@ -81,8 +85,13 @@ class SelectContactActivity : SimpleActivity() { } var contacts = it.filter { - if (isGetEmailIntent) { - (it.source != SMT_PRIVATE && it.emails.isNotEmpty()) + if (specialMimeType != null) { + val hasRequiredValues = when (specialMimeType) { + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE -> it.emails.isNotEmpty() + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> it.phoneNumbers.isNotEmpty() + else -> true + } + it.source != SMT_PRIVATE && hasRequiredValues } else { true } @@ -119,12 +128,15 @@ class SelectContactActivity : SimpleActivity() { } private fun getResultUri(contact: Contact): Uri { - return if (isGetEmailIntent) { - val emailID = ContactsHelper(this).getContactDataId(contact.id.toString()) - Uri.withAppendedPath(ContactsContract.Data.CONTENT_URI, emailID) - } else { - val lookupKey = ContactsHelper(this).getContactLookupKey(contact.id.toString()) - Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookupKey) + return when { + specialMimeType != null -> { + val contactId = ContactsHelper(this).getContactMimeTypeId(contact.id.toString(), specialMimeType!!) + Uri.withAppendedPath(ContactsContract.Data.CONTENT_URI, contactId) + } + else -> { + val lookupKey = ContactsHelper(this).getContactLookupKey(contact.id.toString()) + Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookupKey) + } } } } 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 1bffbd05..d9c17a46 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/ViewContactActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/ViewContactActivity.kt @@ -60,6 +60,7 @@ class ViewContactActivity : ContactActivity() { } private fun initContact() { + var wasLookupKeyUsed = false var contactId = intent.getIntExtra(CONTACT_ID, 0) val action = intent.action if (contactId == 0 && (action == ContactsContract.QuickContact.ACTION_QUICK_CONTACT || action == Intent.ACTION_VIEW)) { @@ -69,6 +70,7 @@ class ViewContactActivity : ContactActivity() { val lookupKey = getLookupKeyFromUri(data) if (lookupKey != null) { contact = ContactsHelper(this).getContactWithLookupKey(lookupKey) + wasLookupKeyUsed = true } getLookupUriRawId(data) @@ -82,7 +84,7 @@ class ViewContactActivity : ContactActivity() { } } - if (contactId != 0 && contact == null) { + if (contactId != 0 && !wasLookupKeyUsed) { contact = ContactsHelper(this).getContactWithId(contactId, intent.getBooleanExtra(IS_PRIVATE, false)) if (contact == null) { toast(R.string.unknown_error_occurred) 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 95a04a55..7cc74c73 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/ContactsAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/ContactsAdapter.kt @@ -11,28 +11,34 @@ import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.signature.ObjectKey import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter import com.simplemobiletools.commons.dialogs.ConfirmationDialog +import com.simplemobiletools.commons.dialogs.RadioGroupDialog import com.simplemobiletools.commons.extensions.beVisibleIf import com.simplemobiletools.commons.extensions.getColoredDrawableWithColor import com.simplemobiletools.commons.extensions.isActivityDestroyed +import com.simplemobiletools.commons.models.RadioItem 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.dialogs.CreateNewGroupDialog +import com.simplemobiletools.contacts.extensions.addContactsToGroup import com.simplemobiletools.contacts.extensions.config import com.simplemobiletools.contacts.extensions.editContact import com.simplemobiletools.contacts.extensions.shareContacts -import com.simplemobiletools.contacts.helpers.ContactsHelper +import com.simplemobiletools.contacts.helpers.* import com.simplemobiletools.contacts.interfaces.RefreshContactsListener +import com.simplemobiletools.contacts.interfaces.RemoveFromGroupListener import com.simplemobiletools.contacts.models.Contact import kotlinx.android.synthetic.main.item_contact_with_number.view.* import java.util.* -class ContactsAdapter(activity: SimpleActivity, var contactItems: ArrayList, private val listener: RefreshContactsListener?, - private val isFavoritesFragment: Boolean, recyclerView: MyRecyclerView, fastScroller: FastScroller, itemClick: (Any) -> Unit) : +class ContactsAdapter(activity: SimpleActivity, var contactItems: ArrayList, private val refreshListener: RefreshContactsListener?, + private val location: Int, private val removeListener: RemoveFromGroupListener?, recyclerView: MyRecyclerView, + fastScroller: FastScroller, itemClick: (Any) -> Unit) : MyRecyclerViewAdapter(activity, recyclerView, fastScroller, itemClick) { private lateinit var contactDrawable: Drawable - var config = activity.config + private var config = activity.config var startNameWithSurname: Boolean var showContactThumbnails: Boolean var showPhoneNumbers: Boolean @@ -52,10 +58,14 @@ class ContactsAdapter(activity: SimpleActivity, var contactItems: ArrayList editContact() R.id.cab_select_all -> selectAll() R.id.cab_add_to_favorites -> addToFavorites() + R.id.cab_add_to_group -> addToGroup() R.id.cab_share -> shareContacts() - R.id.cab_remove -> removeFavorites() + R.id.cab_remove -> removeContacts() R.id.cab_delete -> askConfirmDelete() } } @@ -102,9 +113,11 @@ class ContactsAdapter(activity: SimpleActivity, var contactItems: ArrayList) { - contactItems = newItems - notifyDataSetChanged() - finishActMode() + if (newItems.hashCode() != contactItems.hashCode()) { + contactItems = newItems + notifyDataSetChanged() + finishActMode() + } } private fun editContact() { @@ -130,38 +143,77 @@ class ContactsAdapter(activity: SimpleActivity, var contactItems: ArrayList() + private fun removeContacts() { + val contactsToRemove = ArrayList() selectedPositions.sortedDescending().forEach { - favoritesToRemove.add(contactItems[it]) + contactsToRemove.add(contactItems[it]) } - contactItems.removeAll(favoritesToRemove) + contactItems.removeAll(contactsToRemove) - ContactsHelper(activity).removeFavorites(favoritesToRemove) - if (contactItems.isEmpty()) { - listener?.refreshFavorites() - finishActMode() - } else { + if (location == LOCATION_FAVORITES_TAB) { + ContactsHelper(activity).removeFavorites(contactsToRemove) + if (contactItems.isEmpty()) { + refreshListener?.refreshContacts(FAVORITES_TAB_MASK) + finishActMode() + } else { + removeSelectedItems() + } + } else if (location == LOCATION_GROUP_CONTACTS) { + removeListener?.removeFromGroup(contactsToRemove) removeSelectedItems() } } private fun addToFavorites() { val newFavorites = ArrayList() - selectedPositions.forEach { newFavorites.add(contactItems[it]) } + selectedPositions.forEach { + newFavorites.add(contactItems[it]) + } ContactsHelper(activity).addFavorites(newFavorites) - listener?.refreshFavorites() + refreshListener?.refreshContacts(FAVORITES_TAB_MASK) finishActMode() } + private fun addToGroup() { + val selectedContacts = ArrayList() + selectedPositions.forEach { + selectedContacts.add(contactItems[it]) + } + + val NEW_GROUP_ID = -1 + val items = ArrayList() + ContactsHelper(activity).getStoredGroups().forEach { + items.add(RadioItem(it.id.toInt(), it.title)) + } + items.add(RadioItem(NEW_GROUP_ID, activity.getString(R.string.create_new_group))) + + RadioGroupDialog(activity, items, 0) { + if (it as Int == NEW_GROUP_ID) { + CreateNewGroupDialog(activity) { + Thread { + activity.addContactsToGroup(selectedContacts, it.id) + refreshListener?.refreshContacts(GROUPS_TAB_MASK) + }.start() + finishActMode() + } + } else { + Thread { + activity.addContactsToGroup(selectedContacts, it.toLong()) + refreshListener?.refreshContacts(GROUPS_TAB_MASK) + }.start() + finishActMode() + } + } + } + private fun shareContacts() { val contactsIDs = ArrayList() selectedPositions.forEach { diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/GroupsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/GroupsAdapter.kt new file mode 100644 index 00000000..41cd4464 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/GroupsAdapter.kt @@ -0,0 +1,131 @@ +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.applyColorFilter +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.dialogs.RenameGroupDialog +import com.simplemobiletools.contacts.extensions.config +import com.simplemobiletools.contacts.extensions.dbHelper +import com.simplemobiletools.contacts.helpers.ContactsHelper +import com.simplemobiletools.contacts.helpers.GROUPS_TAB_MASK +import com.simplemobiletools.contacts.interfaces.RefreshContactsListener +import com.simplemobiletools.contacts.models.Group +import kotlinx.android.synthetic.main.item_group.view.* +import java.util.* + +class GroupsAdapter(activity: SimpleActivity, var groups: ArrayList, val refreshListener: RefreshContactsListener?, recyclerView: MyRecyclerView, + fastScroller: FastScroller, itemClick: (Any) -> Unit) : MyRecyclerViewAdapter(activity, recyclerView, fastScroller, itemClick) { + + private var config = activity.config + private var smallPadding = activity.resources.getDimension(R.dimen.small_margin).toInt() + private var bigPadding = activity.resources.getDimension(R.dimen.normal_margin).toInt() + + var showContactThumbnails = config.showContactThumbnails + + override fun getActionMenuId() = R.menu.cab_groups + + override fun prepareActionMode(menu: Menu) { + menu.apply { + findItem(R.id.cab_edit).isVisible = isOneItemSelected() + } + } + + override fun prepareItemSelection(view: View) {} + + override fun markItemSelection(select: Boolean, view: View?) { + view?.group_frame?.isSelected = select + } + + override fun actionItemPressed(id: Int) { + if (selectedPositions.isEmpty()) { + return + } + + when (id) { + R.id.cab_edit -> editGroup() + R.id.cab_select_all -> selectAll() + R.id.cab_delete -> askConfirmDelete() + } + } + + override fun getSelectableItemCount() = groups.size + + 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 -> + setupView(itemView, group) + } + bindViewHolder(holder, position, view) + } + + override fun getItemCount() = groups.size + + fun updateItems(newItems: ArrayList) { + groups = newItems + notifyDataSetChanged() + finishActMode() + } + + private fun editGroup() { + RenameGroupDialog(activity, groups[selectedPositions.first()]) { + finishActMode() + refreshListener?.refreshContacts(GROUPS_TAB_MASK) + } + } + + private fun askConfirmDelete() { + ConfirmationDialog(activity) { + deleteContacts() + } + } + + private fun deleteContacts() { + if (selectedPositions.isEmpty()) { + return + } + + val groupsToRemove = ArrayList() + selectedPositions.sortedDescending().forEach { + val group = groups[it] + groupsToRemove.add(group) + if (group.isPrivateSecretGroup()) { + activity.dbHelper.deleteGroup(group.id) + } else { + ContactsHelper(activity).deleteGroup(group.id) + } + } + groups.removeAll(groupsToRemove) + + if (groups.isEmpty()) { + refreshListener?.refreshContacts(GROUPS_TAB_MASK) + finishActMode() + } else { + removeSelectedItems() + } + } + + private fun setupView(view: View, group: Group) { + view.apply { + group_name.apply { + setTextColor(textColor) + text = String.format(activity.getString(R.string.groups_placeholder), group.title, group.contactsCount.toString()) + setPadding(if (showContactThumbnails) smallPadding else bigPadding, smallPadding, smallPadding, 0) + } + + group_tmb.beVisibleIf(showContactThumbnails) + if (showContactThumbnails) { + group_tmb.applyColorFilter(textColor) + } + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/SelectContactsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/SelectContactsAdapter.kt index b2bc35cb..4e118f24 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/SelectContactsAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/SelectContactsAdapter.kt @@ -23,7 +23,7 @@ import com.simplemobiletools.contacts.models.Contact import kotlinx.android.synthetic.main.item_add_favorite_with_number.view.* import java.util.* -class SelectContactsAdapter(val activity: SimpleActivity, val contacts: List, private val selectedContacts: ArrayList, private val allowPickMultiple: Boolean, +class SelectContactsAdapter(val activity: SimpleActivity, val contacts: List, private val selectedContacts: ArrayList, private val allowPickMultiple: Boolean, private val itemClick: ((Contact) -> Unit)? = null) : RecyclerView.Adapter() { private val itemViews = SparseArray() private val selectedPositions = HashSet() @@ -39,7 +39,7 @@ class SelectContactsAdapter(val activity: SimpleActivity, val contacts: List - if (selectedContacts.contains(contact.id.toString())) { + if (selectedContacts.map { it.id }.contains(contact.id)) { selectedPositions.add(index) } } 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 1adebd21..beb17f42 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/ViewPagerAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/adapters/ViewPagerAdapter.kt @@ -5,7 +5,7 @@ import android.view.View import android.view.ViewGroup import com.simplemobiletools.contacts.R import com.simplemobiletools.contacts.activities.MainActivity -import com.simplemobiletools.contacts.interfaces.FragmentInterface +import com.simplemobiletools.contacts.fragments.MyViewPagerFragment import com.simplemobiletools.contacts.models.Contact class ViewPagerAdapter(val activity: MainActivity, val contacts: ArrayList) : PagerAdapter() { @@ -14,7 +14,7 @@ class ViewPagerAdapter(val activity: MainActivity, val contacts: ArrayList Unit) { - private var view = activity.layoutInflater.inflate(R.layout.layout_select_contact, null) - private val config = activity.config - private var allContacts = ArrayList() - - init { - ContactsHelper(activity).getContacts { - allContacts = it - - val contactSources = config.displayContactSources - if (!activity.config.showAllContacts()) { - allContacts = allContacts.filter { contactSources.contains(it.source) } as ArrayList - } - - val favorites = allContacts.filter { it.starred == 1 }.map { it.id.toString() } as ArrayList - - Contact.sorting = config.sorting - allContacts.sort() - - activity.runOnUiThread { - view.apply { - select_contact_list.adapter = SelectContactsAdapter(activity, allContacts, favorites, true) - select_contact_fastscroller.allowBubbleDisplay = activity.baseConfig.showInfoBubble - select_contact_fastscroller.setViews(select_contact_list) { - select_contact_fastscroller.updateBubbleText(allContacts[it].getBubbleText()) - } - } - } - } - - AlertDialog.Builder(activity) - .setPositiveButton(R.string.ok, { dialog, which -> dialogConfirmed() }) - .setNegativeButton(R.string.cancel, null) - .create().apply { - activity.setupDialogStuff(view, this) - } - } - - private fun dialogConfirmed() { - Thread { - val contactsHelper = ContactsHelper(activity) - val allDisplayedContacts = ArrayList() - allContacts.mapTo(allDisplayedContacts, { it }) - val selectedContacts = (view?.select_contact_list?.adapter as? SelectContactsAdapter)?.getSelectedItemsSet() ?: LinkedHashSet() - val contactsToAdd = selectedContacts.map { it } as ArrayList - contactsHelper.addFavorites(contactsToAdd) - - allDisplayedContacts.removeAll(selectedContacts) - contactsHelper.removeFavorites(allDisplayedContacts) - - callback() - }.start() - } -} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/CreateNewGroupDialog.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/CreateNewGroupDialog.kt index 5897b671..33d9f348 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/CreateNewGroupDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/CreateNewGroupDialog.kt @@ -3,12 +3,17 @@ package com.simplemobiletools.contacts.dialogs import android.support.v7.app.AlertDialog import android.view.View import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.dialogs.RadioGroupDialog import com.simplemobiletools.commons.extensions.setupDialogStuff import com.simplemobiletools.commons.extensions.showKeyboard import com.simplemobiletools.commons.extensions.toast import com.simplemobiletools.commons.extensions.value +import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.contacts.R +import com.simplemobiletools.contacts.extensions.config import com.simplemobiletools.contacts.helpers.ContactsHelper +import com.simplemobiletools.contacts.helpers.SMT_PRIVATE +import com.simplemobiletools.contacts.models.ContactSource import com.simplemobiletools.contacts.models.Group import kotlinx.android.synthetic.main.dialog_create_new_group.view.* @@ -29,13 +34,41 @@ class CreateNewGroupDialog(val activity: BaseSimpleActivity, val callback: (newG return@OnClickListener } - val newGroup = ContactsHelper(activity).createNewGroup(name) - if (newGroup != null) { - callback(newGroup) + val contactSources = ArrayList() + if (activity.config.localAccountName.isNotEmpty()) { + contactSources.add(ContactSource(activity.config.localAccountName, activity.config.localAccountType)) + } + + ContactsHelper(activity).getContactSources { + it.filter { it.type.contains("google", true) }.mapTo(contactSources, { ContactSource(it.name, it.type) }) + contactSources.add(ContactSource(activity.getString(R.string.phone_storage_hidden), SMT_PRIVATE)) + + val items = ArrayList() + contactSources.forEachIndexed { index, contactSource -> + items.add(RadioItem(index, contactSource.name)) + } + + activity.runOnUiThread { + if (items.size == 1) { + createGroupUnder(name, contactSources.first(), this) + } else { + RadioGroupDialog(activity, items, titleId = R.string.create_group_under_account) { + val contactSource = contactSources[it as Int] + createGroupUnder(name, contactSource, this) + } + } + } } - dismiss() }) } } } + + private fun createGroupUnder(name: String, contactSource: ContactSource, dialog: AlertDialog) { + val newGroup = ContactsHelper(activity).createNewGroup(name, contactSource.name, contactSource.type) + if (newGroup != null) { + callback(newGroup) + } + dialog.dismiss() + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/RenameGroupDialog.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/RenameGroupDialog.kt new file mode 100644 index 00000000..de1f654c --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/RenameGroupDialog.kt @@ -0,0 +1,49 @@ +package com.simplemobiletools.contacts.dialogs + +import android.support.v7.app.AlertDialog +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.contacts.R +import com.simplemobiletools.contacts.extensions.dbHelper +import com.simplemobiletools.contacts.helpers.ContactsHelper +import com.simplemobiletools.contacts.models.Group +import kotlinx.android.synthetic.main.dialog_rename_group.view.* + +class RenameGroupDialog(val activity: BaseSimpleActivity, val group: Group, val callback: () -> Unit) { + init { + + val view = activity.layoutInflater.inflate(R.layout.dialog_rename_group, null).apply { + rename_group_title.setText(group.title) + } + + AlertDialog.Builder(activity) + .setPositiveButton(R.string.ok, null) + .setNegativeButton(R.string.cancel, null) + .create().apply { + activity.setupDialogStuff(view, this, R.string.rename) { + showKeyboard(view.rename_group_title) + getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + val newTitle = view.rename_group_title.value + if (newTitle.isEmpty()) { + activity.toast(R.string.empty_name) + return@setOnClickListener + } + + if (!newTitle.isAValidFilename()) { + activity.toast(R.string.invalid_name) + return@setOnClickListener + } + + group.title = newTitle + if (group.isPrivateSecretGroup()) { + activity.dbHelper.renameGroup(group) + } else { + ContactsHelper(activity).renameGroup(group) + } + callback() + dismiss() + } + } + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/SelectContactsDialog.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/SelectContactsDialog.kt new file mode 100644 index 00000000..7f5e259d --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/dialogs/SelectContactsDialog.kt @@ -0,0 +1,62 @@ +package com.simplemobiletools.contacts.dialogs + +import android.support.v7.app.AlertDialog +import com.simplemobiletools.commons.extensions.baseConfig +import com.simplemobiletools.commons.extensions.setupDialogStuff +import com.simplemobiletools.contacts.R +import com.simplemobiletools.contacts.activities.SimpleActivity +import com.simplemobiletools.contacts.adapters.SelectContactsAdapter +import com.simplemobiletools.contacts.extensions.config +import com.simplemobiletools.contacts.models.Contact +import kotlinx.android.synthetic.main.layout_select_contact.view.* + +class SelectContactsDialog(val activity: SimpleActivity, initialContacts: ArrayList, val selectContacts: ArrayList? = null, + val callback: (addedContacts: ArrayList, removedContacts: ArrayList) -> Unit) { + private var view = activity.layoutInflater.inflate(R.layout.layout_select_contact, null) + private var initiallySelectedContacts = ArrayList() + + init { + var allContacts = initialContacts + if (selectContacts == null) { + val contactSources = activity.config.displayContactSources + if (!activity.config.showAllContacts()) { + allContacts = allContacts.filter { contactSources.contains(it.source) } as ArrayList + } + + initiallySelectedContacts = allContacts.filter { it.starred == 1 } as ArrayList + } else { + initiallySelectedContacts = selectContacts + } + + Contact.sorting = activity.config.sorting + allContacts.sort() + + activity.runOnUiThread { + view.apply { + select_contact_list.adapter = SelectContactsAdapter(activity, allContacts, initiallySelectedContacts, true) + select_contact_fastscroller.allowBubbleDisplay = activity.baseConfig.showInfoBubble + select_contact_fastscroller.setViews(select_contact_list) { + select_contact_fastscroller.updateBubbleText(allContacts[it].getBubbleText()) + } + } + } + + AlertDialog.Builder(activity) + .setPositiveButton(R.string.ok, { dialog, which -> dialogConfirmed() }) + .setNegativeButton(R.string.cancel, null) + .create().apply { + activity.setupDialogStuff(view, this) + } + } + + private fun dialogConfirmed() { + Thread { + val adapter = view?.select_contact_list?.adapter as? SelectContactsAdapter + val selectedContacts = adapter?.getSelectedItemsSet()?.toList() ?: ArrayList() + + val newlySelectedContacts = selectedContacts.filter { !initiallySelectedContacts.contains(it) } as ArrayList + val unselectedContacts = initiallySelectedContacts.filter { !selectedContacts.contains(it) } as ArrayList + callback(newlySelectedContacts, unselectedContacts) + }.start() + } +} 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 3aa0661c..5815f5a1 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/Activity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/Activity.kt @@ -5,6 +5,7 @@ import android.net.Uri import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.commons.dialogs.RadioGroupDialog import com.simplemobiletools.commons.extensions.sharePathIntent +import com.simplemobiletools.commons.extensions.showErrorToast import com.simplemobiletools.commons.extensions.toast import com.simplemobiletools.commons.helpers.PERMISSION_CALL_PHONE import com.simplemobiletools.commons.models.RadioItem @@ -93,6 +94,8 @@ fun BaseSimpleActivity.shareContacts(contacts: ArrayList) { VcfExporter().exportContacts(this, file, contacts) { if (it == VcfExporter.ExportResult.EXPORT_OK) { sharePathIntent(file.absolutePath, BuildConfig.APPLICATION_ID) + } else { + showErrorToast("$it") } } } @@ -108,3 +111,27 @@ fun BaseSimpleActivity.getTempFile(): File? { return File(folder, "contacts.vcf") } + +fun BaseSimpleActivity.addContactsToGroup(contacts: ArrayList, groupId: Long) { + val publicContacts = contacts.filter { it.source != SMT_PRIVATE } + val privateContacts = contacts.filter { it.source == SMT_PRIVATE } + if (publicContacts.isNotEmpty()) { + ContactsHelper(this).addContactsToGroup(contacts, groupId) + } + + if (privateContacts.isNotEmpty()) { + dbHelper.addContactsToGroup(contacts, groupId) + } +} + +fun BaseSimpleActivity.removeContactsFromGroup(contacts: ArrayList, groupId: Long) { + val publicContacts = contacts.filter { it.source != SMT_PRIVATE } + val privateContacts = contacts.filter { it.source == SMT_PRIVATE } + if (publicContacts.isNotEmpty()) { + ContactsHelper(this).removeContactsFromGroup(contacts, groupId) + } + + if (privateContacts.isNotEmpty()) { + dbHelper.removeContactsFromGroup(contacts, groupId) + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/FavoritesFragment.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/FavoritesFragment.kt index 104eaa41..a34fba56 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/FavoritesFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/FavoritesFragment.kt @@ -2,7 +2,10 @@ package com.simplemobiletools.contacts.fragments import android.content.Context import android.util.AttributeSet -import com.simplemobiletools.contacts.dialogs.AddFavoritesDialog +import com.simplemobiletools.contacts.activities.SimpleActivity +import com.simplemobiletools.contacts.dialogs.SelectContactsDialog +import com.simplemobiletools.contacts.helpers.ContactsHelper +import com.simplemobiletools.contacts.helpers.FAVORITES_TAB_MASK class FavoritesFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet) { override fun fabClicked() { @@ -15,8 +18,13 @@ class FavoritesFragment(context: Context, attributeSet: AttributeSet) : MyViewPa } private fun showAddFavoritesDialog() { - AddFavoritesDialog(activity!!) { - activity!!.refreshContacts(false, true) + SelectContactsDialog(activity!!, allContacts) { addedContacts, removedContacts -> + ContactsHelper(activity as SimpleActivity).apply { + addFavorites(addedContacts) + removeFavorites(removedContacts) + } + + activity!!.refreshContacts(FAVORITES_TAB_MASK) } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/GroupsFragment.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/GroupsFragment.kt index e52fe7e7..7b40710a 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/GroupsFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/GroupsFragment.kt @@ -1,22 +1,24 @@ package com.simplemobiletools.contacts.fragments import android.content.Context -import android.support.design.widget.CoordinatorLayout import android.util.AttributeSet -import com.simplemobiletools.contacts.activities.MainActivity -import com.simplemobiletools.contacts.interfaces.FragmentInterface -import com.simplemobiletools.contacts.models.Contact +import com.simplemobiletools.contacts.activities.SimpleActivity +import com.simplemobiletools.contacts.dialogs.CreateNewGroupDialog +import com.simplemobiletools.contacts.helpers.GROUPS_TAB_MASK -class GroupsFragment(context: Context, attributeSet: AttributeSet) : CoordinatorLayout(context, attributeSet), FragmentInterface { - override fun setupFragment(activity: MainActivity) { +class GroupsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet) { + override fun fabClicked() { + finishActMode() + showNewGroupsDialog() } - override fun textColorChanged(color: Int) { + override fun placeholderClicked() { + showNewGroupsDialog() } - override fun primaryColorChanged(color: Int) { - } - - override fun refreshContacts(contacts: ArrayList) { + private fun showNewGroupsDialog() { + CreateNewGroupDialog(activity as SimpleActivity) { + activity!!.refreshContacts(GROUPS_TAB_MASK) + } } } 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 e273e94c..1adc2d25 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/MyViewPagerFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/fragments/MyViewPagerFragment.kt @@ -1,37 +1,41 @@ package com.simplemobiletools.contacts.fragments import android.content.Context +import android.content.Intent import android.support.design.widget.CoordinatorLayout import android.util.AttributeSet import android.view.ViewGroup +import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.SORT_BY_FIRST_NAME import com.simplemobiletools.commons.helpers.SORT_BY_SURNAME import com.simplemobiletools.contacts.R +import com.simplemobiletools.contacts.activities.GroupContactsActivity 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.config import com.simplemobiletools.contacts.extensions.editContact import com.simplemobiletools.contacts.extensions.tryStartCall import com.simplemobiletools.contacts.extensions.viewContact -import com.simplemobiletools.contacts.helpers.Config -import com.simplemobiletools.contacts.helpers.ON_CLICK_CALL_CONTACT -import com.simplemobiletools.contacts.helpers.ON_CLICK_EDIT_CONTACT -import com.simplemobiletools.contacts.helpers.ON_CLICK_VIEW_CONTACT -import com.simplemobiletools.contacts.interfaces.FragmentInterface +import com.simplemobiletools.contacts.helpers.* import com.simplemobiletools.contacts.models.Contact +import com.simplemobiletools.contacts.models.Group import kotlinx.android.synthetic.main.fragment_layout.view.* -abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) : CoordinatorLayout(context, attributeSet), FragmentInterface { +abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) : CoordinatorLayout(context, attributeSet) { protected var activity: MainActivity? = null + protected var allContacts = ArrayList() + private var lastHashCode = 0 private var contactsIgnoringSearch = ArrayList() private lateinit var config: Config + var skipHashComparing = false var forceListRedraw = false - override fun setupFragment(activity: MainActivity) { + fun setupFragment(activity: MainActivity) { config = activity.config if (this.activity == null) { this.activity = activity @@ -49,36 +53,51 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) 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) } } } - override fun textColorChanged(color: Int) { - (fragment_list.adapter as ContactsAdapter).apply { - updateTextColor(color) - initDrawables() + fun textColorChanged(color: Int) { + if (this is GroupsFragment) { + (fragment_list.adapter as GroupsAdapter).updateTextColor(color) + } else { + (fragment_list.adapter as ContactsAdapter).apply { + updateTextColor(color) + initDrawables() + } } } - override fun primaryColorChanged(color: Int) { + fun primaryColorChanged() { fragment_fastscroller.updatePrimaryColor() fragment_fastscroller.updateBubblePrimaryColor() } fun startNameWithSurnameChanged(startNameWithSurname: Boolean) { - (fragment_list.adapter as ContactsAdapter).apply { - config.sorting = if (startNameWithSurname) SORT_BY_SURNAME else SORT_BY_FIRST_NAME - this@MyViewPagerFragment.activity!!.refreshContacts(true, true) + if (this !is GroupsFragment) { + (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) + } } } - override fun refreshContacts(contacts: ArrayList) { + fun refreshContacts(contacts: ArrayList) { if (config.lastUsedContactSource.isEmpty()) { val grouped = contacts.groupBy { it.source }.maxWith(compareBy { it.value.size }) config.lastUsedContactSource = grouped?.key ?: "" } - val filtered = if (this is FavoritesFragment) { + Contact.sorting = config.sorting + contacts.sort() + allContacts = contacts + + val filtered = if (this is GroupsFragment) { + contacts + } else if (this is FavoritesFragment) { contacts.filter { it.starred == 1 } as ArrayList } else { val contactSources = config.displayContactSources @@ -89,10 +108,8 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) } } - Contact.sorting = config.sorting - filtered.sort() - - if (filtered.hashCode() != lastHashCode) { + if (filtered.hashCode() != lastHashCode || skipHashComparing) { + skipHashComparing = false lastHashCode = filtered.hashCode() activity?.runOnUiThread { setupContacts(filtered) @@ -101,6 +118,56 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) } private fun setupContacts(contacts: ArrayList) { + if (this is GroupsFragment) { + setupGroupsAdapter(contacts) + } else { + setupContactsFavoritesAdapter(contacts) + } + } + + private fun setupGroupsAdapter(contacts: ArrayList) { + var storedGroups = ContactsHelper(activity!!).getStoredGroups() + contacts.forEach { + it.groups.forEach { + val group = it + val storedGroup = storedGroups.firstOrNull { it.id == group.id } + storedGroup?.addContact() + } + } + + storedGroups = storedGroups.sortedWith(compareBy { it.title }).toMutableList() as ArrayList + + fragment_placeholder_2.beVisibleIf(storedGroups.isEmpty()) + fragment_placeholder.beVisibleIf(storedGroups.isEmpty()) + fragment_list.beVisibleIf(storedGroups.isNotEmpty()) + + val currAdapter = fragment_list.adapter + if (currAdapter == null) { + GroupsAdapter(activity as SimpleActivity, storedGroups, activity, fragment_list, fragment_fastscroller) { + Intent(activity, GroupContactsActivity::class.java).apply { + putExtra(GROUP, it as Group) + activity!!.startActivity(this) + } + }.apply { + setupDragListener(true) + addVerticalDividers(true) + fragment_list.adapter = this + } + + fragment_fastscroller.setScrollTo(0) + fragment_fastscroller.setViews(fragment_list) { + val item = (fragment_list.adapter as GroupsAdapter).groups.getOrNull(it) + fragment_fastscroller.updateBubbleText(item?.getBubbleText() ?: "") + } + } else { + (currAdapter as GroupsAdapter).apply { + showContactThumbnails = activity.config.showContactThumbnails + updateItems(storedGroups) + } + } + } + + private fun setupContactsFavoritesAdapter(contacts: ArrayList) { fragment_placeholder_2.beVisibleIf(contacts.isEmpty()) fragment_placeholder.beVisibleIf(contacts.isEmpty()) fragment_list.beVisibleIf(contacts.isNotEmpty()) @@ -108,7 +175,8 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) val currAdapter = fragment_list.adapter if (currAdapter == null || forceListRedraw) { forceListRedraw = false - ContactsAdapter(activity as SimpleActivity, contacts, activity, this is FavoritesFragment, fragment_list, fragment_fastscroller) { + 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 @@ -143,9 +211,16 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) } fun showContactThumbnailsChanged(showThumbnails: Boolean) { - (fragment_list.adapter as? ContactsAdapter)?.apply { - showContactThumbnails = showThumbnails - notifyDataSetChanged() + if (this is GroupsFragment) { + (fragment_list.adapter as? GroupsAdapter)?.apply { + showContactThumbnails = showThumbnails + notifyDataSetChanged() + } + } else { + (fragment_list.adapter as? ContactsAdapter)?.apply { + showContactThumbnails = showThumbnails + notifyDataSetChanged() + } } } @@ -154,7 +229,7 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) } fun finishActMode() { - (fragment_list.adapter as? ContactsAdapter)?.finishActMode() + (fragment_list.adapter as? MyRecyclerViewAdapter)?.finishActMode() } fun onSearchQueryChanged(text: String) { 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 6f104a57..6a7555f6 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Constants.kt @@ -13,6 +13,18 @@ const val ON_CONTACT_CLICK = "on_contact_click" 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 LOCATION_CONTACTS_TAB = 0 +const val LOCATION_FAVORITES_TAB = 1 +const val LOCATION_GROUPS_TAB = 2 +const val LOCATION_GROUP_CONTACTS = 3 + +const val CONTACTS_TAB_MASK = 1 +const val FAVORITES_TAB_MASK = 2 +const val GROUPS_TAB_MASK = 4 +const val ALL_TABS_MASK = 7 // contact photo changes const val PHOTO_ADDED = 1 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 ee8c0980..06c00a22 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/ContactsHelper.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/ContactsHelper.kt @@ -11,6 +11,7 @@ import android.provider.ContactsContract import android.provider.ContactsContract.CommonDataKinds import android.provider.ContactsContract.CommonDataKinds.Note import android.provider.MediaStore +import android.text.TextUtils import android.util.SparseArray import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.commons.extensions.* @@ -101,14 +102,7 @@ class ContactsHelper(val activity: BaseSimpleActivity) { contacts[key]?.notes = notes.valueAt(i) } - val groups = getContactGroups(getStoredGroups()) - size = groups.size() - for (i in 0 until size) { - val key = groups.keyAt(i) - contacts[key]?.groups = groups.valueAt(i) - } - - activity.dbHelper.getContacts().forEach { + activity.dbHelper.getContacts(activity).forEach { contacts.put(it.id, it) } @@ -116,6 +110,15 @@ class ContactsHelper(val activity: BaseSimpleActivity) { var resultContacts = ArrayList(contactsSize) (0 until contactsSize).mapTo(resultContacts) { contacts.valueAt(it) } resultContacts = resultContacts.distinctBy { it.contactId } as ArrayList + + // groups are obtained with contactID, not rawID, so assign them to proper contacts like this + val groups = getContactGroups(getStoredGroups()) + size = groups.size() + for (i in 0 until size) { + val key = groups.keyAt(i) + resultContacts.firstOrNull { it.contactId == key }?.groups = groups.valueAt(i) + } + activity.runOnUiThread { callback(resultContacts) } @@ -336,12 +339,11 @@ class ContactsHelper(val activity: BaseSimpleActivity) { val id = cursor.getIntValue(ContactsContract.Data.CONTACT_ID) val newRowId = cursor.getLongValue(ContactsContract.Data.DATA1) + val groupTitle = storedGroups.firstOrNull { it.id == newRowId }?.title ?: continue + val group = Group(newRowId, groupTitle) if (groups[id] == null) { groups.put(id, ArrayList()) } - - val groupTitle = storedGroups.firstOrNull { it.id == newRowId }?.title ?: continue - val group = Group(newRowId, groupTitle) groups[id]!!.add(group) } while (cursor.moveToNext()) } @@ -359,10 +361,11 @@ class ContactsHelper(val activity: BaseSimpleActivity) { val uri = ContactsContract.Groups.CONTENT_URI val projection = arrayOf( ContactsContract.Groups._ID, - ContactsContract.Groups.TITLE + ContactsContract.Groups.TITLE, + ContactsContract.Groups.SYSTEM_ID ) - val selection = "${ContactsContract.Groups.AUTO_ADD} = ? AND ${ContactsContract.Groups.FAVORITES} = ? AND ${ContactsContract.Groups.SYSTEM_ID} IS NULL" + val selection = "${ContactsContract.Groups.AUTO_ADD} = ? AND ${ContactsContract.Groups.FAVORITES} = ?" val selectionArgs = arrayOf("0", "0") var cursor: Cursor? = null @@ -372,6 +375,12 @@ class ContactsHelper(val activity: BaseSimpleActivity) { do { val id = cursor.getLongValue(ContactsContract.Groups._ID) val title = cursor.getStringValue(ContactsContract.Groups.TITLE) + + val systemId = cursor.getStringValue(ContactsContract.Groups.SYSTEM_ID) + if (groups.map { it.title }.contains(title) && systemId != null) { + continue + } + groups.add(Group(id, title)) } while (cursor.moveToNext()) } @@ -381,17 +390,25 @@ class ContactsHelper(val activity: BaseSimpleActivity) { cursor?.close() } + groups.addAll(activity.dbHelper.getGroups()) return groups } - fun createNewGroup(title: String): Group? { - try { - val operations = ArrayList() - ContentProviderOperation.newInsert(ContactsContract.Groups.CONTENT_URI).apply { - withValue(ContactsContract.Groups.TITLE, title) - operations.add(build()) - } + fun createNewGroup(title: String, accountName: String, accountType: String): Group? { + if (accountType == SMT_PRIVATE) { + return activity.dbHelper.insertGroup(Group(0, title)) + } + val operations = ArrayList() + ContentProviderOperation.newInsert(ContactsContract.Groups.CONTENT_URI).apply { + withValue(ContactsContract.Groups.TITLE, title) + withValue(ContactsContract.Groups.GROUP_VISIBLE, 1) + withValue(ContactsContract.Groups.ACCOUNT_NAME, accountName) + withValue(ContactsContract.Groups.ACCOUNT_TYPE, accountType) + operations.add(build()) + } + + try { val results = activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) val rawId = ContentUris.parseId(results[0].uri) return Group(rawId, title) @@ -401,14 +418,32 @@ class ContactsHelper(val activity: BaseSimpleActivity) { return null } - fun deleteGroup(id: Long) { - try { - val operations = ArrayList() - val uri = ContentUris.withAppendedId(ContactsContract.Groups.CONTENT_URI, id).buildUpon() - .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") - .build() + fun renameGroup(group: Group) { + val operations = ArrayList() + ContentProviderOperation.newUpdate(ContactsContract.Groups.CONTENT_URI).apply { + val selection = "${ContactsContract.Groups._ID} = ?" + val selectionArgs = arrayOf(group.id.toString()) + withSelection(selection, selectionArgs) + withValue(ContactsContract.Groups.TITLE, group.title) + operations.add(build()) + } - operations.add(ContentProviderOperation.newDelete(uri).build()) + try { + activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + } catch (e: Exception) { + activity.showErrorToast(e) + } + } + + fun deleteGroup(id: Long) { + val operations = ArrayList() + val uri = ContentUris.withAppendedId(ContactsContract.Groups.CONTENT_URI, id).buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .build() + + operations.add(ContentProviderOperation.newDelete(uri).build()) + + try { activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) } catch (e: Exception) { activity.showErrorToast(e) @@ -419,7 +454,7 @@ class ContactsHelper(val activity: BaseSimpleActivity) { if (id == 0) { return null } else if (isLocalPrivate) { - return activity.dbHelper.getContactWithId(id) + return activity.dbHelper.getContactWithId(activity, id) } val selection = "${ContactsContract.Data.MIMETYPE} = ? AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?" @@ -489,6 +524,10 @@ class ContactsHelper(val activity: BaseSimpleActivity) { cursor?.close() } + if (sources.isEmpty() && activity.config.localAccountName.isEmpty() && activity.config.localAccountType.isEmpty()) { + sources.add(ContactSource("", "")) + } + sources.add(ContactSource(activity.getString(R.string.phone_storage_hidden), SMT_PRIVATE)) callback(ArrayList(sources)) }.start() @@ -563,10 +602,12 @@ class ContactsHelper(val activity: BaseSimpleActivity) { } fun updateContact(contact: Contact, photoUpdateStatus: Int): Boolean { - return if (contact.source == SMT_PRIVATE) { - activity.dbHelper.update(contact) - } else try { - activity.toast(R.string.updating) + activity.toast(R.string.updating) + if (contact.source == SMT_PRIVATE) { + return activity.dbHelper.updateContact(contact) + } + + try { val operations = ArrayList() ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI).apply { val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ?" @@ -664,11 +705,15 @@ class ContactsHelper(val activity: BaseSimpleActivity) { } // delete groups - 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.GroupMembership.CONTENT_ITEM_TYPE) - withSelection(selection, selectionArgs) - operations.add(build()) + val relevantGroupIDs = getStoredGroups().map { it.id } + if (relevantGroupIDs.isNotEmpty()) { + val IDsString = TextUtils.join(",", relevantGroupIDs) + ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI).apply { + val selection = "${ContactsContract.Data.CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ? AND ${ContactsContract.Data.DATA1} IN ($IDsString)" + val selectionArgs = arrayOf(contact.contactId.toString(), CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE) + withSelection(selection, selectionArgs) + operations.add(build()) + } } // add groups @@ -698,10 +743,10 @@ class ContactsHelper(val activity: BaseSimpleActivity) { } activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) - true + return true } catch (e: Exception) { activity.showErrorToast(e) - false + return false } } @@ -741,144 +786,170 @@ class ContactsHelper(val activity: BaseSimpleActivity) { return operations } - fun insertContact(contact: Contact): Boolean { - return if (contact.source == SMT_PRIVATE) { - insertLocalContact(contact) - } else { - try { - val operations = ArrayList() - ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI).apply { - withValue(ContactsContract.RawContacts.ACCOUNT_NAME, contact.source) - withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, getContactSourceType(contact.source)) - operations.add(build()) - } - - // names - ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { - withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) - withValue(CommonDataKinds.StructuredName.GIVEN_NAME, contact.firstName) - withValue(CommonDataKinds.StructuredName.MIDDLE_NAME, contact.middleName) - withValue(CommonDataKinds.StructuredName.FAMILY_NAME, contact.surname) - operations.add(build()) - } - - // phone numbers - contact.phoneNumbers.forEach { - ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { - withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) - withValue(CommonDataKinds.Phone.NUMBER, it.value) - withValue(CommonDataKinds.Phone.TYPE, it.type) - operations.add(build()) - } - } - - // emails - contact.emails.forEach { - ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { - withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE) - withValue(CommonDataKinds.Email.DATA, it.value) - withValue(CommonDataKinds.Email.TYPE, it.type) - operations.add(build()) - } - } - - // addresses - contact.addresses.forEach { - ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { - withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) - withValue(CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, it.value) - withValue(CommonDataKinds.StructuredPostal.TYPE, it.type) - operations.add(build()) - } - } - - // events - contact.events.forEach { - ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { - withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Event.CONTENT_ITEM_TYPE) - withValue(CommonDataKinds.Event.START_DATE, it.value) - withValue(CommonDataKinds.Event.TYPE, it.type) - operations.add(build()) - } - } - - // notes - ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { - withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - withValue(ContactsContract.Data.MIMETYPE, Note.CONTENT_ITEM_TYPE) - withValue(Note.NOTE, contact.notes) - operations.add(build()) - } - - // groups - contact.groups.forEach { - ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { - withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE) - withValue(CommonDataKinds.GroupMembership.GROUP_ROW_ID, it.id) - operations.add(build()) - } - } - - // photo (inspired by https://gist.github.com/slightfoot/5985900) - var fullSizePhotoData: ByteArray? = null - var scaledSizePhotoData: ByteArray? - if (contact.photoUri.isNotEmpty()) { - val photoUri = Uri.parse(contact.photoUri) - val bitmap = MediaStore.Images.Media.getBitmap(activity.contentResolver, photoUri) - - val thumbnailSize = activity.getPhotoThumbnailSize() - val scaledPhoto = Bitmap.createScaledBitmap(bitmap, thumbnailSize, thumbnailSize, false) - scaledSizePhotoData = scaledPhoto.getByteArray() - - fullSizePhotoData = bitmap.getByteArray() - scaledPhoto.recycle() - bitmap.recycle() - - ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { - withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE) - withValue(CommonDataKinds.Photo.PHOTO, scaledSizePhotoData) - operations.add(build()) - } - } - - val results: Array - try { - results = activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) - } finally { - scaledSizePhotoData = null - } - - // fullsize photo - val rawId = ContentUris.parseId(results[0].uri) - if (contact.photoUri.isNotEmpty() && fullSizePhotoData != null) { - addFullSizePhoto(rawId, fullSizePhotoData) - } - - // favorite - val userId = getRealContactId(rawId) - if (userId != 0 && contact.starred == 1) { - val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, userId.toString()) - val contentValues = ContentValues(1) - contentValues.put(ContactsContract.Contacts.STARRED, contact.starred) - activity.contentResolver.update(uri, contentValues, null, null) - } - - true - } catch (e: Exception) { - activity.showErrorToast(e) - false + fun addContactsToGroup(contacts: ArrayList, groupId: Long) { + val operations = ArrayList() + contacts.forEach { + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { + withValue(ContactsContract.Data.RAW_CONTACT_ID, it.id) + withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE) + withValue(CommonDataKinds.GroupMembership.GROUP_ROW_ID, groupId) + operations.add(build()) } } + activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + } + + fun removeContactsFromGroup(contacts: ArrayList, groupId: Long) { + val operations = ArrayList() + contacts.forEach { + ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI).apply { + val selection = "${ContactsContract.Data.CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ? AND ${ContactsContract.Data.DATA1} = ?" + val selectionArgs = arrayOf(it.contactId.toString(), CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE, groupId.toString()) + withSelection(selection, selectionArgs) + operations.add(build()) + } + } + activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + } + + fun insertContact(contact: Contact): Boolean { + if (contact.source == SMT_PRIVATE) { + return insertLocalContact(contact) + } + + try { + val operations = ArrayList() + ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI).apply { + withValue(ContactsContract.RawContacts.ACCOUNT_NAME, contact.source) + withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, getContactSourceType(contact.source)) + operations.add(build()) + } + + // names + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { + withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) + withValue(CommonDataKinds.StructuredName.GIVEN_NAME, contact.firstName) + withValue(CommonDataKinds.StructuredName.MIDDLE_NAME, contact.middleName) + withValue(CommonDataKinds.StructuredName.FAMILY_NAME, contact.surname) + operations.add(build()) + } + + // phone numbers + contact.phoneNumbers.forEach { + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { + withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) + withValue(CommonDataKinds.Phone.NUMBER, it.value) + withValue(CommonDataKinds.Phone.TYPE, it.type) + operations.add(build()) + } + } + + // emails + contact.emails.forEach { + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { + withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE) + withValue(CommonDataKinds.Email.DATA, it.value) + withValue(CommonDataKinds.Email.TYPE, it.type) + operations.add(build()) + } + } + + // addresses + contact.addresses.forEach { + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { + withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) + withValue(CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, it.value) + withValue(CommonDataKinds.StructuredPostal.TYPE, it.type) + operations.add(build()) + } + } + + // events + contact.events.forEach { + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { + withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Event.CONTENT_ITEM_TYPE) + withValue(CommonDataKinds.Event.START_DATE, it.value) + withValue(CommonDataKinds.Event.TYPE, it.type) + operations.add(build()) + } + } + + // notes + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { + withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + withValue(ContactsContract.Data.MIMETYPE, Note.CONTENT_ITEM_TYPE) + withValue(Note.NOTE, contact.notes) + operations.add(build()) + } + + // groups + contact.groups.forEach { + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { + withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE) + withValue(CommonDataKinds.GroupMembership.GROUP_ROW_ID, it.id) + operations.add(build()) + } + } + + // photo (inspired by https://gist.github.com/slightfoot/5985900) + var fullSizePhotoData: ByteArray? = null + var scaledSizePhotoData: ByteArray? + if (contact.photoUri.isNotEmpty()) { + val photoUri = Uri.parse(contact.photoUri) + val bitmap = MediaStore.Images.Media.getBitmap(activity.contentResolver, photoUri) + + val thumbnailSize = activity.getPhotoThumbnailSize() + val scaledPhoto = Bitmap.createScaledBitmap(bitmap, thumbnailSize, thumbnailSize, false) + scaledSizePhotoData = scaledPhoto.getByteArray() + + fullSizePhotoData = bitmap.getByteArray() + scaledPhoto.recycle() + bitmap.recycle() + + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).apply { + withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE) + withValue(CommonDataKinds.Photo.PHOTO, scaledSizePhotoData) + operations.add(build()) + } + } + + val results: Array + try { + results = activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations) + } finally { + scaledSizePhotoData = null + } + + // fullsize photo + val rawId = ContentUris.parseId(results[0].uri) + if (contact.photoUri.isNotEmpty() && fullSizePhotoData != null) { + addFullSizePhoto(rawId, fullSizePhotoData) + } + + // favorite + val userId = getRealContactId(rawId) + if (userId != 0 && contact.starred == 1) { + val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, userId.toString()) + val contentValues = ContentValues(1) + contentValues.put(ContactsContract.Contacts.STARRED, contact.starred) + activity.contentResolver.update(uri, contentValues, null, null) + } + + return true + } catch (e: Exception) { + activity.showErrorToast(e) + return false + } } - private fun insertLocalContact(contact: Contact) = activity.dbHelper.insert(contact) + private fun insertLocalContact(contact: Contact) = activity.dbHelper.insertContact(contact) private fun addFullSizePhoto(contactId: Long, fullSizePhotoData: ByteArray) { val baseUri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, contactId) @@ -909,11 +980,11 @@ class ContactsHelper(val activity: BaseSimpleActivity) { return "" } - fun getContactDataId(contactId: String): String { + fun getContactMimeTypeId(contactId: String, mimeType: String): String { val uri = ContactsContract.Data.CONTENT_URI val projection = arrayOf(ContactsContract.Data._ID, ContactsContract.Data.RAW_CONTACT_ID, ContactsContract.Data.MIMETYPE) val selection = "${ContactsContract.Data.MIMETYPE} = ? AND ${ContactsContract.Data.RAW_CONTACT_ID} = ?" - val selectionArgs = arrayOf(CommonDataKinds.Email.CONTENT_ITEM_TYPE, contactId) + val selectionArgs = arrayOf(mimeType, contactId) var cursor: Cursor? = null try { @@ -979,10 +1050,10 @@ class ContactsHelper(val activity: BaseSimpleActivity) { try { val contactIDs = HashSet() val operations = ArrayList() - val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ?" + val selection = "${ContactsContract.Data.CONTACT_ID} = ?" contacts.filter { it.source != SMT_PRIVATE }.forEach { ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI).apply { - val selectionArgs = arrayOf(it.id.toString()) + val selectionArgs = arrayOf(it.contactId.toString()) withSelection(selection, selectionArgs) operations.add(this.build()) } 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 965c5256..0067b33e 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/DBHelper.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/DBHelper.kt @@ -11,8 +11,10 @@ import android.provider.MediaStore import android.text.TextUtils import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.commons.extensions.getBlobValue import com.simplemobiletools.commons.extensions.getIntValue +import com.simplemobiletools.commons.extensions.getLongValue import com.simplemobiletools.commons.extensions.getStringValue import com.simplemobiletools.contacts.extensions.getByteArray import com.simplemobiletools.contacts.extensions.getPhotoThumbnailSize @@ -31,13 +33,17 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont private val COL_STARRED = "starred" private val COL_ADDRESSES = "addresses" private val COL_NOTES = "notes" + private val COL_GROUPS = "groups" + + private val GROUPS_TABLE_NAME = "groups" + private val COL_TITLE = "title" private val FIRST_CONTACT_ID = 1000000 private val mDb = writableDatabase companion object { - private const val DB_VERSION = 2 + private const val DB_VERSION = 3 const val DB_NAME = "contacts.db" var dbInstance: DBHelper? = null @@ -52,10 +58,12 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont override fun onCreate(db: SQLiteDatabase) { 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_ADDRESSES TEXT, $COL_NOTES TEXT, $COL_GROUPS 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)") + + createGroupsTable(db) } override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -63,15 +71,27 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont db.execSQL("ALTER TABLE $CONTACTS_TABLE_NAME ADD COLUMN $COL_ADDRESSES TEXT DEFAULT ''") db.execSQL("ALTER TABLE $CONTACTS_TABLE_NAME ADD COLUMN $COL_NOTES TEXT DEFAULT ''") } + + if (oldVersion < 3) { + createGroupsTable(db) + db.execSQL("ALTER TABLE $CONTACTS_TABLE_NAME ADD COLUMN $COL_GROUPS TEXT DEFAULT ''") + } } - fun insert(contact: Contact): Boolean { + private fun createGroupsTable(db: SQLiteDatabase) { + db.execSQL("CREATE TABLE $GROUPS_TABLE_NAME ($COL_ID INTEGER PRIMARY KEY AUTOINCREMENT, $COL_TITLE TEXT)") + + // start autoincrement ID from FIRST_GROUP_ID to avoid conflicts + db.execSQL("REPLACE INTO sqlite_sequence (name, seq) VALUES ('$GROUPS_TABLE_NAME', $FIRST_GROUP_ID)") + } + + fun insertContact(contact: Contact): Boolean { val contactValues = fillContactValues(contact) val id = mDb.insert(CONTACTS_TABLE_NAME, null, contactValues).toInt() return id != -1 } - fun update(contact: Contact): Boolean { + fun updateContact(contact: Contact): Boolean { val contactValues = fillContactValues(contact) val selection = "$COL_ID = ?" val selectionArgs = arrayOf(contact.id.toString()) @@ -97,6 +117,7 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont put(COL_EVENTS, Gson().toJson(contact.events)) put(COL_STARRED, contact.starred) put(COL_NOTES, contact.notes) + put(COL_GROUPS, Gson().toJson(contact.groups.map { it.id })) if (contact.photoUri.isNotEmpty()) { put(COL_PHOTO, getPhotoByteArray(contact.photoUri)) @@ -126,10 +147,87 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont mDb.update(CONTACTS_TABLE_NAME, contactValues, selection, null) } - fun getContacts(selection: String? = null, selectionArgs: Array? = null): ArrayList { + fun insertGroup(group: Group): Group? { + val contactValues = fillGroupValues(group) + val id = mDb.insert(GROUPS_TABLE_NAME, null, contactValues) + return if (id == -1L) { + null + } else { + Group(id, group.title) + } + } + + fun renameGroup(group: Group): Boolean { + val contactValues = fillGroupValues(group) + val selection = "$COL_ID = ?" + val selectionArgs = arrayOf(group.id.toString()) + return mDb.update(GROUPS_TABLE_NAME, contactValues, selection, selectionArgs) == 1 + } + + fun deleteGroup(id: Long) = deleteGroups(arrayOf(id.toString())) + + fun deleteGroups(ids: Array) { + val args = TextUtils.join(", ", ids) + val selection = "$GROUPS_TABLE_NAME.$COL_ID IN ($args)" + mDb.delete(GROUPS_TABLE_NAME, selection, null) + } + + fun getGroups(): ArrayList { + val groups = ArrayList() + val projection = arrayOf(COL_ID, COL_TITLE) + val cursor = mDb.query(GROUPS_TABLE_NAME, projection, null, null, null, null, null) + cursor.use { + while (cursor.moveToNext()) { + val id = cursor.getLongValue(COL_ID) + val title = cursor.getStringValue(COL_TITLE) + val group = Group(id, title) + groups.add(group) + } + } + return groups + } + + private fun fillGroupValues(group: Group): ContentValues { + return ContentValues().apply { + put(COL_TITLE, group.title) + } + } + + fun addContactsToGroup(contacts: ArrayList, groupId: Long) { + contacts.forEach { + val currentGroupIds = it.groups.map { it.id } as ArrayList + currentGroupIds.add(groupId) + updateContactGroups(it, currentGroupIds) + } + } + + fun removeContactsFromGroup(contacts: ArrayList, groupId: Long) { + contacts.forEach { + val currentGroupIds = it.groups.map { it.id } as ArrayList + currentGroupIds.remove(groupId) + updateContactGroups(it, currentGroupIds) + } + } + + fun updateContactGroups(contact: Contact, groupIds: ArrayList) { + val contactValues = fillContactGroupValues(groupIds) + val selection = "$COL_ID = ?" + val selectionArgs = arrayOf(contact.id.toString()) + mDb.update(CONTACTS_TABLE_NAME, contactValues, selection, selectionArgs) + } + + private fun fillContactGroupValues(groupIds: ArrayList): ContentValues { + return ContentValues().apply { + put(COL_GROUPS, Gson().toJson(groupIds)) + } + } + + + fun getContacts(activity: BaseSimpleActivity, selection: String? = null, selectionArgs: Array? = null): ArrayList { + val storedGroups = ContactsHelper(activity).getStoredGroups() val contacts = ArrayList() val projection = arrayOf(COL_ID, COL_FIRST_NAME, COL_MIDDLE_NAME, COL_SURNAME, COL_PHONE_NUMBERS, COL_EMAILS, COL_EVENTS, COL_STARRED, - COL_PHOTO, COL_ADDRESSES, COL_NOTES) + COL_PHOTO, COL_ADDRESSES, COL_NOTES, COL_GROUPS) val cursor = mDb.query(CONTACTS_TABLE_NAME, projection, selection, selectionArgs, null, null, null) cursor.use { while (cursor.moveToNext()) { @@ -163,7 +261,11 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont val notes = cursor.getStringValue(COL_NOTES) val starred = cursor.getIntValue(COL_STARRED) - val groups = ArrayList() + + val groupIdsJson = cursor.getStringValue(COL_GROUPS) + val groupIdsToken = object : TypeToken>() {}.type + val groupIds = Gson().fromJson>(groupIdsJson, groupIdsToken) ?: ArrayList(1) + val groups = storedGroups.filter { groupIds.contains(it.id) } as ArrayList val contact = Contact(id, firstName, middleName, surname, "", phoneNumbers, emails, addresses, events, SMT_PRIVATE, starred, id, "", photo, notes, groups) contacts.add(contact) @@ -172,9 +274,9 @@ class DBHelper private constructor(val context: Context) : SQLiteOpenHelper(cont return contacts } - fun getContactWithId(id: Int): Contact? { + fun getContactWithId(activity: BaseSimpleActivity, id: Int): Contact? { val selection = "$COL_ID = ?" val selectionArgs = arrayOf(id.toString()) - return getContacts(selection, selectionArgs).firstOrNull() + return getContacts(activity, selection, selectionArgs).firstOrNull() } } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/interfaces/FragmentInterface.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/interfaces/FragmentInterface.kt deleted file mode 100644 index 0f49e205..00000000 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/interfaces/FragmentInterface.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.simplemobiletools.contacts.interfaces - -import com.simplemobiletools.contacts.activities.MainActivity -import com.simplemobiletools.contacts.models.Contact - -interface FragmentInterface { - fun setupFragment(activity: MainActivity) - - fun textColorChanged(color: Int) - - fun primaryColorChanged(color: Int) - - fun refreshContacts(contacts: ArrayList) -} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/interfaces/RefreshContactsListener.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/interfaces/RefreshContactsListener.kt index 7819aa1f..817e6074 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/interfaces/RefreshContactsListener.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/interfaces/RefreshContactsListener.kt @@ -1,7 +1,5 @@ package com.simplemobiletools.contacts.interfaces interface RefreshContactsListener { - fun refreshContacts(refreshContactsTab: Boolean, refreshFavoritesTab: Boolean) - - fun refreshFavorites() + fun refreshContacts(refreshTabsMask: Int) } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/interfaces/RemoveFromGroupListener.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/interfaces/RemoveFromGroupListener.kt new file mode 100644 index 00000000..c435180a --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/interfaces/RemoveFromGroupListener.kt @@ -0,0 +1,7 @@ +package com.simplemobiletools.contacts.interfaces + +import com.simplemobiletools.contacts.models.Contact + +interface RemoveFromGroupListener { + fun removeFromGroup(contacts: ArrayList) +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/models/Group.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/models/Group.kt index dbd23558..1b53d894 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/models/Group.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/models/Group.kt @@ -1,3 +1,16 @@ package com.simplemobiletools.contacts.models -data class Group(var id: Long, var title: String) +import com.simplemobiletools.contacts.helpers.FIRST_GROUP_ID +import java.io.Serializable + +data class Group(var id: Long, var title: String, var contactsCount: Int = 0) : Serializable { + companion object { + private const val serialVersionUID = -1384515348451345L + } + + fun addContact() = contactsCount++ + + fun getBubbleText() = title + + fun isPrivateSecretGroup() = id >= FIRST_GROUP_ID +} diff --git a/app/src/main/res/drawable-hdpi/ic_group_add.png b/app/src/main/res/drawable-hdpi/ic_group_add.png new file mode 100644 index 00000000..af01bae4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_group_add.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_group_add.png b/app/src/main/res/drawable-xhdpi/ic_group_add.png new file mode 100644 index 00000000..a769931c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_group_add.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_group_add.png b/app/src/main/res/drawable-xxhdpi/ic_group_add.png new file mode 100644 index 00000000..f4acdf46 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_group_add.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_group_add.png b/app/src/main/res/drawable-xxxhdpi/ic_group_add.png new file mode 100644 index 00000000..fadcc0f9 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_group_add.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 66cbe244..f7a2fa11 100644 --- a/app/src/main/res/layout/activity_edit_contact.xml +++ b/app/src/main/res/layout/activity_edit_contact.xml @@ -333,7 +333,7 @@ android:layout_toRightOf="@+id/contact_name_image" android:orientation="vertical"> - + diff --git a/app/src/main/res/layout/activity_group_contacts.xml b/app/src/main/res/layout/activity_group_contacts.xml new file mode 100644 index 00000000..197db89c --- /dev/null +++ b/app/src/main/res/layout/activity_group_contacts.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_rename_group.xml b/app/src/main/res/layout/dialog_rename_group.xml new file mode 100644 index 00000000..974d2b59 --- /dev/null +++ b/app/src/main/res/layout/dialog_rename_group.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_groups.xml b/app/src/main/res/layout/fragment_groups.xml index 02751c10..75390472 100644 --- a/app/src/main/res/layout/fragment_groups.xml +++ b/app/src/main/res/layout/fragment_groups.xml @@ -5,4 +5,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + diff --git a/app/src/main/res/layout/item_add_favorite_with_number.xml b/app/src/main/res/layout/item_add_favorite_with_number.xml index 7747f757..c8544807 100644 --- a/app/src/main/res/layout/item_add_favorite_with_number.xml +++ b/app/src/main/res/layout/item_add_favorite_with_number.xml @@ -19,6 +19,7 @@ android:id="@+id/contact_tmb" android:layout_width="@dimen/normal_icon_size" android:layout_height="@dimen/normal_icon_size" + android:layout_centerVertical="true" android:padding="@dimen/medium_margin" android:src="@drawable/ic_person"/> diff --git a/app/src/main/res/layout/item_add_favorite_without_number.xml b/app/src/main/res/layout/item_add_favorite_without_number.xml index 30569bad..a9ecb39d 100644 --- a/app/src/main/res/layout/item_add_favorite_without_number.xml +++ b/app/src/main/res/layout/item_add_favorite_without_number.xml @@ -19,6 +19,7 @@ android:id="@+id/contact_tmb" android:layout_width="@dimen/normal_icon_size" android:layout_height="@dimen/normal_icon_size" + android:layout_centerVertical="true" android:padding="@dimen/medium_margin" android:src="@drawable/ic_person"/> diff --git a/app/src/main/res/layout/item_edit_group.xml b/app/src/main/res/layout/item_edit_group.xml new file mode 100644 index 00000000..5a5620e1 --- /dev/null +++ b/app/src/main/res/layout/item_edit_group.xml @@ -0,0 +1,41 @@ + + + + + + + + diff --git a/app/src/main/res/layout/item_group.xml b/app/src/main/res/layout/item_group.xml index 5a5620e1..d8106772 100644 --- a/app/src/main/res/layout/item_group.xml +++ b/app/src/main/res/layout/item_group.xml @@ -1,41 +1,39 @@ - + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:foreground="@drawable/selector"> - + android:paddingRight="@dimen/activity_margin"> - + - + + + + diff --git a/app/src/main/res/menu/cab.xml b/app/src/main/res/menu/cab.xml index c727d9bd..1e6fe78b 100644 --- a/app/src/main/res/menu/cab.xml +++ b/app/src/main/res/menu/cab.xml @@ -16,6 +16,11 @@ android:icon="@drawable/ic_star_on" android:title="@string/add_to_favorites" app:showAsAction="ifRoom"/> + + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index bda8c04a..28f5755a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -6,8 +6,6 @@ Aktualisiere… Gerätespeicher Gerätespeicher (nicht sichtbar für andere Apps) - No groups - Create a new group Neuer Kontakt Kontakt bearbeiten @@ -17,6 +15,17 @@ Zweiter Vorname Familienname + + No groups + Create a new group + Remove from group + This group is empty + Add contacts + There are no contact groups on the device + Create group + Add to group + Create group under account + Foto machen Foto auswählen @@ -80,7 +89,7 @@ Beinhaltet keine Werbung oder unnötige Berechtigungen. Sie ist komplett Open Source, alle verwendeten Farben sind anpassbar. - Diese App ist nur eine aus einer größeren Serie von schlichten Apps. Der Rest davon findet sich auf http://www.simplemobiletools.com + Diese App ist nur eine aus einer größeren Serie von schlichten Apps. Der Rest davon findet sich auf https://www.simplemobiletools.com + Δεν υπάρχουν ομάδες + Δημιουργία νέας ομάδας + Αφαίρεση από ομάδα + Η ομάδα είναι άδεια + Προσθήκη επαφών + Δεν υπάρχουν ομάδες επαφών στη συσκευή + Δημιουργία ομάδας + Προσθήκη σε ομάδα + Δημιουργία ομάδας κάτω από λογαριασμό + + + Λήψη φωτογραφίας + Επιλογή φωτογραφίας + Αφαίρεση φωτογραφίας + + + Το όνομα ξεκινά με το επώνυμο + Εμφάνιση τηλεφωνικών αριθμών στην κύρια οθόνη + Εμφάνιση μικρογραφιών επαφής + Στην επιλογή επαφής + Κλήση επαφής + Εμφάνιση λεπτομερειών επαφής + Εμφάνιση καρτέλας αγαπημένων + Εμφάνιση καρτέλας ομάδων + + + Email + Σπίτι + Εργασία + Άλλο + + + Αριθμός + Κινητό + Κύριο + Φαξ εργασίας + Φαξ σπιτιού + Βομβητής + Δεν βρέθηκε τηλεφωνικός αριθμός + + + Γενέθλια + Επέτειος + + + Φαίνεται ότι δεν έχεις προσθέσει αγαπημένες επαφές ακόμα. + Προσθήκη αγαπημένων + Προσθήκη στα αγαπημένα + Αφαίρεση από τα αγαπημένα + + + Αναζήτηση επαφών + Αναζήτηση αγαπημένων + + + Εισαγωγή επαφών + Εξαγωγή επαφών + Εισαγωγή επαφών από .vcf αρχείο + Εξαγωγή επαφών σε .vcf αρχείο + Πηγή επαφής προορισμού + Συμπερίληψη πηγών επαφών + Όνομα αρχείου (χωρίς .vcf) + + + + Μια εφαρμογή επαφών για να διαχειρίζεσαι τις επαφές σου χωρίς διαφημίσεις. + + Μια απλή εφαρμογή για δημιουργία και διαχείριση των επαφών σου από κάθε πηγή. Οι επαφές μπορεί να είναι αποθηκευμένες μόνο στη συσκευή σου, αλλά μπορούν να συγχρονίζονται στο Google, ή σε κάποιο άλλο λογαριασμό. Μπορείς να εμφανίσεις τις αγαπημένες σου επαφές σε ξεχωριστή λίστα. + + Μπορείς να τη χρησιμοποιήσεις για τη διαχείριση των email των χρηστών και τα γεγονότα. Έχει τη δυνατότητα ταξινόμησης/φιλτραρίσματος με διάφορες παραμέτρους, προαιρετικά να εμφανίζεται το επώνυμο σαν όνομα. + + Δεν περιέχει διαφημίσεις ή περιττές άδειες. Είναι πλήρως ανοικτού κώδικα, παρέχει δυνατότητα προσαρμογής των χρωμάτων. + + Αυτή η εφαρμογή είναι ένα μικρό κομμάτι μιας μεγαλύτερης συλλογής εφαρμογών. Μπορείς να βρεις τις υπόλοιπες στο https://www.simplemobiletools.com + + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b6e38037..65a677e0 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -6,8 +6,6 @@ Mise à jour… Stockage du téléphone Stockage du téléphone (non visible par d\'autres applis) - No groups - Create a new group Nouveau contact Modifier contact @@ -17,6 +15,17 @@ Nom Surnom + + No groups + Create a new group + Remove from group + This group is empty + Add contacts + There are no contact groups on the device + Create group + Add to group + Create group under account + Prendre une photo Choisir une photo @@ -80,7 +89,7 @@ Aucune publicité ni de permission inutile. Elle est entièrement open source et vous permet de personnaliser les couleurs. - Cette application fait parti d\'un groupe d\'applications. Vous pouvez trouver le reste des applis sur http://www.simplemobiletools.com + Cette application fait parti d\'un groupe d\'applications. Vous pouvez trouver le reste des applis sur https://www.simplemobiletools.com + No groups + Create a new group + Remove from group + This group is empty + Add contacts + There are no contact groups on the device + Create group + Add to group + Create group under account + 사진 촬영 사진 선택 @@ -80,7 +89,7 @@ 광고가 포함되어 있거나, 불필요한 권한을 요청하지 않습니다. 이 앱의 모든 소스는 오픈소스이며, 사용자가 직접 애플리케이션의 컬러를 설정 할 수 있습니다. - 이 앱은 다양한 시리즈의 모바일앱 중 하나입니다. 나머지는 http://www.simplemobiletools.com 에서 찾아 보실 수 있습니다. + 이 앱은 다양한 시리즈의 모바일앱 중 하나입니다. 나머지는 https://www.simplemobiletools.com 에서 찾아 보실 수 있습니다. + Nėra grupių + Sukurti naują grupę + Remove from group + This group is empty + Add contacts + There are no contact groups on the device + Create group + Add to group + Create group under account + + + Nufotografuoti + Pasirinkti nuotrauką + Pašalinti nuotrauką + + + Pavardė rodoma pirma + Rodyti telefono numerius pagrindiniame programos ekrane + Rodyti kontaktų miniatiūras + Ant kontakto paspaudimo + Skambinti kontaktui + Žiūrėti kontakto detales + Rodyti mėgiamiausiųjų skirtuką + Rodyti grupių skirtuką + + + Elektroninis paštas + Namų + Darbo + Kitas + + + Numeris + Mobilus + Pagrindinis + Darbo faksas + Namų faksas + Pranešimų gaviklis + Nerasta telefono numerio + + + Gimtadienis + Sukaktis + + + Atrodo jog Jūs dar neįvedėte nė vieno mėgiamiausiojo kontakto. + Pridėti mėgiamiausiuosius + Pridėti į mėgiamiausiuosius + Pašalinti iš mėgiamiausiųjų + + + Ieškoti kontaktų + Ieškoti mėgiamiausiųjų + + + Importuoti kontaktus + Eksportuoti kontaktus + Importuoti kontaktus iš .vcf bylos + Eksportuoti kontaktus į .vcf bylą + Tikslinis kontakto šaltinis + Įtraukti kontaktų šaltinius + Bylos vardas (be .vcf) + + + + Kontaktų programėlė įrenginio kontaktų tvarkymui, be reklamų. + + Paprasta programėlė įrenginio kontaktų kūrimui ir tvarkymui iš įvairių šaltinių. Kontaktai gali būti saugomi Jūsų įrenginyje, taip pat sinchronizuojami per Google, ar kitas paskyras. Jūs galite matyti mėgiamiausiuosius kontaktus atskirame sąraše. + + Jūs taip pat galite naudoti programėlę elektroninių paštų adresų tvarkymui. Programėlė turi galimybę rikiuoti/filtruoti pagal įvairius parametrus, taip pat rodyti pavardę pirma vardo. + + Neturi reklamų ar nereikalingų leidimų. Programėlė visiškai atviro kodo, yra galimybė keisti spalvas. + + Ši programėle yra vienintelė iš keletos mūsų programėlių. Likusias Jūs galite rasti čia https://www.simplemobiletools.com + + + + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 2d19395b..8c1521ee 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -6,8 +6,6 @@ A atualizar… Armazenamento do telefone Armazenamento do telefone (não visível por outras alicações) - No groups - Create a new group Novo contacto Editar contacto @@ -17,6 +15,17 @@ Segundo nome Apelido + + No groups + Create a new group + Remove from group + This group is empty + Add contacts + There are no contact groups on the device + Create group + Add to group + Create group under account + Tirar foto Escolher foto @@ -80,7 +89,7 @@ Contains no ads or unnecessary permissions. It is fully opensource, provides customizable colors. - This app is just one piece of a bigger series of apps. You can find the rest of them at http://www.simplemobiletools.com + This app is just one piece of a bigger series of apps. You can find the rest of them at https://www.simplemobiletools.com + Нет групп + Создать новую группу + Remove from group + This group is empty + Add contacts + There are no contact groups on the device + Create group + Add to group + Create group under account + Снять фото Выбрать фото @@ -80,7 +89,7 @@ Не содержит рекламы или ненужных разрешений, полностью с открытым исходным кодом. Есть возможность настраивать цвета. - Это приложение — всего лишь частица из большой серии приложений. Вы можете найти остальные на http://www.simplemobiletools.com + Это приложение — всего лишь частица из большой серии приложений. Вы можете найти остальные на https://www.simplemobiletools.com + Žiadne skupiny + Vytvoriť novú skupinu + Odstrániť zo skupiny + Skupina je prázdna + Pridať kontakty + Nemáte v zariadení vytvorené žiadne skupiny kontaktov + Vytvoriť skupinu + Pridať do skupiny + Vytvoriť skupinu pod účet + Vytvoriť foto Zvoliť foto @@ -80,7 +89,7 @@ Neobsahuje žiadne reklamy a nepotrebné oprávnenia. Je opensource, poskytuje možnosť zmeny farieb. - Táto aplikácia je iba jednou zo skupiny aplikácií. Ostatné viete nájsť na http://www.simplemobiletools.com + Táto aplikácia je iba jednou zo skupiny aplikácií. Ostatné viete nájsť na https://www.simplemobiletools.com + No groups + Create a new group + Remove from group + This group is empty + Add contacts + There are no contact groups on the device + Create group + Add to group + Create group under account + Ta foto Välj foto @@ -80,7 +89,7 @@ Innehåller ingen reklam eller onödiga behörigheter. Den har helt öppen källkod och anpassningsbara färger. - Denna app är bara en del av en större serie appar. Du hittar resten av dem på http://www.simplemobiletools.com + Denna app är bara en del av en större serie appar. Du hittar resten av dem på https://www.simplemobiletools.com + 沒有群組 + 建立一個新群組 + 從群組內移除 + 這群組是空白的 + 添加聯絡人 + 裝置內沒有聯絡人群組 + 建立群組 + 添加到群組 + Create group under account + 拍照 選擇相片 @@ -80,7 +89,7 @@ 不包含廣告及非必要的權限,而且完全開放原始碼,並提供自訂顏色。 - 這程式只是一系列眾多應用程式的其中一項,你可以在這發現更多 http://www.simplemobiletools.com + 這程式只是一系列眾多應用程式的其中一項,你可以在這發現更多 https://www.simplemobiletools.com + No groups + Create a new group + Remove from group + This group is empty + Add contacts + There are no contact groups on the device + Create group + Add to group + Create group under account + Take photo Choose photo @@ -80,7 +89,7 @@ Contains no ads or unnecessary permissions. It is fully opensource, provides customizable colors. - This app is just one piece of a bigger series of apps. You can find the rest of them at http://www.simplemobiletools.com + This app is just one piece of a bigger series of apps. You can find the rest of them at https://www.simplemobiletools.com