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 00000000..65f4e014 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/recaptcha.png differ diff --git a/app/src/main/res/drawable/recaptcha_card_border.xml b/app/src/main/res/drawable/recaptcha_card_border.xml new file mode 100644 index 00000000..7ba21f6e --- /dev/null +++ b/app/src/main/res/drawable/recaptcha_card_border.xml @@ -0,0 +1,15 @@ + + + + + + + \ 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 00000000..5fcbdf0d Binary files /dev/null and b/app/src/main/res/drawable/recaptcha_check.png differ 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" /> + + - - + + + + + + +