diff --git a/atox/src/main/AndroidManifest.xml b/atox/src/main/AndroidManifest.xml
index 31258873..7487de10 100644
--- a/atox/src/main/AndroidManifest.xml
+++ b/atox/src/main/AndroidManifest.xml
@@ -17,7 +17,7 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/launcher_icon_round"
android:supportsRtl="true"
- android:theme="@style/AppTheme">
+ android:theme="@style/Theme.aTox.DayNight">
diff --git a/atox/src/main/kotlin/Extensions.kt b/atox/src/main/kotlin/Extensions.kt
index d512dd9b..17859e5a 100644
--- a/atox/src/main/kotlin/Extensions.kt
+++ b/atox/src/main/kotlin/Extensions.kt
@@ -6,6 +6,7 @@ package ltd.evilcorp.atox
import android.content.Context
import android.content.pm.PackageManager
+import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import ltd.evilcorp.atox.di.ViewModelFactory
@@ -21,6 +22,12 @@ class NoSuchArgumentException(arg: String) : Exception("No such argument: $arg")
fun Fragment.requireStringArg(key: String) =
arguments?.getString(key) ?: throw NoSuchArgumentException(key)
+fun Fragment.getColor(@ColorRes id: Int) =
+ ContextCompat.getColor(requireContext(), id)
+
+fun Fragment.getColorStateList(@ColorRes id: Int) =
+ ContextCompat.getColorStateList(requireContext(), id)
+
fun String.truncated(length: Int): String =
if (this.length > length) {
this.take(length - 1) + "…"
diff --git a/atox/src/main/kotlin/di/ViewModelModule.kt b/atox/src/main/kotlin/di/ViewModelModule.kt
index 94088ad5..80801e88 100644
--- a/atox/src/main/kotlin/di/ViewModelModule.kt
+++ b/atox/src/main/kotlin/di/ViewModelModule.kt
@@ -16,6 +16,8 @@ import ltd.evilcorp.atox.ui.chat.ChatViewModel
import ltd.evilcorp.atox.ui.contact_profile.ContactProfileViewModel
import ltd.evilcorp.atox.ui.contactlist.ContactListViewModel
import ltd.evilcorp.atox.ui.create_profile.CreateProfileViewModel
+import ltd.evilcorp.atox.ui.edit_text_value_dialog.EditTextValueDialogViewModel
+import ltd.evilcorp.atox.ui.edit_user_profile.EditUserProfileViewModel
import ltd.evilcorp.atox.ui.friend_request.FriendRequestViewModel
import ltd.evilcorp.atox.ui.settings.SettingsViewModel
import ltd.evilcorp.atox.ui.user_profile.UserProfileViewModel
@@ -77,4 +79,14 @@ abstract class ViewModelModule {
@IntoMap
@ViewModelKey(UserProfileViewModel::class)
abstract fun bindUserProfileViewModel(vm: UserProfileViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(EditUserProfileViewModel::class)
+ abstract fun bindEditUserProfileViewModel(vm: EditUserProfileViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(EditTextValueDialogViewModel::class)
+ abstract fun bindEditTextValueDialogViewModel(vm: EditTextValueDialogViewModel): ViewModel
}
diff --git a/atox/src/main/kotlin/ui/AvatarMaker.kt b/atox/src/main/kotlin/ui/AvatarMaker.kt
new file mode 100644
index 00000000..0923eec2
--- /dev/null
+++ b/atox/src/main/kotlin/ui/AvatarMaker.kt
@@ -0,0 +1,106 @@
+// SPDX-FileCopyrightText: 2019-2021 aTox contributors
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+package ltd.evilcorp.atox.ui
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.Typeface
+import android.net.Uri
+import android.widget.ImageView
+import kotlin.math.abs
+import ltd.evilcorp.atox.R
+import ltd.evilcorp.core.vo.Contact
+import ltd.evilcorp.core.vo.User
+
+internal enum class SizeUnit {
+ DP,
+ PX,
+}
+
+/**
+ * Class for creating an avatar for user or contact and setting it in the ImageView
+ */
+internal class AvatarMaker {
+
+ companion object {
+ private const val DEFAULT_AVATAR_SIZE_DP = 50
+ }
+
+ private var name: String = ""
+ private var publicKey: String = ""
+ private var avatarUri: String = ""
+ private var initials: String = ""
+
+ constructor(contact: Contact) {
+ name = contact.name
+ publicKey = contact.publicKey
+ avatarUri = contact.avatarUri
+ initials = getInitials()
+ }
+ constructor(user: User) {
+ name = user.name
+ publicKey = user.publicKey
+ avatarUri = user.avatarUri
+ initials = getInitials()
+ }
+
+ /**
+ * Method will get the initial characters of the name
+ * @return The initial characters of the name.
+ */
+ private fun getInitials(): String {
+ val segments = name.split(" ")
+ if (segments.size == 1) return segments.first().take(1)
+ return segments.first().take(1) + segments[1][0]
+ }
+
+ /**
+ * Method will set an avatar to an image view. If avatar image exists then it will be set to the image view,
+ * otherwise a new avatar image will be created based on the initials of the name
+ * and the public key for the background color.
+ * @param imageView The image view for whom to set the avatar image.
+ * @param size The size of the avatar image in the units specified in sizeUnit (default: DP units).
+ * @param sizeUnit The size unit of size parameter.
+ */
+ fun setAvatar(imageView: ImageView, size: Int = DEFAULT_AVATAR_SIZE_DP, sizeUnit: SizeUnit = SizeUnit.DP) =
+ if (avatarUri.isNotEmpty()) {
+ imageView.setImageURI(Uri.parse(avatarUri))
+ } else {
+ val side: Int
+ val textScale: Float
+
+ if (sizeUnit == SizeUnit.DP) {
+ side = (size * imageView.resources.displayMetrics.density).toInt()
+ textScale = size.toFloat() / DEFAULT_AVATAR_SIZE_DP
+ } else {
+ side = size
+ textScale = size.toFloat() / dpToPx(DEFAULT_AVATAR_SIZE_DP.toFloat(), imageView.resources)
+ }
+
+ val bitmap = Bitmap.createBitmap(side, side, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(bitmap)
+ val rect = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat())
+ val colors = imageView.resources.getIntArray(R.array.contactBackgrounds)
+ val backgroundPaint = Paint().apply { color = colors[abs(publicKey.hashCode()).rem(colors.size)] }
+
+ val textPaint = Paint().apply {
+ color = Color.WHITE
+ textSize = imageView.resources.getDimension(R.dimen.contact_avatar_placeholder_text) * textScale
+ textAlign = Paint.Align.CENTER
+ isAntiAlias = true
+ typeface = Typeface.create("sans-serif-light", Typeface.NORMAL)
+ }
+
+ val textBounds = Rect()
+ textPaint.getTextBounds(initials, 0, initials.length, textBounds)
+ canvas.drawRoundRect(rect, rect.bottom, rect.right, backgroundPaint)
+ canvas.drawText(initials, rect.centerX(), rect.centerY() - textBounds.exactCenterY(), textPaint)
+ imageView.setImageBitmap(bitmap)
+ }
+}
diff --git a/atox/src/main/kotlin/ui/NotificationHelper.kt b/atox/src/main/kotlin/ui/NotificationHelper.kt
index e2cd77b5..92bd43d1 100644
--- a/atox/src/main/kotlin/ui/NotificationHelper.kt
+++ b/atox/src/main/kotlin/ui/NotificationHelper.kt
@@ -111,7 +111,7 @@ class NotificationHelper @Inject constructor(
.addAction(
NotificationCompat.Action
.Builder(
- IconCompat.createWithResource(context, R.drawable.send),
+ IconCompat.createWithResource(context, R.drawable.ic_send),
context.getString(R.string.reply),
PendingIntentCompat.getBroadcast(
context,
diff --git a/atox/src/main/kotlin/ui/StatusDialog.kt b/atox/src/main/kotlin/ui/StatusDialog.kt
deleted file mode 100644
index 5d0522c8..00000000
--- a/atox/src/main/kotlin/ui/StatusDialog.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-// SPDX-FileCopyrightText: 2020 aTox contributors
-//
-// SPDX-License-Identifier: GPL-3.0-only
-
-package ltd.evilcorp.atox.ui
-
-import android.app.Dialog
-import android.content.Context
-import android.graphics.Color
-import android.graphics.drawable.ColorDrawable
-import android.graphics.drawable.TransitionDrawable
-import android.os.Bundle
-import android.view.Window
-import javax.inject.Inject
-import ltd.evilcorp.atox.R
-import ltd.evilcorp.atox.databinding.DialogStatusBinding
-import ltd.evilcorp.core.vo.UserStatus
-import ltd.evilcorp.domain.feature.UserManager
-
-private const val TRANSITION_TIME = 250
-
-class StatusDialog(
- ctx: Context,
- private var activeStatus: UserStatus,
- private val setStatusFunc: (UserStatus) -> Unit
-) : Dialog(ctx, R.style.DialogSlideAnimation) {
- @Inject
- lateinit var userManager: UserManager
-
- private var _binding: DialogStatusBinding? = null
- private val binding get() = _binding!!
-
- private fun viewByStatus(status: UserStatus): TransitionDrawable = when (status) {
- UserStatus.None -> binding.statusAvailable.background as TransitionDrawable
- UserStatus.Away -> binding.statusAway.background as TransitionDrawable
- UserStatus.Busy -> binding.statusBusy.background as TransitionDrawable
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- requestWindowFeature(Window.FEATURE_NO_TITLE)
- _binding = DialogStatusBinding.inflate(layoutInflater)
- setContentView(binding.root)
- window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
-
- selectStatus(activeStatus)
- binding.run {
- statusAvailable.setOnClickListener { selectStatus(UserStatus.None) }
- statusAway.setOnClickListener { selectStatus(UserStatus.Away) }
- statusBusy.setOnClickListener { selectStatus(UserStatus.Busy) }
-
- cancel.setOnClickListener { dismiss() }
- confirm.setOnClickListener {
- setStatusFunc(activeStatus)
- dismiss()
- }
- }
- }
-
- private fun selectStatus(status: UserStatus) {
- viewByStatus(activeStatus).reverseTransition(TRANSITION_TIME)
- activeStatus = status
- viewByStatus(activeStatus).startTransition(TRANSITION_TIME)
- }
-}
diff --git a/atox/src/main/kotlin/ui/Util.kt b/atox/src/main/kotlin/ui/Util.kt
index 08bc2dd8..73e3314e 100644
--- a/atox/src/main/kotlin/ui/Util.kt
+++ b/atox/src/main/kotlin/ui/Util.kt
@@ -4,66 +4,96 @@
package ltd.evilcorp.atox.ui
+import android.content.Context
+import android.content.res.ColorStateList
+import android.content.res.Configuration
import android.content.res.Resources
-import android.graphics.Bitmap
-import android.graphics.Canvas
import android.graphics.Color
-import android.graphics.Paint
-import android.graphics.Rect
-import android.graphics.RectF
-import android.graphics.Typeface
-import android.net.Uri
-import android.widget.ImageView
+import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.RippleDrawable
+import android.os.Build
+import android.util.TypedValue
+import android.widget.ImageButton
import androidx.core.content.res.ResourcesCompat
-import kotlin.math.abs
import ltd.evilcorp.atox.R
import ltd.evilcorp.core.vo.ConnectionStatus
import ltd.evilcorp.core.vo.Contact
import ltd.evilcorp.core.vo.UserStatus
-internal fun colorByStatus(resources: Resources, contact: Contact): Int {
- if (contact.connectionStatus == ConnectionStatus.None) return ResourcesCompat.getColor(
- resources,
- R.color.statusOffline,
- null
- )
- return when (contact.status) {
- UserStatus.None -> ResourcesCompat.getColor(resources, R.color.statusAvailable, null)
- UserStatus.Away -> ResourcesCompat.getColor(resources, R.color.statusAway, null)
- UserStatus.Busy -> ResourcesCompat.getColor(resources, R.color.statusBusy, null)
- }
-}
+/**
+ * Function will return the color of the status of the input contact
+ * @param resources The resources of the app
+ * @param contact The contact for whom to retrieve the status color.
+ * @return The color int.
+ */
+internal fun colorByContactStatus(resources: Resources, contact: Contact) =
+ if (contact.connectionStatus == ConnectionStatus.None)
+ ResourcesCompat.getColor(
+ resources,
+ R.color.statusOffline,
+ null
+ )
+ else colorFromStatus(resources, contact.status)
-private fun getInitials(contact: Contact): String {
- val segments = contact.name.split(" ")
- if (segments.size == 1) return segments.first().take(1)
- return segments.first().take(1) + segments[1][0]
+/**
+ * Function will return the color of the status of the input user status
+ * @param resources The resources of the app
+ * @param status The user status for whom to return the corresponding color.
+ * @return The color int.
+ */
+internal fun colorFromStatus(resources: Resources, status: UserStatus) = when (status) {
+ UserStatus.None -> ResourcesCompat.getColor(resources, R.color.statusAvailable, null)
+ UserStatus.Away -> ResourcesCompat.getColor(resources, R.color.statusAway, null)
+ UserStatus.Busy -> ResourcesCompat.getColor(resources, R.color.statusBusy, null)
}
-private const val DEFAULT_AVATAR_SIZE_DP = 50
-internal fun setAvatarFromContact(imageView: ImageView, contact: Contact, sizeDp: Int = DEFAULT_AVATAR_SIZE_DP) =
- if (contact.avatarUri.isNotEmpty()) {
- imageView.setImageURI(Uri.parse(contact.avatarUri))
- } else {
- val side = (sizeDp * imageView.resources.displayMetrics.density).toInt()
- val bitmap = Bitmap.createBitmap(side, side, Bitmap.Config.ARGB_8888)
- val canvas = Canvas(bitmap)
- val rect = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat())
- val colors = imageView.resources.getIntArray(R.array.contactBackgrounds)
- val backgroundPaint = Paint().apply { color = colors[abs(contact.publicKey.hashCode()).rem(colors.size)] }
+/**
+ * Function will convert dp (Density Pixels) units to px (Pixels) units
+ * @param dp The dp units.
+ * @return The px units as Int.
+ */
+internal fun dpToPx(dp: Float, res: Resources): Int =
+ TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, res.displayMetrics).toInt()
+
+/**
+ * Function will return whether or not night mode is set.
+ * @param context The related context.
+ * @return Boolean indicating whether or not night mode is set.
+ */
+internal fun isNightMode(context: Context) = context.resources.configuration.uiMode
+ .and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
+
+/**
+ * Function will set a transparent background to an image button with a ripple with color according
+ * to whether or not night mode is set.
+ * @param context The related context.
+ * @param imageButton The ImageButton for whom to set the transparent background with ripple.
+ */
+internal fun setImageButtonRippleDayNight(context: Context, imageButton: ImageButton) =
+ if (isNightMode(context))
+ setImageButtonRipple(imageButton, Color.argb(51, 255, 255, 255))
+ else setImageButtonRipple(imageButton, Color.argb(31, 0, 0, 0))
- val textScale = sizeDp.toFloat() / DEFAULT_AVATAR_SIZE_DP
- val textPaint = Paint().apply {
- color = Color.WHITE
- textSize = imageView.resources.getDimension(R.dimen.contact_avatar_placeholder_text) * textScale
- textAlign = Paint.Align.CENTER
- isAntiAlias = true
- typeface = Typeface.create("sans-serif-light", Typeface.NORMAL)
- }
- val initials = getInitials(contact)
- val textBounds = Rect()
- textPaint.getTextBounds(initials, 0, initials.length, textBounds)
- canvas.drawRoundRect(rect, rect.bottom, rect.right, backgroundPaint)
- canvas.drawText(initials, rect.centerX(), rect.centerY() - textBounds.exactCenterY(), textPaint)
- imageView.setImageBitmap(bitmap)
+/**
+ * Function will set a transparent background to an image button with a ripple with the input color.
+ * @param imageButton The ImageButton for whom to set the transparent background with ripple.
+ * @param colorInt The color of the ripple.
+ */
+internal fun setImageButtonRipple(imageButton: ImageButton, colorInt: Int) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ val background = GradientDrawable()
+ background.shape = GradientDrawable.OVAL
+ background.setColor(0x0FFFFFF)
+
+ background.cornerRadius = 10f
+
+ val mask = GradientDrawable()
+ mask.shape = GradientDrawable.OVAL
+ mask.setColor(-0x1000000)
+ mask.cornerRadius = 5f
+
+ val rippleColorLst = ColorStateList.valueOf(colorInt)
+ val ripple = RippleDrawable(rippleColorLst, background, mask)
+ imageButton.background = ripple
}
+}
diff --git a/atox/src/main/kotlin/ui/addcontact/AddContactFragment.kt b/atox/src/main/kotlin/ui/addcontact/AddContactFragment.kt
index c93ebdce..5486543c 100644
--- a/atox/src/main/kotlin/ui/addcontact/AddContactFragment.kt
+++ b/atox/src/main/kotlin/ui/addcontact/AddContactFragment.kt
@@ -61,7 +61,7 @@ class AddContactFragment : BaseFragment(FragmentAddCo
contacts = it
}
- toolbar.setNavigationIcon(R.drawable.back)
+ toolbar.setNavigationIcon(R.drawable.ic_back)
toolbar.setNavigationOnClickListener {
WindowInsetsControllerCompat(requireActivity().window, view)
.hide(WindowInsetsCompat.Type.ime())
diff --git a/atox/src/main/kotlin/ui/call/CallFragment.kt b/atox/src/main/kotlin/ui/call/CallFragment.kt
index ab41738d..5ec35fc2 100644
--- a/atox/src/main/kotlin/ui/call/CallFragment.kt
+++ b/atox/src/main/kotlin/ui/call/CallFragment.kt
@@ -19,9 +19,9 @@ import ltd.evilcorp.atox.R
import ltd.evilcorp.atox.databinding.FragmentCallBinding
import ltd.evilcorp.atox.hasPermission
import ltd.evilcorp.atox.requireStringArg
+import ltd.evilcorp.atox.ui.AvatarMaker
import ltd.evilcorp.atox.ui.BaseFragment
import ltd.evilcorp.atox.ui.chat.CONTACT_PUBLIC_KEY
-import ltd.evilcorp.atox.ui.setAvatarFromContact
import ltd.evilcorp.atox.vmFactory
import ltd.evilcorp.domain.feature.CallState
import ltd.evilcorp.domain.tox.PublicKey
@@ -52,7 +52,7 @@ class CallFragment : BaseFragment(FragmentCallBinding::infl
vm.setActiveContact(PublicKey(requireStringArg(CONTACT_PUBLIC_KEY)))
vm.contact.observe(viewLifecycleOwner) {
- setAvatarFromContact(callBackground, it, CALL_BACKGROUND_SIZE_DP)
+ AvatarMaker(it).setAvatar(callBackground, CALL_BACKGROUND_SIZE_DP)
}
endCall.setOnClickListener {
diff --git a/atox/src/main/kotlin/ui/chat/ChatFragment.kt b/atox/src/main/kotlin/ui/chat/ChatFragment.kt
index 3e8f2e3f..5edd9801 100644
--- a/atox/src/main/kotlin/ui/chat/ChatFragment.kt
+++ b/atox/src/main/kotlin/ui/chat/ChatFragment.kt
@@ -41,9 +41,9 @@ import ltd.evilcorp.atox.R
import ltd.evilcorp.atox.databinding.FragmentChatBinding
import ltd.evilcorp.atox.requireStringArg
import ltd.evilcorp.atox.truncated
+import ltd.evilcorp.atox.ui.AvatarMaker
import ltd.evilcorp.atox.ui.BaseFragment
-import ltd.evilcorp.atox.ui.colorByStatus
-import ltd.evilcorp.atox.ui.setAvatarFromContact
+import ltd.evilcorp.atox.ui.colorByContactStatus
import ltd.evilcorp.atox.vmFactory
import ltd.evilcorp.core.vo.ConnectionStatus
import ltd.evilcorp.core.vo.FileTransfer
@@ -135,7 +135,7 @@ class ChatFragment : BaseFragment(FragmentChatBinding::infl
}
)
- toolbar.setNavigationIcon(R.drawable.back)
+ toolbar.setNavigationIcon(R.drawable.ic_back)
toolbar.setNavigationOnClickListener {
WindowInsetsControllerCompat(requireActivity().window, view).hide(WindowInsetsCompat.Type.ime())
activity?.onBackPressed()
@@ -189,8 +189,8 @@ class ChatFragment : BaseFragment(FragmentChatBinding::infl
else -> DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT).format(it.lastMessage)
}.lowercase(Locale.getDefault())
- profileLayout.statusIndicator.setColorFilter(colorByStatus(resources, it))
- setAvatarFromContact(profileLayout.profileImage, it)
+ profileLayout.statusIndicator.setColorFilter(colorByContactStatus(resources, it))
+ AvatarMaker(it).setAvatar(profileLayout.profileImage)
if (it.draftMessage.isNotEmpty() && outgoingMessage.text.isEmpty()) {
outgoingMessage.setText(it.draftMessage)
diff --git a/atox/src/main/kotlin/ui/contact_profile/ContactProfileFragment.kt b/atox/src/main/kotlin/ui/contact_profile/ContactProfileFragment.kt
index f1c01f5e..f0d75f0e 100644
--- a/atox/src/main/kotlin/ui/contact_profile/ContactProfileFragment.kt
+++ b/atox/src/main/kotlin/ui/contact_profile/ContactProfileFragment.kt
@@ -13,10 +13,10 @@ import androidx.fragment.app.viewModels
import ltd.evilcorp.atox.R
import ltd.evilcorp.atox.databinding.FragmentContactProfileBinding
import ltd.evilcorp.atox.requireStringArg
+import ltd.evilcorp.atox.ui.AvatarMaker
import ltd.evilcorp.atox.ui.BaseFragment
import ltd.evilcorp.atox.ui.chat.CONTACT_PUBLIC_KEY
-import ltd.evilcorp.atox.ui.colorByStatus
-import ltd.evilcorp.atox.ui.setAvatarFromContact
+import ltd.evilcorp.atox.ui.colorByContactStatus
import ltd.evilcorp.atox.vmFactory
import ltd.evilcorp.domain.tox.PublicKey
@@ -31,7 +31,7 @@ class ContactProfileFragment : BaseFragment(Fragm
compat
}
- toolbar.setNavigationIcon(R.drawable.back)
+ toolbar.setNavigationIcon(R.drawable.ic_back)
toolbar.setNavigationOnClickListener {
activity?.onBackPressed()
}
@@ -41,8 +41,8 @@ class ContactProfileFragment : BaseFragment(Fragm
contact.name = contact.name.ifEmpty { getString(R.string.contact_default_name) }
headerMainText.text = contact.name
- setAvatarFromContact(profileLayout.profileImage, contact)
- profileLayout.statusIndicator.setColorFilter(colorByStatus(resources, contact))
+ AvatarMaker(contact).setAvatar(profileLayout.profileImage)
+ profileLayout.statusIndicator.setColorFilter(colorByContactStatus(resources, contact))
contactPublicKey.text = contact.publicKey
contactName.text = contact.name
diff --git a/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt b/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt
index 0e00e7e1..541b1791 100644
--- a/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt
+++ b/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt
@@ -16,8 +16,8 @@ import java.text.DateFormat
import ltd.evilcorp.atox.R
import ltd.evilcorp.atox.databinding.ContactListViewItemBinding
import ltd.evilcorp.atox.databinding.FriendRequestItemBinding
-import ltd.evilcorp.atox.ui.colorByStatus
-import ltd.evilcorp.atox.ui.setAvatarFromContact
+import ltd.evilcorp.atox.ui.AvatarMaker
+import ltd.evilcorp.atox.ui.colorByContactStatus
import ltd.evilcorp.core.vo.Contact
import ltd.evilcorp.core.vo.FriendRequest
@@ -103,7 +103,7 @@ class ContactAdapter(
draftMessage.isNotEmpty() -> {
vh.statusMessage.text = resources.getString(R.string.draft_message, draftMessage)
vh.statusMessage.setTextColor(
- ResourcesCompat.getColor(resources, R.color.colorAccent, null)
+ ResourcesCompat.getColor(resources, R.color.colorSecondary, null)
)
}
else -> {
@@ -111,8 +111,8 @@ class ContactAdapter(
vh.statusMessage.setTextColor(vh.lastMessage.currentTextColor)
}
}
- vh.status.setColorFilter(colorByStatus(resources, this))
- setAvatarFromContact(vh.image, this)
+ vh.status.setColorFilter(colorByContactStatus(resources, this))
+ AvatarMaker(this).setAvatar(vh.image)
vh.unreadIndicator.visibility = if (hasUnreadMessages) {
View.VISIBLE
} else {
diff --git a/atox/src/main/kotlin/ui/edit_text_value_dialog/EditTextValueDialog.kt b/atox/src/main/kotlin/ui/edit_text_value_dialog/EditTextValueDialog.kt
new file mode 100644
index 00000000..8b58b1f5
--- /dev/null
+++ b/atox/src/main/kotlin/ui/edit_text_value_dialog/EditTextValueDialog.kt
@@ -0,0 +1,128 @@
+// SPDX-FileCopyrightText: 2020 aTox contributors
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+package ltd.evilcorp.atox.ui.edit_text_value_dialog
+
+import android.os.Build
+import android.os.Bundle
+import android.text.InputFilter
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.EditText
+import androidx.fragment.app.viewModels
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import ltd.evilcorp.atox.databinding.EditTextValueDialogBinding
+import ltd.evilcorp.atox.vmFactory
+
+class EditTextValueDialog() : BottomSheetDialogFragment() {
+
+ private val vm: EditTextValueDialogViewModel by viewModels { vmFactory }
+ private var _binding: EditTextValueDialogBinding? = null
+ private val binding get() = _binding!!
+
+ private var title: String? = null
+ private var hint: String? = null
+ private var defaultValue: String? = null
+ private var singleLine: Boolean = true
+ private var filters: Array? = null
+ private lateinit var setTextValueBlock: (String) -> Unit
+
+ constructor(
+ title: String,
+ hint: String,
+ defaultValue: String? = null,
+ singleLine: Boolean = true,
+ filters: Array? = null,
+ setTextValueBlock: (String) -> Unit
+ ) : this() {
+ this.title = title
+ this.hint = hint
+ this.defaultValue = defaultValue
+ this.singleLine = singleLine
+ this.filters = filters
+ this.setTextValueBlock = setTextValueBlock
+ }
+
+ companion object {
+ const val TAG = "EditTextValueDialog"
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ _binding = EditTextValueDialogBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onDestroyView() {
+ _binding = null
+ super.onDestroyView()
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) = binding.run {
+ super.onViewCreated(view, savedInstanceState)
+
+ val editText: EditText? = textField.editText
+
+ // Assigning values from constructor to view model
+ title?.run {
+ vm.title = this
+ hint?.run { vm.hint = this }
+ defaultValue?.run {
+ vm.defaultValue = this
+ vm.selectionStart = length
+ vm.selectionEnd = length
+ }
+ vm.singleLine = singleLine
+ filters?.run { vm.filters = this }
+ setTextValueBlock.run { vm.setTextValueBlock = this }
+ }
+
+ // Assigning values to the views according to the given parameters
+ titleTextView.text = vm.title
+ textField.hint = vm.hint
+ vm.defaultValue?.run {
+ editText?.setText(this)
+ }
+ editText?.isSingleLine = vm.singleLine
+ vm.filters?.run {
+ editText?.filters = this
+
+ // Displaying the max length as a counter
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ try {
+ (this.first { it is InputFilter.LengthFilter } as InputFilter.LengthFilter).run {
+ textField.isCounterEnabled = true
+ textField.counterMaxLength = max
+ }
+ } catch (e: NoSuchElementException) {
+ }
+ }
+ }
+ vm.selectionStart?.let { selectionStart ->
+ vm.selectionEnd?.let { selectionEnd ->
+ editText?.setSelection(selectionStart, selectionEnd)
+ }
+ }
+
+ cancel.setOnClickListener { dismiss() }
+ save.setOnClickListener {
+ vm.setTextValueBlock(textField.editText?.text.toString())
+ dismiss()
+ }
+ }
+
+ override fun onResume() = binding.run {
+ textField.editText?.requestFocus()
+ super.onResume()
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) = binding.run {
+ val editText: EditText? = textField.editText
+
+ vm.defaultValue = editText?.text.toString()
+ vm.selectionStart = editText?.selectionStart
+ vm.selectionEnd = editText?.selectionEnd
+ super.onSaveInstanceState(outState)
+ }
+}
diff --git a/atox/src/main/kotlin/ui/edit_text_value_dialog/EditTextValueDialogViewModel.kt b/atox/src/main/kotlin/ui/edit_text_value_dialog/EditTextValueDialogViewModel.kt
new file mode 100644
index 00000000..9185891c
--- /dev/null
+++ b/atox/src/main/kotlin/ui/edit_text_value_dialog/EditTextValueDialogViewModel.kt
@@ -0,0 +1,17 @@
+package ltd.evilcorp.atox.ui.edit_text_value_dialog
+
+import android.text.InputFilter
+import androidx.lifecycle.ViewModel
+import javax.inject.Inject
+
+class EditTextValueDialogViewModel @Inject constructor() : ViewModel() {
+ var title: String? = null
+ var hint: String? = null
+ var defaultValue: String? = null
+ var singleLine: Boolean = true
+ var filters: Array? = null
+ lateinit var setTextValueBlock: (String) -> Unit
+
+ var selectionStart: Int? = null
+ var selectionEnd: Int? = null
+}
diff --git a/atox/src/main/kotlin/ui/edit_user_profile/EditUserProfileFragment.kt b/atox/src/main/kotlin/ui/edit_user_profile/EditUserProfileFragment.kt
new file mode 100644
index 00000000..809f32d8
--- /dev/null
+++ b/atox/src/main/kotlin/ui/edit_user_profile/EditUserProfileFragment.kt
@@ -0,0 +1,147 @@
+package ltd.evilcorp.atox.ui.edit_user_profile
+
+import android.os.Build
+import android.os.Bundle
+import android.text.InputFilter
+import android.view.View
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import androidx.core.view.updatePadding
+import androidx.core.widget.doOnTextChanged
+import androidx.fragment.app.viewModels
+import kotlin.math.min
+import ltd.evilcorp.atox.R
+import ltd.evilcorp.atox.databinding.FragmentEditUserProfileBinding
+import ltd.evilcorp.atox.getColorStateList
+import ltd.evilcorp.atox.ui.AvatarMaker
+import ltd.evilcorp.atox.ui.BaseFragment
+import ltd.evilcorp.atox.ui.SizeUnit
+import ltd.evilcorp.atox.ui.edit_text_value_dialog.EditTextValueDialog
+import ltd.evilcorp.atox.ui.isNightMode
+import ltd.evilcorp.atox.vmFactory
+import ltd.evilcorp.core.vo.UserStatus
+
+private const val TOX_MAX_NAME_LENGTH = 128
+private const val TOX_MAX_STATUS_MESSAGE_LENGTH = 1007
+
+private const val AVATAR_IMAGE_TO_SCREEN_RATIO = 1f / 3
+
+class EditUserProfileFragment : BaseFragment(FragmentEditUserProfileBinding::inflate) {
+ private val vm: EditUserProfileViewModel by viewModels { vmFactory }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) = binding.run {
+ super.onViewCreated(view, savedInstanceState)
+
+ ViewCompat.setOnApplyWindowInsetsListener(view) { _, compat ->
+ val insets = compat.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime())
+ toolbar.updatePadding(left = insets.left, top = insets.top)
+ mainSection.updatePadding(left = insets.left, right = insets.right, bottom = insets.bottom)
+ compat
+ }
+
+ // Inflating views according to Day/Night theme
+ if (isNightMode(requireContext())) {
+ getColorStateList(R.color.box_stroke_color_night)?.run {
+ editStatus.setBoxStrokeColorStateList(this)
+ }
+ editStatus.defaultHintTextColor = getColorStateList(R.color.hint_text_color_night)
+ editStatus.setEndIconTintList(getColorStateList(R.color.trailing_icon_color_night))
+ }
+
+ toolbar.setNavigationIcon(R.drawable.ic_back)
+ toolbar.setNavigationOnClickListener {
+ WindowInsetsControllerCompat(requireActivity().window, view)
+ .hide(WindowInsetsCompat.Type.ime())
+ activity?.onBackPressed()
+ }
+
+ // Setting the adapter for edit status
+ val statusList = resources.getStringArray(R.array.status_list)
+ val adapter = StatusArrayAdapter(requireContext(), R.layout.edit_status_item, R.id.item_text, statusList)
+ editStatusText.setAdapter(adapter)
+
+ // Setting the icon and the status according to the chosen status
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ editStatus.setStartIconTintList(null)
+ editStatusText.doOnTextChanged { text, _, _, _ ->
+ when (text.toString()) {
+ getString(R.string.status_available) -> {
+ editStatus.setStartIconDrawable(R.drawable.ic_available)
+ vm.setStatus(UserStatus.None)
+ }
+ getString(R.string.status_away) -> {
+ editStatus.setStartIconDrawable(R.drawable.ic_away)
+ vm.setStatus(UserStatus.Away)
+ }
+ getString(R.string.status_busy) -> {
+ editStatus.setStartIconDrawable(R.drawable.ic_busy)
+ vm.setStatus(UserStatus.Busy)
+ }
+ }
+ }
+ } else {
+ editStatusText.doOnTextChanged { text, _, _, _ ->
+ when (text.toString()) {
+ getString(R.string.status_available) -> {
+ editStatus.setStartIconTintList(getColorStateList(R.color.status_available_color_list))
+ vm.setStatus(UserStatus.None)
+ }
+ getString(R.string.status_away) -> {
+ editStatus.setStartIconTintList(getColorStateList(R.color.status_away_color_list))
+ vm.setStatus(UserStatus.Away)
+ }
+ getString(R.string.status_busy) -> {
+ editStatus.setStartIconTintList(getColorStateList(R.color.status_busy_color_list))
+ vm.setStatus(UserStatus.Busy)
+ }
+ }
+ }
+ }
+
+ // Getting the avatar image side value according to the screen resolution
+ val metrics = resources.displayMetrics
+ val side = (min(metrics.widthPixels, metrics.heightPixels) * AVATAR_IMAGE_TO_SCREEN_RATIO).toInt()
+
+ vm.user.observe(viewLifecycleOwner) { user ->
+ if (vm.statusModifiedFromDropdown) {
+ vm.statusModifiedFromDropdown = false
+ } else {
+ AvatarMaker(user).setAvatar(avatarImage, side, SizeUnit.PX)
+ userName.text = user.name
+ editStatusText.setText(
+ when (user.status) {
+ UserStatus.None -> getString(R.string.status_available)
+ UserStatus.Away -> getString(R.string.status_away)
+ UserStatus.Busy -> getString(R.string.status_busy)
+ },
+ false
+ )
+ statusMessage.text = user.statusMessage
+ }
+ }
+
+ editName.setOnClickListener {
+ EditTextValueDialog(
+ title = getString(R.string.edit_name),
+ hint = getString(R.string.name),
+ defaultValue = userName.text.toString(),
+ filters = arrayOf(InputFilter.LengthFilter(TOX_MAX_NAME_LENGTH))
+ ) {
+ vm.setName(it)
+ }.show(requireActivity().supportFragmentManager, EditTextValueDialog.TAG)
+ }
+
+ editStatusMessage.setOnClickListener {
+ EditTextValueDialog(
+ title = getString(R.string.edit_status_message),
+ hint = getString(R.string.status_message),
+ defaultValue = statusMessage.text.toString(),
+ singleLine = false,
+ filters = arrayOf(InputFilter.LengthFilter(TOX_MAX_STATUS_MESSAGE_LENGTH))
+ ) {
+ vm.setStatusMessage(it)
+ }.show(requireActivity().supportFragmentManager, EditTextValueDialog.TAG)
+ }
+ }
+}
diff --git a/atox/src/main/kotlin/ui/edit_user_profile/EditUserProfileViewModel.kt b/atox/src/main/kotlin/ui/edit_user_profile/EditUserProfileViewModel.kt
new file mode 100644
index 00000000..7037a6d4
--- /dev/null
+++ b/atox/src/main/kotlin/ui/edit_user_profile/EditUserProfileViewModel.kt
@@ -0,0 +1,26 @@
+package ltd.evilcorp.atox.ui.edit_user_profile
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.asLiveData
+import javax.inject.Inject
+import ltd.evilcorp.core.vo.User
+import ltd.evilcorp.core.vo.UserStatus
+import ltd.evilcorp.domain.feature.UserManager
+import ltd.evilcorp.domain.tox.Tox
+
+class EditUserProfileViewModel @Inject constructor(
+ private val userManager: UserManager,
+ private val tox: Tox
+) : ViewModel() {
+ val publicKey by lazy { tox.publicKey }
+ val user: LiveData = userManager.get(publicKey).asLiveData()
+ var statusModifiedFromDropdown: Boolean = false
+
+ fun setName(name: String) = userManager.setName(name)
+ fun setStatusMessage(statusMessage: String) = userManager.setStatusMessage(statusMessage)
+ fun setStatus(status: UserStatus) {
+ statusModifiedFromDropdown = true
+ userManager.setStatus(status)
+ }
+}
diff --git a/atox/src/main/kotlin/ui/edit_user_profile/StatusArrayAdapter.kt b/atox/src/main/kotlin/ui/edit_user_profile/StatusArrayAdapter.kt
new file mode 100644
index 00000000..68bf3cdb
--- /dev/null
+++ b/atox/src/main/kotlin/ui/edit_user_profile/StatusArrayAdapter.kt
@@ -0,0 +1,48 @@
+package ltd.evilcorp.atox.ui.edit_user_profile
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.Filter
+import android.widget.ImageView
+import ltd.evilcorp.atox.R
+
+class StatusArrayAdapter(
+ context: Context,
+ resource: Int,
+ textViewResourceId: Int,
+ private val strings: Array,
+) : ArrayAdapter(context, resource, textViewResourceId, strings) {
+
+ private val statusFilter = StatusFilter() // This filter doesn't filter :)
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ val view = super.getView(position, convertView, parent)
+ val imageView: ImageView = view.findViewById(R.id.ic_status_indicator)
+
+ when (getItem(position)) {
+ context.getString(R.string.status_available) -> imageView.setImageResource(R.drawable.ic_available)
+ context.getString(R.string.status_away) -> imageView.setImageResource(R.drawable.ic_away)
+ context.getString(R.string.status_busy) -> imageView.setImageResource(R.drawable.ic_busy)
+ }
+
+ return view
+ }
+
+ override fun getFilter(): Filter {
+ return statusFilter
+ }
+
+ inner class StatusFilter : Filter() {
+ override fun performFiltering(prefix: CharSequence): FilterResults {
+ val results = FilterResults()
+ results.values = strings
+ results.count = strings.size
+
+ return results
+ }
+
+ override fun publishResults(constraint: CharSequence, results: FilterResults) = Unit
+ }
+}
diff --git a/atox/src/main/kotlin/ui/friend_request/FriendRequestFragment.kt b/atox/src/main/kotlin/ui/friend_request/FriendRequestFragment.kt
index 132fe32a..3f4a8304 100644
--- a/atox/src/main/kotlin/ui/friend_request/FriendRequestFragment.kt
+++ b/atox/src/main/kotlin/ui/friend_request/FriendRequestFragment.kt
@@ -33,7 +33,7 @@ class FriendRequestFragment : BaseFragment(Fragmen
compat
}
- toolbar.setNavigationIcon(R.drawable.back)
+ toolbar.setNavigationIcon(R.drawable.ic_back)
toolbar.setNavigationOnClickListener {
activity?.onBackPressed()
}
diff --git a/atox/src/main/kotlin/ui/settings/SettingsFragment.kt b/atox/src/main/kotlin/ui/settings/SettingsFragment.kt
index a4229676..01698aac 100644
--- a/atox/src/main/kotlin/ui/settings/SettingsFragment.kt
+++ b/atox/src/main/kotlin/ui/settings/SettingsFragment.kt
@@ -96,7 +96,7 @@ class SettingsFragment : BaseFragment(FragmentSettingsB
}
toolbar.apply {
- setNavigationIcon(R.drawable.back)
+ setNavigationIcon(R.drawable.ic_back)
setNavigationOnClickListener {
WindowInsetsControllerCompat(requireActivity().window, view)
.hide(WindowInsetsCompat.Type.ime())
diff --git a/atox/src/main/kotlin/ui/user_profile/UserProfileFragment.kt b/atox/src/main/kotlin/ui/user_profile/UserProfileFragment.kt
index 99312bf6..6cbf20a5 100644
--- a/atox/src/main/kotlin/ui/user_profile/UserProfileFragment.kt
+++ b/atox/src/main/kotlin/ui/user_profile/UserProfileFragment.kt
@@ -7,83 +7,94 @@ package ltd.evilcorp.atox.ui.user_profile
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
-import android.content.res.Resources
import android.graphics.Bitmap
+import android.graphics.Canvas
import android.graphics.Color
+import android.net.Uri
import android.os.Bundle
-import android.text.InputFilter
-import android.util.TypedValue
-import android.view.ContextMenu
-import android.view.MenuItem
+import android.util.Log
import android.view.View
-import android.widget.EditText
import android.widget.ImageView
import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
+import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
-import androidx.core.content.res.ResourcesCompat
+import androidx.core.graphics.alpha
+import androidx.core.graphics.blue
+import androidx.core.graphics.green
+import androidx.core.graphics.red
import androidx.core.graphics.scale
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.setPadding
import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.fragment.findNavController
import io.nayuki.qrcodegen.QrCode
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
import kotlin.math.min
import kotlin.math.roundToInt
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import ltd.evilcorp.atox.BuildConfig
import ltd.evilcorp.atox.R
import ltd.evilcorp.atox.databinding.FragmentUserProfileBinding
+import ltd.evilcorp.atox.getColor
+import ltd.evilcorp.atox.ui.AvatarMaker
import ltd.evilcorp.atox.ui.BaseFragment
-import ltd.evilcorp.atox.ui.StatusDialog
+import ltd.evilcorp.atox.ui.colorFromStatus
+import ltd.evilcorp.atox.ui.dpToPx
+import ltd.evilcorp.atox.ui.isNightMode
+import ltd.evilcorp.atox.ui.setImageButtonRippleDayNight
import ltd.evilcorp.atox.vmFactory
-import ltd.evilcorp.core.vo.UserStatus
-
-private const val TOX_MAX_NAME_LENGTH = 128
-private const val TOX_MAX_STATUS_MESSAGE_LENGTH = 1007
private const val QR_CODE_TO_SCREEN_RATIO = 0.5f
-private const val QR_CODE_DIALOG_PADDING = 16f // in dp
-
-private fun dpToPx(dp: Float, res: Resources): Int =
- TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, res.displayMetrics).toInt()
+private const val QR_CODE_PADDING = 16f // in dp
+private const val QR_CODE_SHARED_IMAGE_PADDING = 30f // in dp
class UserProfileFragment : BaseFragment(FragmentUserProfileBinding::inflate) {
private val vm: UserProfileViewModel by viewModels { vmFactory }
- private lateinit var currentStatus: UserStatus
-
- private fun colorFromStatus(status: UserStatus) = when (status) {
- UserStatus.None -> ResourcesCompat.getColor(resources, R.color.statusAvailable, null)
- UserStatus.Away -> ResourcesCompat.getColor(resources, R.color.statusAway, null)
- UserStatus.Busy -> ResourcesCompat.getColor(resources, R.color.statusBusy, null)
- }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) = binding.run {
ViewCompat.setOnApplyWindowInsetsListener(view) { _, compat ->
val insets = compat.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime())
- profileCollapsingToolbar.updatePadding(left = insets.left, right = insets.right)
- profileToolbar.updatePadding(top = insets.top)
- mainSection.updatePadding(left = insets.left, right = insets.right)
+ toolbar.updatePadding(left = insets.left, top = insets.top)
+ mainSection.updatePadding(left = insets.left, right = insets.right, bottom = insets.bottom)
compat
}
- profileToolbar.apply {
- setNavigationOnClickListener {
- activity?.onBackPressed()
- }
- }
-
vm.user.observe(viewLifecycleOwner) { user ->
- currentStatus = user.status
-
userName.text = user.name
userStatusMessage.text = user.statusMessage
- userStatus.setColorFilter(colorFromStatus(user.status))
+ profileImageLayout.statusIndicator.setColorFilter(colorFromStatus(resources, user.status))
+ AvatarMaker(user).setAvatar(profileImageLayout.profileImage)
}
- userToxId.text = vm.toxId.string()
+ // Inflating views according to Day/Night theme
+ if (isNightMode(requireContext())) {
+ createQrCode(getColor(R.color.pleasantWhite), Color.TRANSPARENT, imageView = toxIdQr)
+ } else {
+ createQrCode(Color.BLACK, Color.TRANSPARENT, imageView = toxIdQr)
+ }
+ setImageButtonRippleDayNight(requireContext(), copyToxId)
+
+ toolbar.setNavigationIcon(R.drawable.ic_back)
+ toolbar.setNavigationOnClickListener {
+ WindowInsetsControllerCompat(requireActivity().window, view)
+ .hide(WindowInsetsCompat.Type.ime())
+ activity?.onBackPressed()
+ }
+
+ editProfile.setOnClickListener {
+ findNavController().navigate(R.id.action_userProfileFragment_to_editUserProfileFragment)
+ }
- // TODO(robinlinden): This should open a nice dialog where you show the QR and have both share and copy buttons.
- profileShareId.setOnClickListener {
+ userToxId.text = vm.toxId.string()
+ userToxId.setOnClickListener {
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
@@ -91,97 +102,105 @@ class UserProfileFragment : BaseFragment(FragmentUse
}
startActivity(Intent.createChooser(shareIntent, getString(R.string.tox_id_share)))
}
- registerForContextMenu(profileShareId)
- profileOptions.profileChangeNickname.setOnClickListener {
- val nameEdit = EditText(requireContext()).apply {
- text.append(binding.userName.text)
- filters = arrayOf(InputFilter.LengthFilter(TOX_MAX_NAME_LENGTH))
- setSingleLine()
- }
- AlertDialog.Builder(requireContext())
- .setTitle(R.string.name)
- .setView(nameEdit)
- .setPositiveButton(R.string.update) { _, _ ->
- vm.setName(nameEdit.text.toString())
- }
- .setNegativeButton(R.string.cancel) { _, _ -> }
- .show()
+ copyToxId.setOnClickListener {
+ val clipboard = requireActivity().getSystemService()!!
+ clipboard.setPrimaryClip(ClipData.newPlainText(getText(R.string.tox_id), vm.toxId.string()))
+ Toast.makeText(requireContext(), getText(R.string.copied), Toast.LENGTH_SHORT).show()
}
- profileOptions.profileChangeStatusText.setOnClickListener {
- val statusMessageEdit =
- EditText(requireContext()).apply {
- text.append(binding.userStatusMessage.text)
- filters = arrayOf(InputFilter.LengthFilter(TOX_MAX_STATUS_MESSAGE_LENGTH))
- }
- AlertDialog.Builder(requireContext())
- .setTitle(R.string.status_message)
- .setView(statusMessageEdit)
- .setPositiveButton(R.string.update) { _, _ ->
- vm.setStatusMessage(statusMessageEdit.text.toString())
+ toxIdQr.setOnClickListener {
+ vm.viewModelScope.launch {
+ val qrImageUri = getQrForSharing("tox_id_qr_code")
+ val shareIntent = Intent().apply {
+ action = Intent.ACTION_SEND
+ clipData = ClipData.newRawUri(null, qrImageUri)
+ type = "image/png"
+ putExtra(Intent.EXTRA_STREAM, qrImageUri)
+ flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
- .setNegativeButton(R.string.cancel) { _, _ -> }
- .show()
- }
-
- profileOptions.profileChangeStatus.setOnClickListener {
- StatusDialog(requireContext(), currentStatus) { status -> vm.setStatus(status) }.show()
- }
-
- // TODO(robinlinden): Remove hack. It's used to make sure we can scroll to the settings
- // further down when in landscape orientation. This is only needed if the view is recreated
- // while we're on this screen as Android changes the size of the contents of the NestedScrollView
- // when that happens.
- if (savedInstanceState != null) {
- needsHacks.updatePadding(bottom = (150 * resources.displayMetrics.density).toInt())
- }
- }
-
- override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) = binding.run {
- super.onCreateContextMenu(menu, v, menuInfo)
- when (v.id) {
- R.id.profile_share_id -> requireActivity().menuInflater.inflate(
- R.menu.user_profile_share_id_context_menu,
- menu
- )
- }
- }
-
- override fun onContextItemSelected(item: MenuItem): Boolean = binding.run {
- return when (item.itemId) {
- R.id.copy -> {
- val clipboard = requireActivity().getSystemService()!!
- clipboard.setPrimaryClip(ClipData.newPlainText(getText(R.string.tox_id), vm.toxId.string()))
- Toast.makeText(requireContext(), getText(R.string.copied), Toast.LENGTH_SHORT).show()
- true
- }
- R.id.qr -> {
- createQrCodeDialog().show()
- true
+ startActivity(Intent.createChooser(shareIntent, getString(R.string.tox_id_share)))
}
- else -> super.onContextItemSelected(item)
}
}
- private fun createQrCodeDialog(): AlertDialog {
+ /**
+ * Function will create a QR code for the Tox ID and assign it to the image view if specified.
+ * @param qrCodeColor The color of the QR code.
+ * @param backgroundColor The color of the background.
+ * @param paddingDp The padding to add after each side in the QR code bitmap. Will be painted with backgroundColor.
+ * @param imageView The image view for whom to assign the QR code bitmap. Can be null.
+ * @return The QR code bitmap with the specified padding.
+ */
+ private fun createQrCode(
+ qrCodeColor: Int = Color.BLACK,
+ backgroundColor: Int = Color.WHITE,
+ paddingDp: Float = QR_CODE_PADDING,
+ imageView: ImageView? = null,
+ ): Bitmap {
+ // Creating the QR bitmap
val qrData = QrCode.encodeText("tox:%s".format(vm.toxId.string()), QrCode.Ecc.LOW)
- var bmp: Bitmap = Bitmap.createBitmap(qrData.size, qrData.size, Bitmap.Config.RGB_565)
+ var bmpQr: Bitmap = Bitmap.createBitmap(qrData.size, qrData.size, Bitmap.Config.ARGB_8888)
+ bmpQr.setHasAlpha(true)
for (x in 0 until qrData.size) {
for (y in 0 until qrData.size) {
- bmp.setPixel(x, y, if (qrData.getModule(x, y)) Color.BLACK else Color.WHITE)
+ bmpQr.setPixel(x, y, if (qrData.getModule(x, y)) qrCodeColor else backgroundColor)
}
}
+ // Scaling the QR bitmap to be half of the screen's width
val metrics = resources.displayMetrics
val size = (min(metrics.widthPixels, metrics.heightPixels) * QR_CODE_TO_SCREEN_RATIO).roundToInt()
- bmp = bmp.scale(size, size, false)
- val qrCode = ImageView(requireContext()).apply {
- setPadding(dpToPx(QR_CODE_DIALOG_PADDING, resources))
- setImageBitmap(bmp)
+ bmpQr = bmpQr.scale(size, size, false)
+
+ // Adding a padding to the QR bitmap
+ val paddingPx = dpToPx(paddingDp, resources)
+ val bmpQrWithPadding =
+ Bitmap.createBitmap(bmpQr.width + 2 * paddingPx, bmpQr.height + 2 * paddingPx, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(bmpQrWithPadding)
+ canvas.drawARGB(backgroundColor.alpha, backgroundColor.red, backgroundColor.green, backgroundColor.blue)
+ canvas.drawBitmap(bmpQr, paddingPx.toFloat(), paddingPx.toFloat(), null)
+
+ imageView?.apply {
+ setPadding(paddingPx)
+ setImageBitmap(bmpQr)
+ }
+
+ return bmpQrWithPadding
+ }
+
+ /**
+ * Function will save the image with the input bitmap and name to the cache directory as a png format.
+ * Then it will return the Uri of the image.
+ * @param image The bitmap of the image.
+ * @param name Image file name.
+ * @return The Uri of the image or null.
+ */
+ private fun saveImageForSharing(image: Bitmap, name: String): Uri? {
+ val imagesFolder = File(requireActivity().cacheDir, "images")
+ var uri: Uri? = null
+ try {
+ imagesFolder.mkdirs()
+ val file = File(imagesFolder, "$name.png")
+ val stream = FileOutputStream(file)
+ image.compress(Bitmap.CompressFormat.PNG, 90, stream)
+ stream.flush()
+ stream.close()
+ uri = FileProvider.getUriForFile(requireActivity(), "${BuildConfig.APPLICATION_ID}.fileprovider", file)
+ } catch (e: IOException) {
+ Log.d("corp.atox.debug", "IOException while trying to write file for sharing: " + e.message)
+ }
+ return uri
+ }
+
+ /**
+ * Function will run in a different thread, create a new QR code for sharing and return the Uri.
+ * @param name The image name.
+ * @return The Uri for the QR code.
+ */
+ private suspend fun getQrForSharing(name: String): Uri? {
+ return withContext(Dispatchers.IO) {
+ val bmp = createQrCode(paddingDp = QR_CODE_SHARED_IMAGE_PADDING)
+ saveImageForSharing(bmp, name)
}
- return AlertDialog.Builder(requireContext())
- .setTitle(R.string.tox_id)
- .setView(qrCode)
- .create()
}
}
diff --git a/atox/src/main/res/anim/slide_in_left.xml b/atox/src/main/res/anim/slide_in_left.xml
new file mode 100644
index 00000000..7bba0912
--- /dev/null
+++ b/atox/src/main/res/anim/slide_in_left.xml
@@ -0,0 +1,6 @@
+
+
diff --git a/atox/src/main/res/anim/slide_in_right.xml b/atox/src/main/res/anim/slide_in_right.xml
index b2769790..1871adf3 100644
--- a/atox/src/main/res/anim/slide_in_right.xml
+++ b/atox/src/main/res/anim/slide_in_right.xml
@@ -2,5 +2,5 @@
diff --git a/atox/src/main/res/anim/slide_out_left.xml b/atox/src/main/res/anim/slide_out_left.xml
new file mode 100644
index 00000000..e480fd59
--- /dev/null
+++ b/atox/src/main/res/anim/slide_out_left.xml
@@ -0,0 +1,6 @@
+
+
diff --git a/atox/src/main/res/anim/slide_out_right.xml b/atox/src/main/res/anim/slide_out_right.xml
index c970a130..28e919a3 100644
--- a/atox/src/main/res/anim/slide_out_right.xml
+++ b/atox/src/main/res/anim/slide_out_right.xml
@@ -2,5 +2,5 @@
diff --git a/atox/src/main/res/color/box_stroke_color_day.xml b/atox/src/main/res/color/box_stroke_color_day.xml
new file mode 100644
index 00000000..28cadab8
--- /dev/null
+++ b/atox/src/main/res/color/box_stroke_color_day.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/atox/src/main/res/color/box_stroke_color_night.xml b/atox/src/main/res/color/box_stroke_color_night.xml
new file mode 100644
index 00000000..4a82eab6
--- /dev/null
+++ b/atox/src/main/res/color/box_stroke_color_night.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/atox/src/main/res/color/hint_text_color_day.xml b/atox/src/main/res/color/hint_text_color_day.xml
new file mode 100644
index 00000000..a8549898
--- /dev/null
+++ b/atox/src/main/res/color/hint_text_color_day.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/atox/src/main/res/color/hint_text_color_night.xml b/atox/src/main/res/color/hint_text_color_night.xml
new file mode 100644
index 00000000..c32a268a
--- /dev/null
+++ b/atox/src/main/res/color/hint_text_color_night.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/atox/src/main/res/color/status_available_color_list.xml b/atox/src/main/res/color/status_available_color_list.xml
new file mode 100644
index 00000000..dba5b8da
--- /dev/null
+++ b/atox/src/main/res/color/status_available_color_list.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/atox/src/main/res/color/status_away_color_list.xml b/atox/src/main/res/color/status_away_color_list.xml
new file mode 100644
index 00000000..50d1dbae
--- /dev/null
+++ b/atox/src/main/res/color/status_away_color_list.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/atox/src/main/res/color/status_busy_color_list.xml b/atox/src/main/res/color/status_busy_color_list.xml
new file mode 100644
index 00000000..826ef5d0
--- /dev/null
+++ b/atox/src/main/res/color/status_busy_color_list.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/atox/src/main/res/color/trailing_icon_color_day.xml b/atox/src/main/res/color/trailing_icon_color_day.xml
new file mode 100644
index 00000000..9f631fb1
--- /dev/null
+++ b/atox/src/main/res/color/trailing_icon_color_day.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/atox/src/main/res/color/trailing_icon_color_night.xml b/atox/src/main/res/color/trailing_icon_color_night.xml
new file mode 100644
index 00000000..cbae2526
--- /dev/null
+++ b/atox/src/main/res/color/trailing_icon_color_night.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/atox/src/main/res/drawable/attach_file.xml b/atox/src/main/res/drawable/ic_attach_file.xml
similarity index 100%
rename from atox/src/main/res/drawable/attach_file.xml
rename to atox/src/main/res/drawable/ic_attach_file.xml
diff --git a/atox/src/main/res/drawable/ic_available.xml b/atox/src/main/res/drawable/ic_available.xml
new file mode 100644
index 00000000..3b0240d3
--- /dev/null
+++ b/atox/src/main/res/drawable/ic_available.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/atox/src/main/res/drawable/ic_away.xml b/atox/src/main/res/drawable/ic_away.xml
new file mode 100644
index 00000000..d6e632a3
--- /dev/null
+++ b/atox/src/main/res/drawable/ic_away.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/atox/src/main/res/drawable/back.xml b/atox/src/main/res/drawable/ic_back.xml
similarity index 89%
rename from atox/src/main/res/drawable/back.xml
rename to atox/src/main/res/drawable/ic_back.xml
index 9f690964..0ee09c33 100644
--- a/atox/src/main/res/drawable/back.xml
+++ b/atox/src/main/res/drawable/ic_back.xml
@@ -4,6 +4,6 @@
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
-
diff --git a/atox/src/main/res/drawable/ic_busy.xml b/atox/src/main/res/drawable/ic_busy.xml
new file mode 100644
index 00000000..8fddfd38
--- /dev/null
+++ b/atox/src/main/res/drawable/ic_busy.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/atox/src/main/res/drawable/ic_copy.xml b/atox/src/main/res/drawable/ic_copy.xml
new file mode 100644
index 00000000..3ede6153
--- /dev/null
+++ b/atox/src/main/res/drawable/ic_copy.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/atox/src/main/res/drawable/ic_edit.xml b/atox/src/main/res/drawable/ic_edit.xml
new file mode 100644
index 00000000..be60d088
--- /dev/null
+++ b/atox/src/main/res/drawable/ic_edit.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/atox/src/main/res/drawable/ic_person.xml b/atox/src/main/res/drawable/ic_person.xml
new file mode 100644
index 00000000..66a93840
--- /dev/null
+++ b/atox/src/main/res/drawable/ic_person.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/atox/src/main/res/drawable/send.xml b/atox/src/main/res/drawable/ic_send.xml
similarity index 100%
rename from atox/src/main/res/drawable/send.xml
rename to atox/src/main/res/drawable/ic_send.xml
diff --git a/atox/src/main/res/drawable/ic_status.xml b/atox/src/main/res/drawable/ic_status.xml
new file mode 100644
index 00000000..1abe09a4
--- /dev/null
+++ b/atox/src/main/res/drawable/ic_status.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/atox/src/main/res/drawable/ic_status_message.xml b/atox/src/main/res/drawable/ic_status_message.xml
new file mode 100644
index 00000000..81da7238
--- /dev/null
+++ b/atox/src/main/res/drawable/ic_status_message.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/atox/src/main/res/drawable/side_nav_bar_background.xml b/atox/src/main/res/drawable/side_nav_bar_background.xml
index 467ca417..853f574a 100644
--- a/atox/src/main/res/drawable/side_nav_bar_background.xml
+++ b/atox/src/main/res/drawable/side_nav_bar_background.xml
@@ -3,7 +3,7 @@
diff --git a/atox/src/main/res/layout/dialog_receive_share.xml b/atox/src/main/res/layout/dialog_receive_share.xml
index 34980c82..698909f4 100644
--- a/atox/src/main/res/layout/dialog_receive_share.xml
+++ b/atox/src/main/res/layout/dialog_receive_share.xml
@@ -27,7 +27,7 @@
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/receive_share_share_to"
- android:textColor="@color/textWhiteColor"
+ android:textColor="@android:color/white"
android:textSize="20sp" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/atox/src/main/res/layout/edit_status_item.xml b/atox/src/main/res/layout/edit_status_item.xml
new file mode 100644
index 00000000..1d0c4d5f
--- /dev/null
+++ b/atox/src/main/res/layout/edit_status_item.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/atox/src/main/res/layout/edit_text_value_dialog.xml b/atox/src/main/res/layout/edit_text_value_dialog.xml
new file mode 100644
index 00000000..2a41c67d
--- /dev/null
+++ b/atox/src/main/res/layout/edit_text_value_dialog.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/atox/src/main/res/layout/fragment_add_contact.xml b/atox/src/main/res/layout/fragment_add_contact.xml
index 348f1fd8..31597305 100644
--- a/atox/src/main/res/layout/fragment_add_contact.xml
+++ b/atox/src/main/res/layout/fragment_add_contact.xml
@@ -9,7 +9,7 @@
diff --git a/atox/src/main/res/layout/fragment_chat.xml b/atox/src/main/res/layout/fragment_chat.xml
index 904fda03..209c3320 100644
--- a/atox/src/main/res/layout/fragment_chat.xml
+++ b/atox/src/main/res/layout/fragment_chat.xml
@@ -7,7 +7,7 @@
@@ -88,6 +88,6 @@
android:background="@android:color/transparent"
android:contentDescription="@string/attach_file"
android:paddingHorizontal="4dp"
- android:src="@drawable/attach_file"/>
+ android:src="@drawable/ic_attach_file"/>
diff --git a/atox/src/main/res/layout/fragment_contact_list.xml b/atox/src/main/res/layout/fragment_contact_list.xml
index 06f36e8a..d1fbb997 100644
--- a/atox/src/main/res/layout/fragment_contact_list.xml
+++ b/atox/src/main/res/layout/fragment_contact_list.xml
@@ -12,12 +12,12 @@
+ android:theme="@style/Theme.aTox.AppBarOverlay">
+ android:background="?attr/colorSurface"
+ app:popupTheme="@style/Theme.aTox.PopupOverlay"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/atox/src/main/res/layout/fragment_friend_request.xml b/atox/src/main/res/layout/fragment_friend_request.xml
index 44dd7da8..8d15bd38 100644
--- a/atox/src/main/res/layout/fragment_friend_request.xml
+++ b/atox/src/main/res/layout/fragment_friend_request.xml
@@ -9,7 +9,7 @@
diff --git a/atox/src/main/res/layout/fragment_profile.xml b/atox/src/main/res/layout/fragment_profile.xml
index 5643fdde..52e9187f 100644
--- a/atox/src/main/res/layout/fragment_profile.xml
+++ b/atox/src/main/res/layout/fragment_profile.xml
@@ -8,7 +8,7 @@
@@ -209,7 +209,7 @@
android:layout_height="match_parent"
android:padding="16dp"
android:textSize="16sp"
- android:textColor="@color/colorAccent"
+ android:textColor="@color/colorSecondary"
tools:text="Warning: Proxy is bork"/>
+
+
+
-
+
+
+
+
+
+
+
+
-
+
+
-
+
+
+
+
-
-
-
-
-
-
-
-
-
+ android:layout_marginStart="25dp"
+ android:layout_toEndOf="@id/profileImageLayout"
+ android:singleLine="true"
+ tools:text="Name goes here" />
+
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+
+
-
+ android:layout_alignParentStart="true"
+ android:layout_marginStart="25dp"
+ android:layout_marginTop="25dp"
+ android:layout_marginEnd="10dp"
+ android:layout_marginBottom="25dp"
+ android:layout_toStartOf="@id/copy_tox_id">
+
-
-
-
-
-
-
-
-
+ android:id="@+id/user_tox_id"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:textSize="14sp"
+ tools:text="TOX ID GOES HERE" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/atox/src/main/res/layout/profile_image_layout.xml b/atox/src/main/res/layout/profile_image_layout.xml
index 9005096d..2b22349c 100644
--- a/atox/src/main/res/layout/profile_image_layout.xml
+++ b/atox/src/main/res/layout/profile_image_layout.xml
@@ -7,16 +7,16 @@
android:layout_height="wrap_content">
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/atox/src/main/res/menu/user_profile_share_id_context_menu.xml b/atox/src/main/res/menu/user_profile_share_id_context_menu.xml
deleted file mode 100644
index ac108283..00000000
--- a/atox/src/main/res/menu/user_profile_share_id_context_menu.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
diff --git a/atox/src/main/res/navigation/nav_graph.xml b/atox/src/main/res/navigation/nav_graph.xml
index 5cbc5824..852ac277 100644
--- a/atox/src/main/res/navigation/nav_graph.xml
+++ b/atox/src/main/res/navigation/nav_graph.xml
@@ -113,5 +113,18 @@
android:id="@+id/userProfileFragment"
android:name="ltd.evilcorp.atox.ui.user_profile.UserProfileFragment"
android:label="fragment_user_profile"
- tools:layout="@layout/fragment_user_profile"/>
+ tools:layout="@layout/fragment_user_profile">
+
+
+
diff --git a/atox/src/main/res/values-ar/strings.xml b/atox/src/main/res/values-ar/strings.xml
index 0ada0b6f..ffb8a04b 100644
--- a/atox/src/main/res/values-ar/strings.xml
+++ b/atox/src/main/res/values-ar/strings.xml
@@ -84,11 +84,6 @@
aTox يعمل…
يوجد جهة اتصال بنفس عنوان Tox
الحساب
- تغيير الحالة
- تغيير رسالة الحالة
- تغيير الاسم المستعار
- اعدادات الحساب
- معلومات الحساب
او استورد من ملف Tox
ملفات JSON المخصصة لا بد ان تكون بنفس الصيغة علي https://nodes.tox.chat/json
مخصص
diff --git a/atox/src/main/res/values-bs/strings.xml b/atox/src/main/res/values-bs/strings.xml
index 83fbc070..e0b52ffe 100644
--- a/atox/src/main/res/values-bs/strings.xml
+++ b/atox/src/main/res/values-bs/strings.xml
@@ -55,11 +55,6 @@
aTox je aktivan…
Kontakt sa tim Tox ID-om već postoji
Profil
- Promjeni status
- Promjeni statusnu poruku
- Promjeni korisničko ime
- Podešavanja profila
- Informacije profila
ili uvezi postojeći Tox ključ
Posalji kao akciju
Korisnički dodane JSON datoteke moraju imati isti format kao na https://nodes.tox.chat/json
diff --git a/atox/src/main/res/values-de/strings.xml b/atox/src/main/res/values-de/strings.xml
index b2d350e9..9e8e96f2 100644
--- a/atox/src/main/res/values-de/strings.xml
+++ b/atox/src/main/res/values-de/strings.xml
@@ -58,11 +58,6 @@
Typ
Als Aktion senden
Oder importiere ein bestehendes Tox-Profil
- Profil-Informationen
- Profil-Einstellungen
- Ändere deinen Namen
- Ändere deine Statusmeldung
- Ändere deinen Status
Profil
Einen Kontakt mit dieser Tox-ID gibt es bereits
aTox läuft …
diff --git a/atox/src/main/res/values-el/strings.xml b/atox/src/main/res/values-el/strings.xml
index 1e8a8636..ea5d49c1 100644
--- a/atox/src/main/res/values-el/strings.xml
+++ b/atox/src/main/res/values-el/strings.xml
@@ -46,12 +46,7 @@
Το aTox εκτελείται…
Υπάρχει ήδη επαφή με αυτό το αναγνωριστικό
Προφίλ
- Αλλάξτε την κατάστασή σας
- Αλλάξτε το μήνυμα κατάστασής σας
- Αλλάξτε το ψευδώνυμό σας
- Ρυθμίσεις προφίλ
Για προχωρημένους
- Πληροφορίες προφίλ
ή εισαγάγετε ένα υπάρχων Tox save
Αποστολή ως ενέργεια
Τα αρχεία JSON που παρέχονται από τον χρήστη πρέπει να έχουν την ίδια μορφή όπως στο https://nodes.tox.chat/json
diff --git a/atox/src/main/res/values-es/strings.xml b/atox/src/main/res/values-es/strings.xml
index 01966159..241853ee 100644
--- a/atox/src/main/res/values-es/strings.xml
+++ b/atox/src/main/res/values-es/strings.xml
@@ -63,11 +63,6 @@
Los archivos JSON personalizados deben tener el mismo formato que https://nodes.tox.chat/json
Enviar como acción
o importar un archivo de Tox existente
- Información del perfil
- Configuración del perfil
- Cambiar tu nick
- Cambiar tu mensaje de estado
- Cambiar tu estado
Perfil
Un contacto con ese Tox ID ya existe
aTox se está ejecutando…
diff --git a/atox/src/main/res/values-et/strings.xml b/atox/src/main/res/values-et/strings.xml
index a60a0ba0..15a9b614 100644
--- a/atox/src/main/res/values-et/strings.xml
+++ b/atox/src/main/res/values-et/strings.xml
@@ -47,11 +47,6 @@
aTox töötab …
Selle ID-ga kontakt on juba olemas
Profiil
- Muuda oma staatust
- Muuda oma staatuse sõnumit
- Hüüdnime muutmine
- Profiili seaded
- Profiili teave
või importige olemasolev Toxi salvestus
Saada tegevusena
Kohandatud JSON-failidel peaks olema sama struktuur nagu siin https://nodes.tox.chat/json
diff --git a/atox/src/main/res/values-fa/strings.xml b/atox/src/main/res/values-fa/strings.xml
index 4ef138e4..a83f9f5b 100644
--- a/atox/src/main/res/values-fa/strings.xml
+++ b/atox/src/main/res/values-fa/strings.xml
@@ -6,11 +6,6 @@
aTox در حال اجراست…
یک مخاطب با همین شناسهی تاکس در حال حاضر وجود دارد
پروفایل
- عوض کردن وضعیت
- عوض کردن متن وضعیت
- عوض کردن ناممستعار
- تنظیمات پروفایل
- اطلاعات پروفایل
ارسال به عنوان اقدام
فایل JSON ارائهشده توسط کاربر باید دقیقا همان فرمت ارائهشده در https://nodes.tox.chat/json را داشته باشد
منبع بوتاسترپنود
diff --git a/atox/src/main/res/values-fr/strings.xml b/atox/src/main/res/values-fr/strings.xml
index 7be0c0da..78cde796 100644
--- a/atox/src/main/res/values-fr/strings.xml
+++ b/atox/src/main/res/values-fr/strings.xml
@@ -40,11 +40,6 @@
aTox est en exécution…
Un contact avec cet identifiant Tox existe déjà
Profil
- Changez de statut
- Changez votre message de statut
- Changez votre pseudo
- Paramètres du profil
- Informations du profil
ou importer une sauvegarde existante de Tox
Envoyer comme action
Les fichiers JSON fournis par l’utilisateur doivent avoir le même format que sur https://nodes.tox.chat/json
diff --git a/atox/src/main/res/values-hr/strings.xml b/atox/src/main/res/values-hr/strings.xml
index 80c5a2ac..3609e937 100644
--- a/atox/src/main/res/values-hr/strings.xml
+++ b/atox/src/main/res/values-hr/strings.xml
@@ -102,11 +102,6 @@
aTox radi …
Kontakt s tim Tox ID već postoji
Profil
- Promijeni svoje stanje
- Promijeni svoju poruku stanja
- Promijeni svoj nadimak
- Postavke profila
- Podaci profila
ili uvezi postojeći Tox save
Pošalji kao radnju
Korisničke JSON datoteke moraju imati isti format kao na https://nodes.tox.chat/json
diff --git a/atox/src/main/res/values-hu/strings.xml b/atox/src/main/res/values-hu/strings.xml
index ce04627b..cb520b10 100644
--- a/atox/src/main/res/values-hu/strings.xml
+++ b/atox/src/main/res/values-hu/strings.xml
@@ -8,11 +8,6 @@
Hálózat
aTox fut…
Profil
- Állapot változtatás
- Állapotüzenet változtatás
- Becenév változtatás
- Profil beállítások
- Profil információ
Beépített
Bootstrap csomópont forrás
Haladó
diff --git a/atox/src/main/res/values-hy/strings.xml b/atox/src/main/res/values-hy/strings.xml
index 83742f4f..46d0a5e3 100644
--- a/atox/src/main/res/values-hy/strings.xml
+++ b/atox/src/main/res/values-hy/strings.xml
@@ -51,11 +51,6 @@
aTox-ը միացված է…
Այդ Tox ID կոնտակն արդեն գրանցված է
Պրոֆիլ
- Փոխել ստատուսը
- Փոխել ստատուսը
- Փոխել մականունը
- Պրոֆիլի պարամետրեր
- Պրոֆիլի ինֆորմացիա
SOCKS5
HTTP
Տեսալ
diff --git a/atox/src/main/res/values-it/strings.xml b/atox/src/main/res/values-it/strings.xml
index a7a91661..27fbb6f4 100644
--- a/atox/src/main/res/values-it/strings.xml
+++ b/atox/src/main/res/values-it/strings.xml
@@ -40,11 +40,6 @@
aTox è in esecuzione…
Esiste già un contatto con quel ID Tox
Profilo
- Cambia il tuo stato
- Cambia il tuo messaggio di stato
- Cambia il tuo soprannome
- Impostazioni del profilo
- Informazioni sul profilo
o importare una copia del profilo Tox esistente
Invia come azione
I file JSON forniti dall’utente devono avere lo stesso formato di https://nodes.tox.chat/json
diff --git a/atox/src/main/res/values-lt/strings.xml b/atox/src/main/res/values-lt/strings.xml
index 0e69c9ab..f1400fd3 100644
--- a/atox/src/main/res/values-lt/strings.xml
+++ b/atox/src/main/res/values-lt/strings.xml
@@ -29,11 +29,6 @@
aTox paleistas…
Kontaktas su tokiu Tox ID jau yra
Profilis
- Pakeisti savo būklę
- Pakeisti savo būsenos žinutę
- Pasikeisti savo slapyvardį
- Profilio nustatymai
- Profilio informacija
arba importuoti jau išsaugotą Tox profilį
Nusiųsti kaip veiksmą
Vartotojų pateikti JSOn failai turi būti tokio paties formato kaip čia https://nodes.tox.chat/json
diff --git a/atox/src/main/res/values-nb-rNO/strings.xml b/atox/src/main/res/values-nb-rNO/strings.xml
index a8406c5d..39aed8d1 100644
--- a/atox/src/main/res/values-nb-rNO/strings.xml
+++ b/atox/src/main/res/values-nb-rNO/strings.xml
@@ -48,11 +48,6 @@
aTox kjører …
En kontakt med den Tox-ID-en finnes allerede
Profil
- Endre egen status
- Endre din statusmelding
- Endre ditt brukernavn
- Profilinnstillinger
- Profilinfo
Send en handling
Avansert
Type
diff --git a/atox/src/main/res/values-night-v21/styles.xml b/atox/src/main/res/values-night-v21/styles.xml
deleted file mode 100644
index 70747779..00000000
--- a/atox/src/main/res/values-night-v21/styles.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/atox/src/main/res/values-night-v29/styles.xml b/atox/src/main/res/values-night-v29/styles.xml
deleted file mode 100644
index fe32258a..00000000
--- a/atox/src/main/res/values-night-v29/styles.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/atox/src/main/res/values-night/styles.xml b/atox/src/main/res/values-night/styles.xml
new file mode 100644
index 00000000..e921b913
--- /dev/null
+++ b/atox/src/main/res/values-night/styles.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/atox/src/main/res/values-pl/strings.xml b/atox/src/main/res/values-pl/strings.xml
index 36bc9b3f..170a3db4 100644
--- a/atox/src/main/res/values-pl/strings.xml
+++ b/atox/src/main/res/values-pl/strings.xml
@@ -67,11 +67,6 @@
Dostępny/a
Nieobecny/a
Zaproszenie do znajomych
- Zmiana statusu
- Zmiana opisu statusu
- Zmiana pseudonimu
- Ustawienia profilu
- Informacje o profilu
Pomyślnie wyeksportowano profil Tox
Eksportuj profil Tox
lub zaimportuj profil Tox z pliku
diff --git a/atox/src/main/res/values-pt-rBR/strings.xml b/atox/src/main/res/values-pt-rBR/strings.xml
index 2e276720..a05f5dde 100644
--- a/atox/src/main/res/values-pt-rBR/strings.xml
+++ b/atox/src/main/res/values-pt-rBR/strings.xml
@@ -59,11 +59,6 @@
Tema
Enviar como ação
ou importar um arquivo de Tox existente
- Informações do perfil
- Configurações do perfil
- Mudar seu apelido
- Alterar sua mensagem de status
- Alterar seu status
Perfil
Já existe um contato com essa ID Tox
aTox está em andamento…
diff --git a/atox/src/main/res/values-pt/strings.xml b/atox/src/main/res/values-pt/strings.xml
index 4ac5b5e5..58fbe8d9 100644
--- a/atox/src/main/res/values-pt/strings.xml
+++ b/atox/src/main/res/values-pt/strings.xml
@@ -39,11 +39,6 @@
aTox já está a funcionar…
Já existe um contato com essa ID Tox
Perfil
- Alterar o seu estado
- Alterar sua mensagem de estado
- Escolha seu apelido
- Configurações de perfil
- Informação do perfil
ou importar um arquivo do Tox existente
Enviar como acção
Os ficheiros JSON fornecidos pelo utilizador devem ter o mesmo formato que em https://nodes.tox.chat/json
diff --git a/atox/src/main/res/values-ro/strings.xml b/atox/src/main/res/values-ro/strings.xml
index c686bdb0..55ab6c7d 100644
--- a/atox/src/main/res/values-ro/strings.xml
+++ b/atox/src/main/res/values-ro/strings.xml
@@ -64,11 +64,6 @@
Fișierele JSON personalizate vin cu structura https://nodes.tox.chat/json
Trimite ca acțiune
sau importă profilul Tox existent
- Informație profil
- Setări profil
- Modifică nume utilizator
- Modifică mesaj stare
- Modifică stare
Profil
Contactul cu acest Tox ID deja există
aTox rulează…
diff --git a/atox/src/main/res/values-ru/strings.xml b/atox/src/main/res/values-ru/strings.xml
index 49d93890..e172c608 100644
--- a/atox/src/main/res/values-ru/strings.xml
+++ b/atox/src/main/res/values-ru/strings.xml
@@ -64,11 +64,6 @@
Пользовательские JSON файлы должны иметь ту же структуру как и здесь https://nodes.tox.chat/json
Отправить как действие
или импортировать уже существующий профиль Tox
- Информация профиля
- Настройки профиля
- Сменить ваше имя пользователя
- Сменить ваше сообщение в статусе
- Изменить ваш статус
Профиль
Контакт с данным Tox ID уже существует
aTox запущен…
diff --git a/atox/src/main/res/values-sk/strings.xml b/atox/src/main/res/values-sk/strings.xml
index ac07a7ce..4bed9e67 100644
--- a/atox/src/main/res/values-sk/strings.xml
+++ b/atox/src/main/res/values-sk/strings.xml
@@ -74,11 +74,6 @@
aTox je spustený…
Kontakt s takým Tox-ovým ID už existuje
Profil
- Zmeň svoj status
- Zmeň svoju správu v statuse
- Zmeň svoju prezývku
- Nastavenia profilu
- Informácie o profile
alebo importuj existujúci Tox save
Používateľsky určené
Používateľsky určený JSON súbor musí mať taký formát ako je na https://nodes.tox.chat/json
diff --git a/atox/src/main/res/values-sv/strings.xml b/atox/src/main/res/values-sv/strings.xml
index abf90f84..324d242b 100644
--- a/atox/src/main/res/values-sv/strings.xml
+++ b/atox/src/main/res/values-sv/strings.xml
@@ -57,11 +57,6 @@
Tema
Skicka som handling
eller importera en Tox profil som redan finns
- Profilinformation
- Profilinställningar
- Ändra ditt smeknamn
- Ändra ditt statusmeddelande
- Ändra din status
Profil
En kontakt med samma Tox ID finns redan
aTox kör…
diff --git a/atox/src/main/res/values-tr/strings.xml b/atox/src/main/res/values-tr/strings.xml
index ec7463c3..f99ef9f9 100644
--- a/atox/src/main/res/values-tr/strings.xml
+++ b/atox/src/main/res/values-tr/strings.xml
@@ -73,11 +73,6 @@
aTox çalışıyor…
Bu Tox Kimliğiyle bir kişi zaten var
Profil
- Durumunuzu değiştirin
- Durum iletinizi değiştirin
- Takma adınızı değiştirin
- Profil ayarları
- Profil bilgisi
veya zaten var olan Tox kaydı olarak içeri al
İşlem olarak gönder
Kullanıcı tarafından sağlanan JSON dosyaları https://nodes.tox.chat/json ile aynı şekli sağlamalıdır
diff --git a/atox/src/main/res/values-uk/strings.xml b/atox/src/main/res/values-uk/strings.xml
index be3b295f..8135c3a0 100644
--- a/atox/src/main/res/values-uk/strings.xml
+++ b/atox/src/main/res/values-uk/strings.xml
@@ -96,11 +96,6 @@
UDP увімкнено
aTox працює…
Контакт із таким ідентифікатором уже існує
- Змінити статус
- Змінити повідомлення у статусі
- Змінити Ваше прізвисько
- Налаштування профілю
- Відомості профілю
або імпортувати існуючий профіль Tox
Надіслати як дію
Власні JSON файли повинні мати ту ж структуру як і тут https://nodes.tox.chat/json
diff --git a/atox/src/main/res/values-v21/styles.xml b/atox/src/main/res/values-v21/styles.xml
deleted file mode 100644
index f51e3dc4..00000000
--- a/atox/src/main/res/values-v21/styles.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/atox/src/main/res/values-v23/styles.xml b/atox/src/main/res/values-v23/styles.xml
new file mode 100644
index 00000000..0efaadb9
--- /dev/null
+++ b/atox/src/main/res/values-v23/styles.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/atox/src/main/res/values-v27/styles.xml b/atox/src/main/res/values-v27/styles.xml
new file mode 100644
index 00000000..e34598b1
--- /dev/null
+++ b/atox/src/main/res/values-v27/styles.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/atox/src/main/res/values-v29/styles.xml b/atox/src/main/res/values-v29/styles.xml
deleted file mode 100644
index fe32258a..00000000
--- a/atox/src/main/res/values-v29/styles.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/atox/src/main/res/values-zh-rCN/strings.xml b/atox/src/main/res/values-zh-rCN/strings.xml
index 5de4812e..2ffc21cd 100644
--- a/atox/src/main/res/values-zh-rCN/strings.xml
+++ b/atox/src/main/res/values-zh-rCN/strings.xml
@@ -43,9 +43,6 @@
删除
附加文件
没有安装用于处理\"%1$s\"媒体类型的应用程序
- 更改您的状态
- 更改您的状态消息
- 更改您的昵称
编辑您的状态
来自
忙碌
@@ -59,8 +56,6 @@
aTox 正在运行……
已存在具有该 Tox ID 的联系人
用户配置文件
- 配置文件设置
- 配置文件信息
或导入现有的 Tox 存档
作为操作发送
用户提供的 JSON 文件必须与 https://nodes.tox.chat/json 具有相同的格式
diff --git a/atox/src/main/res/values/colors.xml b/atox/src/main/res/values/colors.xml
index e0e60755..cbf5728b 100644
--- a/atox/src/main/res/values/colors.xml
+++ b/atox/src/main/res/values/colors.xml
@@ -1,11 +1,11 @@
- #1976D2
- #1565C0
- #0D47A1
- #D81B60
+ #0053e1
+ #73a0f4
#555
+ #f2f5ed
+
@color/colorGray
@android:color/holo_red_dark
@android:color/holo_orange_light
@@ -13,8 +13,6 @@
- #E0E0E0
- - #FFFFFF
-
- #546E7A
- #ECEFF1
- #3E4651
diff --git a/atox/src/main/res/values/strings.xml b/atox/src/main/res/values/strings.xml
index 5c89fc3b..6b421b2d 100644
--- a/atox/src/main/res/values/strings.xml
+++ b/atox/src/main/res/values/strings.xml
@@ -96,12 +96,10 @@
aTox v%1$s — %2$d
Send as action
or import an existing Tox save
- Profile information
- Profile settings
- Change your nickname
- Change your status message
- Change your status
+ Edit your name
+ Edit your status message
Profile
+ Edit Profile
A contact with that Tox ID already exists
aTox is running…
Network
@@ -177,9 +175,19 @@
This **only** blocks screenshots on **your** device, and provides no protection against your contacts taking screenshots or otherwise saving your conversations
Return to chat
Toggle speakerphone
+ Share this Tox ID with others who you want them to send you a friendship request.
+ Just click on it to share, or click the copy icon.
+ Or ask them to scan in the app this QR code.
+ QR code of Tox ID
+ Status
+
+ - @string/status_available
+ - @string/status_away
+ - @string/status_busy
+
Sent when your profile needs to be unlocked for aTox to run
aTox profile locked
Tap to unlock your profile and start aTox
-
\ No newline at end of file
+
diff --git a/atox/src/main/res/values/styles.xml b/atox/src/main/res/values/styles.xml
index c8c41c42..e39e3530 100644
--- a/atox/src/main/res/values/styles.xml
+++ b/atox/src/main/res/values/styles.xml
@@ -1,11 +1,45 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
diff --git a/atox/src/main/res/xml/file_paths.xml b/atox/src/main/res/xml/file_paths.xml
index 46567504..a3bfdc98 100644
--- a/atox/src/main/res/xml/file_paths.xml
+++ b/atox/src/main/res/xml/file_paths.xml
@@ -1,4 +1,6 @@
+
+
diff --git a/core/schemas/ltd.evilcorp.core.db.Database/6.json b/core/schemas/ltd.evilcorp.core.db.Database/6.json
new file mode 100644
index 00000000..2aae39d3
--- /dev/null
+++ b/core/schemas/ltd.evilcorp.core.db.Database/6.json
@@ -0,0 +1,294 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 6,
+ "identityHash": "e5acbeae0a3d3479e8e4d1e722bfc1c4",
+ "entities": [
+ {
+ "tableName": "contacts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`public_key` TEXT NOT NULL, `name` TEXT NOT NULL, `status_message` TEXT NOT NULL, `last_message` INTEGER NOT NULL, `status` INTEGER NOT NULL, `connection_status` INTEGER NOT NULL, `typing` INTEGER NOT NULL, `avatar_uri` TEXT NOT NULL, `has_unread_messages` INTEGER NOT NULL, `draft_message` TEXT NOT NULL, PRIMARY KEY(`public_key`))",
+ "fields": [
+ {
+ "fieldPath": "publicKey",
+ "columnName": "public_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusMessage",
+ "columnName": "status_message",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastMessage",
+ "columnName": "last_message",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "connectionStatus",
+ "columnName": "connection_status",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "typing",
+ "columnName": "typing",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "avatarUri",
+ "columnName": "avatar_uri",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasUnreadMessages",
+ "columnName": "has_unread_messages",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "draftMessage",
+ "columnName": "draft_message",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "public_key"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "file_transfers",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`public_key` TEXT NOT NULL, `file_number` INTEGER NOT NULL, `file_kind` INTEGER NOT NULL, `file_size` INTEGER NOT NULL, `file_name` TEXT NOT NULL, `outgoing` INTEGER NOT NULL, `progress` INTEGER NOT NULL, `destination` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "publicKey",
+ "columnName": "public_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fileNumber",
+ "columnName": "file_number",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fileKind",
+ "columnName": "file_kind",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fileSize",
+ "columnName": "file_size",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fileName",
+ "columnName": "file_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "outgoing",
+ "columnName": "outgoing",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "progress",
+ "columnName": "progress",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "destination",
+ "columnName": "destination",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "friend_requests",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`public_key` TEXT NOT NULL, `message` TEXT NOT NULL, PRIMARY KEY(`public_key`))",
+ "fields": [
+ {
+ "fieldPath": "publicKey",
+ "columnName": "public_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "message",
+ "columnName": "message",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "public_key"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "messages",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversation` TEXT NOT NULL, `message` TEXT NOT NULL, `sender` INTEGER NOT NULL, `type` INTEGER NOT NULL, `correlation_id` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "publicKey",
+ "columnName": "conversation",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "message",
+ "columnName": "message",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sender",
+ "columnName": "sender",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "correlationId",
+ "columnName": "correlation_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "users",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`public_key` TEXT NOT NULL, `name` TEXT NOT NULL, `status_message` TEXT NOT NULL, `status` INTEGER NOT NULL, `connection_status` INTEGER NOT NULL, `avatar_uri` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`public_key`))",
+ "fields": [
+ {
+ "fieldPath": "publicKey",
+ "columnName": "public_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusMessage",
+ "columnName": "status_message",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "connectionStatus",
+ "columnName": "connection_status",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "avatarUri",
+ "columnName": "avatar_uri",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "password",
+ "columnName": "password",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "public_key"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e5acbeae0a3d3479e8e4d1e722bfc1c4')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/core/src/androidTest/kotlin/db/DatabaseMigrationTest.kt b/core/src/androidTest/kotlin/db/DatabaseMigrationTest.kt
index 5597b482..c43a6073 100644
--- a/core/src/androidTest/kotlin/db/DatabaseMigrationTest.kt
+++ b/core/src/androidTest/kotlin/db/DatabaseMigrationTest.kt
@@ -16,6 +16,7 @@ import ltd.evilcorp.core.vo.FtNotStarted
import ltd.evilcorp.core.vo.Message
import ltd.evilcorp.core.vo.MessageType
import ltd.evilcorp.core.vo.Sender
+import ltd.evilcorp.core.vo.User
import ltd.evilcorp.core.vo.UserStatus
import org.junit.Assert.assertEquals
import org.junit.Rule
@@ -66,6 +67,16 @@ class DatabaseMigrationTest {
"file:///home/robin/fantastic_bird.png"
)
+ private val user = User(
+ "096E9EADEDF0DACC7A4ED2CF469EB48D91F0E91DE85200D65651543FC27E8BF4",
+ "Test User",
+ "Testing...",
+ UserStatus.Busy,
+ ConnectionStatus.TCP,
+ "password",
+ "",
+ )
+
@Test
fun migrate_1_to_2() {
helper.createDatabase(TEST_DB, 1).use { db ->
@@ -252,6 +263,49 @@ class DatabaseMigrationTest {
}
}
+ @Test
+ fun migrate_5_to_6() {
+ helper.createDatabase(TEST_DB, 5).use { db ->
+ with(user) {
+ db.execSQL(
+ """INSERT INTO users VALUES (
+ '$publicKey',
+ '$name',
+ '$statusMessage',
+ ${status.ordinal},
+ ${connectionStatus.ordinal},
+ '$password')
+ """.trimIndent()
+ )
+ }
+
+ val cursor = db.query("SELECT * FROM users").apply { moveToFirst() }
+ assertEquals(6, cursor.columnCount)
+ with(user) {
+ assertEquals(publicKey, cursor.getString(0))
+ assertEquals(name, cursor.getString(1))
+ assertEquals(statusMessage, cursor.getString(2))
+ assertEquals(status.ordinal, cursor.getInt(3))
+ assertEquals(connectionStatus.ordinal, cursor.getInt(4))
+ assertEquals(password, cursor.getString(5))
+ }
+ }
+
+ helper.runMigrationsAndValidate(TEST_DB, 6, true, MIGRATION_5_6).use { db ->
+ val cursor = db.query("SELECT * FROM users").apply { moveToFirst() }
+ assertEquals(7, cursor.columnCount)
+ with(user) {
+ assertEquals(publicKey, cursor.getString(0))
+ assertEquals(name, cursor.getString(1))
+ assertEquals(statusMessage, cursor.getString(2))
+ assertEquals(status.ordinal, cursor.getInt(3))
+ assertEquals(connectionStatus.ordinal, cursor.getInt(4))
+ assertEquals(password, cursor.getString(5))
+ assertEquals(avatarUri, cursor.getString(6))
+ }
+ }
+ }
+
@Test
fun run_all_migrations() {
helper.createDatabase(TEST_DB, 1).use { db ->
@@ -294,6 +348,18 @@ class DatabaseMigrationTest {
""".trimIndent()
)
}
+ with(user) {
+ db.execSQL(
+ """INSERT INTO users VALUES (
+ '$publicKey',
+ '$name',
+ '$statusMessage',
+ ${status.ordinal},
+ ${connectionStatus.ordinal},
+ '$password')
+ """.trimIndent()
+ )
+ }
db.query("SELECT * FROM contacts").let { cursor ->
assertEquals(cursor.columnCount, 8)
@@ -334,9 +400,21 @@ class DatabaseMigrationTest {
assertEquals(progress, cursor.getLong(6))
}
}
+ db.query("SELECT * FROM users").let { cursor ->
+ assertEquals(6, cursor.columnCount)
+ with(user) {
+ cursor.moveToFirst()
+ assertEquals(publicKey, cursor.getString(0))
+ assertEquals(name, cursor.getString(1))
+ assertEquals(statusMessage, cursor.getString(2))
+ assertEquals(status.ordinal, cursor.getInt(3))
+ assertEquals(connectionStatus.ordinal, cursor.getInt(4))
+ assertEquals(password, cursor.getString(5))
+ }
+ }
}
- helper.runMigrationsAndValidate(TEST_DB, 5, true, *ALL_MIGRATIONS).use { db ->
+ helper.runMigrationsAndValidate(TEST_DB, 6, true, *ALL_MIGRATIONS).use { db ->
db.query("SELECT * FROM contacts").let { cursor ->
assertEquals(cursor.columnCount, 10)
with(contact) {
@@ -366,6 +444,19 @@ class DatabaseMigrationTest {
assertEquals(type.ordinal, cursor.getInt(6))
}
}
+ db.query("SELECT * FROM users").let { cursor ->
+ assertEquals(7, cursor.columnCount)
+ with(user) {
+ cursor.moveToFirst()
+ assertEquals(publicKey, cursor.getString(0))
+ assertEquals(name, cursor.getString(1))
+ assertEquals(statusMessage, cursor.getString(2))
+ assertEquals(status.ordinal, cursor.getInt(3))
+ assertEquals(connectionStatus.ordinal, cursor.getInt(4))
+ assertEquals(password, cursor.getString(5))
+ assertEquals(avatarUri, cursor.getString(6))
+ }
+ }
}
}
}
diff --git a/core/src/main/kotlin/db/Database.kt b/core/src/main/kotlin/db/Database.kt
index 841ccf55..f90b444c 100644
--- a/core/src/main/kotlin/db/Database.kt
+++ b/core/src/main/kotlin/db/Database.kt
@@ -15,7 +15,7 @@ import ltd.evilcorp.core.vo.User
@Database(
entities = [Contact::class, FileTransfer::class, FriendRequest::class, Message::class, User::class],
- version = 5
+ version = 6
)
@TypeConverters(Converters::class)
abstract class Database : RoomDatabase() {
diff --git a/core/src/main/kotlin/db/Migration.kt b/core/src/main/kotlin/db/Migration.kt
index 12008c69..67421d38 100644
--- a/core/src/main/kotlin/db/Migration.kt
+++ b/core/src/main/kotlin/db/Migration.kt
@@ -44,4 +44,10 @@ val MIGRATION_4_5 = object : Migration(4, 5) {
)
}
-val ALL_MIGRATIONS = arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5)
+val MIGRATION_5_6 = object : Migration(5, 6) {
+ override fun migrate(db: SupportSQLiteDatabase) = db.execSQL(
+ "ALTER TABLE users ADD COLUMN avatar_uri TEXT NOT NULL DEFAULT ''"
+ )
+}
+
+val ALL_MIGRATIONS = arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6)
diff --git a/core/src/main/kotlin/db/UserDao.kt b/core/src/main/kotlin/db/UserDao.kt
index 75d3d186..3d14754d 100644
--- a/core/src/main/kotlin/db/UserDao.kt
+++ b/core/src/main/kotlin/db/UserDao.kt
@@ -34,6 +34,9 @@ interface UserDao {
@Query("UPDATE users SET status = :status WHERE public_key == :publicKey")
fun updateStatus(publicKey: String, status: UserStatus)
+ @Query("UPDATE users SET avatar_uri = :uri WHERE public_key = :publicKey")
+ fun updateAvatarUri(publicKey: String, uri: String)
+
@Query("SELECT COUNT(*) FROM users WHERE public_key = :publicKey")
fun exists(publicKey: String): Boolean
diff --git a/core/src/main/kotlin/repository/UserRepository.kt b/core/src/main/kotlin/repository/UserRepository.kt
index 710ca370..7f6b9900 100644
--- a/core/src/main/kotlin/repository/UserRepository.kt
+++ b/core/src/main/kotlin/repository/UserRepository.kt
@@ -39,4 +39,7 @@ class UserRepository @Inject internal constructor(
fun updateStatus(publicKey: String, status: UserStatus) =
userDao.updateStatus(publicKey, status)
+
+ fun updateAvatarUri(publicKey: String, uri: String) =
+ userDao.updateAvatarUri(publicKey, uri)
}
diff --git a/core/src/main/kotlin/vo/User.kt b/core/src/main/kotlin/vo/User.kt
index 9f89bccd..71f699ae 100644
--- a/core/src/main/kotlin/vo/User.kt
+++ b/core/src/main/kotlin/vo/User.kt
@@ -27,5 +27,8 @@ data class User(
var connectionStatus: ConnectionStatus = ConnectionStatus.None,
@ColumnInfo(name = "password")
- var password: String = ""
+ var password: String = "",
+
+ @ColumnInfo(name = "avatar_uri")
+ var avatarUri: String = "",
)