Compare commits

...

16 Commits

Author SHA1 Message Date
leca edd9c666ac bugfixes and translation 2024-11-20 22:00:32 +03:00
leca 6bbf436f45 Мимо собственного кода
Я без шуток не хожу -
То комменты не коммичу,
То весь класс перепишу

Кволити - не кволити,
Хуй меня уволите
2024-11-20 15:42:40 +03:00
leca a860e4d59f added functionality to group rename & leave buttons, added ability to transfer ownership of a group 2024-11-18 14:20:34 +03:00
leca 37e52b3f2f Fixed category filter 2024-11-17 22:37:54 +03:00
leca f18d7ad1e0 performance fixes 2024-11-17 19:26:25 +03:00
leca 1d7050c687 some ui improvements 2024-11-17 17:56:30 +03:00
leca 153506fc83 fixed some layout overflows 2024-11-16 15:40:08 +03:00
leca abe2a850d6 logging out on user data changes 2024-11-15 11:36:44 +03:00
leca 58a7ea7357 added ability to log out 2024-11-14 13:38:33 +03:00
leca 2ebcfff51a fixed using network in offline mode 2024-11-14 13:32:56 +03:00
leca 2676d8083e fixed net weight parsing 2024-11-14 13:11:13 +03:00
leca e5772bcad3 now handling unexpected ws close 2024-11-14 04:02:36 +03:00
leca 988272070d fixed websockets, synchronization works properly, and a bunch of bugfixes 2024-11-14 03:53:28 +03:00
leca fbf630090c real-time sync with websockets (untested) 2024-11-13 18:51:46 +03:00
leca 8c3845a07e added ability to use application offline 2024-11-12 18:17:01 +03:00
leca d08a79e981 handling no internet situations 2024-11-12 17:57:45 +03:00
43 changed files with 1744 additions and 923 deletions

View File

@ -13,6 +13,7 @@
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application
android:allowBackup="true"

View File

@ -0,0 +1,14 @@
package org.foxarmy.barcodescannerforemployees
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
class LoadingDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState)
}
}

View File

@ -1,5 +1,9 @@
package org.foxarmy.barcodescannerforemployees
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -10,16 +14,38 @@ import org.foxarmy.barcodescannerforemployees.dataclasses.Product
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlin.concurrent.thread
import java.util.concurrent.TimeUnit
class Net {
var language = "en-US"
var server = "bsfe.foxarmy.org"
var token = ""
fun requestProductFromOnlineDB(barcode: String): String {
var response = ""
thread {
fun serverIsAvailable(context: Context, callback: (Boolean) -> Unit) {
if (!isInternetConnectionAvailable(context)) {
callback(false)
}
CoroutineScope(Dispatchers.IO).launch {
var flag = false
val client = OkHttpClient();
val request = Request.Builder()
.url("https://$server/status")
.get()
.build();
try {
val response = client.newCall(request).execute();
flag = response.code == 200;
} catch (e: Exception) {
flag = false;
}
callback(flag)
}
}
fun requestProductFromOnlineDB(barcode: String, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
val url = "https://ean-online.ru/match.php"
val client = OkHttpClient()
@ -35,19 +61,15 @@ class Net {
.addHeader("accept-language", language)
.build()
response = client.newCall(request).execute().body!!.string()
}.join()
return if (response == "") {
"Not found 404"
} else {
response
response = client.newCall(request).execute()
callback(response)
}
}
fun registerAccount(username: String, password: String): Response {
lateinit var response: Response
thread {
fun registerAccount(username: String, password: String, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
val client = OkHttpClient()
val body = FormBody.Builder()
@ -63,14 +85,13 @@ class Net {
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun login(username: String, password: String): Response {
lateinit var response: Response
thread {
fun login(username: String, password: String, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
val client = OkHttpClient()
val body = FormBody.Builder()
@ -84,15 +105,19 @@ class Net {
.addHeader("accept-language", language)
.build()
response = client.newCall(requestLogin).execute()
}.join()
return response
callback(response)
}
}
fun uploadAbstractProduct(groupId: Int, abstractProduct: AbstractProduct, imageFile: File): Response {
lateinit var response: Response
fun uploadAbstractProduct(
groupId: Int,
abstractProduct: AbstractProduct,
imageFile: File,
callback: (Response) -> Unit,
) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val body = MultipartBody.Builder()
@ -115,16 +140,14 @@ class Net {
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun changeUsername(newUsername: String): Response {
fun changeUsername(newUsername: String, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
lateinit var response: Response
thread {
val client = OkHttpClient()
val body = FormBody.Builder()
@ -139,16 +162,14 @@ class Net {
.addHeader("accept-language", language)
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun changePassword(newPassword: String): Response {
fun changePassword(newPassword: String, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
lateinit var response: Response
thread {
val client = OkHttpClient()
val body = FormBody.Builder()
@ -163,15 +184,12 @@ class Net {
.addHeader("accept-language", language)
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun createGroup(name: String, password: String): Response {
lateinit var response: Response
thread {
fun createGroup(name: String, password: String, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
val client = OkHttpClient()
val body = FormBody.Builder()
@ -184,19 +202,19 @@ class Net {
.addHeader("Authorization", "Bearer $token")
.addHeader("accept-language", language)
.build()
response = client.newCall(request).execute()
}.join()
val response = client.newCall(request).execute()
changeGroupPassword(name, password, { responseFromPassword ->
callback(response)
})
changeGroupPassword(name, password)
return response
}
}
fun joinGroup(id: Int, password: String): Response {
lateinit var response: Response
fun joinGroup(id: Int, password: String, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val body = FormBody.Builder()
@ -210,41 +228,38 @@ class Net {
.addHeader("accept-language", language)
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun changeGroupPassword(name: String, password: String): Response {
lateinit var response: Response
fun changeGroupPassword(name: String, password: String, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
getGroupId(name, { groupId ->
val client = OkHttpClient()
val groupId = getGroupId(name);
val body = FormBody.Builder()
body.add("password", password)
val client = OkHttpClient()
val requestBody = body.build()
val body = FormBody.Builder()
body.add("password", password)
val requestBody = body.build()
val request = Request.Builder()
.url("https://$server/api/group/password/$groupId")
.post(requestBody)
.addHeader("Authorization", "Bearer $token")
.addHeader("accept-language", language)
.build()
response = client.newCall(request).execute()
}.join()
return response
val request = Request.Builder()
.url("https://$server/api/group/password/$groupId")
.post(requestBody)
.addHeader("Authorization", "Bearer $token")
.addHeader("accept-language", language)
.build()
response = client.newCall(request).execute()
callback(response)
});
}
}
fun getGroupId(name: String): String {
lateinit var response: Response
fun getGroupId(name: String, callback: (String) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val request = Request.Builder()
@ -254,17 +269,14 @@ class Net {
.addHeader("accept-language", language)
.build()
response = client.newCall(request).execute()
}.join()
val responseText = response.body!!.string()
return responseText
callback(response.body!!.string())
}
}
fun getGroupName(id: Int): String {
lateinit var response: Response
fun getGroupName(id: Int, callback: (String) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val request = Request.Builder()
@ -274,17 +286,14 @@ class Net {
.addHeader("accept-language", language)
.build()
response = client.newCall(request).execute()
}.join()
val responseText = response.body!!.string()
return responseText
callback(response.body!!.string())
}
}
fun getUsersInGroup(groupId: Int): Response {
lateinit var response: Response
fun getUsersInGroup(groupId: Int, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val request = Request.Builder()
@ -295,15 +304,14 @@ class Net {
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun getMyGroups(): Response {
lateinit var response: Response
fun getMyGroups(callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val request = Request.Builder()
@ -314,15 +322,14 @@ class Net {
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun getUsernameById(userId: Int): Response {
lateinit var response: Response
fun getUsernameById(userId: Int, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val request = Request.Builder()
@ -333,15 +340,14 @@ class Net {
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun getGroupAdminId(groupId: Int): Response {
lateinit var response: Response
fun getGroupAdminId(groupId: Int, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val request = Request.Builder()
@ -352,15 +358,19 @@ class Net {
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun updateAbstractProduct(groupId: Int, abstractProduct: AbstractProduct, imageFile: File): Response {
lateinit var response: Response
fun updateAbstractProduct(
groupId: Int,
abstractProduct: AbstractProduct,
imageFile: File,
callback: (Response) -> Unit,
) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val body = MultipartBody.Builder()
@ -383,15 +393,14 @@ class Net {
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun uploadCategory(groupId: Int, category: Category): Response {
lateinit var response: Response
fun uploadCategory(groupId: Int, category: Category, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val body = FormBody.Builder()
@ -408,15 +417,14 @@ class Net {
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun updateCategory(groupId: Int, category: Category): Response {
lateinit var response: Response
fun updateCategory(groupId: Int, category: Category, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val body = FormBody.Builder()
@ -433,15 +441,14 @@ class Net {
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun uploadProduct(groupId: Int, product: Product): Response {
lateinit var response: Response
fun uploadProduct(groupId: Int, product: Product, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val body = FormBody.Builder()
@ -461,15 +468,14 @@ class Net {
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun updateProduct(groupId: Int, product: Product): Response {
lateinit var response: Response
fun updateProduct(groupId: Int, product: Product, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val body = FormBody.Builder()
@ -489,15 +495,14 @@ class Net {
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun synchronize(groupId: Int): Response {
lateinit var response: Response
fun synchronize(groupId: Int, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val request = Request.Builder()
@ -508,76 +513,41 @@ class Net {
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun getProduct(groupId: Int, localId: Int): Response {
lateinit var response: Response
thread {
val client = OkHttpClient()
val request = Request.Builder()
.url("https://$server/api/product/$groupId/$localId")
.get()
.addHeader("Authorization", "Bearer $token")
.addHeader("accept-language", language)
fun downloadImage(url: String, file: File, callback: () -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.MINUTES)
.build()
response = client.newCall(request).execute()
}.join()
return response
}
fun getAbstractProduct(groupId: Int, localId: Int): Response {
lateinit var response: Response
thread {
val client = OkHttpClient()
val request = Request.Builder()
.url("https://$server/api/abstractproduct/$groupId/$localId")
.get()
.addHeader("Authorization", "Bearer $token")
.addHeader("accept-language", language)
.build()
response = client.newCall(request).execute()
}.join()
return response
}
fun downloadImage(url: String, file: File) {
thread {
val client = OkHttpClient()
val request = Request.Builder()
.url(url)
.addHeader("Authorization", "Bearer $token")
.addHeader("accept-language", language)
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val response = client.newCall(request).execute()
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val fos = FileOutputStream(file)
val fos = FileOutputStream(file)
response.body?.byteStream()?.use { inputStream ->
fos.use {
inputStream.copyTo(fos)
}
response.body?.byteStream()?.use { inputStream ->
fos.use {
inputStream.copyTo(fos)
}
}
}.join()
callback()
}
}
fun deleteCategory(groupId: Int, localId: Int): Response {
lateinit var response: Response
fun deleteCategory(groupId: Int, localId: Int, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val request = Request.Builder()
@ -588,15 +558,14 @@ class Net {
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun deleteAbstractProduct(groupId: Int, localId: Int): Response {
lateinit var response: Response
fun deleteAbstractProduct(groupId: Int, localId: Int, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val request = Request.Builder()
@ -607,15 +576,14 @@ class Net {
.build()
response = client.newCall(request).execute()
}.join()
return response
callback(response)
}
}
fun deleteProduct(groupId: Int, localId: Int): Response {
lateinit var response: Response
fun deleteProduct(groupId: Int, localId: Int, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
thread {
val client = OkHttpClient()
val request = Request.Builder()
@ -626,8 +594,69 @@ class Net {
.build()
response = client.newCall(request).execute()
}.join()
callback(response)
}
}
return response
fun renameGroup(groupId: Int, newName: String, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
val client = OkHttpClient()
val body = FormBody.Builder()
.add("name", newName)
.build()
val request = Request.Builder()
.url("https://$server/api/group/rename/$groupId")
.post(body)
.addHeader("Authorization", "Bearer $token")
.addHeader("accept-language", language)
.build()
response = client.newCall(request).execute()
callback(response)
}
}
fun transfer_ownership(groupId: Int, userId: Int, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
val client = OkHttpClient()
val body = FormBody.Builder()
.add("userId", userId.toString())
.build()
val request = Request.Builder()
.url("https://$server/api/group/transferOwnership/$groupId")
.post(body)
.addHeader("Authorization", "Bearer $token")
.addHeader("accept-language", language)
.build()
response = client.newCall(request).execute()
callback(response)
}
}
fun leaveGroup(groupId: Int, callback: (Response) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
lateinit var response: Response
val client = OkHttpClient()
val request = Request.Builder()
.url("https://$server/api/group/leave/$groupId")
.get()
.addHeader("Authorization", "Bearer $token")
.addHeader("accept-language", language)
.build()
response = client.newCall(request).execute()
callback(response)
}
}
}

View File

@ -14,8 +14,6 @@ class Parser constructor() {
text = text.replace(found, "")
netWeight = stripNetWeight(found)
return Triple(text, netWeight, found)
} else {
return Triple(text, 0.0, "")
}
}
return Triple("", 0.0, "")
@ -48,4 +46,4 @@ class Parser constructor() {
return AbstractProduct(0, "", name, netWeight, "", 0, unitNumber)
}
}
}

View File

@ -3,21 +3,30 @@ package org.foxarmy.barcodescannerforemployees
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.media.ExifInterface
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import com.google.firebase.components.BuildConfig
import org.foxarmy.barcodescannerforemployees.activities.LoginActivity
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.*
import kotlin.system.exitProcess
fun convertToUnixEpochTimestamp(dateString: String): Long {
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
@ -27,7 +36,11 @@ fun convertToUnixEpochTimestamp(dateString: String): Long {
}
fun getImageUri(activity: Activity, imageFile: File): Uri? {
return FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + "." + activity.localClassName + ".provider", imageFile)
return FileProvider.getUriForFile(
activity,
BuildConfig.APPLICATION_ID + "." + activity.localClassName + ".provider",
imageFile
)
}
@RequiresApi(Build.VERSION_CODES.R)
@ -47,13 +60,13 @@ fun generateThumbnailForImage(context: Context, imageHash: String) {
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
val matrix = Matrix()
when(orientation){
when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90F)
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180F)
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270F)
}
val rotated = Bitmap.createBitmap(img, 0, 0, img.width, img.height, matrix, true)
val scaled = Bitmap.createScaledBitmap(rotated, rotated.width/4, rotated.height/4, true)
val scaled = Bitmap.createScaledBitmap(rotated, rotated.width / 4, rotated.height / 4, true)
scaled.compress(Bitmap.CompressFormat.WEBP_LOSSY, 25, FileOutputStream(thumbnailFile))
}
@ -64,8 +77,11 @@ fun String.md5(): String {
return digest.toHexString()
}
fun stripNetWeight (netWeight: String): Double {
return removeSubstringsFromString(netWeight, arrayOf("Л", "л", "мл", "Мл", "г", "Г", "кг", "Кг", "шт", "Шт", ",", " ", ".")).toDouble()
fun stripNetWeight(netWeight: String): Double {
return removeSubstringsFromString(
netWeight,
arrayOf("Л", "л", "мл", "Мл", "г", "Г", "кг", "Кг", "шт", "Шт", ",", " ", ".")
).toDouble()
}
fun removeSubstringsFromString(text: String, toRemove: Array<String>): String {
@ -99,21 +115,30 @@ fun calculateProductFreshness(dateOfProduction: Long, dateOfExpiry: Long): Doubl
return lifeSpanLeft / productLifeSpan.toDouble()
}
fun getUnitNameById (context: Context, id: Int): String {
return when(id) {
fun getUnitNameById(context: Context, id: Int): String {
return when (id) {
0 -> { context.getString(R.string.kilogram) }
1 -> { context.getString(R.string.gram) }
2 -> { context.getString(R.string.liter) }
3 -> { context.getString(R.string.milliliter) }
4 -> { context.getString(R.string.pieces) }
else -> { "" }
}
}
fun parseArray(input: String): IntArray {
fun parseIntArray(input: String): IntArray {
return input.trim('[', ']').split(",").map { it.trim().toInt() }.toIntArray()
}
fun parseStringList(input: String): List<String> {
return input.trim('[', ']').split(",").map { it.trim() }.toList()
}
fun calculateMd5Hash(file: File): String {
val digest = MessageDigest.getInstance("MD5")
val fis = FileInputStream(file)
@ -137,4 +162,56 @@ fun bytesToHex(bytes: ByteArray): String {
hexString.append(hex)
}
return hexString.toString()
}
fun isInternetConnectionAvailable(context: Context): Boolean {
if (context.getSystemService(Context.CONNECTIVITY_SERVICE) == null) return false
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (connectivityManager != null) {
val capabilities =
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
if (capabilities != null) {
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
return true
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
return true
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
return true
}
}
}
return false
}
fun noInternetConnectionAvailableNotification(context: Context) {
(context as Activity).runOnUiThread {
AlertDialog.Builder(context)
.setMessage(context.getString(R.string.no_internet_connection))
.setCancelable(false)
.setPositiveButton(R.string.quit) { _, _ ->
exitProcess(0)
}
.setNeutralButton(R.string.logout) { _, _ ->
val sharedPreferences = getPreferences(context)
sharedPreferences.edit().putString("token", "").apply()
sharedPreferences.edit().putString("server", "").apply()
sharedPreferences.edit().putStringSet("groups", emptySet()).apply()
sharedPreferences.edit().putString("currentGroup", "").apply()
val intent = Intent(context, LoginActivity::class.java)
context.startActivity(intent)
context.finish()
}.show()
}
}
fun getPreferences(context: Context): SharedPreferences {
return EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}

View File

@ -0,0 +1,182 @@
package org.foxarmy.barcodescannerforemployees
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import okhttp3.*
import org.foxarmy.barcodescannerforemployees.activities.MainActivity
import org.foxarmy.barcodescannerforemployees.database.AbstractProductDAO
import org.foxarmy.barcodescannerforemployees.database.CategoryDAO
import org.foxarmy.barcodescannerforemployees.database.DBStorageController
import org.foxarmy.barcodescannerforemployees.database.ProductDAO
import org.foxarmy.barcodescannerforemployees.dataclasses.AbstractProduct
import org.foxarmy.barcodescannerforemployees.dataclasses.Category
import org.foxarmy.barcodescannerforemployees.dataclasses.Product
import org.json.JSONObject
import java.io.File
import java.util.concurrent.TimeUnit
class WebSocketClient(private val context: Context, private val server: String) {
private lateinit var webSocket: WebSocket
private lateinit var token: String
private lateinit var currentGroup: String
fun connect(token: String, currentGroup: String) {
this.token = token
this.currentGroup = currentGroup
val client = OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(1, TimeUnit.MINUTES)
.writeTimeout(10, TimeUnit.SECONDS)
.callTimeout(1, TimeUnit.MINUTES)
.build()
val request = Request.Builder()
.url("wss://$server")
.build()
client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
this@WebSocketClient.webSocket = webSocket
sendMessage("{\"token\":\"$token\",\"currentGroup\":\"$currentGroup\"}")
keepAlive()
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onMessage(webSocket: WebSocket, text: String) {
val payload = JSONObject(text)
val item = payload["item"] as String
val data = payload["data"] as JSONObject
val action = payload["action"] as String
val currentGroup = payload["groupId"] as String
val dbHelper = DBStorageController(context, currentGroup)
val abstractProductDAO = AbstractProductDAO(dbHelper)
val productDAO = ProductDAO(dbHelper)
val categoryDAO = CategoryDAO(dbHelper)
when (action) {
"create" -> {
when (item) {
"abstractproduct" -> {
val newAbstractProduct = AbstractProduct.createFromJSON(data)
val net = Net()
net.server = server
net.token = token
val picturesDir = File(context.filesDir, "pictures")
picturesDir.mkdirs()
val pictureFile =
File(picturesDir, "${data["image_filename"]}.png")
val url = "https://${net.server}/api/abstractproduct/getImage/${currentGroup}/${data["local_id"]}"
net.downloadImage(url, pictureFile, {
abstractProductDAO.addAbstractProduct(newAbstractProduct)
(context as MainActivity).updateAll()
})
}
"product" -> {
val newProduct = Product.createFromJSON(data)
productDAO.insertNewProduct(newProduct)
(context as MainActivity).updateAll()
}
"category" -> {
val newCategory = Category.createFromJSON(data)
categoryDAO.addCategory(newCategory)
(context as MainActivity).updateAll()
}
}
}
"update" -> {
when (item) {
"abstractproduct" -> {
val updatedAbstractProduct = AbstractProduct.createFromJSON(data)
val net = Net()
net.server = server
net.token = token
val picturesDir = File(context.filesDir, "pictures")
picturesDir.mkdirs()
val pictureFile =
File(picturesDir, "${data["image_filename"]}.png")
val url = "https://${net.server}/api/abstractproduct/getImage/${currentGroup}/${data["local_id"]}"
net.downloadImage(url, pictureFile, {
abstractProductDAO.updateAbstractProduct(updatedAbstractProduct)
(context as MainActivity).updateAll()
})
}
"product" -> {
val updatedProduct = Product.createFromJSON(data)
productDAO.updateProduct(updatedProduct)
(context as MainActivity).updateAll()
}
"category" -> {
val updatedCategory = Category.createFromJSON(data)
categoryDAO.updateCategory(updatedCategory)
(context as MainActivity).updateAll()
}
}
}
"delete" -> {
val id = data["local_id"] as String
when(item) {
"abstractproduct" -> {
abstractProductDAO.eraseAbstractProduct(id.toInt(), context)
(context as MainActivity).updateAll()
}
"product" -> {
productDAO.eraseProduct(id.toInt())
(context as MainActivity).updateAll()
}
"category" -> {
categoryDAO.eraseCategory(id.toInt(), context)
(context as MainActivity).updateAll()
}
}
}
}
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
Log.d("QWERTYUIOP", "Closing ws. Reason: $reason")
// noInternetConnectionAvailableNotification(context)
this@WebSocketClient.connect(token, currentGroup)
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.d("QWERTYUIOP","Connection failed: ${t.message}")
noInternetConnectionAvailableNotification(context)
}
})
}
private fun keepAlive() {
Thread {
while (true) {
Thread.sleep(30000)
webSocket.send("keepalive")
}
}.start()
}
fun sendMessage(message: String) {
webSocket.send(message)
}
fun close() {
webSocket.close(1000, "Closing WebSocket connection")
}
}

View File

@ -1,11 +1,11 @@
package org.foxarmy.barcodescannerforemployees.activities
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.Net
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.databinding.ActivityAccountSettingsBinding
class AccountSettingsActivity : AppCompatActivity() {
@ -19,15 +19,7 @@ class AccountSettingsActivity : AppCompatActivity() {
setContentView(binding.root)
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
val sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
masterKeyAlias,
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
val sharedPreferences = org.foxarmy.barcodescannerforemployees.getPreferences(this)
val net = Net()
@ -36,13 +28,39 @@ class AccountSettingsActivity : AppCompatActivity() {
net.token = sharedPreferences.getString("token", "")!!
binding.saveUsernameButton.setOnClickListener {
val response = net.changeUsername(binding.newUsernameTextEdit.text.toString())
Toast.makeText(this, response.body!!.string(), Toast.LENGTH_LONG).show()
net.changeUsername(binding.newUsernameTextEdit.text.toString(), { response ->
if (response.code == 200) {
runOnUiThread {
Toast.makeText(this, getString(R.string.username_changed), Toast.LENGTH_LONG).show()
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
finish()
}
} else {
runOnUiThread {
Toast.makeText(this, response.body!!.string(), Toast.LENGTH_LONG).show()
}
}
})
}
binding.savePasswordButton.setOnClickListener {
val response = net.changePassword(binding.newPasswordTextEdit.text.toString())
Toast.makeText(this, response.body!!.string(), Toast.LENGTH_LONG).show()
net.changePassword(binding.newPasswordTextEdit.text.toString(), { response ->
if (response.code == 200) {
runOnUiThread {
Toast.makeText(this, getString(R.string.password_changed), Toast.LENGTH_LONG).show()
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
finish()
}
} else {
runOnUiThread {
Toast.makeText(this, response.body!!.string(), Toast.LENGTH_LONG).show()
}
}
})
}
}
}

View File

@ -1,5 +1,6 @@
package org.foxarmy.barcodescannerforemployees.activities
import android.app.ProgressDialog
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
@ -17,8 +18,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.widget.addTextChangedListener
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanIntentResult
import com.journeyapps.barcodescanner.ScanOptions
@ -32,7 +31,6 @@ import java.io.File
import java.io.FileOutputStream
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import kotlin.concurrent.thread
class AddAbstractProductActivity : AppCompatActivity() {
private lateinit var imageView: ImageView
@ -60,20 +58,21 @@ class AddAbstractProductActivity : AppCompatActivity() {
private lateinit var DAO: AbstractProductDAO
private lateinit var sharedPreferences: SharedPreferences
private lateinit var loadingDialog: ProgressDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.fragment_add_abstract_product)
sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
loadingDialog = ProgressDialog(this)
loadingDialog.setMessage(getString(R.string.loading_please_wait))
loadingDialog.setCancelable(false)
loadingDialog.setTitle(getString(R.string.loading))
val dbHelper = DBStorageController(this, sharedPreferences.getString("currentGroup", "database")!!)
sharedPreferences = getPreferences(this)
val dbHelper = DBStorageController(this, sharedPreferences.getString("currentGroup", "offline")!!)
DAO = AbstractProductDAO(dbHelper)
picturesPath = File(filesDir, "pictures")
@ -109,25 +108,28 @@ class AddAbstractProductActivity : AppCompatActivity() {
action = extras!!.get("action") as String
when (action) {
"update" -> {
abstractProduct = extras.get("abstractProduct") as AbstractProduct?
abstractProduct = extras.get("abstractProduct") as AbstractProduct
}
"new_from_barcode" -> {
abstractProduct = extras.get("abstractProduct") as AbstractProduct?
abstractProduct = extras.get("abstractProduct") as AbstractProduct
barcode = abstractProduct!!.barcode
performRequest(abstractProduct!!.barcode)
}
}
if (abstractProduct != null) {
if (abstractProduct != null && action == "update") {
val imageThumbnailUri = getImageUri(this, File(thumbnailsDir, "${abstractProduct!!.imageHash}.webp"))
pictureFile = File(picturesPath, "${abstractProduct!!.imageHash}.png]")
pictureFile = File(picturesPath, "${abstractProduct!!.imageHash}.png")
imageView.setImageURI(imageThumbnailUri)
barcodeText.setText(abstractProduct!!.barcode)
productNameText.text = abstractProduct!!.name
netWeightText.text = abstractProduct!!.netWeight.toString()
categorySpinner.setSelection(abstractProduct!!.category)
categorySpinner.setSelection(abstractProduct!!.category - 1)
unitTypeSpinner.setSelection(abstractProduct!!.unit)
if (abstractProduct!!.barcode == "" || abstractProduct!!.barcode == " ") {
noBarcodeCheckBox.isChecked = true
}
}
saveButton.setOnClickListener {
@ -149,20 +151,29 @@ class AddAbstractProductActivity : AppCompatActivity() {
}
if (netWeight.toString() == "" || netWeight.toString().toDoubleOrNull() == null) {
Toast.makeText(this, getString(R.string.product_net_weight_request), Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
loadingDialog.show()
val currentGroup: Int
val currentGroupString = sharedPreferences.getString("currentGroup", "offline")!!
val net = Net()
net.token = sharedPreferences.getString("token", "")!!
net.server = sharedPreferences.getString("server", "")!!
net.language = sharedPreferences.getString("language", "en-US")!!
if (currentGroupString != "offline") {
currentGroup = currentGroupString.toInt()
val currentGroup = sharedPreferences.getString("currentGroup", "offline")!!
net.token = sharedPreferences.getString("token", "")!!
net.server = sharedPreferences.getString("server", "")!!
net.language = sharedPreferences.getString("language", "en-US")!!
lateinit var response: Response
} else {
currentGroup = 0
}
abstractProduct = AbstractProduct(
if(abstractProduct == null) 0 else abstractProduct!!.id,
if (abstractProduct == null) 0 else abstractProduct!!.id,
if (noBarcodeCheckBox.isChecked) "" else barcode,
productName,
netWeight.toString().toDouble(),
@ -170,18 +181,17 @@ class AddAbstractProductActivity : AppCompatActivity() {
categorySpinner.selectedItemPosition + 1,
unitTypeSpinner.selectedItemPosition
)
val pictureFile = File(File(filesDir, "pictures"), "${abstractProduct!!.imageHash}.png")
if (action == "update") {
DAO.updateAbstractProduct(abstractProduct!!)
response = net.updateAbstractProduct(currentGroup.toInt(), abstractProduct!!, pictureFile)
if (currentGroup > 0) net.updateAbstractProduct(currentGroup, abstractProduct!!, pictureFile, this::notifyUserAndExit)
} else if (action == "new" || action == "new_from_barcode") {
abstractProduct!!.id = DAO.addAbstractProduct(abstractProduct!!).toInt()
response = net.uploadAbstractProduct(currentGroup.toInt(), abstractProduct!!, pictureFile);
if (currentGroup > 0)
net.uploadAbstractProduct(currentGroup, abstractProduct!!, pictureFile, this::notifyUserAndExit)
}
Toast.makeText(this, response.body!!.string(), Toast.LENGTH_LONG).show()
finish()
}
takePictureButton.setOnClickListener {
@ -194,11 +204,16 @@ class AddAbstractProductActivity : AppCompatActivity() {
}
}
fun notifyUserAndExit(response: Response) {
runOnUiThread {
Toast.makeText(this, response.body!!.string(), Toast.LENGTH_LONG).show()
loadingDialog.dismiss()
finish()
}
}
fun performRequest(barcode: String) {
barcodeText.setText(this.barcode)
val net = Net();
val result = net.requestProductFromOnlineDB(barcode)
Log.d("QWERTYUIOP", "Result of request: $result")
var abstractProduct: AbstractProduct
@ -220,24 +235,25 @@ class AddAbstractProductActivity : AppCompatActivity() {
}.show()
}
thread {
if (result == "Not found 404") {
val net = Net();
net.requestProductFromOnlineDB(barcode, {response ->
if (response.code == 404) {
runOnUiThread {
Toast.makeText(this, getString(R.string.no_product_in_online_database), Toast.LENGTH_LONG)
.show()
productNameText.setText("")
netWeightText.setText("")
}
return@thread
return@requestProductFromOnlineDB
}
abstractProduct = Parser().parse(result)
abstractProduct = Parser().parse(response.body!!.string())
runOnUiThread {
productNameText.text = abstractProduct.name
netWeightText.text = abstractProduct.netWeight.toString()
unitTypeSpinner.setSelection(abstractProduct.unit)
}
}
})
}
private fun fillupUnitsSpinner() {
@ -257,7 +273,8 @@ class AddAbstractProductActivity : AppCompatActivity() {
fun fillupCategorySpinner() {
val categoriesDAO = CategoryDAO(DBStorageController(this, sharedPreferences.getString("currentGroup", "database")!!))
val categoriesDAO =
CategoryDAO(DBStorageController(this, sharedPreferences.getString("currentGroup", "database")!!))
val categories = categoriesDAO.getAllCategories().map { category -> category.name }
@ -272,22 +289,15 @@ class AddAbstractProductActivity : AppCompatActivity() {
if (success) {
//Move picture to a proper directory according to its calculated hash
val tempfile = File(filesDir, "image.png")
val imageHash = calculateMd5Hash(tempfile)
// val imageContent = tempfile.inputStream().readBytes()
// val imageHash = imageContent.toString(Charsets.UTF_8).md5()
pictureFile = File(picturesPath, "$imageHash.png")
Files.move(tempfile.toPath(), pictureFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
tempfile.delete()
val imageContent = pictureFile.inputStream().readBytes()
val imageContent = tempfile.inputStream().readBytes()
var img = BitmapFactory.decodeByteArray(imageContent, 0, imageContent.size)
val exif = ExifInterface(pictureFile.absoluteFile.toString())
val exif = ExifInterface(tempfile.absoluteFile.toString())
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
val matrix = Matrix()
when(orientation){
when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90F)
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180F)
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270F)
@ -298,9 +308,15 @@ class AddAbstractProductActivity : AppCompatActivity() {
rotated.compress(
Bitmap.CompressFormat.WEBP_LOSSY,
100 * (15 / (16 - compressionFactor)),
FileOutputStream(pictureFile)
FileOutputStream(tempfile)
)
val imageHash = calculateMd5Hash(tempfile)
pictureFile = File(picturesPath, "$imageHash.png")
Files.move(tempfile.toPath(), pictureFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
tempfile.delete()
generateThumbnailForImage(this, imageHash)
imageView.setImageURI(getImageUri(this, pictureFile))

View File

@ -1,13 +1,13 @@
package org.foxarmy.barcodescannerforemployees.activities
import android.app.Activity
import android.app.ProgressDialog
import android.content.SharedPreferences
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import okhttp3.Response
import org.foxarmy.barcodescannerforemployees.Net
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.database.CategoryDAO
@ -18,19 +18,19 @@ class AddCategoryActivity : Activity() {
private lateinit var DAO: CategoryDAO
private lateinit var sharedPreferences: SharedPreferences
private lateinit var loadingDialog: ProgressDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_category)
sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
loadingDialog = ProgressDialog(this)
loadingDialog.setMessage(getString(R.string.loading_please_wait))
loadingDialog.setCancelable(false)
loadingDialog.setTitle(getString(R.string.loading))
sharedPreferences = org.foxarmy.barcodescannerforemployees.getPreferences(this)
val dbHelper = DBStorageController(this, sharedPreferences.getString("currentGroup", "offline")!!)
DAO = CategoryDAO(dbHelper)
@ -48,25 +48,35 @@ class AddCategoryActivity : Activity() {
net.server = sharedPreferences.getString("server", "")!!
net.language = sharedPreferences.getString("language", "")!!
val currentGroup = sharedPreferences.getString("currentGroup", "offline")!!.toInt()
val currentGroup: Int = if (sharedPreferences.getString(
"currentGroup",
"offline"
)!! == "offline"
) 0 else sharedPreferences.getString("currentGroup", "")!!.toInt()
findViewById<Button>(R.id.saveButton).setOnClickListener {
if (categoryNameTextEdit.text.toString() == "") {
Toast.makeText(this, getString(R.string.category_name_required), Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
loadingDialog.show()
if (category.id == 0) { // Inserting new category
val newCategory = Category(0, categoryNameTextEdit.text.toString())
newCategory.id = DAO.addCategory(newCategory).toInt()
net.uploadCategory(currentGroup, newCategory)
if (currentGroup > 0) net.uploadCategory(currentGroup, newCategory, this::notifyUserAndExit)
} else { // Updating existing category
category.name = categoryNameTextEdit.text.toString()
DAO.updateCategory(category)
net.updateCategory(currentGroup, category)
if (currentGroup > 0) net.updateCategory(currentGroup, category, this::notifyUserAndExit)
}
finish()
}
}
fun notifyUserAndExit(response: Response) {
runOnUiThread {
Toast.makeText(this, response.body!!.string(), Toast.LENGTH_SHORT).show()
}
loadingDialog.dismiss()
finish()
}
}

View File

@ -1,6 +1,7 @@
package org.foxarmy.barcodescannerforemployees.activities
import android.app.Activity
import android.app.ProgressDialog
import android.content.Intent
import android.content.SharedPreferences
import android.os.Build
@ -13,8 +14,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.widget.addTextChangedListener
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanIntentResult
import com.journeyapps.barcodescanner.ScanOptions
@ -56,18 +55,19 @@ class AddProductActivity : AppCompatActivity() {
private lateinit var abstractProductDAO: AbstractProductDAO
private lateinit var sharedPreferences: SharedPreferences
private lateinit var loadingDialog: ProgressDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.fragment_add_product)
sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
loadingDialog = ProgressDialog(this)
loadingDialog.setMessage(getString(R.string.loading_please_wait))
loadingDialog.setCancelable(false)
loadingDialog.setTitle(getString(R.string.loading))
sharedPreferences = org.foxarmy.barcodescannerforemployees.getPreferences(this)
val dbHelper = DBStorageController(this, sharedPreferences.getString("currentGroup", "offline")!!)
productDAO = ProductDAO(dbHelper)
@ -177,31 +177,36 @@ class AddProductActivity : AppCompatActivity() {
return@setOnClickListener
}
val currentGroup: Int = if (sharedPreferences.getString("currentGroup", "")!! == "") 0 else sharedPreferences.getString("currentGroup", "")!!.toInt()
loadingDialog.show()
var response: Response? = null
val currentGroup: Int = if (sharedPreferences.getString("currentGroup", "offline")!! == "offline") 0 else sharedPreferences.getString("currentGroup", "")!!.toInt()
if (updatingExistentProduct) {
productDAO.updateProduct(product!!)
if (currentGroup > 0) response = net.updateProduct(currentGroup, product!!)
if (currentGroup > 0) net.updateProduct(currentGroup, product!!, this::notifyUserAndExit)
} else {
product!!.id = productDAO.insertNewProduct(product!!).toInt()
if (currentGroup > 0) response = net.uploadProduct(currentGroup, product!!)
if (currentGroup > 0) net.uploadProduct(currentGroup, product!!, this::notifyUserAndExit)
}
if (response != null) {
Toast.makeText(this, response.body!!.string(), Toast.LENGTH_LONG).show()
}
finish()
}
update()
val today = SimpleDateFormat("dd.MM.yyyy").format(Calendar.getInstance().time).split(".")
dateOfProductionDatePicker.updateDate(today[2].toInt(), today[1].toInt(), today[0].toInt())
expiryDatePicker.updateDate(today[2].toInt(), today[1].toInt(), today[0].toInt())
dateOfProductionDatePicker.updateDate(today[2].toInt(), today[1].toInt() - 1, today[0].toInt())
expiryDatePicker.updateDate(today[2].toInt(), today[1].toInt() - 1, today[0].toInt())
product!!.dateOfProduction = SimpleDateFormat("dd.MM.yyyy").parse("${today[0]}.${today[1]}.${today[2]}")!!.time / 1000
}
fun notifyUserAndExit(response: Response) {
runOnUiThread {
Toast.makeText(this, response.body!!.string(), Toast.LENGTH_LONG).show()
loadingDialog.dismiss()
finish()
}
}
private val intentLauncher =

View File

@ -7,8 +7,6 @@ import android.os.Bundle
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.database.DBStorageController
import org.foxarmy.barcodescannerforemployees.database.ProductDAO
@ -26,13 +24,7 @@ class ExpiryCalendarActivity : AppCompatActivity() {
setContentView(R.layout.fragment_expiry_dates)
sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
sharedPreferences = org.foxarmy.barcodescannerforemployees.getPreferences(this)
val dbHelper = DBStorageController(this, sharedPreferences.getString("currentGroup", "database")!!)
productDAO = ProductDAO(dbHelper)

View File

@ -18,7 +18,7 @@ class FindBarcodelessAbstractProduct() : AppCompatActivity() {
binding = ActivityFindBarcodelessAbstractProductBinding.inflate(layoutInflater)
val ft = supportFragmentManager.beginTransaction()
val fragment = StorageFragment.newInstance("barcodeless", arrayOf("", " "))
val fragment = StorageFragment.newInstance("barcodeless", arrayOf("", " ", " "))
ft.replace(R.id.content, fragment)
ft.commit()

View File

@ -15,10 +15,7 @@ import java.io.File
class FullscreenActivity : Activity() {
private lateinit var binding: ActivityFullscreenBinding
private lateinit var fullscreenImageView: ImageView
private lateinit var fullscreenContentControls: LinearLayout
private val hideHandler = Handler(Looper.myLooper()!!)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@ -1,13 +1,14 @@
package org.foxarmy.barcodescannerforemployees.activities
import android.app.ProgressDialog
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.Net
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.databinding.ActivityGroupBinding
import org.foxarmy.barcodescannerforemployees.noInternetConnectionAvailableNotification
class GroupActivity : AppCompatActivity() {
@ -19,62 +20,103 @@ class GroupActivity : AppCompatActivity() {
binding = ActivityGroupBinding.inflate(layoutInflater)
setContentView(binding.root)
val sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
val loadingDialog = ProgressDialog(this)
loadingDialog.setMessage(getString(R.string.loading_please_wait))
loadingDialog.setCancelable(false)
loadingDialog.setTitle(getString(R.string.loading))
val sharedPreferences = org.foxarmy.barcodescannerforemployees.getPreferences(this)
binding.createGroupButton.setOnClickListener {
val groupName = binding.groupNameTextEdit.text.toString()
val groupPassword = binding.groupPasswordTextEdit.text.toString()
// group is set to "successful"
val n = Net()
n.language = sharedPreferences.getString("language", "en-US")!!
n.server = sharedPreferences.getString("server", "")!!
n.token = sharedPreferences.getString("token", "")!!
val net = Net()
net.language = sharedPreferences.getString("language", "en-US")!!
net.server = sharedPreferences.getString("server", "")!!
net.token = sharedPreferences.getString("token", "")!!
val response = n.createGroup(groupName, groupPassword)
val responseText = response.body!!.string()
net.serverIsAvailable(this, { isServerAvailable ->
if (!isServerAvailable) {
noInternetConnectionAvailableNotification(this)
} else {
runOnUiThread {
loadingDialog.show()
}
net.createGroup(groupName, groupPassword, { response ->
val responseText = response.body!!.string()
if (response.code == 200) {
val currentGroups = sharedPreferences.getStringSet("groups", mutableSetOf())
currentGroups!!.add(responseText)
sharedPreferences.edit().putStringSet("groups", currentGroups).apply()
sharedPreferences.edit().putString("currentGroup", responseText).apply()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
} else {
Toast.makeText(this, responseText, Toast.LENGTH_LONG).show()
}
if (response.code == 200) {
val currentGroups = sharedPreferences.getStringSet("groups", mutableSetOf())
currentGroups!!.add(responseText)
sharedPreferences.edit().putStringSet("groups", currentGroups).apply()
sharedPreferences.edit().putString("currentGroup", responseText).apply()
runOnUiThread {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
loadingDialog.dismiss()
finish()
}
} else {
runOnUiThread {
loadingDialog.dismiss()
Toast.makeText(this, responseText, Toast.LENGTH_LONG).show()
}
}
})
}
})
}
binding.joinGroupButton.setOnClickListener {
val groupName = binding.groupNameTextEdit.text.toString()
val groupPassword = binding.groupPasswordTextEdit.text.toString()
val n = Net()
n.language = sharedPreferences.getString("language", "en-US")!!
n.server = sharedPreferences.getString("server", "")!!
n.token = sharedPreferences.getString("token", "")!!
val net = Net()
net.language = sharedPreferences.getString("language", "en-US")!!
net.server = sharedPreferences.getString("server", "")!!
net.token = sharedPreferences.getString("token", "")!!
val groupId = n.getGroupId(groupName).toInt()
val response = n.joinGroup(groupId, groupPassword)
val responseText = response.body!!.string()
net.serverIsAvailable(this) { isServerAvailable ->
if (!isServerAvailable) {
noInternetConnectionAvailableNotification(this)
} else {
runOnUiThread {
loadingDialog.show()
}
net.getGroupId(groupName, { requestGroupIdResponse ->
var groupId: Int
try {
groupId = requestGroupIdResponse.toInt()
} catch (e: Exception) {
runOnUiThread {
loadingDialog.dismiss()
Toast.makeText(this, requestGroupIdResponse, Toast.LENGTH_SHORT).show()
}
return@getGroupId
}
net.joinGroup(groupId, groupPassword, { response ->
val responseText = response.body!!.string()
if (response.code == 200) {
val currentGroups = sharedPreferences.getStringSet("groups", mutableSetOf())
currentGroups!!.add(groupId.toString())
sharedPreferences.edit().putStringSet("groups", currentGroups).apply()
sharedPreferences.edit().putString("currentGroup", groupId.toString()).apply()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
} else {
Toast.makeText(this, responseText, Toast.LENGTH_LONG).show()
if (response.code == 200) {
val currentGroups = sharedPreferences.getStringSet("groups", mutableSetOf())
currentGroups!!.add(groupId.toString())
sharedPreferences.edit().putStringSet("groups", currentGroups).apply()
sharedPreferences.edit().putString("currentGroup", groupId.toString()).apply()
runOnUiThread {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
loadingDialog.dismiss()
finish()
}
} else {
runOnUiThread {
loadingDialog.dismiss()
Toast.makeText(this, responseText, Toast.LENGTH_LONG).show()
}
}
})
})
}
}
}
}

View File

@ -1,16 +1,16 @@
package org.foxarmy.barcodescannerforemployees.activities
import android.app.ProgressDialog
import android.content.Intent
import android.os.Bundle
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.Net
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.databinding.ActivityLoginBinding
import org.foxarmy.barcodescannerforemployees.parseArray
import org.foxarmy.barcodescannerforemployees.noInternetConnectionAvailableNotification
import org.foxarmy.barcodescannerforemployees.parseIntArray
import org.json.JSONObject
class LoginActivity : AppCompatActivity() {
@ -23,15 +23,14 @@ class LoginActivity : AppCompatActivity() {
binding = ActivityLoginBinding.inflate(layoutInflater);
setContentView(binding.root)
val loadingDialog = ProgressDialog(this)
loadingDialog.setMessage(getString(R.string.loading_please_wait))
loadingDialog.setCancelable(false)
loadingDialog.setTitle(getString(R.string.loading))
fillUpLanguagesSpinner()
val sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
val sharedPreferences = org.foxarmy.barcodescannerforemployees.getPreferences(this)
binding.loginButton.setOnClickListener {
val server = binding.serverTextEdit.text.toString()
@ -40,32 +39,49 @@ class LoginActivity : AppCompatActivity() {
val language = resources.getStringArray(R.array.languages)[binding.languageSpinner.selectedItemPosition]
sharedPreferences.edit().putString("language", language).apply()
val n = Net()
n.language = sharedPreferences.getString("language", "en-US")!!
n.server = server
val net = Net()
net.language = sharedPreferences.getString("language", "en-US")!!
net.server = server
val response = n.login(username, password)
val responseText = response.body!!.string()
if (response.code != 200) {
Toast.makeText(this, responseText, Toast.LENGTH_SHORT).show()
} else {
val json = JSONObject(responseText)
sharedPreferences.edit().putString("token", json["token"].toString()).apply()
n.token = json["token"].toString()
sharedPreferences.edit().putInt("userId", json["id"].toString().toInt()).apply()
sharedPreferences.edit().putString("server", server).apply()
net.serverIsAvailable(this, { isServerAvailable ->
if (!isServerAvailable) {
noInternetConnectionAvailableNotification(this)
} else {
runOnUiThread {
loadingDialog.show()
}
net.login(username, password, { response ->
val responseText = response.body!!.string()
if (response.code != 200) {
runOnUiThread {
Toast.makeText(this, responseText, Toast.LENGTH_SHORT).show()
}
} else {
val json = JSONObject(responseText)
sharedPreferences.edit().putString("token", json["token"].toString()).apply()
net.token = json["token"].toString()
sharedPreferences.edit().putInt("userId", json["id"].toString().toInt()).apply()
sharedPreferences.edit().putString("server", server).apply()
net.getMyGroups({ response ->
runOnUiThread {
loadingDialog.dismiss()
}
val r = response.body!!.string()
if (r == "" || r == "[]") {
goToActivity("GroupActivity")
}
val myGroups = parseIntArray(r).map { a -> a.toString() }
val r = n.getMyGroups().body!!.string()
val myGroups = parseArray(r).map { a -> a.toString()}
sharedPreferences.edit().putStringSet("groups", myGroups.toSet()).apply()
sharedPreferences.edit().putString("currentGroup", myGroups[0]).apply()
sharedPreferences.edit().putStringSet("groups", myGroups.toSet()).apply()
sharedPreferences.edit().putString("currentGroup", myGroups[0]).apply()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
goToActivity("MainActivity")
})
}
})
}
})
}
binding.registerButton.setOnClickListener {
@ -76,31 +92,64 @@ class LoginActivity : AppCompatActivity() {
sharedPreferences.edit().putString("language", language).apply()
sharedPreferences.edit().putString("server", server).apply()
val n = Net()
n.language = language
n.server = server
val response = n.registerAccount(username, password);
val responseText = response.body!!.string()
if (response.code != 200) {
Toast.makeText(this, responseText, Toast.LENGTH_SHORT).show();
} else {
sharedPreferences.edit().putInt("userId", responseText.toInt()).apply()
val token = JSONObject(n.login(username, password).body!!.string())["token"].toString()
sharedPreferences.edit().putString("token", token).apply()
val net = Net()
net.language = language
net.server = server
sharedPreferences.edit().putString("server", server).apply()
val intent = Intent(this, GroupActivity::class.java)
startActivity(intent)
finish()
}
net.serverIsAvailable(this, {isServerAvailable ->
if (!isServerAvailable) {
noInternetConnectionAvailableNotification(this)
} else {
net.registerAccount(username, password, {response ->
val responseText = response.body!!.string()
if (response.code != 200) {
Toast.makeText(this, responseText, Toast.LENGTH_SHORT).show();
} else {
sharedPreferences.edit().putInt("userId", responseText.toInt()).apply()
net.login(username, password, {response ->
val token = JSONObject(response.body!!.string())["token"].toString()
sharedPreferences.edit().putString("token", token).apply()
sharedPreferences.edit().putString("server", server).apply()
runOnUiThread {
loadingDialog.dismiss()
}
goToActivity("GroupActivity")
})
}
})
}
})
}
binding.offlineButton.setOnClickListener {
sharedPreferences.edit().putString("currentGroup", "offline").apply()
sharedPreferences.edit().putStringSet("groups", setOf("offline")).apply()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
}
private fun fillUpLanguagesSpinner() {
val languages = resources.getStringArray(R.array.languages)
val arrayAdapter = ArrayAdapter(this, androidx.appcompat.R.layout.support_simple_spinner_dropdown_item, languages)
val arrayAdapter =
ArrayAdapter(this, androidx.appcompat.R.layout.support_simple_spinner_dropdown_item, languages)
arrayAdapter.setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item)
binding.languageSpinner.adapter = arrayAdapter
}
private fun goToActivity(activityName: String) {
runOnUiThread {
val intent = Intent(this, when(activityName) {
"MainActivity" -> MainActivity::class.java
"GroupActivity" -> GroupActivity::class.java
"LoginActivity" -> LoginActivity::class.java
else -> LoginActivity::class.java
})
startActivity(intent)
finish()
}
}
}

View File

@ -1,25 +1,24 @@
package org.foxarmy.barcodescannerforemployees.activities
import android.app.Activity
import android.app.ProgressDialog
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import androidx.viewpager.widget.ViewPager
import com.google.android.material.navigation.NavigationView
import org.foxarmy.barcodescannerforemployees.Net
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.ViewPagerAdapter
import org.foxarmy.barcodescannerforemployees.convertToUnixEpochTimestamp
import org.foxarmy.barcodescannerforemployees.*
import org.foxarmy.barcodescannerforemployees.database.AbstractProductDAO
import org.foxarmy.barcodescannerforemployees.database.CategoryDAO
import org.foxarmy.barcodescannerforemployees.database.DBStorageController
@ -39,9 +38,17 @@ import java.io.File
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
private lateinit var binding: ActivityMainBinding
lateinit var adapter: ViewPagerAdapter
var drawerLayout: DrawerLayout? = null
var actionBarDrawerToggle: ActionBarDrawerToggle? = null
private lateinit var adapter: ViewPagerAdapter
private var drawerLayout: DrawerLayout? = null
private var actionBarDrawerToggle: ActionBarDrawerToggle? = null
private lateinit var ws: WebSocketClient
var selectionMode = false
private var selectedAmount = 0
private lateinit var sharedPreferences: SharedPreferences
private lateinit var loadingDialog: ProgressDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -49,10 +56,23 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
loadingDialog = ProgressDialog(this)
loadingDialog.setMessage(getString(R.string.loading_please_wait))
loadingDialog.setCancelable(false)
loadingDialog.setTitle(getString(R.string.loading))
sharedPreferences = getPreferences(this)
setSupportActionBar(binding.toolbar)
setupViewPager(binding.tabViewpager)
binding.tabTablayout.setupWithViewPager(binding.tabViewpager)
binding.tabTablayout.setOnDragListener { _, _ ->
selectionMode = false
selectedAmount = 0
true
}
drawerLayout = binding.drawerLayout
actionBarDrawerToggle = ActionBarDrawerToggle(this, drawerLayout, R.string.nav_open, R.string.nav_close)
@ -60,7 +80,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
actionBarDrawerToggle!!.syncState()
binding.navView.setNavigationItemSelectedListener(this)
// to make the Navigation drawer icon always appear on the action bar
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
binding.expiryCalendarFab.setOnClickListener { _ ->
@ -70,7 +89,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
ContextCompat.startActivity(this, expiryCalendarIntent, extras)
}
binding.newElementFab.setOnClickListener { view ->
binding.newElementFab.setOnClickListener { _ ->
val currentPosition = binding.tabTablayout.selectedTabPosition
val fragment = adapter.getItem(currentPosition)
@ -78,8 +97,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
"StorageFragment" -> {
val addAbstractProductIntent = Intent(this, AddAbstractProductActivity::class.java)
val extras = Bundle()
// I reuse the same stuff for editing and adding new product.
// if abstractProduct == null, it means that we need to create new object
extras.putParcelable("abstractProduct", null)
extras.putString("action", "new")
addAbstractProductIntent.putExtras(extras)
@ -104,19 +121,24 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
}
}
}
synchronize()
val net = Net()
net.server = sharedPreferences.getString("server", "")!!
val currentGroup = sharedPreferences.getString("currentGroup", "offline")!!
net.serverIsAvailable(this, {isServerAvailable ->
if ( currentGroup != "offline" && !isServerAvailable) {
runOnUiThread {
noInternetConnectionAvailableNotification(this)
}
} else if (currentGroup != "offline" && isServerAvailable) {
synchronize()
ws = WebSocketClient(this, sharedPreferences.getString("server", "")!!)
val token = sharedPreferences.getString("token", "")!!
ws.connect(token, currentGroup)
}
})
}
private fun synchronize() {
val sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
if (sharedPreferences.getString("currentGroup", "offline")!! == "offline") return
val net = Net()
@ -127,26 +149,27 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
val currentGroup = sharedPreferences.getString("currentGroup", "")!!.toInt()
val data = JSONObject(net.synchronize(currentGroup).body!!.string())
net.synchronize(currentGroup, {response ->
val data = JSONObject(response.body!!.string())
val remoteAbstractProducts = data["abstract_products"] as JSONArray
val remoteProducts = data["products"] as JSONArray
val remoteCategories = data["categories"] as JSONArray
val remoteAbstractProducts = data["abstract_products"] as JSONArray
val remoteProducts = data["products"] as JSONArray
val remoteCategories = data["categories"] as JSONArray
val dbHelper = DBStorageController(this, sharedPreferences.getString("currentGroup", "")!!)
val dbHelper = DBStorageController(this, sharedPreferences.getString("currentGroup", "")!!)
val abstractProductDAO = AbstractProductDAO(dbHelper)
val productDAO = ProductDAO(dbHelper)
val categoryDAO = CategoryDAO(dbHelper)
val abstractProductDAO = AbstractProductDAO(dbHelper)
val productDAO = ProductDAO(dbHelper)
val categoryDAO = CategoryDAO(dbHelper)
val localAbstractProducts = abstractProductDAO.getSortedListOfAbstractProducts(0, "", arrayOf(""))
val localProducts = productDAO.getSortedListOfProducts(0, "", "")
val localCategories = categoryDAO.getAllCategories()
syncAbstractProducts(net, currentGroup, abstractProductDAO, remoteAbstractProducts, localAbstractProducts)
syncProducts(productDAO, remoteProducts, localProducts)
syncCategories(categoryDAO, remoteCategories, localCategories)
val localAbstractProducts = abstractProductDAO.getSortedListOfAbstractProducts(0, "", arrayOf(""))
val localProducts = productDAO.getSortedListOfProducts(0, "", "")
val localCategories = categoryDAO.getAllCategories()
syncAbstractProducts(net, currentGroup, abstractProductDAO, remoteAbstractProducts, localAbstractProducts)
syncProducts(productDAO, remoteProducts, localProducts)
syncCategories(categoryDAO, remoteCategories, localCategories)
})
}
private fun syncCategories(
@ -194,15 +217,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
if (categoryInRemoteDB != null && categoryInLocalDB != null) {
if (categoryInRemoteDB["hash"] != categoryInLocalDB.calculateHash()) {
var updatedData: Category
with(categoryInRemoteDB) {
updatedData = Category(
this["local_id"].toString().toInt(),
this["name"].toString()
)
}
val updatedData = Category.createFromJSON(categoryInRemoteDB)
categoryDAO.updateCategory(updatedData)
}
@ -241,35 +256,14 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
}
if (productInLocalDB == null && remoteProduct != null) {
var newProduct: Product
with(remoteProduct) {
newProduct = Product(
this["local_id"].toString().toInt(),
this["abstract_product_id"].toString().toInt(),
this["amount"].toString().toInt(),
convertToUnixEpochTimestamp(this["date_of_production"].toString()),
convertToUnixEpochTimestamp(this["expiry_date"].toString())
)
}
val newProduct = Product.createFromJSON(remoteProduct)
productDAO.insertNewProduct(newProduct)
}
if (productInRemoteDB != null && productInLocalDB != null) {
if (productInRemoteDB["hash"] != productInLocalDB.calculateHash()) {
var updatedData: Product
with(productInRemoteDB) {
updatedData = Product(
this["local_id"].toString().toInt(),
this["abstract_product_id"].toString().toInt(),
this["amount"].toString().toInt(),
convertToUnixEpochTimestamp(this["date_of_production"].toString()),
convertToUnixEpochTimestamp(this["expiry_date"].toString())
)
}
val updatedData = Product.createFromJSON(productInRemoteDB)
productDAO.updateProduct(updatedData)
}
@ -318,24 +312,11 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
val pictureFile =
File(picturesDir, "${remoteAbstractProduct["image_filename"]}.png")
val url = "https://${net.server}/api/abstractproduct/getImage/${currentGroup}/${localId}"
net.downloadImage(url, pictureFile)
var newAbstractProduct: AbstractProduct
with(remoteAbstractProduct) {
newAbstractProduct = AbstractProduct(
this["local_id"].toString().toInt(),
this["barcode"].toString(),
this["name"].toString(),
this["net_weight"].toString().toDouble(),
this["image_filename"].toString(),
this["category"].toString().toInt(),
this["unit"].toString().toInt()
)
}
abstractProductDAO.addAbstractProduct(newAbstractProduct)
net.downloadImage(url, pictureFile, {
val newAbstractProduct = AbstractProduct.createFromJSON(remoteAbstractProduct)
abstractProductDAO.addAbstractProduct(newAbstractProduct)
})
}
if (abstractProductInRemoteDB != null && abstractProductInLocalDB != null) {
@ -345,24 +326,11 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
val pictureFile =
File(File(filesDir, "pictures"), "${abstractProductInRemoteDB["image_filename"]}.png")
val url = "https://${net.server}/api/abstractproduct/getImage/${currentGroup}/${localId}"
net.downloadImage(url, pictureFile)
net.downloadImage(url, pictureFile, {
val updatedData = AbstractProduct.createFromJSON(abstractProductInRemoteDB)
var updatedData: AbstractProduct
with(abstractProductInRemoteDB) {
updatedData = AbstractProduct(
this["local_id"].toString().toInt(),
this["barcode"].toString(),
this["name"].toString(),
this["net_weight"].toString().toDouble(),
this["image_filename"].toString(),
this["category"].toString().toInt(),
this["unit"].toString().toInt()
)
}
abstractProductDAO.updateAbstractProduct(updatedData)
abstractProductDAO.updateAbstractProduct(updatedData)
})
}
}
}
@ -376,6 +344,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
adapter.addFragment(CategoriesFragment(), getString(R.string.categories_title))
viewpager.adapter = adapter
viewpager.offscreenPageLimit = 3
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -384,22 +353,43 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
return true
}
private fun isOffline(): Boolean {
return sharedPreferences.getString("currentGroup", "") == "offline"
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
lateinit var intent: Intent
when (item.itemId) {
R.id.nav_account -> {
if (isOffline()) {
Toast.makeText(this, getString(R.string.online_only_feature), Toast.LENGTH_SHORT).show()
return false
}
intent = Intent(this, AccountSettingsActivity::class.java)
settingsActivityLauncher.launch(intent)
return true
}
R.id.nav_groups -> {
if (isOffline()) {
Toast.makeText(this, getString(R.string.online_only_feature), Toast.LENGTH_SHORT).show()
return false
}
intent = Intent(this, MyGroupsActivity::class.java)
}
R.id.nav_settings -> {
intent = Intent(this, SettingsActivity::class.java)
}
R.id.nav_logout -> {
sharedPreferences.edit().putString("currentGroup", "").apply()
sharedPreferences.edit().putStringSet("groups", emptySet()).apply()
sharedPreferences.edit().putString("token", "").apply()
sharedPreferences.edit().putString("server", "").apply()
intent = Intent(this, LoginActivity::class.java)
}
}
startActivity(intent)
@ -416,13 +406,19 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
}
fun updateAll() {
val storageFragment: StorageFragment = adapter.getItem(0) as StorageFragment
val shelfFragment: ShelfFragment = adapter.getItem(1) as ShelfFragment
val categoriesFragment: CategoriesFragment = adapter.getItem(2) as CategoriesFragment
runOnUiThread {
try {
val storageFragment: StorageFragment = adapter.getItem(0) as StorageFragment
val shelfFragment: ShelfFragment = adapter.getItem(1) as ShelfFragment
val categoriesFragment: CategoriesFragment = adapter.getItem(2) as CategoriesFragment
storageFragment.updateContent()
shelfFragment.updateContent()
categoriesFragment.updateContent()
storageFragment.updateContent()
shelfFragment.updateContent()
categoriesFragment.updateContent()
} catch (e:Exception) {
Log.e("BSFE/MainActivity", e.message!!)
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -509,4 +505,17 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
val storageFragment = fragment as StorageFragment
storageFragment.filterByCategory(id)
}
fun addSelection() {
selectionMode = true
selectedAmount ++
}
fun removeSelection() {
selectedAmount --
if (selectedAmount <= 0) {
selectedAmount = 0
selectionMode = false
}
}
}

View File

@ -1,19 +1,27 @@
package org.foxarmy.barcodescannerforemployees.activities
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.PopupMenu
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.Net
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.databinding.ActivityManageGroupBinding
import org.foxarmy.barcodescannerforemployees.parseArray
import org.foxarmy.barcodescannerforemployees.parseIntArray
import org.foxarmy.barcodescannerforemployees.views.GroupMemberView
class ManageGroupActivity : AppCompatActivity(){
class ManageGroupActivity : AppCompatActivity() {
private lateinit var binding: ActivityManageGroupBinding
private var isAdmin: Boolean = true
private var isAdmin: Boolean = false
private lateinit var sharedPreferences: SharedPreferences
private lateinit var net: Net
private var groupId: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -21,56 +29,150 @@ class ManageGroupActivity : AppCompatActivity(){
binding = ActivityManageGroupBinding.inflate(layoutInflater)
setContentView(binding.root)
val groupId = intent.extras!!.getInt("groupId")
groupId = intent.extras!!.getInt("groupId")
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
sharedPreferences = org.foxarmy.barcodescannerforemployees.getPreferences(this)
val sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
masterKeyAlias,
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
val net = Net()
net = Net()
net.token = sharedPreferences.getString("token", "")!!
net.server = sharedPreferences.getString("server", "")!!
net.language = sharedPreferences.getString("language", "en-US")!!
val users = parseArray(net.getUsersInGroup(groupId).body!!.string())
setUpMembers()
for (user in users) {
val groupMemberView = GroupMemberView(this, this as Context, net.getUsernameById(user).body!!.string(), user)
binding.renameButton.setOnClickListener {
val input = EditText(this)
AlertDialog.Builder(this)
.setTitle(getString(R.string.new_group_name))
.setView(input)
.setPositiveButton(getString(R.string.ok)) { _, _ ->
val newName = input.text.toString()
net.renameGroup(groupId, newName, { response ->
runOnUiThread {
Toast.makeText(this, response.body!!.string(), Toast.LENGTH_LONG).show()
}
})
binding.groupsContent.addView(groupMemberView)
}
.setNegativeButton(getString(R.string.cancel)) { _, _ ->
}.show()
}
isAdmin = amIAnAdminIn(groupId)
binding.leaveButton.setOnClickListener {
net.leaveGroup(groupId, { response ->
runOnUiThread {
Toast.makeText(this, response.body!!.string(), Toast.LENGTH_SHORT).show()
}
if (response.code == 200) {
val groups = sharedPreferences.getStringSet("groups", emptySet())!!
groups.remove(groupId.toString())
sharedPreferences.edit().putStringSet("groups", groups).apply()
sharedPreferences.edit().putString("currentGroup", "").apply()
if (!isAdmin) {
binding.renameButton.visibility = View.GONE
if (groups.isEmpty()) {
runOnUiThread {
val intent = Intent(this, GroupActivity::class.java)
startActivity(intent)
finish()
}
}
sharedPreferences.edit().putString("currentGroup", groups.toList()[0]).apply()
runOnUiThread {
finish()
}
}
})
}
setUpAdminRelatedButtons()
}
fun amIAnAdminIn(groupId: Int): Boolean {
val sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
private fun setUpMembers() {
net.getUsersInGroup(groupId, { response ->
val users = parseIntArray(response.body!!.string())
for (user in users) {
net.getUsernameById(user, { response ->
val groupMemberView = GroupMemberView(this, this as Context, response.body!!.string(), user)
groupMemberView.setOnLongClickListener { view ->
amIAnAdminIn(groupId, { amIAnAdmin ->
if (!amIAnAdmin) return@amIAnAdminIn
runOnUiThread {
val popupMenu = PopupMenu(this, groupMemberView)
popupMenu.inflate(R.menu.user_pop_menu)
popupMenu.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.kick -> {
setUpMembers()
false
}
R.id.transfer_ownership -> {
AlertDialog.Builder(this)
.setMessage(getString(R.string.transfer_ownership_confirmation))
.setPositiveButton(R.string.yes) { _, _ ->
net.transfer_ownership(
groupId,
(view as GroupMemberView).userId,
{ response ->
runOnUiThread {
Toast.makeText(
this,
response.body!!.string(),
Toast.LENGTH_SHORT
).show()
setUpAdminRelatedButtons()
}
})
}
.setNegativeButton(R.string.no) { _, _ -> }
.show()
true
}
else -> false
}
}
runOnUiThread {
popupMenu.show()
}
}
})
true
}
runOnUiThread {
binding.groupsContent.addView(groupMemberView)
}
})
}
})
}
fun amIAnAdminIn(groupId: Int, callback: (Boolean) -> Unit) {
val net = Net()
net.token = sharedPreferences.getString("token", "")!!
net.server = sharedPreferences.getString("server", "")!!
net.language = sharedPreferences.getString("language", "en-US")!!
val result = sharedPreferences.getInt("userId", 0) == net.getGroupAdminId(groupId).body!!.string().toInt()
return result
net.getGroupAdminId(groupId, { response ->
val result = sharedPreferences.getInt("userId", 0) == response.body!!.string().toInt()
callback(result)
})
}
fun setUpAdminRelatedButtons() {
amIAnAdminIn(groupId, { isAdmin ->
runOnUiThread {
if (!isAdmin) {
binding.renameButton.visibility = View.GONE
}
}
})
}
}

View File

@ -5,12 +5,10 @@ import android.content.Intent
import android.os.Bundle
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.Net
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.databinding.ActivityMyGroupsBinding
import org.foxarmy.barcodescannerforemployees.parseArray
import org.foxarmy.barcodescannerforemployees.parseIntArray
import org.foxarmy.barcodescannerforemployees.views.GroupView
class MyGroupsActivity : AppCompatActivity(){
@ -22,13 +20,7 @@ class MyGroupsActivity : AppCompatActivity(){
binding = ActivityMyGroupsBinding.inflate(layoutInflater)
setContentView(binding.root)
val sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
val sharedPreferences = org.foxarmy.barcodescannerforemployees.getPreferences(this)
val net = Net()
@ -36,20 +28,28 @@ class MyGroupsActivity : AppCompatActivity(){
net.server = sharedPreferences.getString("server", "")!!
net.language = sharedPreferences.getString("language", "en-US")!!
val groups = parseArray(net.getMyGroups().body!!.string())
val container = findViewById<LinearLayout>(R.id.groupsLayout)
for (group in groups) {
val groupView = GroupView(this, this as Context, net.getGroupName(group), group)
net.getMyGroups { response ->
val groups = parseIntArray(response.body!!.string())
container.addView(groupView)
}
val container = findViewById<LinearLayout>(R.id.groupsLayout)
binding.newGroup.setOnClickListener {
val intent = Intent(this, GroupActivity::class.java)
startActivity(intent)
for (group in groups) {
net.getGroupName(group, {response ->
val groupView = GroupView(this, this as Context, response, group)
runOnUiThread {
container.addView(groupView)
}
})
}
binding.newGroup.setOnClickListener {
runOnUiThread {
val intent = Intent(this, GroupActivity::class.java)
startActivity(intent)
}
}
}
}
}

View File

@ -4,8 +4,8 @@ import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.Net
import org.foxarmy.barcodescannerforemployees.parseStringList
class NavigatorActivity : Activity() {
@ -14,34 +14,55 @@ class NavigatorActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
sharedPreferences = org.foxarmy.barcodescannerforemployees.getPreferences(this)
var intent = Intent(this, MainActivity::class.java)
if (!isInGroup()) {
intent = Intent(this, GroupActivity::class.java)
isInGroup { flag ->
if (!flag && !isOffline()) {
intent = Intent(this, GroupActivity::class.java)
}
if (!isAuthenticated() && !isOffline()) {
intent = Intent(this, LoginActivity::class.java);
}
runOnUiThread {
startActivity(intent);
finish();
}
}
}
if (!isAuthenticated()) {
intent = Intent(this, LoginActivity::class.java);
}
startActivity(intent);
finish();
private fun isOffline(): Boolean {
return sharedPreferences.getString("currentGroup", "")!! == "offline"
}
private fun isAuthenticated(): Boolean {
return sharedPreferences.getString("token", "") != ""
}
private fun isInGroup(): Boolean {
return sharedPreferences.getStringSet("groups", emptySet())!!.isNotEmpty()
private fun isInGroup(callback: (Boolean) -> Unit) {
val groups = sharedPreferences.getStringSet("groups", emptySet())!!
if (groups.isEmpty()) {
if (sharedPreferences.getString("token", "")!! == "") callback(false)
val net = Net()
net.language = sharedPreferences.getString("language", "")!!
net.server = sharedPreferences.getString("server", "")!!
net.token = sharedPreferences.getString("token", "")!!
net.getMyGroups { response ->
val responseBody = response.body!!.string()
if (responseBody == "" || responseBody == "[]") callback(false)
val groupsFromServer = parseStringList(responseBody)
if (groupsFromServer.isNotEmpty()) {
sharedPreferences.edit().putStringSet("groups", groupsFromServer.toSet()).apply()
sharedPreferences.edit().putString("currentGroup", groupsFromServer[0]).apply()
callback(true)
} else {
callback(false)
}
}
}
callback(true)
}
}

View File

@ -6,8 +6,6 @@ import android.os.Bundle
import android.view.View
import android.widget.ArrayAdapter
import androidx.appcompat.app.AppCompatActivity
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.Net
import org.foxarmy.barcodescannerforemployees.databinding.ActivitySettingsBinding
@ -17,17 +15,12 @@ class SettingsActivity : AppCompatActivity() {
lateinit var myGroups: List<String>
lateinit var currentGroup: String
lateinit var groupsNames: MutableList<String>
lateinit var namesMap: MutableMap<Int, String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
sharedPreferences = org.foxarmy.barcodescannerforemployees.getPreferences(this)
myGroups = sharedPreferences.getStringSet("groups", emptySet())!!.toList()
currentGroup = sharedPreferences.getString("currentGroup", "offline")!!
@ -37,23 +30,35 @@ class SettingsActivity : AppCompatActivity() {
net.language = sharedPreferences.getString("language", "")!!
net.server = sharedPreferences.getString("server", "")!!
groupsNames = mutableListOf()
for (myGroup in myGroups) {
groupsNames.add(net.getGroupName(myGroup.toInt()))
}
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
setUpImageCompressionSeekBar()
fillUpCurrentGroupSpinner()
if (isOffline()) {
binding.currentGroupSetting.visibility = View.GONE
} else {
namesMap = mutableMapOf()
for (myGroup in myGroups) {
net.getGroupName(myGroup.toInt(), { name ->
namesMap[myGroup.toInt()] = name
fillUpCurrentGroupSpinner()
})
}
}
binding.saveButton.setOnClickListener {
sharedPreferences.edit().putString("currentGroup", net.getGroupId(binding.currentGroupSpinner.selectedItem.toString())).apply()
sharedPreferences.edit().putInt("imageCompression", binding.imageCompressionFactorSeekBar.progress + 1).apply()
setResult(Activity.RESULT_OK)
finish()
net.getGroupId(binding.currentGroupSpinner.selectedItem.toString(), { groupId ->
sharedPreferences.edit().putString("currentGroup", groupId).apply()
sharedPreferences.edit().putInt("imageCompression", binding.imageCompressionFactorSeekBar.progress + 1)
.apply()
runOnUiThread {
setResult(Activity.RESULT_OK)
finish()
}
})
}
binding.cancelButton.setOnClickListener {
@ -62,22 +67,37 @@ class SettingsActivity : AppCompatActivity() {
}
}
private fun isOffline(): Boolean {
return sharedPreferences.getString("currentGroup", "offline") == "offline"
}
private fun setUpImageCompressionSeekBar() {
val compressionFactor = sharedPreferences.getInt("imageCompression", 1)
binding.imageCompressionFactorSeekBar.progress = compressionFactor - 1
}
private fun fillUpCurrentGroupSpinner() {
runOnUiThread {
if (currentGroup == "offline") {
binding.currentGroupSetting.visibility = View.GONE
}
groupsNames = mutableListOf()
groupsNames.clear()
val sortedMap = namesMap.entries.sortedBy { it.key }.associate { it.toPair() }
for ((id, name) in sortedMap) {
groupsNames.add(name)
}
if (currentGroup == "offline") {
binding.currentGroupSetting.visibility = View.GONE
val arrayAdapter =
ArrayAdapter(this, androidx.appcompat.R.layout.support_simple_spinner_dropdown_item, groupsNames)
arrayAdapter.setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item)
binding.currentGroupSpinner.adapter = arrayAdapter
if (myGroups.indexOf(currentGroup) < groupsNames.size) binding.currentGroupSpinner.setSelection(
myGroups.indexOf(
currentGroup
)
)
}
val arrayAdapter =
ArrayAdapter(this, androidx.appcompat.R.layout.support_simple_spinner_dropdown_item, groupsNames)
arrayAdapter.setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item)
binding.currentGroupSpinner.adapter = arrayAdapter
binding.currentGroupSpinner.setSelection(myGroups.indexOf(currentGroup))
}
}

View File

@ -163,7 +163,7 @@ class AbstractProductDAO(private val dbHelper: DBStorageController) {
with(cursor) {
while (moveToNext()) {
val id = getInt(getColumnIndexOrThrow(android.provider.BaseColumns._ID))
val id = getInt(getColumnIndexOrThrow(BaseColumns._ID))
val productName =
getString(getColumnIndexOrThrow(AbstractProductContract.AbstractProductEntry.PRODUCT_NAME))
val imageFilename =
@ -308,7 +308,7 @@ class AbstractProductDAO(private val dbHelper: DBStorageController) {
}
"barcodeless" -> {
selection = "${AbstractProductContract.AbstractProductEntry.BARCODE} = ? OR ${AbstractProductContract.AbstractProductEntry.BARCODE} = ?"
selection = "${AbstractProductContract.AbstractProductEntry.BARCODE} = ? OR ${AbstractProductContract.AbstractProductEntry.BARCODE} = ? OR ${AbstractProductContract.AbstractProductEntry.BARCODE} = ?"
selectionArgs = filter
}
}

View File

@ -3,6 +3,7 @@ package org.foxarmy.barcodescannerforemployees.dataclasses
import android.os.Parcel
import android.os.Parcelable
import org.foxarmy.barcodescannerforemployees.md5
import org.json.JSONObject
class AbstractProduct() : Parcelable {
var id: Int = 0
@ -59,5 +60,17 @@ class AbstractProduct() : Parcelable {
override fun newArray(size: Int): Array<AbstractProduct?> {
return arrayOfNulls(size)
}
fun createFromJSON(json: JSONObject): AbstractProduct {
return AbstractProduct(
json["local_id"].toString().toInt(),
json["barcode"].toString(),
json["name"].toString(),
json["net_weight"].toString().toDouble(),
json["image_filename"].toString(),
json["category"].toString().toInt(),
json["unit"].toString().toInt()
)
}
}
}

View File

@ -3,6 +3,7 @@ package org.foxarmy.barcodescannerforemployees.dataclasses
import android.os.Parcel
import android.os.Parcelable
import org.foxarmy.barcodescannerforemployees.md5
import org.json.JSONObject
class Category() : Parcelable {
var id = 0
@ -40,5 +41,12 @@ class Category() : Parcelable {
override fun newArray(size: Int): Array<Category?> {
return arrayOfNulls(size)
}
fun createFromJSON(json: JSONObject): Category {
return Category(
json["local_id"].toString().toInt(),
json["name"].toString()
)
}
}
}

View File

@ -5,7 +5,9 @@ import android.os.Parcel
import android.os.Parcelable
import androidx.annotation.RequiresApi
import org.foxarmy.barcodescannerforemployees.calculateProductFreshness
import org.foxarmy.barcodescannerforemployees.convertToUnixEpochTimestamp
import org.foxarmy.barcodescannerforemployees.md5
import org.json.JSONObject
class Product() : Parcelable {
var id = 0
@ -59,5 +61,16 @@ class Product() : Parcelable {
override fun newArray(size: Int): Array<Product?> {
return arrayOfNulls(size)
}
@RequiresApi(Build.VERSION_CODES.O)
fun createFromJSON(json: JSONObject): Product {
return Product(
json["local_id"].toString().toInt(),
json["abstract_product_id"].toString().toInt(),
json["amount"].toString().toInt(),
convertToUnixEpochTimestamp(json["date_of_production"].toString()),
convertToUnixEpochTimestamp(json["expiry_date"].toString())
)
}
}
}

View File

@ -11,36 +11,31 @@ import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.fragment.app.Fragment
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.Net
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.activities.AddCategoryActivity
import org.foxarmy.barcodescannerforemployees.database.CategoryDAO
import org.foxarmy.barcodescannerforemployees.database.DBStorageController
import org.foxarmy.barcodescannerforemployees.getPreferences
import org.foxarmy.barcodescannerforemployees.views.CategoryView
import kotlin.concurrent.thread
class CategoriesFragment : Fragment() {
private lateinit var sharedPreferences: SharedPreferences
private lateinit var categoryDAO: CategoryDAO
private var updateInProgress = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
requireContext(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
sharedPreferences = getPreferences(requireContext())
prepareDatabaseConnection()
}
private fun prepareDatabaseConnection() {
val dbHelper = DBStorageController(requireContext(), sharedPreferences.getString("currentGroup", "database")!!)
val dbHelper = DBStorageController(requireContext(), sharedPreferences.getString("currentGroup", "offline")!!)
categoryDAO = CategoryDAO(dbHelper)
}
@ -58,56 +53,75 @@ class CategoriesFragment : Fragment() {
}
fun removeSelected() {
val layout = view?.findViewById<LinearLayout>(R.id.categoriesLayout)
thread {
val layout = view?.findViewById<LinearLayout>(R.id.categoriesLayout)
var deleted = false
for (view: CategoryView in layout?.children!!.iterator() as Iterator<CategoryView>) {
if (view.isCategorySelected) {
val net = Net()
net.token = sharedPreferences.getString("token", "")!!
net.server = sharedPreferences.getString("server", "")!!
net.language = sharedPreferences.getString("language", "en-US")!!
val currentGroup = sharedPreferences.getString("currentGroup", "")!!.toInt()
var deleted = false
for (view: CategoryView in layout?.children!!.iterator() as Iterator<CategoryView>) {
if (view.isCategorySelected) {
val net = Net()
net.token = sharedPreferences.getString("token", "")!!
net.server = sharedPreferences.getString("server", "")!!
net.language = sharedPreferences.getString("language", "en-US")!!
val currentGroup = sharedPreferences.getString("currentGroup", "")!!.toInt()
categoryDAO.eraseCategory(view.category.id, requireContext())
val response = net.deleteCategory(currentGroup, view.category.id)
activity!!.runOnUiThread{
Toast.makeText(context, response.body!!.string(), Toast.LENGTH_SHORT).show()
categoryDAO.eraseCategory(view.category.id, requireContext())
net.deleteCategory(currentGroup, view.category.id, {response ->
activity!!.runOnUiThread{
Toast.makeText(context, response.body!!.string(), Toast.LENGTH_SHORT).show()
}
})
deleted = true
}
deleted = true
}
}
if (!deleted) {
Toast.makeText(requireContext(), getString(R.string.nothing_to_delete), Toast.LENGTH_SHORT).show()
if (!deleted) {
activity!!.runOnUiThread {
Toast.makeText(requireContext(), getString(R.string.nothing_to_delete), Toast.LENGTH_SHORT).show()
}
}
updateContent()
}
updateContent()
}
fun updateSelected() {
val layout = view?.findViewById<LinearLayout>(R.id.categoriesLayout)
for (view: CategoryView in layout?.children!!.iterator() as Iterator<CategoryView>) {
if (view.isCategorySelected) {
val addCategoryIntent = Intent(context, AddCategoryActivity::class.java)
val extras = Bundle()
extras.putParcelable("category", view.category)
addCategoryIntent.putExtras(extras)
ContextCompat.startActivity(context!!, addCategoryIntent, extras)
thread {
val layout = view?.findViewById<LinearLayout>(R.id.categoriesLayout)
for (view: CategoryView in layout?.children!!.iterator() as Iterator<CategoryView>) {
if (view.isCategorySelected) {
val addCategoryIntent = Intent(context, AddCategoryActivity::class.java)
val extras = Bundle()
extras.putParcelable("category", view.category)
addCategoryIntent.putExtras(extras)
activity!!.runOnUiThread {
ContextCompat.startActivity(context!!, addCategoryIntent, extras)
}
}
}
}
}
fun updateContent() {
prepareDatabaseConnection()
val layout = view?.findViewById<LinearLayout>(R.id.categoriesLayout)
layout?.removeAllViews()
thread {
if (updateInProgress) return@thread
updateInProgress = true
prepareDatabaseConnection()
val layout = view?.findViewById<LinearLayout>(R.id.categoriesLayout)
activity!!.runOnUiThread {
layout?.removeAllViews()
}
val categories = categoryDAO.getAllCategories()
val categories = categoryDAO.getAllCategories()
for (category in categories) {
val categoryView = CategoryView(requireActivity(), requireContext(), category)
layout?.addView(categoryView)
for (category in categories) {
val categoryView = CategoryView(requireActivity(), requireContext(), category)
activity!!.runOnUiThread {
layout?.addView(categoryView)
}
}
updateInProgress = false
}
}

View File

@ -14,14 +14,13 @@ import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.fragment.app.Fragment
import androidx.gridlayout.widget.GridLayout
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.Net
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.activities.AddProductActivity
import org.foxarmy.barcodescannerforemployees.database.DBStorageController
import org.foxarmy.barcodescannerforemployees.database.ProductDAO
import org.foxarmy.barcodescannerforemployees.databinding.FragmentShelfBinding
import org.foxarmy.barcodescannerforemployees.getPreferences
import org.foxarmy.barcodescannerforemployees.views.ProductView
import kotlin.concurrent.thread
@ -38,16 +37,9 @@ class ShelfFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
requireContext(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
sharedPreferences = getPreferences(requireContext())
prepareDatabaseConnection()
}
override fun onCreateView(
@ -122,11 +114,12 @@ class ShelfFragment : Fragment() {
}
fun updateContent() {
prepareDatabaseConnection()
thread {
if (updateInProgress) return@thread
updateInProgress = true
prepareDatabaseConnection()
val grv = view?.findViewById<GridLayout>(R.id.contentGridLayout)
activity!!.runOnUiThread {
grv?.removeAllViews()
}
@ -144,7 +137,6 @@ class ShelfFragment : Fragment() {
}
}
updateInProgress = false
}
}
@ -165,11 +157,12 @@ class ShelfFragment : Fragment() {
val currentGroup = sharedPreferences.getString("currentGroup", "")!!.toInt()
productDAO.eraseProduct(view.product.id)
val response = net.deleteProduct(currentGroup, view.product.id)
activity!!.runOnUiThread{
Toast.makeText(context, response.body!!.string(), Toast.LENGTH_SHORT).show()
}
net.deleteProduct(currentGroup, view.product.id, {response ->
activity!!.runOnUiThread{
Toast.makeText(context, response.body!!.string(), Toast.LENGTH_SHORT).show()
}
})
deleted = true
}
}

View File

@ -7,11 +7,10 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.fragment.app.Fragment
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.Net
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.activities.AddAbstractProductActivity
@ -20,6 +19,7 @@ import org.foxarmy.barcodescannerforemployees.database.AbstractProductDAO
import org.foxarmy.barcodescannerforemployees.database.DBStorageController
import org.foxarmy.barcodescannerforemployees.databinding.FragmentStorageBinding
import org.foxarmy.barcodescannerforemployees.generateThumbnailForImage
import org.foxarmy.barcodescannerforemployees.getPreferences
import org.foxarmy.barcodescannerforemployees.views.AbstractProductView
import kotlin.concurrent.thread
@ -32,23 +32,18 @@ class StorageFragment : Fragment() {
private lateinit var sharedPreferences: SharedPreferences
private lateinit var abstractProductDAO: AbstractProductDAO
private var updateInProcess = false
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
requireContext(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
binding = FragmentStorageBinding.inflate(inflater)
sharedPreferences = getPreferences(requireContext())
prepareDatabaseConnection()
binding = FragmentStorageBinding.inflate(inflater)
fillUpSortBySpinner()
binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
@ -107,10 +102,12 @@ class StorageFragment : Fragment() {
val currentGroup = sharedPreferences.getString("currentGroup", "")!!.toInt()
abstractProductDAO.eraseAbstractProduct(view.abstractProduct.id, requireContext())
val response = net.deleteAbstractProduct(currentGroup, view.abstractProduct.id)
activity!!.runOnUiThread{
Toast.makeText(context, response.body!!.string(), Toast.LENGTH_SHORT).show()
}
net.deleteAbstractProduct(currentGroup, view.abstractProduct.id, {response ->
activity!!.runOnUiThread{
Toast.makeText(context, response.body!!.string(), Toast.LENGTH_SHORT).show()
}
})
deleted = true
}
@ -141,10 +138,14 @@ class StorageFragment : Fragment() {
}
fun updateContent() {
prepareDatabaseConnection()
thread {
if (updateInProcess) return@thread
updateInProcess = true
prepareDatabaseConnection()
val grv = binding.contentGridLayout
activity!!.runOnUiThread{
activity!!.runOnUiThread {
grv.removeAllViews()
}
@ -160,11 +161,25 @@ class StorageFragment : Fragment() {
)
if (filterBy == "barcodeless") {
abstractProductView.setOnClickListener {
(activity as FindBarcodelessAbstractProduct).selected(abstractProductView.abstractProduct)
}
abstractProductView.findViewById<TextView>(R.id.productNameView).setOnClickListener {
(activity as FindBarcodelessAbstractProduct).selected(abstractProductView.abstractProduct)
abstractProductView.also {
it.setOnClickListener {
(activity as FindBarcodelessAbstractProduct).selected(abstractProductView.abstractProduct)
}
it.findViewById<TextView>(R.id.productNameView).setOnClickListener {
(activity as FindBarcodelessAbstractProduct).selected(abstractProductView.abstractProduct)
}
it.findViewById<ConstraintLayout>(R.id.productLayout).setOnClickListener {
(activity as FindBarcodelessAbstractProduct).selected(abstractProductView.abstractProduct)
}
it.findViewById<TextView>(R.id.categoryView).setOnClickListener {
(activity as FindBarcodelessAbstractProduct).selected(abstractProductView.abstractProduct)
}
it.findViewById<TextView>(R.id.unitView).setOnClickListener {
(activity as FindBarcodelessAbstractProduct).selected(abstractProductView.abstractProduct)
}
it.findViewById<TextView>(R.id.productNetWeightView).setOnClickListener {
(activity as FindBarcodelessAbstractProduct).selected(abstractProductView.abstractProduct)
}
}
}
@ -172,14 +187,10 @@ class StorageFragment : Fragment() {
grv.addView(abstractProductView)
}
}
}.join()
updateInProcess = false
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updateContent()
}
fun filterByCategory(id: Int) {
filterBy = "category"
@ -194,7 +205,7 @@ class StorageFragment : Fragment() {
val args = Bundle()
args.putString("filterBy", filterBy)
args.putStringArray("filter", filter)
fragment.setArguments(args)
fragment.arguments = args
return fragment
}

View File

@ -14,19 +14,15 @@ import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.*
import org.foxarmy.barcodescannerforemployees.activities.FullscreenActivity
import org.foxarmy.barcodescannerforemployees.activities.MainActivity
import org.foxarmy.barcodescannerforemployees.database.CategoryDAO
import org.foxarmy.barcodescannerforemployees.database.DBStorageController
import org.foxarmy.barcodescannerforemployees.dataclasses.AbstractProduct
import org.foxarmy.barcodescannerforemployees.getActivity
import org.foxarmy.barcodescannerforemployees.getImageUri
import org.foxarmy.barcodescannerforemployees.getUnitNameById
import java.io.File
class AbstractProductView: LinearLayout {
class AbstractProductView : LinearLayout {
private var productLayout: ConstraintLayout? = null
private var productPicture: ImageView? = null
private var productNameField: TextView? = null
@ -42,16 +38,10 @@ class AbstractProductView: LinearLayout {
constructor(context: Context, a: AttributeSet) : super(context, a) {
activity = getActivity(context)!!
val inflater:LayoutInflater = activity.layoutInflater
val inflater: LayoutInflater = activity.layoutInflater
inflater.inflate(R.layout.abstract_product_view, this)
sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
sharedPreferences = getPreferences(context)
val dbHelper = DBStorageController(context, sharedPreferences.getString("currentGroup", "database")!!)
categoryDAO = CategoryDAO(dbHelper)
@ -60,23 +50,17 @@ class AbstractProductView: LinearLayout {
constructor(activity: Activity, context: Context, abstractProduct: AbstractProduct) : super(context) {
this.abstractProduct = abstractProduct
this.activity = activity
val inflater:LayoutInflater = activity.layoutInflater
val inflater: LayoutInflater = activity.layoutInflater
inflater.inflate(R.layout.abstract_product_view, this)
sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
sharedPreferences = getPreferences(context)
val dbHelper = DBStorageController(context, sharedPreferences.getString("currentGroup", "database")!!)
categoryDAO = CategoryDAO(dbHelper)
update()
}
fun init () {
private fun init() {
productLayout = findViewById(R.id.productLayout)
productPicture = findViewById(R.id.productPicture)
productNameField = findViewById(R.id.productNameView)
@ -84,30 +68,8 @@ class AbstractProductView: LinearLayout {
categoryField = findViewById(R.id.categoryView)
unitField = findViewById(R.id.unitView)
productPicture!!.setOnClickListener {
val fullscreenIntent = Intent(activity, FullscreenActivity::class.java)
val extras = Bundle()
extras.putString("imagehash", abstractProduct.imageHash)
fullscreenIntent.putExtras(extras)
startActivity(context, fullscreenIntent, extras)
}
productLayout!!.setOnClickListener {
}
setUpClicks()
unitField?.text = getUnitNameById(context, abstractProduct.unit)
productLayout!!.setOnLongClickListener {
isProductSelected = !isProductSelected
this.background = ContextCompat.getDrawable(context, if (isProductSelected) R.drawable.outline_selected else R.drawable.outline)
true
}
productNameField!!.setOnClickListener {
Toast.makeText(activity, productNameField!!.text, Toast.LENGTH_SHORT).show()
}
}
fun update() {
@ -117,10 +79,126 @@ class AbstractProductView: LinearLayout {
thumbnailsDir.mkdirs()
val imageUri = getImageUri(activity, File(thumbnailsDir, "${abstractProduct.imageHash}.webp"))
productPicture!!.setImageURI(imageUri)
// productPicture!!.rotation = 90f
productNameField!!.text = abstractProduct.name
netWeightField!!.text = abstractProduct.netWeight.toString()
categoryField!!.text = categoryDAO.getCategoryNameById(abstractProduct.category)
}
private fun setUpClicks() {
productLayout!!.setOnLongClickListener { onAnyLongClick() }
productNameField!!.setOnLongClickListener { onNameLongClick() }
productPicture!!.setOnLongClickListener { onImageLongClick() }
netWeightField!!.setOnLongClickListener { onAnyLongClick() }
categoryField!!.setOnLongClickListener { onAnyLongClick() }
unitField!!.setOnLongClickListener { onAnyLongClick() }
productLayout!!.setOnClickListener { onAnyClick() }
netWeightField!!.setOnClickListener { onAnyClick() }
categoryField!!.setOnClickListener { onAnyClick() }
unitField!!.setOnClickListener { onAnyClick() }
productPicture!!.setOnClickListener { onImageLongClick() }
productNameField!!.setOnClickListener { onNameLongClick() }
}
private fun onAnyClick(): Boolean {
if (activity !is MainActivity) return false
val mainActivity = activity as MainActivity
if (mainActivity.selectionMode) {
isProductSelected = !isProductSelected
if (isProductSelected) {
mainActivity.addSelection()
} else {
mainActivity.removeSelection()
}
activity.runOnUiThread {
this.background = ContextCompat.getDrawable(
context,
if (isProductSelected) R.drawable.outline_selected else R.drawable.outline
)
}
return true
} else {
return false
}
}
private fun onAnyLongClick(): Boolean {
if (activity !is MainActivity) return false
val mainActivity = activity as MainActivity
if (mainActivity.selectionMode) {
isProductSelected = !isProductSelected
if (isProductSelected) {
mainActivity.addSelection()
} else {
mainActivity.removeSelection()
}
activity.runOnUiThread {
this.background = ContextCompat.getDrawable(
context,
if (isProductSelected) R.drawable.outline_selected else R.drawable.outline
)
}
return true
} else {
mainActivity.addSelection()
isProductSelected = !isProductSelected
activity.runOnUiThread {
this.background = ContextCompat.getDrawable(
context,
if (isProductSelected) R.drawable.outline_selected else R.drawable.outline
)
}
return true
}
}
private fun onImageLongClick(): Boolean {
if (activity !is MainActivity) {
val fullscreenIntent = Intent(activity, FullscreenActivity::class.java)
val extras = Bundle()
extras.putString("imagehash", abstractProduct.imageHash)
fullscreenIntent.putExtras(extras)
startActivity(context, fullscreenIntent, extras)
return true
}
val mainActivity = activity as MainActivity
if (mainActivity.selectionMode) {
return onAnyLongClick()
} else {
val fullscreenIntent = Intent(activity, FullscreenActivity::class.java)
val extras = Bundle()
extras.putString("imagehash", abstractProduct.imageHash)
fullscreenIntent.putExtras(extras)
startActivity(context, fullscreenIntent, extras)
}
return true
}
private fun onNameLongClick(): Boolean {
if (activity !is MainActivity) {
activity.runOnUiThread {
Toast.makeText(activity, productNameField!!.text, Toast.LENGTH_SHORT).show()
}
return true
}
val mainActivity = activity as MainActivity
if (mainActivity.selectionMode) {
return onAnyLongClick()
} else {
activity.runOnUiThread {
Toast.makeText(activity, productNameField!!.text, Toast.LENGTH_SHORT).show()
}
}
return true
}
}

View File

@ -7,16 +7,16 @@ import android.view.LayoutInflater
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.activities.MainActivity
import org.foxarmy.barcodescannerforemployees.database.AbstractProductDAO
import org.foxarmy.barcodescannerforemployees.database.DBStorageController
import org.foxarmy.barcodescannerforemployees.dataclasses.Category
import org.foxarmy.barcodescannerforemployees.getPreferences
class CategoryView : LinearLayout {
var category: Category
val categoryName: TextView
val amountOfProducts: TextView
var isCategorySelected = false
@ -27,13 +27,7 @@ class CategoryView : LinearLayout {
constructor(activity: Activity, context: Context, category: Category) : super(context) {
this.category = category
sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
sharedPreferences = getPreferences(context)
val dbHelper = DBStorageController(context, sharedPreferences.getString("currentGroup", "database")!!)
abstractProductDAO = AbstractProductDAO(dbHelper)
@ -41,7 +35,7 @@ class CategoryView : LinearLayout {
val inflater: LayoutInflater = activity.layoutInflater
inflater.inflate(R.layout.category_view, this)
this.background = ContextCompat.getDrawable(context, if (isCategorySelected) R.drawable.outline_selected else R.drawable.outline)
updateStroke()
categoryName = findViewById(R.id.categoryNameTextView)
amountOfProducts = findViewById(R.id.amountOfProducts)
@ -49,13 +43,47 @@ class CategoryView : LinearLayout {
categoryName.text = category.name
amountOfProducts.text = abstractProductDAO.getAmountOfAbstractProductsInCategory(category.id).toString()
setOnLongClickListener {
isCategorySelected = !isCategorySelected
this.background = ContextCompat.getDrawable(context, if (isCategorySelected) R.drawable.outline_selected else R.drawable.outline)
true
setUpClicks()
}
private fun setUpClicks() {
categoryName.setOnClickListener { onCategoryClick() }
amountOfProducts.setOnClickListener { onCategoryClick() }
categoryName.setOnLongClickListener { onAnyLongClick() }
amountOfProducts.setOnLongClickListener { onAnyLongClick() }
}
private fun onCategoryClick(): Boolean {
if (context !is MainActivity) return false
val mainActivity = context as MainActivity
if (mainActivity.selectionMode) return onAnyLongClick()
else (context as MainActivity).filterAbstractProductsByCategory(category.id)
return true
}
private fun onAnyLongClick(): Boolean {
if (context !is MainActivity) return false
val mainActivity = context as MainActivity
isCategorySelected = !isCategorySelected
updateStroke()
if (mainActivity.selectionMode) {
if (isCategorySelected) mainActivity.addSelection()
else mainActivity.removeSelection()
} else {
mainActivity.addSelection()
}
setOnClickListener {
(activity as MainActivity).filterAbstractProductsByCategory(category.id)
return true
}
private fun updateStroke() {
(context as Activity).runOnUiThread {
this.background = ContextCompat.getDrawable(context, if (isCategorySelected) R.drawable.outline_selected else R.drawable.outline)
}
}
}

View File

@ -7,12 +7,11 @@ import android.view.LayoutInflater
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.activities.ExpiryCalendarActivity
import org.foxarmy.barcodescannerforemployees.database.DBStorageController
import org.foxarmy.barcodescannerforemployees.database.ProductDAO
import org.foxarmy.barcodescannerforemployees.getPreferences
import java.text.SimpleDateFormat
class ExpiryGroupView : LinearLayout {
@ -24,13 +23,7 @@ class ExpiryGroupView : LinearLayout {
constructor(activity: Activity, context: Context, representingDate: Long) : super(context) {
sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
sharedPreferences = getPreferences(context)
val dbHelper = DBStorageController(context, sharedPreferences.getString("currentGroup", "database")!!)
productDAO = ProductDAO(dbHelper)

View File

@ -10,7 +10,7 @@ import org.foxarmy.barcodescannerforemployees.R
class GroupMemberView : LinearLayout {
private var username: String
private var userId: Int
var userId: Int
constructor(activity: Activity, context: Context, username: String, userId: Int) : super(context) {

View File

@ -13,6 +13,7 @@ import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
@ -21,29 +22,26 @@ import androidx.core.graphics.blue
import androidx.core.graphics.green
import androidx.core.graphics.red
import androidx.core.math.MathUtils.clamp
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.*
import org.foxarmy.barcodescannerforemployees.activities.FullscreenActivity
import org.foxarmy.barcodescannerforemployees.calculateProductFreshness
import org.foxarmy.barcodescannerforemployees.activities.MainActivity
import org.foxarmy.barcodescannerforemployees.database.AbstractProductDAO
import org.foxarmy.barcodescannerforemployees.database.CategoryDAO
import org.foxarmy.barcodescannerforemployees.database.DBStorageController
import org.foxarmy.barcodescannerforemployees.dataclasses.AbstractProduct
import org.foxarmy.barcodescannerforemployees.dataclasses.Product
import org.foxarmy.barcodescannerforemployees.getActivity
import org.foxarmy.barcodescannerforemployees.getImageUri
import java.io.File
import java.text.DecimalFormat
import java.text.SimpleDateFormat
import java.util.*
import kotlin.concurrent.thread
class ProductView: LinearLayout {
class ProductView : LinearLayout {
var product: Product = Product()
var isProductSelected = false
private var activity: Activity
private lateinit var productLayout: ConstraintLayout
private lateinit var productImageView: ImageView
private lateinit var productNameTextView: TextView
private lateinit var productNetWeightTextView: TextView
@ -63,13 +61,7 @@ class ProductView: LinearLayout {
constructor(context: Context, a: AttributeSet) : super(context, a) {
activity = getActivity(context)!!
sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
sharedPreferences = getPreferences(context)
val dbHelper = DBStorageController(context, sharedPreferences.getString("currentGroup", "offline")!!)
abstractProductDAO = AbstractProductDAO(dbHelper)
@ -86,18 +78,13 @@ class ProductView: LinearLayout {
val inflater: LayoutInflater = activity.layoutInflater
inflater.inflate(R.layout.product_view, this)
sharedPreferences = EncryptedSharedPreferences.create(
"sensitive",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
sharedPreferences = getPreferences(context)
val dbHelper = DBStorageController(context, sharedPreferences.getString("currentGroup", "offline")!!)
abstractProductDAO = AbstractProductDAO(dbHelper)
categoryDAO = CategoryDAO(dbHelper)
productLayout = findViewById(R.id.productLayout)
productImageView = findViewById(R.id.productPicture)
productNameTextView = findViewById(R.id.productNameView)
productNetWeightTextView = findViewById(R.id.productNetWeightView)
@ -106,68 +93,152 @@ class ProductView: LinearLayout {
productLifeSpan = findViewById(R.id.dateSpan)
productFreshnessTextView = findViewById(R.id.freshnessPercentTextView)
findViewById<ConstraintLayout>(R.id.productLayout).setOnLongClickListener {
isProductSelected = !isProductSelected
updateStroke()
true
}
productImageView.setOnClickListener {
val fullscreenIntent = Intent(activity, FullscreenActivity::class.java)
val extras = Bundle()
val abstractProduct = abstractProductDAO.findAbstractProductById(product.abstractProductId)
extras.putString("imagehash", abstractProduct!!.imageHash)
fullscreenIntent.putExtras(extras)
startActivity(context, fullscreenIntent, extras)
}
setUpClicks()
update()
}
@RequiresApi(Build.VERSION_CODES.O)
fun update () {
val linkedAbstractProduct: AbstractProduct = abstractProductDAO.findAbstractProductById(product.abstractProductId) ?: return
private fun setUpClicks() {
productLayout.setOnLongClickListener { onAnyLongClick() }
productImageView.setOnLongClickListener { onImageLongClick() }
productNameTextView.setOnLongClickListener { onNameLongClick() }
productNetWeightTextView.setOnLongClickListener { onAnyLongClick() }
productAmountTextView.setOnLongClickListener { onAnyLongClick() }
productCategoryView.setOnLongClickListener { onAnyLongClick() }
productLifeSpan.setOnLongClickListener { onAnyLongClick() }
productFreshnessTextView.setOnLongClickListener { onAnyLongClick() }
productLayout.setOnClickListener { onAnyClick() }
productImageView.setOnClickListener { onImageLongClick() }
productNameTextView.setOnClickListener { onNameLongClick() }
productNetWeightTextView.setOnClickListener { onAnyClick() }
productAmountTextView.setOnClickListener { onAnyClick() }
productCategoryView.setOnClickListener { onAnyClick() }
productLifeSpan.setOnClickListener { onAnyClick() }
productFreshnessTextView.setOnClickListener { onAnyClick() }
}
@RequiresApi(Build.VERSION_CODES.O)
private fun onAnyClick(): Boolean {
if (activity !is MainActivity) return false
val mainActivity = activity as MainActivity
if (mainActivity.selectionMode) {
isProductSelected = !isProductSelected
if (isProductSelected) mainActivity.addSelection()
else mainActivity.removeSelection()
updateStroke()
return true
} else {
return false
}
}
@RequiresApi(Build.VERSION_CODES.O)
private fun onAnyLongClick(): Boolean {
if (activity !is MainActivity) return false
val mainActivity = activity as MainActivity
isProductSelected = !isProductSelected
updateStroke()
if (mainActivity.selectionMode) {
if (isProductSelected) mainActivity.addSelection()
else mainActivity.removeSelection()
} else {
mainActivity.addSelection()
}
return true
}
private fun onNameLongClick(): Boolean {
if (activity !is MainActivity) {
Toast.makeText(activity, productNameTextView.text, Toast.LENGTH_SHORT).show()
return true
}
val mainActivity = activity as MainActivity
if (mainActivity.selectionMode) return onAnyLongClick()
else Toast.makeText(activity, productNameTextView.text, Toast.LENGTH_SHORT).show()
return true
}
private fun onImageLongClick(): Boolean {
val abstractProduct = abstractProductDAO.findAbstractProductById(product.abstractProductId)
if (activity !is MainActivity) {
val fullscreenIntent = Intent(activity, FullscreenActivity::class.java)
val extras = Bundle()
extras.putString("imagehash", abstractProduct!!.imageHash)
fullscreenIntent.putExtras(extras)
startActivity(context, fullscreenIntent, extras)
return true
}
val mainActivity = activity as MainActivity
if (mainActivity.selectionMode) {
return onAnyLongClick()
} else {
val fullscreenIntent = Intent(activity, FullscreenActivity::class.java)
val extras = Bundle()
extras.putString("imagehash", abstractProduct!!.imageHash)
fullscreenIntent.putExtras(extras)
startActivity(context, fullscreenIntent, extras)
}
return true
}
@RequiresApi(Build.VERSION_CODES.O)
fun update() {
val linkedAbstractProduct: AbstractProduct =
abstractProductDAO.findAbstractProductById(product.abstractProductId) ?: return
val thumbnailsDir = File(activity.cacheDir, "thumbnails")
thumbnailsDir.mkdirs()
val pictureFile = File(thumbnailsDir, "${linkedAbstractProduct.imageHash}.webp")
val imageUri = getImageUri(activity, pictureFile)
productImageView.setImageURI(imageUri)
// productImageView.rotation = 90f
productNameTextView.text = linkedAbstractProduct.name
productNetWeightTextView.text = linkedAbstractProduct.netWeight.toString()
productAmountTextView.text = product.amount.toString()
productCategoryView.text = categoryDAO.getCategoryNameById(linkedAbstractProduct.category)
productLifeSpan.text = "${SimpleDateFormat("dd.MM.yyyy").format(Date(product.dateOfProduction*1000))}-${SimpleDateFormat("dd.MM.yyyy").format(Date(product.dateOfExpiry*1000))}"
productLifeSpan.text = "${SimpleDateFormat("dd.MM.yyyy").format(Date(product.dateOfProduction * 1000))}-${
SimpleDateFormat("dd.MM.yyyy").format(Date(product.dateOfExpiry * 1000))
}"
productFreshnessTextView.text =
if (product.freshness == Double.NEGATIVE_INFINITY || product.freshness == Double.POSITIVE_INFINITY || product.freshness < 0) {
context.getString(R.string.expired)
} else {
"${DecimalFormat("#.#").format(product.freshness*100)}%"
}
if (product.freshness == Double.NEGATIVE_INFINITY || product.freshness == Double.POSITIVE_INFINITY || product.freshness < 0) {
context.getString(R.string.expired)
} else {
"${DecimalFormat("#.#").format(product.freshness * 100)}%"
}
updateStroke()
}
@RequiresApi(Build.VERSION_CODES.O)
fun updateStroke() {
if (isProductSelected) {
this.background = ContextCompat.getDrawable(context, R.drawable.outline_selected)
} else {
if (product.id != 0) {
thread {
this.outline = GradientDrawable()
backgroundColor = evaluateColor()
strokeColor = darkenColor(backgroundColor, 0.25) // (backgroundColor and 0xfefefe ) shr 1
this.outline.setColor(backgroundColor)
this.outline.setStroke(4, strokeColor)
this.outline.alpha = 84
this.background = outline
}
} else {
this.background = ContextCompat.getDrawable(context, R.drawable.outline)
}
activity.runOnUiThread {
if (isProductSelected) {
this.background = ContextCompat.getDrawable(context, R.drawable.outline_selected)
} else {
if (product.id != 0) {
this.outline = GradientDrawable()
backgroundColor = evaluateColor()
strokeColor = darkenColor(backgroundColor, 0.25) // (backgroundColor and 0xfefefe ) shr 1
this.outline.setColor(backgroundColor)
this.outline.setStroke(4, strokeColor)
this.outline.alpha = 84
this.background = outline
} else {
this.background = ContextCompat.getDrawable(context, R.drawable.outline)
}
}
}
}
@ -178,13 +249,15 @@ class ProductView: LinearLayout {
}
fun calculateFreshnessGradient(percentage: Double): Int {
val startColor = ContextCompat.getColor(context, if (percentage > 0.5) R.color.full_freshness else R.color.half_freshness)
val endColor = ContextCompat.getColor(context, if (percentage > 0.5) R.color.half_freshness else R.color.expired_freshness)
val startColor =
ContextCompat.getColor(context, if (percentage > 0.5) R.color.full_freshness else R.color.half_freshness)
val endColor =
ContextCompat.getColor(context, if (percentage > 0.5) R.color.half_freshness else R.color.expired_freshness)
val gradientPosition = 1 - if (percentage > 0.5) (percentage - 0.5) * 2 else percentage * 2
val red = clamp((startColor.red + gradientPosition * (endColor.red - startColor.red )).toInt(), 0, 255)
val green = clamp((startColor.green + gradientPosition * (endColor.green - startColor.green)).toInt(), 0, 255)
val red = clamp((startColor.red + gradientPosition * (endColor.red - startColor.red)).toInt(), 0, 255)
val green = clamp((startColor.green + gradientPosition * (endColor.green - startColor.green)).toInt(), 0, 255)
val blue = clamp((startColor.blue + gradientPosition * (endColor.blue - startColor.blue)).toInt(), 0, 255)
var redHex = Integer.toHexString(red)
@ -194,7 +267,7 @@ class ProductView: LinearLayout {
if (greenHex.length == 1) greenHex = "0$greenHex"
var blueHex = Integer.toHexString(blue)
if (blueHex.length == 1 ) blueHex = "0$blueHex"
if (blueHex.length == 1) blueHex = "0$blueHex"
val colorString = "#$redHex$greenHex$blueHex"
@ -202,7 +275,7 @@ class ProductView: LinearLayout {
}
@RequiresApi(Build.VERSION_CODES.O)
fun darkenColor(color: Int, darkPercent: Double) : Int {
fun darkenColor(color: Int, darkPercent: Double): Int {
val c = Color.valueOf(color)
val red = c.red() * (1 - darkPercent)

View File

@ -8,7 +8,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent" android:id="@+id/productLayout" android:outlineProvider="bounds"
android:background="#00FFFEFE" android:clickable="true">
android:background="#00FFFEFE" android:clickable="true" android:translationZ="0sp">
<ImageView
android:layout_width="match_parent"
android:layout_height="200dp" app:srcCompat="@android:drawable/ic_menu_gallery"

View File

@ -1,10 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.ShelfFragment"
android:id="@+id/content">
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_height="match_parent">
</androidx.core.widget.NestedScrollView>
<androidx.core.widget.NestedScrollView
android:layout_height="0dp"
tools:context=".fragments.ShelfFragment"
android:id="@+id/content" android:layout_width="0dp" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -39,12 +39,13 @@
<androidx.viewpager.widget.ViewPager
android:id="@+id/tab_viewpager"
android:layout_width="0dp"
android:layout_height="777dp"
android:layout_height="0dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" android:layout_gravity="bottom"
android:layout_marginTop="2dp"
app:layout_constraintTop_toBottomOf="@+id/appBarLayout"
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="6dp"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="6dp"/>
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="6dp"
app:layout_constraintBottom_toBottomOf="parent"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/new_element_fab"
android:layout_width="wrap_content"

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" android:id="@+id/storageLayout">
<fragment
android:id="@+id/nav_host_fragment_content_storage"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph_main"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -62,7 +62,7 @@
android:ems="10"
android:id="@+id/barcodeTextEdit" app:layout_constraintTop_toBottomOf="@+id/imageView"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="8dp" android:hint="Barcode" android:textColorHint="#737373"
android:layout_marginTop="8dp" android:hint="@string/barcode" android:textColorHint="#737373"
android:layout_marginStart="5dp"/>
<CheckBox
android:text="@string/no_barcode"

View File

@ -2,7 +2,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#141218">
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@ -26,7 +26,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent" app:layout_constraintTop_toBottomOf="@+id/spinner"
android:layout_marginTop="32dp" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" android:id="@+id/scrollView">
app:layout_constraintEnd_toEndOf="parent" android:id="@+id/scrollView"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.gridlayout.widget.GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/contentGridLayout" app:columnCount="2"

View File

@ -1,13 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fragment_storage"
tools:context=".fragments.StorageFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:text="@string/sort_by"
@ -16,7 +12,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:textAlignment="center"
app:layout_constraintBottom_toBottomOf="@+id/scrollView2"/>
app:layout_constraintBottom_toTopOf="@+id/scrollView2"/>
<Spinner
android:layout_width="0dp"
android:layout_height="32dp" android:id="@+id/spinner"
@ -34,15 +30,16 @@
android:layout_marginEnd="10dp" app:layout_constraintTop_toTopOf="parent"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:id="@+id/scrollView2"
app:layout_constraintTop_toBottomOf="@+id/dropFiltersButton">
app:layout_constraintTop_toBottomOf="@+id/dropFiltersButton"
android:layout_marginTop="4dp" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<androidx.gridlayout.widget.GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/contentGridLayout" app:columnCount="2"
android:layout_height="match_parent" android:id="@+id/contentGridLayout" app:columnCount="2"
app:rowCount="100" android:nestedScrollingEnabled="true">
</androidx.gridlayout.widget.GridLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -12,4 +12,5 @@
<item
android:id="@+id/nav_settings"
android:title="@string/settings"/>
<item android:title="@string/logout" android:id="@+id/nav_logout"/>
</menu>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="@string/kick" android:id="@+id/kick"/>
<item android:title="@string/transfer_ownership" android:id="@+id/transfer_ownership"/>
</menu>

View File

@ -114,6 +114,18 @@
<string name="image_compress_factor">Степень сжатия изображения</string>
<string name="current_group">Текущая группа</string>
<string name="cancel">Отмена</string>
<string name="no_internet_connection">Не могу установить соединение с сервером. Возможно, вы отключены от сети?</string>
<string name="ok">Ок</string>
<string name="logout">Выйти</string>
<string name="online_only_feature">Эта возможность доступна только с онлайн аккаунтом</string>
<string name="username_changed">Имя пользователя изменено. Пожалуйста, перезайдите</string>
<string name="password_changed">Пароль изменён. Пожалуйста, перезайдите</string>
<string name="new_group_name">Введите новое имя группы</string>
<string name="kick">Выгнать</string>
<string name="transfer_ownership">Передать владение группой</string>
<string name="transfer_ownership_confirmation">Вы увереных, что хотите передать владение группой этому пользователю?</string>
<string name="loading_please_wait">Загрузка, пожалуйста ждите</string>
<string name="loading">Загрузка</string>
<string-array name="languages">
<item>en-US</item>
<item>ru-RU</item>

View File

@ -112,6 +112,18 @@
<string name="image_compress_factor">Image compression factor</string>
<string name="current_group">Current group</string>
<string name="cancel">Cancel</string>
<string name="no_internet_connection">Cannot reach the server. Maybe, you lost internet connection?</string>
<string name="ok">OK</string>
<string name="logout">Log out</string>
<string name="online_only_feature">This feature is online only</string>
<string name="username_changed">Your username has been changed. Please, relogin</string>
<string name="password_changed">You password was changed. Please, relogin</string>
<string name="new_group_name">Enter new group name</string>
<string name="kick">Kick</string>
<string name="transfer_ownership">Transfer ownership</string>
<string name="transfer_ownership_confirmation">Are you sure you want to transfer ownership on current group to that user?</string>
<string name="loading_please_wait">Loading, please wait</string>
<string name="loading">Loading</string>
<string-array name="languages">
<item>en-US</item>
<item>ru-RU</item>