From 4ad826ebe80954abc51963ae445eac59e92be558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 16 Feb 2020 13:42:14 +0100 Subject: [PATCH] [API] Implement Librus Captcha. Refactor notification constants. Update empty account error as a dialog. --- .../java/pl/szczodrzynski/edziennik/App.kt | 53 +---- .../szczodrzynski/edziennik/MainActivity.kt | 15 +- .../edziennik/data/api/ApiService.kt | 15 +- .../data/api/EdziennikNotification.kt | 2 +- .../edziennik/data/api/Errors.kt | 3 + .../librus/firstlogin/LibrusFirstLogin.kt | 5 +- .../librus/login/LibrusLoginPortal.kt | 33 ++- .../vulcan/firstlogin/VulcanFirstLogin.kt | 7 +- .../api/events/UserActionRequiredEvent.kt | 16 ++ .../data/api/task/PostNotifications.kt | 16 +- .../edziennik/sync/UpdateDownloaderService.kt | 4 +- .../edziennik/sync/UpdateWorker.kt | 6 +- .../edziennik/ui/dialogs/TemplateDialog.kt | 66 ++++++ .../ui/dialogs/captcha/LibrusCaptchaDialog.kt | 91 +++++++++ .../ui/dialogs/captcha/RecaptchaDialog.kt | 188 +++++++++++++++++ .../ui/modules/error/ErrorSnackbar.kt | 1 + .../ui/modules/home/cards/HomeDebugCard.kt | 30 ++- .../ui/modules/login/LoginProgressFragment.kt | 27 +++ .../modules/settings/SettingsNewFragment.java | 2 +- .../edziennik/utils/CheckableImageView.java | 77 +++++++ .../managers/NotificationChannelsManager.kt | 123 +++++++++++ .../utils/managers/UserActionManager.kt | 93 +++++++++ app/src/main/res/drawable-xhdpi/recaptcha.png | Bin 0 -> 2228 bytes .../res/drawable/recaptcha_card_border.xml | 15 ++ app/src/main/res/drawable/recaptcha_check.png | Bin 0 -> 7327 bytes .../drawable/recaptcha_checkbox_border.xml | 11 + .../recaptcha_checkbox_border_active.xml | 16 ++ .../recaptcha_checkbox_border_focused.xml | 15 ++ .../recaptcha_checkbox_border_hovered.xml | 16 ++ .../recaptcha_checkbox_border_normal.xml | 15 ++ app/src/main/res/drawable/recaptcha_image.xml | 9 + .../res/drawable/recaptcha_image_checked.xml | 19 ++ .../res/drawable/recaptcha_image_pressed.xml | 8 + .../main/res/layout/captcha_dialog_librus.xml | 10 + app/src/main/res/layout/card_home_debug.xml | 14 +- app/src/main/res/layout/dialog_template.xml | 25 +++ app/src/main/res/layout/recaptcha_dialog.xml | 193 ++++++++++++++++++ app/src/main/res/layout/recaptcha_view.xml | 100 +++++++++ app/src/main/res/values/errors.xml | 6 + app/src/main/res/values/strings.xml | 5 + 40 files changed, 1257 insertions(+), 93 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/TemplateDialog.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/captcha/LibrusCaptchaDialog.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/captcha/RecaptchaDialog.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/utils/CheckableImageView.java create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/NotificationChannelsManager.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt create mode 100644 app/src/main/res/drawable-xhdpi/recaptcha.png create mode 100644 app/src/main/res/drawable/recaptcha_card_border.xml create mode 100644 app/src/main/res/drawable/recaptcha_check.png create mode 100644 app/src/main/res/drawable/recaptcha_checkbox_border.xml create mode 100644 app/src/main/res/drawable/recaptcha_checkbox_border_active.xml create mode 100644 app/src/main/res/drawable/recaptcha_checkbox_border_focused.xml create mode 100644 app/src/main/res/drawable/recaptcha_checkbox_border_hovered.xml create mode 100644 app/src/main/res/drawable/recaptcha_checkbox_border_normal.xml create mode 100644 app/src/main/res/drawable/recaptcha_image.xml create mode 100644 app/src/main/res/drawable/recaptcha_image_checked.xml create mode 100644 app/src/main/res/drawable/recaptcha_image_pressed.xml create mode 100644 app/src/main/res/layout/captcha_dialog_librus.xml create mode 100644 app/src/main/res/layout/dialog_template.xml create mode 100644 app/src/main/res/layout/recaptcha_dialog.xml create mode 100644 app/src/main/res/layout/recaptcha_view.xml diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt index 96ae5cde..6469fc0d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt @@ -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) @@ -394,4 +353,4 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { false } } -} \ No newline at end of file +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index d5857a7e..8a5e1e52 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -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) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt index dacd956a..2d0507c8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt @@ -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 } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EdziennikNotification.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EdziennikNotification.kt index cc4d0d83..8582fc03 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EdziennikNotification.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EdziennikNotification.kt @@ -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) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt index 94930aed..8a59b937 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt @@ -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 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/firstlogin/LibrusFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/firstlogin/LibrusFirstLogin.kt index 8aa60ee3..65a55838 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/firstlogin/LibrusFirstLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/firstlogin/LibrusFirstLogin.kt @@ -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") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt index 41ad6255..68be8b72 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt @@ -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) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt index d3059513..f8d317ac 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt @@ -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 } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt new file mode 100644 index 00000000..d88fa652 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt @@ -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 + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt index b581a993..d7caa776 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt @@ -73,7 +73,7 @@ class PostNotifications(val app: App, nList: List) { 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) { } // 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) { .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) { // 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) { .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) { } 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) { .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) } } }} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateDownloaderService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateDownloaderService.kt index cce045d7..31c24d2f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateDownloaderService.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateDownloaderService.kt @@ -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) { @@ -117,4 +117,4 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav } downloadId = downloadManager.enqueue(request) } -} \ No newline at end of file +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt index f527f071..515c1703 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt @@ -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) { } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/TemplateDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/TemplateDialog.kt new file mode 100644 index 00000000..a5e2dbeb --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/TemplateDialog.kt @@ -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() + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/captcha/LibrusCaptchaDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/captcha/LibrusCaptchaDialog.kt new file mode 100644 index 00000000..e502ef28 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/captcha/LibrusCaptchaDialog.kt @@ -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 + } + ) + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/captcha/RecaptchaDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/captcha/RecaptchaDialog.kt new file mode 100644 index 00000000..427ca44c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/captcha/RecaptchaDialog.kt @@ -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)?\">(.+?) (.+?)".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 = "([A-z0-9-_]+)".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) { + + } + }) + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorSnackbar.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorSnackbar.kt index 607501c3..d74a91e8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorSnackbar.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorSnackbar.kt @@ -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 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeDebugCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeDebugCard.kt index 10485b4d..3d561020 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeDebugCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeDebugCard.kt @@ -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 } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginProgressFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginProgressFragment.kt index a9ece7e0..929d9551 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginProgressFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginProgressFragment.kt @@ -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() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java index 60de1f41..6adeeb24 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java @@ -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); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/CheckableImageView.java b/app/src/main/java/pl/szczodrzynski/edziennik/utils/CheckableImageView.java new file mode 100644 index 00000000..8765c7e1 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/CheckableImageView.java @@ -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; + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/NotificationChannelsManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/NotificationChannelsManager.kt new file mode 100644 index 00000000..1cc4ac72 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/NotificationChannelsManager.kt @@ -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 + ) } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt new file mode 100644 index 00000000..5145ad3a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt @@ -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) + } +} diff --git a/app/src/main/res/drawable-xhdpi/recaptcha.png b/app/src/main/res/drawable-xhdpi/recaptcha.png new file mode 100644 index 0000000000000000000000000000000000000000..65f4e0147fd40aca822db867dee902794e30db33 GIT binary patch literal 2228 zcmV;l2ut^gP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv00000008+zyMF)x010qNS#tmY3ljhU3ljkVnw%H_00T1qZoK+t#nD+} z6P+$96in@nLDI6u_g+?MtV}(pYrij#{$=R%pmiFgrN9DM6M857q z!IJRsRBhu2fACbGlqsM=0)0fXW{i+C1Xil2?a9zjzPb5@k#t|Th+U$1-+OPf;oT9~ z@G1xt_5gT4Ay*F*$!epMK%xpqJw;@eSVpU=nwGhc5Y^X@O57DiKMD~qU7DdkaN`D7 z?Yk0427zQXB_LNpE2zz!0tu=qGIEXWE8W(iCvj^y@#yt0NK@8Zthn2`b%=j@@ST@X z7b^F#|C24CK>~dM{u%@cj1x&9;ZXYwffWT)&>GW(vV6Rf1u4?$wo6kqrjxPCNdnHL z?!fz3w*U1&xvQ?jQ~PrQ{c{2uBw&D8IpOKZ@}~$z*?0{-z;(JRX-c8#6oSVBp|0Tl zo@@QMh7aDI+9$h^njxTJ3K)r%Dl4Y|f~TV>8{V3eQx=+BXqt)3qu{YXm=OHp?XSG0 z;_LIS;JF2cNr5Ew1SOy{fq)4FY`g;T0F2II+_B8``PcbPB_l zI0>Af1XM;0Zvt=>E3ZI224}*@=Gp`W#1$={O}d2d5bip(K#Jxl!1v7ws0^e)qJw}L zz^y(J5IwtZW^EZ)O?v^40%hg~EQ!!W%W}5K(yF@?lmmAX9R$o&3QPcRbAtC&KvH(( z);pep>kfZ8tKRp~H8wmJcpT3d5s({!6dB;3(SPT)Q3=5tRkkhNA){!QrW@m)?DghGz({pYBLxX;4_i zlX2PIpD+bv@Hals1hLxT-4aMrC_?pZU;jr4*a5!n?sw6$)djeq#rCxmNaWZ3@%I7( z_Nf|u7ap0Yh*O-HrQs<76pLMg-F>(OM8&&@GfD-3*#kAR;!q=8w+w?gE&xNqS0M?# zumJh6Oh`zJUYR%eAsappMM+7p;bRoe0)ERHbm;Rt0RaIro&^<_ zShgy^;e$(PG06A4DKGzdXj;+O3=J>Kx8Rzm;89}9zc!ZwlA@+hMrpv3wtG~Le8ZXS zpN(|knMEYD6c1dwJQkJB_5G$zI{^N~DIs{&3b9Hn8Bzkna|r}#58nI`!Re2DP3T+i zQfc{;jE=fChL7ijhvcGjeu3ciuiXAh2hh61|00SH{e#e< z9K6MusKm55R^NB~C8@mJ86M63Z1}i57JRN1!09*(gz8)V`9OF0HU-)IIidaS4MKb1 zI-z~}Q!+)2!0#X9*?kavCCtfrCMa$F@acs7ahNhuCLjhvriA1gi1^$IkU%j2G&+YU zCWttNflO=;l^6(nIURQS@@2x4T)K40DMq95-I9_Lp%)D_9-e??K+yAJtrtPptw!D6 zLEyrL3xu~hfBrl%Ffed6BqT)6GiaIPLV|BTp9Hd7I?P*el-}OnKTD<33>P!gf*xU> z05gN(e}v%|FM32r7#EzY0Q@zvSp3N0y%HE38@mbcnLdCQRaaL_=iBu>7C3qGB8&~?O-{|sd^0jO*4@?BrQEY;Pn29Pm!P1} z-QC^F(b3VPr%#{0#|6%osH>}6<%xt(1eifBb@AdwGU(hDG>?SaW5VYG-as>R=FAxw zU#C!aN_J0J8e$zri3&Gdd*Pp^%l30Wbh}pDxb2aqG?-vpr#bU91UsPPY z3)g(0yHEK5IO6dZh=}JUBq};OT6OH$vG>u97NXWWIy(LY_fW&Vmg9T6y+q;LL2!8d zL(ps>*J`!fp}T$NjR+TbUtizfluG4`sBC->|Akt1)8ir%lA4{Jy{)yi^$oMx{9jMO zZ8qEemX?+`GBba?4V8s=u>&90dxalXd$;&k(c*8OnJvUJ3Ft8jEwxIkl^3=4EldJL-i{tl@l)j0000 + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/recaptcha_check.png b/app/src/main/res/drawable/recaptcha_check.png new file mode 100644 index 0000000000000000000000000000000000000000..5fcbdf0d227acc178a403dd43984e644099262fd GIT binary patch literal 7327 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 FV1n`dfmr|m literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/recaptcha_checkbox_border.xml b/app/src/main/res/drawable/recaptcha_checkbox_border.xml new file mode 100644 index 00000000..ce47b70e --- /dev/null +++ b/app/src/main/res/drawable/recaptcha_checkbox_border.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/recaptcha_checkbox_border_active.xml b/app/src/main/res/drawable/recaptcha_checkbox_border_active.xml new file mode 100644 index 00000000..bb172dfc --- /dev/null +++ b/app/src/main/res/drawable/recaptcha_checkbox_border_active.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/recaptcha_checkbox_border_focused.xml b/app/src/main/res/drawable/recaptcha_checkbox_border_focused.xml new file mode 100644 index 00000000..a73a4d22 --- /dev/null +++ b/app/src/main/res/drawable/recaptcha_checkbox_border_focused.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/recaptcha_checkbox_border_hovered.xml b/app/src/main/res/drawable/recaptcha_checkbox_border_hovered.xml new file mode 100644 index 00000000..701824a9 --- /dev/null +++ b/app/src/main/res/drawable/recaptcha_checkbox_border_hovered.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/recaptcha_checkbox_border_normal.xml b/app/src/main/res/drawable/recaptcha_checkbox_border_normal.xml new file mode 100644 index 00000000..c6acf25e --- /dev/null +++ b/app/src/main/res/drawable/recaptcha_checkbox_border_normal.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/recaptcha_image.xml b/app/src/main/res/drawable/recaptcha_image.xml new file mode 100644 index 00000000..e55e0ab9 --- /dev/null +++ b/app/src/main/res/drawable/recaptcha_image.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/recaptcha_image_checked.xml b/app/src/main/res/drawable/recaptcha_image_checked.xml new file mode 100644 index 00000000..764cb0ee --- /dev/null +++ b/app/src/main/res/drawable/recaptcha_image_checked.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/recaptcha_image_pressed.xml b/app/src/main/res/drawable/recaptcha_image_pressed.xml new file mode 100644 index 00000000..342683d6 --- /dev/null +++ b/app/src/main/res/drawable/recaptcha_image_pressed.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/captcha_dialog_librus.xml b/app/src/main/res/layout/captcha_dialog_librus.xml new file mode 100644 index 00000000..3eb08a88 --- /dev/null +++ b/app/src/main/res/layout/captcha_dialog_librus.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/card_home_debug.xml b/app/src/main/res/layout/card_home_debug.xml index 3bb5c2d3..b0e2f41c 100644 --- a/app/src/main/res/layout/card_home_debug.xml +++ b/app/src/main/res/layout/card_home_debug.xml @@ -32,6 +32,13 @@ android:layout_height="wrap_content" android:text="Librus Captcha" /> + + - - + + + + + + +