[API] Implement Librus Captcha. Refactor notification constants. Update empty account error as a dialog.

This commit is contained in:
Kuba Szczodrzyński 2020-02-16 13:42:14 +01:00
parent f5e1e9fdd9
commit 4ad826ebe8
40 changed files with 1257 additions and 93 deletions

View File

@ -4,9 +4,6 @@
package pl.szczodrzynski.edziennik
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ShortcutInfo
@ -48,6 +45,8 @@ import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.modules.base.CrashActivity
import pl.szczodrzynski.edziennik.utils.*
import pl.szczodrzynski.edziennik.utils.managers.NotificationChannelsManager
import pl.szczodrzynski.edziennik.utils.managers.UserActionManager
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
@ -63,25 +62,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
var devMode = false
}
val notifications by lazy { Notifications() }
inner class Notifications {
val syncId = 1
val syncKey = "pl.szczodrzynski.edziennik.SYNC"
val syncChannelName: String by lazy { getString(R.string.notification_channel_get_data_name) }
val syncChannelDesc: String by lazy { getString(R.string.notification_channel_get_data_desc) }
val dataId = 50
val dataKey = "pl.szczodrzynski.edziennik.DATA"
val dataChannelName: String by lazy { getString(R.string.notification_channel_notifications_name) }
val dataChannelDesc: String by lazy { getString(R.string.notification_channel_notifications_desc) }
val dataQuietId = 60
val dataQuietKey = "pl.szczodrzynski.edziennik.DATA_QUIET"
val dataQuietChannelName: String by lazy { getString(R.string.notification_channel_notifications_quiet_name) }
val dataQuietChannelDesc: String by lazy { getString(R.string.notification_channel_notifications_quiet_desc) }
val updatesId = 100
val updatesKey = "pl.szczodrzynski.edziennik.UPDATES"
val updatesChannelName: String by lazy { getString(R.string.notification_channel_updates_name) }
val updatesChannelDesc: String by lazy { getString(R.string.notification_channel_updates_desc) }
}
val notificationChannelsManager by lazy { NotificationChannelsManager(this) }
val userActionManager by lazy { UserActionManager(this) }
val db
get() = App.db
@ -99,7 +81,6 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
.setMinimumLoggingLevel(Log.VERBOSE)
.build()
val preferences by lazy { getSharedPreferences(getString(R.string.preference_file), Context.MODE_PRIVATE) }
val permissionChecker by lazy { PermissionChecker(this) }
val networkUtils by lazy { NetworkUtils(this) }
val gson by lazy { Gson() }
@ -251,29 +232,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
)
} // shortcuts - end
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(
NotificationChannel(notifications.syncKey, notifications.syncChannelName, NotificationManager.IMPORTANCE_MIN).apply {
description = notifications.syncChannelDesc
})
notificationManager.createNotificationChannel(
NotificationChannel(notifications.dataKey, notifications.dataChannelName, NotificationManager.IMPORTANCE_HIGH).apply {
description = notifications.dataChannelDesc
enableLights(true)
lightColor = 0xff2196f3.toInt()
})
notificationManager.createNotificationChannel(
NotificationChannel(notifications.dataQuietKey, notifications.dataQuietChannelName, NotificationManager.IMPORTANCE_LOW).apply {
description = notifications.dataQuietChannelDesc
setSound(null, null)
enableVibration(false)
})
notificationManager.createNotificationChannel(
NotificationChannel(notifications.updatesKey, notifications.updatesChannelName, NotificationManager.IMPORTANCE_DEFAULT).apply {
description = notifications.updatesChannelDesc
})
}
notificationChannelsManager.registerAllChannels()
if (config.appInstalledTime == 0L)

View File

@ -659,6 +659,10 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
.setCancelable(false)
.show()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onUserActionRequiredEvent(event: UserActionRequiredEvent) {
app.userActionManager.execute(this, event.profileId, event.type)
}
private fun fragmentToSyncName(currentFragment: Int): Int {
return when (currentFragment) {
@ -713,10 +717,19 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
intentTargetId = TARGET_FEEDBACK
false
}
"userActionRequired" -> {
app.userActionManager.execute(
this,
extras.getInt("profileId"),
extras.getInt("type")
)
true
}
else -> false
}
if (handled)
if (handled && !navLoading) {
return
}
}
if (extras?.containsKey("reloadProfileId") == true) {

View File

@ -86,9 +86,16 @@ class ApiService : Service() {
lastEventTime = System.currentTimeMillis()
d(TAG, "Task $taskRunningId threw an error - $apiError")
apiError.profileId = taskProfileId
EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError))
errorList.add(apiError)
apiError.throwable?.printStackTrace()
if (app.userActionManager.requiresUserAction(apiError)) {
app.userActionManager.sendToUser(apiError)
}
else {
EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError))
errorList.add(apiError)
apiError.throwable?.printStackTrace()
}
if (apiError.isCritical) {
taskRunning?.cancel()
notification.setCriticalError().post()
@ -301,7 +308,7 @@ class ApiService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
d(TAG, "Foreground service onStartCommand")
startForeground(app.notifications.syncId, notification.notification)
startForeground(app.notificationChannelsManager.sync.id, notification.notification)
return START_NOT_STICKY
}

View File

@ -134,7 +134,7 @@ class EdziennikNotification(val app: App) {
fun post() {
if (serviceClosed)
return
notificationManager.notify(app.notifications.syncId, notification)
notificationManager.notify(app.notificationChannelsManager.sync.id, notification)
}
}

View File

@ -59,6 +59,9 @@ const val ERROR_FILE_DOWNLOAD = 113
const val ERROR_NO_STUDENTS_IN_ACCOUNT = 115
const val ERROR_CAPTCHA_NEEDED = 3000
const val ERROR_CAPTCHA_LIBRUS_PORTAL = 3001
const val CODE_INTERNAL_LIBRUS_ACCOUNT_410 = 120
const val CODE_INTERNAL_LIBRUS_SYNERGIA_EXPIRED = 121
const val ERROR_LOGIN_LIBRUS_API_CAPTCHA_NEEDED = 124

View File

@ -33,9 +33,8 @@ class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) {
val accounts = json.getJsonArray("accounts")
if (accounts == null || accounts.size() < 1) {
data.error(ApiError(TAG, ERROR_NO_STUDENTS_IN_ACCOUNT)
.withResponse(response)
.withApiResponse(json))
EventBus.getDefault().post(FirstLoginFinishedEvent(listOf(), data.loginStore))
onSuccess()
return@portalGet
}
val accountDataTime = json.getLong("lastModification")

View File

@ -7,12 +7,10 @@ import im.wangchao.mhttp.Response
import im.wangchao.mhttp.body.MediaTypeUtils
import im.wangchao.mhttp.callback.JsonCallbackHandler
import im.wangchao.mhttp.callback.TextCallbackHandler
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.getInt
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.getUnixDate
import pl.szczodrzynski.edziennik.utils.Utils.d
import java.net.HttpURLConnection.HTTP_UNAUTHORIZED
import java.util.*
@ -38,11 +36,21 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
onSuccess()
}
else if (data.portalRefreshToken != null) {
data.app.cookieJar.clearForDomain("portal.librus.pl")
if (data.fakeLogin) {
data.app.cookieJar.clearForDomain("librus.szkolny.eu")
}
else {
data.app.cookieJar.clearForDomain("portal.librus.pl")
}
accessToken(null, data.portalRefreshToken)
}
else {
data.app.cookieJar.clearForDomain("portal.librus.pl")
if (data.fakeLogin) {
data.app.cookieJar.clearForDomain("librus.szkolny.eu")
}
else {
data.app.cookieJar.clearForDomain("portal.librus.pl")
}
authorize(if (data.fakeLogin) FAKE_LIBRUS_AUTHORIZE else LIBRUS_AUTHORIZE_URL)
}
}}
@ -89,11 +97,20 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
private fun login(csrfToken: String) {
d(TAG, "Request: Librus/Login/Portal - ${if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL}")
val recaptchaCode = data.arguments?.getString("recaptchaCode") ?: data.loginStore.getLoginData("recaptchaCode", null)
val recaptchaTime = data.arguments?.getLong("recaptchaTime") ?: data.loginStore.getLoginData("recaptchaTime", 0L)
data.loginStore.removeLoginData("recaptchaCode")
data.loginStore.removeLoginData("recaptchaTime")
Request.builder()
.url(if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL)
.userAgent(LIBRUS_USER_AGENT)
.addParameter("email", data.portalEmail)
.addParameter("password", data.portalPassword)
.also {
if (recaptchaCode != null && System.currentTimeMillis() - recaptchaTime < 2*60*1000 /* 2 minutes */)
it.addParameter("g-recaptcha-response", recaptchaCode)
}
.addHeader("X-CSRF-TOKEN", csrfToken)
.contentType(MediaTypeUtils.APPLICATION_JSON)
.post()
@ -117,6 +134,12 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
.withResponse(response))
return
}
if (json.getBoolean("captchaRequired") == true) {
data.error(ApiError(TAG, ERROR_CAPTCHA_LIBRUS_PORTAL)
.withResponse(response)
.withApiResponse(json))
return
}
if (json.get("errors") != null) {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR)
.withResponse(response)

View File

@ -6,14 +6,12 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.firstlogin
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_NO_STUDENTS_IN_ACCOUNT
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_VULCAN
import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_STUDENT_LIST
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginApi
import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.utils.models.Date
@ -35,9 +33,8 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
val students = json.getJsonArray("Data")
if (students == null || students.isEmpty()) {
data.error(ApiError(TAG, ERROR_NO_STUDENTS_IN_ACCOUNT)
.withResponse(response)
.withApiResponse(json))
EventBus.getDefault().post(FirstLoginFinishedEvent(listOf(), data.loginStore))
onSuccess()
return@apiGet
}

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-2-15.
*/
package pl.szczodrzynski.edziennik.data.api.events
data class UserActionRequiredEvent(val profileId: Int, val type: Int) {
companion object {
const val LOGIN_DATA_MOBIDZIENNIK = 101
const val LOGIN_DATA_LIBRUS = 102
const val LOGIN_DATA_IDZIENNIK = 103
const val LOGIN_DATA_VULCAN = 104
const val LOGIN_DATA_EDUDZIENNIK = 105
const val CAPTCHA_LIBRUS = 202
}
}

View File

@ -73,7 +73,7 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
MainActivity::class.java,
"fragmentId" to MainActivity.DRAWER_ITEM_NOTIFICATIONS
)
val summaryIntent = PendingIntent.getActivity(app, app.notifications.dataId, intent, PendingIntent.FLAG_ONE_SHOT)
val summaryIntent = PendingIntent.getActivity(app, app.notificationChannelsManager.data.id, intent, PendingIntent.FLAG_ONE_SHOT)
// On Nougat or newer - show maximum 8 notifications
// On Marshmallow or older - show maximum 4 notifications
@ -89,7 +89,7 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
}
// Create a summary to show *instead* of notifications
val combined = NotificationCompat.Builder(app, app.notifications.dataKey)
val combined = NotificationCompat.Builder(app, app.notificationChannelsManager.data.key)
.setContentTitle(app.getString(R.string.app_name))
.setContentText(buildSummaryText(summaryCounts))
.setTicker(newNotificationsText)
@ -112,7 +112,7 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
.setLights(0xff2196f3.toInt(), 2000, 2000)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setGroup(app.notifications.dataKey)
.setGroup(app.notificationChannelsManager.data.key)
.setContentIntent(summaryIntent)
.setAutoCancel(true)
.build()
@ -122,7 +122,7 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
// Less than 8 notifications
val notifications = nList.map {
summaryCounts[it.type]++
NotificationCompat.Builder(app, app.notifications.dataKey)
NotificationCompat.Builder(app, app.notificationChannelsManager.data.key)
.setContentTitle(it.profileName ?: app.getString(R.string.app_name))
.setContentText(it.text)
.setSubText(if (it.type == TYPE_SERVER_MESSAGE) null else it.title)
@ -135,7 +135,7 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
.setLights(0xff2196f3.toInt(), 2000, 2000)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setGroup(app.notifications.dataKey)
.setGroup(app.notificationChannelsManager.data.key)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setContentIntent(it.getPendingIntent(app))
.setAutoCancel(true)
@ -150,7 +150,7 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val summary = NotificationCompat.Builder(app, app.notifications.dataKey)
val summary = NotificationCompat.Builder(app, app.notificationChannelsManager.data.key)
.setContentTitle(newNotificationsText)
.setContentText(buildSummaryText(summaryCounts))
.setTicker(newNotificationsText)
@ -159,13 +159,13 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
.setLights(0xff2196f3.toInt(), 2000, 2000)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setGroup(app.notifications.dataKey)
.setGroup(app.notificationChannelsManager.data.key)
.setGroupSummary(true)
.setContentIntent(summaryIntent)
.setAutoCancel(true)
.build()
notificationManager.notify(app.notifications.dataId, summary)
notificationManager.notify(app.notificationChannelsManager.data.id, summary)
}
}
}}

View File

@ -92,7 +92,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
return
}
(app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).cancel(app.notifications.updatesId)
(app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).cancel(app.notificationChannelsManager.updates.id)
val dir: File? = app.getExternalFilesDir(null)
if (dir?.isDirectory == true) {

View File

@ -94,7 +94,7 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker(
val notificationIntent = Intent(app, UpdateDownloaderService::class.java)
val pendingIntent = PendingIntent.getService(app, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val notification = NotificationCompat.Builder(app, app.notifications.updatesKey)
val notification = NotificationCompat.Builder(app, app.notificationChannelsManager.updates.key)
.setContentTitle(app.getString(R.string.notification_updates_title))
.setContentText(app.getString(R.string.notification_updates_text, update.versionName))
.setTicker(app.getString(R.string.notification_updates_summary))
@ -108,11 +108,11 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker(
.setLights(0xFF00FFFF.toInt(), 2000, 2000)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setGroup(app.notifications.updatesKey)
.setGroup(app.notificationChannelsManager.updates.key)
.setContentIntent(pendingIntent)
.setAutoCancel(false)
.build()
(app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(app.notifications.updatesId, notification)
(app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(app.notificationChannelsManager.updates.id, notification)
} catch (ignore: Exception) { }
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-8.
*/
package pl.szczodrzynski.edziennik.ui.dialogs
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.DialogTemplateBinding
import pl.szczodrzynski.edziennik.onClick
import kotlin.coroutines.CoroutineContext
class TemplateDialog(
val activity: AppCompatActivity,
val onActionPerformed: (() -> Unit)? = null,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
private const val TAG = "TemplateDialog"
}
private lateinit var app: App
private lateinit var b: DialogTemplateBinding
private lateinit var dialog: AlertDialog
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local variables go here
init { run {
if (activity.isFinishing)
return@run
onShowListener?.invoke(TAG)
app = activity.applicationContext as App
b = DialogTemplateBinding.inflate(activity.layoutInflater)
dialog = MaterialAlertDialogBuilder(activity)
.setView(b.root)
.setPositiveButton(R.string.close) { dialog, _ ->
dialog.dismiss()
}
.setNeutralButton(R.string.add, null)
.setOnDismissListener {
onDismissListener?.invoke(TAG)
}
.show()
dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.onClick {
// do custom action on neutral button click
// (does not dismiss the dialog)
}
b.clickMe.onClick {
onActionPerformed?.invoke()
dialog.dismiss()
}
}}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-2-15.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.captcha
import android.graphics.drawable.Drawable
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.RecaptchaViewBinding
import pl.szczodrzynski.edziennik.onClick
import kotlin.coroutines.CoroutineContext
class LibrusCaptchaDialog(
val activity: AppCompatActivity,
val onSuccess: (recaptchaCode: String) -> Unit,
val onFailure: (() -> Unit)?,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
private const val TAG = "LibrusCaptchaDialog"
}
private lateinit var app: App
private lateinit var b: RecaptchaViewBinding
private lateinit var dialog: AlertDialog
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private lateinit var checkboxBackground: Drawable
private lateinit var checkboxForeground: Drawable
private var success = false
init { run {
if (activity.isFinishing)
return@run
onShowListener?.invoke(TAG)
app = activity.applicationContext as App
b = RecaptchaViewBinding.inflate(activity.layoutInflater)
dialog = MaterialAlertDialogBuilder(activity)
.setView(b.root)
.setNegativeButton(R.string.cancel, null)
.setOnDismissListener {
if (!success)
onFailure?.invoke()
onDismissListener?.invoke(TAG)
}
.show()
checkboxBackground = b.checkbox.background
checkboxForeground = b.checkbox.foreground
success = false
b.root.onClick {
b.checkbox.performClick()
}
b.checkbox.onClick {
b.checkbox.background = null
b.checkbox.foreground = null
b.progress.visibility = View.VISIBLE
RecaptchaDialog(
activity,
siteKey = "6Lf48moUAAAAAB9ClhdvHr46gRWR-CN31CXQPG2U",
referer = "https://portal.librus.pl/rodzina/login",
onSuccess = { recaptchaCode ->
b.checkbox.background = checkboxBackground
b.checkbox.foreground = checkboxForeground
b.progress.visibility = View.GONE
success = true
onSuccess(recaptchaCode)
dialog.dismiss()
},
onFailure = {
b.checkbox.background = checkboxBackground
b.checkbox.foreground = checkboxForeground
b.progress.visibility = View.GONE
}
)
}
}}
}

View File

@ -0,0 +1,188 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-2-15.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.captcha
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Handler
import android.util.Log
import android.view.LayoutInflater
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.*
import okhttp3.*
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.databinding.RecaptchaDialogBinding
import java.io.IOException
import kotlin.coroutines.CoroutineContext
class RecaptchaDialog(
val activity: AppCompatActivity,
val siteKey: String,
val referer: String,
val autoRetry: Boolean = true,
val onSuccess: (recaptchaCode: String) -> Unit,
val onFailure: (() -> Unit)? = null,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
private const val TAG = "RecaptchaDialog"
}
private lateinit var app: App
private val b by lazy { RecaptchaDialogBinding.inflate(LayoutInflater.from(activity)) }
private var dialog: AlertDialog? = null
private val captchaUrl = "https://www.google.com/recaptcha/api/fallback?k=$siteKey"
private var success = false
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private var code = ""
private var payload = ""
init { run {
if (activity.isFinishing)
return@run
app = activity.applicationContext as App
onShowListener?.invoke(TAG)
success = false
launch { initCaptcha() }
}}
private suspend fun initCaptcha() {
withContext(Dispatchers.Default) {
val request = Request.Builder()
.url(captchaUrl)
.addHeader("Referer", referer)
.addHeader("Accept-Language", "pl")
.build()
app.http.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
val html = response.body()?.string() ?: return
Log.d(TAG, html)
parseHtml(html)
}
override fun onFailure(call: Call, e: IOException) {
}
})
}
}
private fun parseHtml(html: String) {
launch {
"class=\"rc-imageselect-desc(?:-no-canonical)?\">(.+?) <strong>(.+?)</strong>".toRegex().find(html)?.let {
b.descTitle.text = it.groupValues[1]
b.descText.text = it.groupValues[2]
}
code = "name=\"c\" value=\"([A-z0-9-_]+)\"".toRegex().find(html)?.let { it.groupValues[1] } ?: return@launch
payload = "https://www.google.com/recaptcha/api2/payload?c=$code&k=$siteKey"
withContext(Dispatchers.Default) {
val request = Request.Builder()
.url(payload)
.addHeader("Referer", captchaUrl)
.addHeader("Accept-Language", "pl")
.build()
app.http.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
val bitmap: Bitmap? = BitmapFactory.decodeStream(response.body()?.byteStream())
Handler(activity.mainLooper).post {
if (bitmap == null) {
onFailure?.invoke()
Toast.makeText(activity, "Nie udało się załadować reCAPTCHA.", Toast.LENGTH_SHORT).show()
return@post
}
b.payload.setImageBitmap(bitmap)
showDialog()
}
}
override fun onFailure(call: Call, e: IOException) {
onFailure?.invoke()
}
})
}
}
}
private fun showDialog() {
if (dialog == null) {
dialog = MaterialAlertDialogBuilder(activity)
.setView(b.root)
.setPositiveButton("OK") { _, _ ->
validateAnswer()
}
.setOnDismissListener {
if (!success)
onFailure?.invoke()
onDismissListener?.invoke(TAG)
}
.create()
}
b.image0.isChecked = false
b.image1.isChecked = false
b.image2.isChecked = false
b.image3.isChecked = false
b.image4.isChecked = false
b.image5.isChecked = false
b.image6.isChecked = false
b.image7.isChecked = false
b.image8.isChecked = false
dialog!!.show()
}
private fun validateAnswer() {
launch {
val list = mutableListOf(
"c=$code"
)
if (b.image0.isChecked) list += "response=0"
if (b.image1.isChecked) list += "response=1"
if (b.image2.isChecked) list += "response=2"
if (b.image3.isChecked) list += "response=3"
if (b.image4.isChecked) list += "response=4"
if (b.image5.isChecked) list += "response=5"
if (b.image6.isChecked) list += "response=6"
if (b.image7.isChecked) list += "response=7"
if (b.image8.isChecked) list += "response=8"
val request = Request.Builder()
.url(captchaUrl)
.addHeader("Referer", captchaUrl)
.addHeader("Accept-Language", "pl")
.addHeader("Origin", "https://www.google.com")
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.post(RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), list.joinToString("&")))
.build()
withContext(Dispatchers.Default) {
app.http.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
val html = response.body()?.string() ?: return
val match = "<textarea.+?>([A-z0-9-_]+)</textarea>".toRegex().find(html)
if (match == null) {
parseHtml(html)
return
}
Handler(activity.mainLooper).post {
success = true
onSuccess(match.groupValues[1])
}
}
override fun onFailure(call: Call, e: IOException) {
}
})
}
}
}
}

View File

@ -41,6 +41,7 @@ class ErrorSnackbar(val activity: AppCompatActivity) : CoroutineScope {
val message = errors.map {
listOf(
it.getStringReason(activity).asBoldSpannable().asColoredSpannable(R.attr.colorOnBackground.resolveAttr(activity)),
activity.getString(R.string.error_unknown_format, it.errorCode, it.tag),
if (App.devMode)
it.throwable?.stackTraceString ?: it.throwable?.localizedMessage
else

View File

@ -4,6 +4,8 @@
package pl.szczodrzynski.edziennik.ui.modules.home.cards
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Intent
import android.net.Uri
import android.view.LayoutInflater
@ -25,10 +27,14 @@ import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.databinding.CardHomeDebugBinding
import pl.szczodrzynski.edziennik.dp
import pl.szczodrzynski.edziennik.onClick
import pl.szczodrzynski.edziennik.ui.dialogs.captcha.LibrusCaptchaDialog
import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard
import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment
import pl.szczodrzynski.edziennik.ui.modules.login.LoginLibrusCaptchaActivity
import pl.szczodrzynski.edziennik.ui.widgets.WidgetConfigActivity
import pl.szczodrzynski.edziennik.ui.widgets.luckynumber.WidgetLuckyNumberProvider
import pl.szczodrzynski.edziennik.ui.widgets.notifications.WidgetNotificationsProvider
import pl.szczodrzynski.edziennik.ui.widgets.timetable.WidgetTimetableProvider
import kotlin.coroutines.CoroutineContext
class HomeDebugCard(
@ -54,10 +60,6 @@ class HomeDebugCard(
}
holder.root += b.root
b.composeNewButton.onClick {
activity.loadTarget(MainActivity.TARGET_MESSAGES_COMPOSE)
}
b.migrate71.onClick {
app.db.compileStatement("DELETE FROM messages WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0);").executeUpdateDelete()
app.db.compileStatement("DELETE FROM messageRecipients WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0);").executeUpdateDelete()
@ -84,7 +86,8 @@ class HomeDebugCard(
}
b.librusCaptchaButton.onClick {
app.startActivity(Intent(activity, LoginLibrusCaptchaActivity::class.java))
//app.startActivity(Intent(activity, LoginLibrusCaptchaActivity::class.java))
LibrusCaptchaDialog(activity, onSuccess = {}, onFailure = {})
}
b.getLogs.onClick {
@ -100,6 +103,21 @@ class HomeDebugCard(
}
}
b.refreshWidget.onClick {
for (widgetType in 0..2) {
val theClass = when (widgetType) {
WidgetConfigActivity.WIDGET_TIMETABLE -> WidgetTimetableProvider::class.java
WidgetConfigActivity.WIDGET_NOTIFICATIONS -> WidgetNotificationsProvider::class.java
WidgetConfigActivity.WIDGET_LUCKY_NUMBER -> WidgetLuckyNumberProvider::class.java
else -> WidgetTimetableProvider::class.java
}
val intent = Intent(app, theClass)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, AppWidgetManager.getInstance(app).getAppWidgetIds(ComponentName(app, theClass)))
app.sendBroadcast(intent)
}
}
holder.root.onClick {
// do stuff
}

View File

@ -19,10 +19,12 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.ERROR_CAPTCHA_NEEDED
import pl.szczodrzynski.edziennik.data.api.LOGIN_NO_ARGUMENTS
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent
import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.databinding.FragmentLoginProgressBinding
@ -57,7 +59,13 @@ class LoginProgressFragment : Fragment(), CoroutineScope {
return
}
doFirstLogin(args)
}
private fun doFirstLogin(args: Bundle) {
launch {
activity.errorSnackbar.dismiss()
val firstProfileId = (app.db.profileDao().lastId ?: 0) + 1
val loginType = args.getInt("loginType", -1)
val loginMode = args.getInt("loginMode", 0)
@ -88,6 +96,7 @@ class LoginProgressFragment : Fragment(), CoroutineScope {
}
activity.loginStores += event.loginStore
activity.profiles += event.profileList.map { LoginSummaryProfileAdapter.Item(it) }
activity.errorSnackbar.dismiss()
nav.navigate(R.id.loginSummaryFragment, null, LoginActivity.navOptions)
}
@ -98,6 +107,24 @@ class LoginProgressFragment : Fragment(), CoroutineScope {
nav.navigateUp()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onUserActionRequiredEvent(event: UserActionRequiredEvent) {
val args = arguments ?: run {
activity.error(ApiError(TAG, LOGIN_NO_ARGUMENTS))
nav.navigateUp()
return
}
app.userActionManager.execute(activity, event.profileId, event.type, onSuccess = { code ->
args.putString("recaptchaCode", code)
args.putLong("recaptchaTime", System.currentTimeMillis())
doFirstLogin(args)
}, onFailure = {
activity.error(ApiError(TAG, ERROR_CAPTCHA_NEEDED))
nav.navigateUp()
})
}
override fun onStart() {
EventBus.getDefault().register(this)
super.onStart()

View File

@ -767,7 +767,7 @@ public class SettingsNewFragment extends MaterialAboutFragment {
.color(IconicsColor.colorInt(iconColor))
)
.setOnClickAction(() -> {
String channel = app.getNotifications().getDataKey();
String channel = app.getNotificationChannelsManager().getData().getKey();
Intent intent = new Intent();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-8.
*/
package pl.szczodrzynski.edziennik.utils;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.Checkable;
import androidx.appcompat.widget.AppCompatImageView;
public class CheckableImageView extends AppCompatImageView implements Checkable {
private boolean checked;
private boolean broadcasting;
private OnCheckedChangeListener onCheckedChangeListener;
private static final int[] CHECKED_STATE_SET = {
android.R.attr.state_checked
};
public interface OnCheckedChangeListener {
void onCheckedChanged(CheckableImageView checkableImageView, boolean isChecked);
}
public CheckableImageView(final Context context) {
this(context, null);
}
public CheckableImageView(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
public CheckableImageView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOnClickListener(v -> toggle());
}
@Override public int[] onCreateDrawableState(final int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isChecked()) {
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}
@Override public void toggle() {
setChecked(!checked);
}
@Override public boolean isChecked() {
return checked;
}
@Override public void setChecked(final boolean checked) {
if (this.checked != checked) {
this.checked = checked;
refreshDrawableState();
// Avoid infinite recursions if setChecked() is called from a listener
if (broadcasting) {
return;
}
broadcasting = true;
if (onCheckedChangeListener != null) {
onCheckedChangeListener.onCheckedChanged(this, checked);
}
broadcasting = false;
}
}
public void setOnCheckedChangeListener( final OnCheckedChangeListener onCheckedChangeListener) {
this.onCheckedChangeListener = onCheckedChangeListener;
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-2-15.
*/
package pl.szczodrzynski.edziennik.utils.managers
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat.*
import androidx.core.app.NotificationManagerCompat.*
import pl.szczodrzynski.edziennik.R
class NotificationChannelsManager(val c: Context) {
data class Channel(
val id: Int,
val key: String,
val name: String,
val description: String,
val importance: Int,
val priority: Int,
val quiet: Boolean = false,
val lightColor: Int? = null
)
fun registerAllChannels() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
return
val manager = c.getSystemService(NotificationManager::class.java)
val registered = manager.notificationChannels.map { it.id }
val all = all.map { it.key }
val toRegister = all - registered
val toDelete = registered - all
for (key in toRegister) {
val channel = this.all.firstOrNull { it.key == key } ?: continue
manager.createNotificationChannel(NotificationChannel(key, channel.name, channel.importance).apply {
description = channel.description
if (channel.quiet) {
enableVibration(false)
setSound(null, null)
}
channel.lightColor?.let {
enableLights(true)
lightColor = it
}
})
}
for (key in toDelete) {
manager.deleteNotificationChannel(key)
}
}
val sync by lazy {
Channel(
1,
"pl.szczodrzynski.edziennik.SYNC",
c.getString(R.string.notification_channel_get_data_name),
c.getString(R.string.notification_channel_get_data_desc),
IMPORTANCE_MIN,
PRIORITY_MIN,
quiet = true
)
}
val data by lazy {
Channel(
50,
"pl.szczodrzynski.edziennik.DATA",
c.getString(R.string.notification_channel_notifications_name),
c.getString(R.string.notification_channel_notifications_desc),
IMPORTANCE_HIGH,
PRIORITY_MAX,
lightColor = 0xff2196f3.toInt()
)
}
val dataQuiet by lazy {
Channel(
60,
"pl.szczodrzynski.edziennik.DATA_QUIET",
c.getString(R.string.notification_channel_notifications_quiet_name),
c.getString(R.string.notification_channel_notifications_quiet_desc),
IMPORTANCE_LOW,
PRIORITY_LOW,
quiet = true
)
}
val updates by lazy {
Channel(
100,
"pl.szczodrzynski.edziennik.UPDATES",
c.getString(R.string.notification_channel_updates_name),
c.getString(R.string.notification_channel_updates_desc),
IMPORTANCE_DEFAULT,
PRIORITY_DEFAULT
)
}
val userAttention by lazy {
Channel(
200,
"pl.szczodrzynski.edziennik.USER_ATTENTION",
c.getString(R.string.notification_channel_user_attention_name),
c.getString(R.string.notification_channel_user_attention_desc),
IMPORTANCE_DEFAULT,
PRIORITY_DEFAULT
)
}
val all by lazy { listOf(
sync,
data,
dataQuiet,
updates,
userAttention
) }
}

View File

@ -0,0 +1,93 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-2-15.
*/
package pl.szczodrzynski.edziennik.utils.managers
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_CAPTCHA_LIBRUS_PORTAL
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.ui.dialogs.captcha.LibrusCaptchaDialog
class UserActionManager(val app: App) {
companion object {
private const val TAG = "UserActionManager"
}
fun requiresUserAction(apiError: ApiError): Boolean {
return apiError.errorCode == ERROR_CAPTCHA_LIBRUS_PORTAL
}
fun sendToUser(apiError: ApiError) {
val type = when (apiError.errorCode) {
ERROR_CAPTCHA_LIBRUS_PORTAL -> UserActionRequiredEvent.CAPTCHA_LIBRUS
else -> 0
}
if (EventBus.getDefault().hasSubscriberForEvent(UserActionRequiredEvent::class.java)) {
EventBus.getDefault().post(UserActionRequiredEvent(apiError.profileId ?: -1, type))
return
}
val manager = app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val text = app.getString(when (type) {
UserActionRequiredEvent.CAPTCHA_LIBRUS -> R.string.notification_user_action_required_captcha_librus
else -> R.string.notification_user_action_required_text
}, apiError.profileId)
val intent = Intent(
app,
MainActivity::class.java,
"action" to "userActionRequired",
"profileId" to (apiError.profileId ?: -1),
"type" to type
)
val pendingIntent = PendingIntent.getActivity(app, System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_ONE_SHOT)
val notification = NotificationCompat.Builder(app, app.notificationChannelsManager.userAttention.key)
.setContentTitle(app.getString(R.string.notification_user_action_required_title))
.setContentText(text)
.setSmallIcon(R.drawable.ic_error_outline)
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
.setColor(0xff2196f3.toInt())
.setLights(0xff2196f3.toInt(), 2000, 2000)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
manager.notify(System.currentTimeMillis().toInt(), notification)
}
fun execute(
activity: AppCompatActivity,
profileId: Int?,
type: Int,
onSuccess: ((code: String) -> Unit)? = null,
onFailure: (() -> Unit)? = null
) {
if (type != UserActionRequiredEvent.CAPTCHA_LIBRUS)
return
if (profileId == null)
return
// show captcha dialog
// use passed onSuccess listener, else sync profile
LibrusCaptchaDialog(activity, onSuccess = onSuccess ?: { code ->
EdziennikTask.syncProfile(profileId, arguments = JsonObject(
"recaptchaCode" to code,
"recaptchaTime" to System.currentTimeMillis()
)).enqueue(activity)
}, onFailure = onFailure)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-1-7.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="1dp"
android:color="#d3ffffff" />
<corners
android:bottomLeftRadius="4dp"
android:bottomRightRadius="4dp"
android:topLeftRadius="4dp"
android:topRightRadius="4dp" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-1-7.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true" android:drawable="@drawable/recaptcha_checkbox_border_normal" />
<item android:state_hovered="true" android:drawable="@drawable/recaptcha_checkbox_border_hovered" />
<item android:state_focused="true" android:drawable="@drawable/recaptcha_checkbox_border_focused" />
<item android:state_active="true" android:drawable="@drawable/recaptcha_checkbox_border_active" />
</selector>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-1-7.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="2dp"
android:color="#c1c1c1" />
<solid android:color="#ebebeb" />
<corners
android:bottomLeftRadius="2dp"
android:bottomRightRadius="2dp"
android:topLeftRadius="2dp"
android:topRightRadius="2dp" />
</shape>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-1-7.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="2dp"
android:color="#4d90fe" />
<corners
android:bottomLeftRadius="2dp"
android:bottomRightRadius="2dp"
android:topLeftRadius="2dp"
android:topRightRadius="2dp" />
</shape>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-1-7.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="2dp"
android:color="#b2b2b2" />
<solid android:color="#e0e0e0" />
<corners
android:bottomLeftRadius="2dp"
android:bottomRightRadius="2dp"
android:topLeftRadius="2dp"
android:topRightRadius="2dp" />
</shape>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-1-7.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="2dp"
android:color="#c1c1c1" />
<corners
android:bottomLeftRadius="2dp"
android:bottomRightRadius="2dp"
android:topLeftRadius="2dp"
android:topRightRadius="2dp" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-1-7.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checkable="true" android:drawable="@drawable/recaptcha_image_pressed" /> <!-- pressed -->
<item android:state_checked="true" android:drawable="@drawable/recaptcha_image_checked" /> <!-- focused -->
</selector>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-1-7.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="#7fffffff"/>
</shape>
</item>
<item>
<inset android:drawable="@drawable/recaptcha_check"
android:insetLeft="45dp"
android:insetBottom="45dp"
android:insetRight="45dp"
android:insetTop="45dp"/>
</item>
</layer-list>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-1-7.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#30000000"/>
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-1-8.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -32,6 +32,13 @@
android:layout_height="wrap_content"
android:text="Librus Captcha" />
<com.google.android.material.button.MaterialButton
android:id="@+id/refreshWidget"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Refresh all widgets" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -51,13 +58,6 @@
android:text="Messages"
android:textSize="16sp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/composeNewButton"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Compose 2" />
<com.google.android.material.button.MaterialButton
android:id="@+id/migrate71"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-1-8.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="16dp"
android:paddingTop="24dp"
android:paddingRight="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello world"/>
<Button
android:id="@+id/clickMe"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click me"/>
</LinearLayout>
</layout>

View File

@ -0,0 +1,193 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-1-7.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="386dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="113dp"
android:layout_marginBottom="4dp"
android:background="#4a90e2"
android:padding="24dp"
android:orientation="vertical">
<TextView
android:id="@+id/descTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Wybierz wszystkie zdjęcia, na których jest"
android:textSize="12sp"
android:textColor="@android:color/white" />
<TextView
android:id="@+id/descText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="pan fotograf"
android:textSize="28sp"
android:textColor="@android:color/white"
android:textStyle="bold" />
</LinearLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/payload"
android:layout_width="386dp"
android:layout_height="386dp"
android:scaleType="centerCrop"
tools:srcCompat="@tools:sample/backgrounds/scenic[1]" />
<LinearLayout
android:layout_width="386dp"
android:layout_height="386dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<pl.szczodrzynski.edziennik.utils.CheckableImageView
android:id="@+id/image0"
android:layout_width="126dp"
android:layout_height="126dp"
android:clickable="true"
android:background="@drawable/recaptcha_image"
android:focusable="true" />
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:background="?colorSurface" />
<pl.szczodrzynski.edziennik.utils.CheckableImageView
android:id="@+id/image1"
android:layout_width="126dp"
android:layout_height="126dp"
android:clickable="true"
android:background="@drawable/recaptcha_image"
android:focusable="true" />
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:background="?colorSurface" />
<pl.szczodrzynski.edziennik.utils.CheckableImageView
android:id="@+id/image2"
android:layout_width="126dp"
android:layout_height="126dp"
android:clickable="true"
android:background="@drawable/recaptcha_image"
android:focusable="true" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="4dp"
android:background="?colorSurface" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<pl.szczodrzynski.edziennik.utils.CheckableImageView
android:id="@+id/image3"
android:layout_width="126dp"
android:layout_height="126dp"
android:clickable="true"
android:background="@drawable/recaptcha_image"
android:focusable="true" />
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:background="?colorSurface" />
<pl.szczodrzynski.edziennik.utils.CheckableImageView
android:id="@+id/image4"
android:layout_width="126dp"
android:layout_height="126dp"
android:clickable="true"
android:background="@drawable/recaptcha_image"
android:focusable="true" />
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:background="?colorSurface" />
<pl.szczodrzynski.edziennik.utils.CheckableImageView
android:id="@+id/image5"
android:layout_width="126dp"
android:layout_height="126dp"
android:clickable="true"
android:background="@drawable/recaptcha_image"
android:focusable="true" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="4dp"
android:background="?colorSurface" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<pl.szczodrzynski.edziennik.utils.CheckableImageView
android:id="@+id/image6"
android:layout_width="126dp"
android:layout_height="126dp"
android:clickable="true"
android:background="@drawable/recaptcha_image"
android:focusable="true" />
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:background="?colorSurface" />
<pl.szczodrzynski.edziennik.utils.CheckableImageView
android:id="@+id/image7"
android:layout_width="126dp"
android:layout_height="126dp"
android:clickable="true"
android:background="@drawable/recaptcha_image"
android:focusable="true" />
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:background="?colorSurface" />
<pl.szczodrzynski.edziennik.utils.CheckableImageView
android:id="@+id/image8"
android:layout_width="126dp"
android:layout_height="126dp"
android:clickable="true"
android:background="@drawable/recaptcha_image"
android:focusable="true" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
</LinearLayout>
</layout>

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-1-7.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingTop="24dp"
android:paddingRight="16dp"
android:paddingBottom="8dp">
<com.google.android.material.card.MaterialCardView
android:layout_width="304dp"
android:layout_height="78dp"
android:layout_gravity="center"
android:layout_margin="8dp"
app:cardBackgroundColor="#f9f9f9"
app:cardForegroundColor="@android:color/transparent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/checkbox"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:background="@drawable/recaptcha_checkbox_border"
android:foreground="?selectableItemBackgroundBorderless">
<ProgressBar
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>
<TextView
android:layout_width="152dp"
android:layout_height="wrap_content"
android:textSize="14sp"
android:text="Nie jestem robotem"
android:textColor="@android:color/black"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="10dp"
android:paddingEnd="13dp"
android:paddingRight="13dp"
android:orientation="vertical"
android:gravity="end">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="26dp"
android:layout_marginStart="26dp"
android:orientation="vertical"
android:gravity="center_horizontal">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginLeft="13dp"
android:layout_marginRight="13dp"
app:srcCompat="@drawable/recaptcha"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="reCAPTCHA"
android:textColor="#555"
android:textSize="10sp"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Prywatność - Warunki"
android:textColor="#555"
android:textSize="8dp"
android:singleLine="true"
android:visibility="invisible"/>
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/recaptcha_card_border"/>
</com.google.android.material.card.MaterialCardView>
</FrameLayout>
</layout>

View File

@ -33,6 +33,9 @@
<string name="error_115" translatable="false">ERROR_NO_STUDENTS_IN_ACCOUNT</string>
<string name="error_3000" translatable="false">ERROR_CAPTCHA_NEEDED</string>
<string name="error_3001" translatable="false">ERROR_CAPTCHA_LIBRUS_PORTAL</string>
<string name="error_120" translatable="false">CODE_INTERNAL_LIBRUS_ACCOUNT_410</string>
<string name="error_121" translatable="false">CODE_INTERNAL_LIBRUS_SYNERGIA_EXPIRED</string>
<string name="error_124" translatable="false">ERROR_LOGIN_LIBRUS_API_CAPTCHA_NEEDED</string>
@ -200,6 +203,9 @@
<string name="error_115_reason">Brak uczniów przypisanych do konta</string>
<string name="error_3000_reason">Wymagane rozwiązanie zadania Captcha</string>
<string name="error_3001_reason">Librus: wymagane rozwiązanie zadania Captcha</string>
<string name="error_120_reason">CODE_INTERNAL_LIBRUS_ACCOUNT_410</string>
<string name="error_121_reason">CODE_INTERNAL_LIBRUS_SYNERGIA_EXPIRED</string>
<string name="error_124_reason">Wymagane wypełnienie CAPTCHA</string>

View File

@ -1176,4 +1176,9 @@
<string name="timetable_generate_show_teachers_names">Pokaż imiona i nazwiska nauczycieli</string>
<string name="messages_compose_confirm_title">Potwierdź wysłanie wiadomości</string>
<string name="messages_compose_confirm_text">Czy na pewno chcesz wysłać wiadomość do wybranych odbiorców?</string>
<string name="notification_channel_user_attention_name">Wymagane działanie</string>
<string name="notification_channel_user_attention_desc">Powiadomienia o problemie, który wymaga działania użytkownika (np. weryfikacja Captcha). Zalecane jest pozostawienie tej kategorii włączonej.</string>
<string name="notification_user_action_required_title">Wymagane działanie w aplikacji</string>
<string name="notification_user_action_required_text">Problem, który uniemożliwia synchronizację musi być rozwiązany przez użytkownika. Kliknij, aby uzyskać więcej informacji.</string>
<string name="notification_user_action_required_captcha_librus">Librus: wymagane rozwiązanie zadania Captcha. Kliknij, aby kontynuować logowanie do dziennika.</string>
</resources>