Nareshkumar Rao
4 years ago
committed by
GitHub
22 changed files with 675 additions and 54 deletions
Binary file not shown.
@ -0,0 +1,113 @@ |
|||
package com.nareshkumarrao.eiweblog |
|||
|
|||
import android.app.AlertDialog |
|||
import android.app.Dialog |
|||
import android.content.Context |
|||
import android.os.Bundle |
|||
import android.view.View |
|||
import android.widget.EditText |
|||
import android.widget.ProgressBar |
|||
import android.widget.TextView |
|||
import androidx.appcompat.app.AppCompatActivity |
|||
import androidx.appcompat.widget.Toolbar |
|||
import androidx.fragment.app.DialogFragment |
|||
import androidx.recyclerview.widget.DividerItemDecoration |
|||
import androidx.recyclerview.widget.LinearLayoutManager |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout |
|||
import com.nareshkumarrao.eiweblog.ui.main.ItemArticleAdapter |
|||
import com.nareshkumarrao.eiweblog.ui.main.ItemGradesAdapter |
|||
|
|||
|
|||
class GradesActivity : AppCompatActivity() { |
|||
override fun onCreate(savedInstanceState: Bundle?) { |
|||
super.onCreate(savedInstanceState) |
|||
setContentView(R.layout.activity_grades) |
|||
|
|||
|
|||
val myToolbar = findViewById<View>(R.id.grades_toolbar) as Toolbar |
|||
setSupportActionBar(myToolbar) |
|||
supportActionBar?.setDisplayHomeAsUpEnabled(true) |
|||
supportActionBar?.setDisplayShowTitleEnabled(false) |
|||
|
|||
findViewById<RecyclerView>(R.id.grades_recycler).apply { |
|||
layoutManager = LinearLayoutManager(this@GradesActivity) |
|||
adapter = ItemArticleAdapter(listOf()) |
|||
} |
|||
findViewById<RecyclerView>(R.id.grades_recycler).addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) |
|||
val swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.grades_swipe_refresh) |
|||
swipeRefreshLayout?.setOnRefreshListener { |
|||
HISUtility.fetchExamRows(this, ::updateExamRows) |
|||
} |
|||
|
|||
val sharedPref = getSharedPreferences( |
|||
getString(R.string.preference_file_key), |
|||
Context.MODE_PRIVATE |
|||
) |
|||
|
|||
val username = sharedPref?.getString(getString(R.string.username_key), null) |
|||
if( username == null ){ |
|||
val loginDialog = LoginDialogFragment(this, false) { |
|||
HISUtility.fetchExamRows(this, ::updateExamRows) |
|||
} |
|||
loginDialog.show(supportFragmentManager, "loginDialog") |
|||
}else{ |
|||
val savedRows = HISUtility.getSavedExamRows(this) |
|||
if(savedRows == null){ |
|||
HISUtility.fetchExamRows(this, ::updateExamRows) |
|||
}else{ |
|||
updateExamRows(savedRows) |
|||
} |
|||
} |
|||
} |
|||
|
|||
private fun updateExamRows(examRows: List<ExamRow>?){ |
|||
examRows ?: run { |
|||
val loginDialog = LoginDialogFragment(this, true){ |
|||
HISUtility.fetchExamRows(this, ::updateExamRows) |
|||
} |
|||
loginDialog.show(supportFragmentManager, "loginDialog") |
|||
return |
|||
} |
|||
this@GradesActivity.runOnUiThread { |
|||
findViewById<RecyclerView>(R.id.grades_recycler)?.apply { |
|||
layoutManager = LinearLayoutManager(this@GradesActivity) |
|||
adapter = ItemGradesAdapter(examRows) |
|||
} |
|||
findViewById<SwipeRefreshLayout>(R.id.grades_swipe_refresh).isRefreshing=false |
|||
findViewById<ProgressBar>(R.id.gradesProgressBar).visibility=View.GONE |
|||
} |
|||
} |
|||
} |
|||
|
|||
class LoginDialogFragment(val context: GradesActivity, private val isError: Boolean, val loginCallback: () -> Unit?) : DialogFragment() { |
|||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { |
|||
return activity?.let { |
|||
val builder = AlertDialog.Builder(it) |
|||
val inflater = requireActivity().layoutInflater |
|||
val dialogView = inflater.inflate(R.layout.dialog_login, null) |
|||
if(isError){ |
|||
dialogView.findViewById<TextView>(R.id.errorText).visibility=View.VISIBLE |
|||
}else{ |
|||
dialogView.findViewById<TextView>(R.id.errorText).visibility=View.INVISIBLE |
|||
} |
|||
builder.setView(dialogView) |
|||
.setPositiveButton( |
|||
R.string.login |
|||
) { _, _ -> |
|||
val username = dialogView.findViewById<EditText>(R.id.loginUsername).text.toString() |
|||
val password = dialogView.findViewById<EditText>(R.id.loginPassword).text.toString() |
|||
HISUtility.setUsernamePassword(context, username, password) |
|||
loginCallback() |
|||
} |
|||
.setNegativeButton( |
|||
R.string.cancel |
|||
) { _, _ -> |
|||
dialog?.cancel() |
|||
context.finish() |
|||
} |
|||
builder.create() |
|||
} ?: throw IllegalStateException("Activity cannot be null") |
|||
|
|||
} |
|||
} |
@ -0,0 +1,173 @@ |
|||
package com.nareshkumarrao.eiweblog |
|||
|
|||
import android.app.NotificationChannel |
|||
import android.app.NotificationManager |
|||
import android.app.PendingIntent |
|||
import android.content.Context |
|||
import android.content.Intent |
|||
import android.os.Build |
|||
import androidx.core.app.NotificationCompat |
|||
import androidx.core.app.NotificationManagerCompat |
|||
import com.google.gson.Gson |
|||
import com.google.gson.reflect.TypeToken |
|||
import org.jsoup.Connection |
|||
import org.jsoup.Jsoup |
|||
|
|||
data class ExamRow(val name: String, val grade: String, val attempt: String, val date: String) |
|||
|
|||
internal object HISUtility { |
|||
|
|||
fun setUsernamePassword(context: Context?, username: String, password: String) { |
|||
val sharedPref = context?.getSharedPreferences(context.getString(R.string.preference_file_key), Context.MODE_PRIVATE) |
|||
if (sharedPref != null) { |
|||
with(sharedPref.edit()) { |
|||
putString(context.getString(R.string.username_key), username) |
|||
putString(context.getString(R.string.password_key), password) |
|||
apply() |
|||
} |
|||
} |
|||
} |
|||
|
|||
fun checkForUpdates(context: Context?, callback: (examRows: List<ExamRow>?) -> Unit) { |
|||
val savedRows = getSavedExamRows(context) ?: run { |
|||
callback(null) |
|||
return |
|||
} |
|||
|
|||
val newRows: MutableList<ExamRow> = mutableListOf() |
|||
fetchExamRows(context) { examRows -> |
|||
if (examRows != null) { |
|||
for (examRow in examRows) { |
|||
if (!savedRows.contains(examRow)) { |
|||
newRows.add(examRow) |
|||
} |
|||
} |
|||
} |
|||
callback(newRows) |
|||
} |
|||
|
|||
|
|||
} |
|||
|
|||
fun getSavedExamRows(context: Context?): List<ExamRow>? { |
|||
val sharedPref = context?.getSharedPreferences(context.getString(R.string.preference_file_key), Context.MODE_PRIVATE) |
|||
val examRowsJson = sharedPref?.getString(context.getString(R.string.exam_rows_key), null) |
|||
?: return null |
|||
val examRowsType = object : TypeToken<List<ExamRow>>() {}.type |
|||
return Gson().fromJson(examRowsJson, examRowsType) |
|||
} |
|||
|
|||
private fun saveExamRows(context: Context?, examRows: List<ExamRow>) { |
|||
val examRowsJson = Gson().toJson(examRows) |
|||
val sharedPref = context?.getSharedPreferences(context.getString(R.string.preference_file_key), Context.MODE_PRIVATE) |
|||
if (sharedPref != null) { |
|||
with(sharedPref.edit()) { |
|||
putString(context.getString(R.string.exam_rows_key), examRowsJson) |
|||
apply() |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Throws(LoginFailedException::class) |
|||
fun fetchExamRows(context: Context?, callback: (examRows: List<ExamRow>?) -> Unit): Unit? { |
|||
val sharedPref = context?.getSharedPreferences(context.getString(R.string.preference_file_key), Context.MODE_PRIVATE) |
|||
val username = sharedPref?.getString(context.getString(R.string.username_key), null) |
|||
?: return null |
|||
val password = sharedPref.getString(context.getString(R.string.password_key), null) |
|||
?: return null |
|||
|
|||
val runnable = Runnable { |
|||
val postData: MutableMap<String, String> = mutableMapOf() |
|||
postData["asdf"] = username |
|||
postData["fdsa"] = password |
|||
|
|||
val loginPage = Jsoup.connect(context.getString(R.string.ossc_login_post)) |
|||
.method(Connection.Method.POST) |
|||
.userAgent("Mozilla") |
|||
.data(postData) |
|||
.execute() |
|||
|
|||
val selectNotenspiegel = Jsoup.connect(context.getString(R.string.ossc_select_noten)) |
|||
.userAgent("Mozilla") |
|||
.cookies(loginPage.cookies()) |
|||
.get() |
|||
|
|||
val notenspiegelURL = selectNotenspiegel.select("a[href]:containsOwn(Notenspiegel)").first()?.attr("href") |
|||
?: kotlin.run { |
|||
callback(null) |
|||
return@Runnable |
|||
} |
|||
|
|||
val selectStudiengangUnhide = Jsoup.connect(notenspiegelURL) |
|||
.userAgent("Mozilla") |
|||
.cookies(loginPage.cookies()) |
|||
.get() |
|||
val selectStudiengangUnhideURL = selectStudiengangUnhide.select("a[href]:containsOwn(Abschluss)").first().attr("href") |
|||
|
|||
val selectStudiengang = Jsoup.connect(selectStudiengangUnhideURL) |
|||
.userAgent("Mozilla") |
|||
.cookies(loginPage.cookies()) |
|||
.get() |
|||
val studiengangURL = selectStudiengang.select("a[href]:containsOwn(Leistungen anzeigen)").first().attr("href") |
|||
|
|||
|
|||
val notenSpiegelPage = Jsoup.connect(studiengangURL) |
|||
.userAgent("Mozilla") |
|||
.cookies(loginPage.cookies()) |
|||
.get() |
|||
|
|||
val allGradesRows = notenSpiegelPage.select("div.fixedContainer > table > tbody > tr") |
|||
val examRows: MutableList<ExamRow> = mutableListOf() |
|||
for (row in allGradesRows) { |
|||
if (row.select("td.tabelle1_alignleft").size < 1) { |
|||
continue |
|||
} |
|||
val columns = row.select("td") |
|||
if (columns.size < 1) { |
|||
continue |
|||
} |
|||
val examRow = ExamRow(columns[1].text(), columns[3].text(), columns[6].text(), columns[7].text()) |
|||
examRows.add(examRow) |
|||
} |
|||
saveExamRows(context, examRows) |
|||
callback(examRows) |
|||
} |
|||
|
|||
return Thread(runnable).start() |
|||
} |
|||
fun sendNotification(context: Context?, examRow: ExamRow, id:Int) { |
|||
val intent = Intent(context, NotificationSettingsActivity::class.java).apply { |
|||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK |
|||
} |
|||
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0) |
|||
|
|||
val builder = NotificationCompat.Builder(context!!, context.getString(R.string.grades_notification_channel_id)) |
|||
.setSmallIcon(R.drawable.ic_stat_name) |
|||
.setContentTitle(context.getString(R.string.exam_results_notification)) |
|||
.setStyle(NotificationCompat.BigTextStyle() |
|||
.bigText("${examRow.name}: ${examRow.grade}")) |
|||
.setContentIntent(pendingIntent) |
|||
.setPriority(NotificationCompat.PRIORITY_DEFAULT) |
|||
.setAutoCancel(true) |
|||
with(NotificationManagerCompat.from(context)) { |
|||
notify(id, builder.build()) |
|||
} |
|||
|
|||
} |
|||
|
|||
fun createNotificationChannel(context: Context?){ |
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
|||
val name = context?.getString(R.string.grades_notification_channel_name) |
|||
val descriptionText = context?.getString(R.string.grades_notification_channel_description) |
|||
val importance = NotificationManager.IMPORTANCE_DEFAULT |
|||
val channel = NotificationChannel(context?.getString(R.string.grades_notification_channel_id), name, importance).apply { |
|||
description = descriptionText |
|||
} |
|||
val notificationManager: NotificationManager = context?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager |
|||
notificationManager.createNotificationChannel(channel) |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
class LoginFailedException : Throwable() |
@ -0,0 +1,46 @@ |
|||
package com.nareshkumarrao.eiweblog.ui.main |
|||
|
|||
import android.view.LayoutInflater |
|||
import android.view.ViewGroup |
|||
import android.widget.TextView |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
import com.nareshkumarrao.eiweblog.ExamRow |
|||
import com.nareshkumarrao.eiweblog.R |
|||
|
|||
class ItemGradesAdapter(private val examRows: List<ExamRow>) : RecyclerView.Adapter<ItemGradesAdapter.ViewHolder>() { |
|||
inner class ViewHolder(inflater: LayoutInflater, parent: ViewGroup) : RecyclerView.ViewHolder(inflater.inflate(R.layout.item_grades, parent, false)) { |
|||
private var name: TextView? = null |
|||
private var attempt: TextView? = null |
|||
private var date: TextView? = null |
|||
private var grade: TextView? = null |
|||
|
|||
init { |
|||
name = itemView.findViewById(R.id.examNameText) |
|||
attempt = itemView.findViewById(R.id.versuchText) |
|||
date = itemView.findViewById(R.id.examDateText) |
|||
grade = itemView.findViewById(R.id.gradeText) |
|||
} |
|||
|
|||
fun bind(examRow: ExamRow) { |
|||
name?.text = examRow.name |
|||
attempt?.text = examRow.attempt |
|||
grade?.text = examRow.grade |
|||
date?.text = examRow.date |
|||
} |
|||
|
|||
} |
|||
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemGradesAdapter.ViewHolder { |
|||
val inflater = LayoutInflater.from(parent.context) |
|||
return ViewHolder(inflater, parent) |
|||
} |
|||
|
|||
override fun onBindViewHolder(holder: ViewHolder, position: Int) { |
|||
val examRow = examRows[position] |
|||
holder.bind(examRow) |
|||
} |
|||
|
|||
override fun getItemCount(): Int { |
|||
return examRows.size |
|||
} |
|||
} |
@ -0,0 +1,54 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<LinearLayout 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" |
|||
tools:context=".GradesActivity" |
|||
android:orientation="vertical"> |
|||
|
|||
<com.google.android.material.appbar.AppBarLayout |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:background="@color/white" |
|||
android:theme="@style/Theme.EIWeblog.AppBarOverlay"> |
|||
|
|||
<androidx.appcompat.widget.Toolbar |
|||
android:id="@+id/grades_toolbar" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
app:contentInsetStart="0px"> |
|||
|
|||
<TextView |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:fontFamily="@font/hsdsans_elektro" |
|||
android:minHeight="?actionBarSize" |
|||
android:padding="@dimen/appbar_padding" |
|||
android:paddingStart="0px" |
|||
android:paddingLeft="0px" |
|||
android:text="@string/grades_title" |
|||
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title" |
|||
android:textColor="@color/black" |
|||
android:textSize="25sp" |
|||
tools:ignore="RtlSymmetry" /> |
|||
</androidx.appcompat.widget.Toolbar> |
|||
</com.google.android.material.appbar.AppBarLayout> |
|||
|
|||
<ProgressBar |
|||
android:id="@+id/gradesProgressBar" |
|||
style="?android:attr/progressBarStyle" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" /> |
|||
|
|||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout |
|||
android:id="@+id/grades_swipe_refresh" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent"> |
|||
<androidx.recyclerview.widget.RecyclerView |
|||
android:id="@+id/grades_recycler" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" /> |
|||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> |
|||
|
|||
</LinearLayout> |
@ -0,0 +1,64 @@ |
|||
<?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" |
|||
xmlns:tools="http://schemas.android.com/tools" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:orientation="vertical"> |
|||
|
|||
<TextView |
|||
android:id="@+id/loginTitle" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginStart="16dp" |
|||
android:layout_marginLeft="16dp" |
|||
android:layout_marginTop="16dp" |
|||
android:fontFamily="@font/hsdsans_regular" |
|||
android:text="@string/login_dialog_title" |
|||
android:textColor="@color/black" |
|||
android:textSize="18sp" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
app:layout_constraintTop_toTopOf="parent" /> |
|||
|
|||
<EditText |
|||
android:id="@+id/loginUsername" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginTop="8dp" |
|||
android:hint="@string/username" |
|||
android:inputType="text" |
|||
android:autofillHints="username" |
|||
app:layout_constraintEnd_toEndOf="@+id/errorText" |
|||
app:layout_constraintStart_toStartOf="@+id/loginTitle" |
|||
app:layout_constraintTop_toBottomOf="@+id/loginTitle" /> |
|||
|
|||
<EditText |
|||
android:id="@+id/loginPassword" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginTop="8dp" |
|||
android:layout_marginBottom="16dp" |
|||
android:hint="@string/password" |
|||
android:autofillHints="current-password" |
|||
android:inputType="textPassword" |
|||
app:layout_constraintBottom_toBottomOf="parent" |
|||
app:layout_constraintEnd_toEndOf="@+id/loginUsername" |
|||
app:layout_constraintStart_toStartOf="@+id/loginUsername" |
|||
app:layout_constraintTop_toBottomOf="@+id/loginUsername" /> |
|||
|
|||
<TextView |
|||
android:id="@+id/errorText" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginEnd="16dp" |
|||
android:layout_marginRight="16dp" |
|||
android:fontFamily="@font/hsdsans_regular" |
|||
android:text="@string/error" |
|||
android:textColor="@color/fh_red" |
|||
android:textSize="18sp" |
|||
android:textStyle="bold" |
|||
android:visibility="invisible" |
|||
app:layout_constraintEnd_toEndOf="parent" |
|||
app:layout_constraintTop_toTopOf="@+id/loginTitle" /> |
|||
|
|||
</androidx.constraintlayout.widget.ConstraintLayout> |
@ -0,0 +1,79 @@ |
|||
<?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" |
|||
xmlns:tools="http://schemas.android.com/tools" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content"> |
|||
|
|||
<TextView |
|||
android:id="@+id/examNameText" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginStart="16dp" |
|||
android:layout_marginLeft="16dp" |
|||
android:layout_marginTop="16dp" |
|||
android:text="G 19 Höhere Mathematik " |
|||
android:textColor="@color/black" |
|||
android:textSize="18sp" |
|||
android:textStyle="bold" |
|||
app:layout_constraintEnd_toEndOf="@+id/gradeText" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
app:layout_constraintTop_toTopOf="parent" |
|||
tools:ignore="HardcodedText" /> |
|||
|
|||
<TextView |
|||
android:id="@+id/textView5" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginTop="8dp" |
|||
android:text="@string/versuch" |
|||
android:textColor="@color/black" |
|||
app:layout_constraintStart_toStartOf="@+id/examNameText" |
|||
app:layout_constraintTop_toBottomOf="@+id/examNameText" /> |
|||
|
|||
<TextView |
|||
android:id="@+id/textView6" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginTop="8dp" |
|||
android:layout_marginBottom="16dp" |
|||
android:text="@string/pruefungsdatum" |
|||
android:textColor="@color/black" |
|||
app:layout_constraintBottom_toBottomOf="parent" |
|||
app:layout_constraintStart_toStartOf="@+id/textView5" |
|||
app:layout_constraintTop_toBottomOf="@+id/textView5" /> |
|||
|
|||
<TextView |
|||
android:id="@+id/gradeText" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginEnd="32dp" |
|||
android:layout_marginRight="32dp" |
|||
android:text="4.0" |
|||
android:textColor="@color/black" |
|||
android:textSize="30sp" |
|||
app:layout_constraintBottom_toBottomOf="@+id/textView6" |
|||
app:layout_constraintEnd_toEndOf="parent" |
|||
app:layout_constraintTop_toTopOf="@+id/versuchText" |
|||
tools:ignore="HardcodedText" /> |
|||
|
|||
<TextView |
|||
android:id="@+id/versuchText" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginEnd="32dp" |
|||
android:layout_marginRight="32dp" |
|||
android:text="1" |
|||
app:layout_constraintEnd_toStartOf="@+id/gradeText" |
|||
app:layout_constraintTop_toTopOf="@+id/textView5" |
|||
tools:ignore="HardcodedText" /> |
|||
|
|||
<TextView |
|||
android:id="@+id/examDateText" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:text="05.02.2020" |
|||
app:layout_constraintEnd_toEndOf="@+id/versuchText" |
|||
app:layout_constraintTop_toTopOf="@+id/textView6" |
|||
tools:ignore="HardcodedText" /> |
|||
</androidx.constraintlayout.widget.ConstraintLayout> |
Loading…
Reference in new issue