adds rest communication
- test cases for push and pop - rest singleton instantiated in MainApplication access through context - pop and peek work, push needs encryption process still
This commit is contained in:
parent
3d42f0ca2e
commit
10ba4cf31b
@ -4,12 +4,12 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 32
|
compileSdk 30
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.maenle.bump"
|
applicationId "com.maenle.bump"
|
||||||
minSdk 29
|
minSdk 29
|
||||||
targetSdk 32
|
targetSdk 30
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
|
|
||||||
@ -37,6 +37,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:16.1.4'
|
implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:16.1.4'
|
||||||
|
implementation 'com.android.volley:volley:1.2.0'
|
||||||
def camerax_version = "1.0.2"
|
def camerax_version = "1.0.2"
|
||||||
implementation 'androidx.core:core-ktx:1.3.2'
|
implementation 'androidx.core:core-ktx:1.3.2'
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
@ -47,7 +48,7 @@ dependencies {
|
|||||||
implementation "androidx.camera:camera-camera2:$camerax_version"
|
implementation "androidx.camera:camera-camera2:$camerax_version"
|
||||||
implementation "androidx.camera:camera-lifecycle:$camerax_version"
|
implementation "androidx.camera:camera-lifecycle:$camerax_version"
|
||||||
implementation 'com.google.zxing:core:3.3.0'
|
implementation 'com.google.zxing:core:3.3.0'
|
||||||
implementation "androidx.camera:camera-view:1.0.0-alpha31"
|
implementation "androidx.camera:camera-view:1.0.0-alpha20"
|
||||||
implementation 'com.macasaet.fernet:fernet-java8:1.4.2'
|
implementation 'com.macasaet.fernet:fernet-java8:1.4.2'
|
||||||
testImplementation 'junit:junit:4.+'
|
testImplementation 'junit:junit:4.+'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
package com.maenle.bump
|
package com.maenle.bump
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.example.bump.MessageProcessor
|
||||||
|
import com.example.bump.RestSingleton
|
||||||
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
@ -21,4 +26,83 @@ class ExampleInstrumentedTest {
|
|||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
assertEquals("com.maenle.bump", appContext.packageName)
|
assertEquals("com.maenle.bump", appContext.packageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class MessageProcessorTest {
|
||||||
|
@Test
|
||||||
|
fun decryptMessages() {
|
||||||
|
val code = "dydoes-unknowledgeable-indiscretion-househusbands-pot-walloper-indiscretion-discophorous-transcriptions-dydoes-poodle-faker-transcriptions-budlike"
|
||||||
|
val messageRaw = "M1dEAxKZ5HUHCJoRkgGOvAABhqCAAAAAAGG2eKTSlKXWLDQx5B_wssZsNwsanzQID2UyUm4KKuKYKgfwH5MG2N-qzt6K4mg3pfZmWPaiDB9PiqlX236k6zo9Yvvq"
|
||||||
|
val data = decryptMessage(code, messageRaw)
|
||||||
|
assertEquals(data, "hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun decryptMessage(code: String, messageRaw: String): String {
|
||||||
|
val message = MessageProcessor(code)
|
||||||
|
val data = message.decrypt(messageRaw)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class RestTest{
|
||||||
|
@Test
|
||||||
|
fun getPeek() {
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
val sender = "dydoes-unknowledgeable-indiscretion-househusbands"
|
||||||
|
RestSingleton.getInstance(context).peek(sender) { i -> assertEquals(i, "hello") }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class IntegrationTest{
|
||||||
|
@Test
|
||||||
|
fun pushPopAndDecrypt() {
|
||||||
|
val testMessage = "Hi There Sir"
|
||||||
|
val sender = "dydoes-unknowledgeable-indiscretion-househusbands"
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
val lock = CountDownLatch(1);
|
||||||
|
|
||||||
|
fun messageTester(encrypted: String) {
|
||||||
|
val code = "dydoes-unknowledgeable-indiscretion-househusbands-pot-walloper-indiscretion-discophorous-transcriptions-dydoes-poodle-faker-transcriptions-budlike"
|
||||||
|
Log.d("TEST", encrypted)
|
||||||
|
val message = MessageProcessor(code)
|
||||||
|
val data = message.decrypt(encrypted)
|
||||||
|
Log.d("TEST", "data")
|
||||||
|
assertEquals(data, testMessage)
|
||||||
|
lock.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
RestSingleton.getInstance(context).push(sender, testMessage) {
|
||||||
|
RestSingleton.getInstance(context).pop(sender) { i -> messageTester(i) }
|
||||||
|
}
|
||||||
|
Log.d("TEST", "waiting")
|
||||||
|
lock.await(20000, TimeUnit.MILLISECONDS)
|
||||||
|
Log.d("TEST", "done")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun popAndDectypt() {
|
||||||
|
val testMessage = "Hi There Sir"
|
||||||
|
val sender = "raphael"
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
val lock = CountDownLatch(1);
|
||||||
|
|
||||||
|
|
||||||
|
fun messageTester(encrypted: String) {
|
||||||
|
val code = "dydoes-unknowledgeable-indiscretion-househusbands-pot-walloper-indiscretion-discophorous-transcriptions-dydoes-poodle-faker-transcriptions-budlike"
|
||||||
|
val message = MessageProcessor(code)
|
||||||
|
val data = message.decrypt(encrypted)
|
||||||
|
assertEquals(data, testMessage)
|
||||||
|
lock.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
RestSingleton.getInstance(context).pop(sender) { i -> messageTester(i) }
|
||||||
|
lock.await(20000, TimeUnit.MILLISECONDS)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.maenle.bump">
|
package="com.maenle.bump">
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.camera.any" />
|
<uses-feature android:name="android.hardware.camera.any" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name="com.example.bump.MainApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
@ -14,6 +18,7 @@
|
|||||||
android:theme="@style/Theme.BumpForAndroid">
|
android:theme="@style/Theme.BumpForAndroid">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
tools:node="merge"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.BumpForAndroid.NoActionBar">
|
android:theme="@style/Theme.BumpForAndroid.NoActionBar">
|
||||||
|
@ -41,14 +41,6 @@ class FirstFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun testDecryption() {
|
fun testDecryption() {
|
||||||
val code = "dydoes-unknowledgeable-indiscretion-househusbands-pot-walloper-indiscretion-discophorous-transcriptions-dydoes-poodle-faker-transcriptions-budlike"
|
|
||||||
var mp = MessageProcessor()
|
|
||||||
if(mp.codeValid(code)) {
|
|
||||||
mp.codeSave(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
mp.decrypt("M1dEAxKZ5HUHCJoRkgGOvAABhqCAAAAAAGG2eKTSlKXWLDQx5B_wssZsNwsanzQID2UyUm4KKuKYKgfwH5MG2N-qzt6K4mg3pfZmWPaiDB9PiqlX236k6zo9Yvvq")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
31
app/src/main/java/com/example/bump/MainApplication.kt
Normal file
31
app/src/main/java/com/example/bump/MainApplication.kt
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package com.example.bump
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
// Not object class. AndroidManifest.xml error happen.
|
||||||
|
class MainApplication : Application() {
|
||||||
|
lateinit var rest : RestSingleton
|
||||||
|
|
||||||
|
init {
|
||||||
|
instance = this
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private var instance: MainApplication? = null
|
||||||
|
|
||||||
|
fun applicationContext() : Context {
|
||||||
|
return instance!!.applicationContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
// initialize for any
|
||||||
|
|
||||||
|
// Use ApplicationContext.
|
||||||
|
// example: SharedPreferences etc...
|
||||||
|
val context: Context = MainApplication.applicationContext()
|
||||||
|
rest = RestSingleton(context)
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,5 @@
|
|||||||
package com.example.bump
|
package com.example.bump
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.google.zxing.common.StringUtils
|
|
||||||
import java.security.SecureRandom
|
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
import javax.crypto.spec.PBEKeySpec
|
import javax.crypto.spec.PBEKeySpec
|
||||||
import javax.crypto.SecretKeyFactory
|
import javax.crypto.SecretKeyFactory
|
||||||
@ -15,59 +12,97 @@ import java.math.BigInteger
|
|||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.temporal.TemporalAmount
|
import java.time.temporal.TemporalAmount
|
||||||
|
|
||||||
class MessageProcessor {
|
class MessageProcessor(code: String) {
|
||||||
lateinit var sender:String
|
private var sender: String
|
||||||
lateinit var key: String
|
private var password: String
|
||||||
|
|
||||||
fun codeValid(code: String): Boolean {
|
init {
|
||||||
|
codeValid(code).let {
|
||||||
|
val codeSplit: List<String> = code.split("-")
|
||||||
|
sender = codeSplit.subList(0, SENDER_LENGTH).joinToString("-")
|
||||||
|
password = codeSplit.subList(SENDER_LENGTH, codeSplit.size).joinToString("-")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun codeValid(code: String): Boolean {
|
||||||
return code.split("-").size >= KEY_LENGTH + SENDER_LENGTH
|
return code.split("-").size >= KEY_LENGTH + SENDER_LENGTH
|
||||||
}
|
}
|
||||||
|
|
||||||
fun codeSave(new_code: String) {
|
|
||||||
var code: List<String> = new_code.split("-")
|
fun decrypt(messageRaw: String): String {
|
||||||
sender = code.subList(0, SENDER_LENGTH).joinToString("-")
|
val message = Message(messageRaw)
|
||||||
key = code.subList(SENDER_LENGTH, code.size).joinToString("-")
|
val fernetKey: Key = message.deriveMessageKeyFromPassword(password)
|
||||||
Log.d(TAG, sender)
|
return message.validateAndDecrypt(fernetKey)
|
||||||
Log.d(TAG, key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decrypt(message : String) {
|
|
||||||
// Data from encryption
|
|
||||||
val decoded : ByteArray = Base64.getUrlDecoder().decode(message)
|
|
||||||
val salt = decoded.copyOfRange(0, 16)
|
|
||||||
val iter : Int = BigInteger(decoded.copyOfRange(16, 20)).toInt()
|
|
||||||
val str_token = String(Base64.getUrlEncoder().encode(decoded.copyOfRange(20, decoded.size)))
|
|
||||||
|
|
||||||
// Derive Fernet key
|
private class Message(messageRaw : String) {
|
||||||
val saltedKey = deriveKey(key, salt, iter)
|
lateinit var salt : ByteArray
|
||||||
val fernetKey = Key(saltedKey)
|
var iterations: Int = 100_00
|
||||||
|
lateinit var token : Token
|
||||||
|
|
||||||
val token =
|
init {
|
||||||
Token.fromString(str_token);
|
splitMessage(messageRaw)
|
||||||
|
}
|
||||||
|
|
||||||
// Decrypt
|
private fun splitMessage(message : String) {
|
||||||
val validator: Validator<String> = object : StringValidator {
|
val decodedMessage : ByteArray = Base64.getUrlDecoder().decode(message)
|
||||||
override fun getTimeToLive(): TemporalAmount {
|
val iterationsDecoded = decodedMessage.copyOfRange(SALT_LENGTH, SALT_LENGTH+ ITERATIONS_LENGTH)
|
||||||
return Duration.ofHours(196)
|
val tokenString = String(Base64.getUrlEncoder().encode(decodedMessage.copyOfRange(20, decodedMessage.size)))
|
||||||
}
|
|
||||||
|
iterations = BigInteger(iterationsDecoded).toInt()
|
||||||
|
salt = decodedMessage.copyOfRange(0, SALT_LENGTH)
|
||||||
|
token = Token.fromString(tokenString)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Message
|
||||||
|
|
||||||
|
if (!salt.contentEquals(other.salt)) return false
|
||||||
|
if (iterations != other.iterations) return false
|
||||||
|
if (token != other.token) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = salt.contentHashCode()
|
||||||
|
result = 31 * result + iterations
|
||||||
|
result = 31 * result + token.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deriveMessageKeyFromPassword(password: String): Key{
|
||||||
|
val derivedKeyLength = 256
|
||||||
|
val spec = PBEKeySpec(password.toCharArray(), salt, iterations, derivedKeyLength)
|
||||||
|
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||||
|
val key = secretKeyFactory.generateSecret(spec).encoded
|
||||||
|
val saltedKey = Base64.getUrlEncoder().encodeToString(key)
|
||||||
|
|
||||||
|
return Key(saltedKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateAndDecrypt(fernetKey: Key, timeToLive: Long = 24*365*1000): String {
|
||||||
|
|
||||||
|
val validator: Validator<String> = object : StringValidator {
|
||||||
|
override fun getTimeToLive(): TemporalAmount {
|
||||||
|
return Duration.ofHours(timeToLive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return token.validateAndDecrypt(fernetKey, validator)
|
||||||
}
|
}
|
||||||
val data = token.validateAndDecrypt(fernetKey, validator)
|
|
||||||
Log.d(TAG, data )
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deriveKey(password: String, salt: ByteArray, iterations : Int): String {
|
|
||||||
val derivedKeyLength = 256
|
|
||||||
val spec = PBEKeySpec(password.toCharArray(), salt, iterations, derivedKeyLength)
|
|
||||||
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
|
||||||
val key = secretKeyFactory.generateSecret(spec).encoded
|
|
||||||
return Base64.getUrlEncoder().encodeToString(key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = MainActivity::class.java.simpleName
|
private val TAG = MainActivity::class.java.simpleName
|
||||||
val KEY_LENGTH = 8
|
const val KEY_LENGTH = 8
|
||||||
val SENDER_LENGTH = 4
|
const val SENDER_LENGTH = 4
|
||||||
|
const val SALT_LENGTH = 16
|
||||||
|
const val ITERATIONS_LENGTH = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
78
app/src/main/java/com/example/bump/RestSingleton.kt
Normal file
78
app/src/main/java/com/example/bump/RestSingleton.kt
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package com.example.bump
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.android.volley.Request
|
||||||
|
import com.android.volley.RequestQueue
|
||||||
|
import com.android.volley.toolbox.*
|
||||||
|
import com.maenle.bump.MainActivity
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class RestSingleton constructor(context: Context){
|
||||||
|
|
||||||
|
init {
|
||||||
|
MessageProcessor
|
||||||
|
}
|
||||||
|
companion object {
|
||||||
|
@Volatile
|
||||||
|
private var INSTANCE: RestSingleton? = null
|
||||||
|
fun getInstance(context: Context) =
|
||||||
|
INSTANCE ?: synchronized(this) {
|
||||||
|
INSTANCE ?: RestSingleton(context).also {
|
||||||
|
INSTANCE = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val TAG = MainActivity::class.java.simpleName
|
||||||
|
private const val URL = "http://192.168.68.127:4000/api/"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun peek(sender: String, callback: (String) -> Unit){
|
||||||
|
val url = URL + "peek/"
|
||||||
|
val data = JSONObject()
|
||||||
|
data.put("sender", sender)
|
||||||
|
val jsonRequest = JsonObjectRequest(Request.Method.POST, url,
|
||||||
|
data,
|
||||||
|
{ response -> callback(response.toString()) },
|
||||||
|
{callback("")})
|
||||||
|
|
||||||
|
requestQueue.add(jsonRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun push(sender: String, message: String, function: () -> Unit){
|
||||||
|
val url = URL + "push/"
|
||||||
|
val data = JSONObject()
|
||||||
|
data.put("sender", sender)
|
||||||
|
data.put("data", message)
|
||||||
|
val stringRequest = JsonObjectRequest(Request.Method.POST, url,
|
||||||
|
data,
|
||||||
|
{_ -> function()},
|
||||||
|
{Log.d(TAG, "none")}
|
||||||
|
)
|
||||||
|
|
||||||
|
requestQueue.add(stringRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pop(sender: String,callback: (String) -> Unit){
|
||||||
|
val url = URL + "pop/"
|
||||||
|
val data = JSONObject()
|
||||||
|
data.put("sender", sender)
|
||||||
|
val stringRequest = JsonObjectRequest(Request.Method.POST, url,
|
||||||
|
data,
|
||||||
|
{ response -> callback(response.toString()) },
|
||||||
|
{callback("")})
|
||||||
|
|
||||||
|
requestQueue.add(stringRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val requestQueue: RequestQueue by lazy {
|
||||||
|
// applicationContext is key, it keeps you from leaking the
|
||||||
|
// Activity or BroadcastReceiver if someone passes one in.
|
||||||
|
Volley.newRequestQueue(context.applicationContext)
|
||||||
|
}
|
||||||
|
fun <T> addToRequestQueue(req: Request<T>) {
|
||||||
|
requestQueue.add(req)
|
||||||
|
}
|
||||||
|
}
|
@ -6,9 +6,9 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".SecondFragment">
|
tools:context=".SecondFragment">
|
||||||
|
|
||||||
<androidx.camera.view.PreviewView
|
<!--androidx.camera.view.PreviewView
|
||||||
android:id="@+id/viewFinder"
|
android:id="@+id/viewFinder"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" /-->
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Reference in New Issue
Block a user