/* * Copyright 2018 Allan Wang * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.pitchedapps.frost.activities import android.graphics.drawable.Drawable import android.os.Bundle import android.widget.ImageView import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.Toolbar import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import ca.allanwang.kau.utils.bindView import ca.allanwang.kau.utils.fadeIn import ca.allanwang.kau.utils.fadeOut import ca.allanwang.kau.utils.withMainContext import com.bumptech.glide.RequestManager import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target import com.pitchedapps.frost.R import com.pitchedapps.frost.db.CookieDao import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.db.save import com.pitchedapps.frost.db.selectAll import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.profilePictureUrl import com.pitchedapps.frost.glide.FrostGlide import com.pitchedapps.frost.glide.GlideApp import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.frostEvent import com.pitchedapps.frost.utils.frostJsoup import com.pitchedapps.frost.utils.launchNewTask import com.pitchedapps.frost.utils.logFrostEvent import com.pitchedapps.frost.web.FrostEmitter import com.pitchedapps.frost.web.LoginWebView import com.pitchedapps.frost.web.asFrostEmitter import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import java.net.UnknownHostException import javax.inject.Inject import kotlin.coroutines.resume /** * Created by Allan Wang on 2017-06-01. */ @AndroidEntryPoint class LoginActivity : BaseActivity() { @Inject lateinit var cookieDao: CookieDao private val toolbar: Toolbar by bindView(R.id.toolbar) private val web: LoginWebView by bindView(R.id.login_webview) private val swipeRefresh: SwipeRefreshLayout by bindView(R.id.swipe_refresh) private val textview: AppCompatTextView by bindView(R.id.textview) private val profile: ImageView by bindView(R.id.profile) private lateinit var profileLoader: RequestManager private val refreshMutableFlow = MutableSharedFlow( extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST ) private val refreshFlow: SharedFlow = refreshMutableFlow.asSharedFlow() private val refreshEmit: FrostEmitter = refreshMutableFlow.asFrostEmitter() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) setSupportActionBar(toolbar) setTitle(R.string.kau_login) activityThemer.setFrostColors { toolbar(toolbar) } profileLoader = GlideApp.with(profile) refreshFlow .distinctUntilChanged() .onEach { swipeRefresh.isRefreshing = it } .launchIn(this) launch { val cookie = web.loadLogin { refresh(it != 100) }.await() L.d { "Login found" } fbCookie.save(cookie.id) webFadeOut() profile.fadeIn() loadInfo(cookie) } } private suspend fun webFadeOut(): Unit = suspendCancellableCoroutine { cont -> web.fadeOut { cont.resume(Unit) } } private fun refresh(refreshing: Boolean) { refreshEmit(refreshing) } private suspend fun loadInfo(cookie: CookieEntity): Unit = withMainContext { refresh(true) val imageDeferred = async { loadProfile(cookie.id) } val nameDeferred = async { loadUsername(cookie) } val name: String? = nameDeferred.await() val foundImage: Boolean = imageDeferred.await() L._d { "Logged in and received data" } refresh(false) if (!foundImage) { L.e { "Could not get profile photo; Invalid userId?" } L._i { cookie } } textview.text = String.format(getString(R.string.welcome), name ?: "") textview.fadeIn() frostEvent("Login", "success" to true) /* * The user may have logged into an account that is already in the database * We will let the db handle duplicates and load it now after the new account has been saved */ val cookies = ArrayList(cookieDao.selectAll()) delay(1000) if (prefs.intro) launchNewTask(cookies, true) else launchNewTask(cookies, true) } private suspend fun loadProfile(id: Long): Boolean = withMainContext { suspendCancellableCoroutine { cont -> profileLoader.load(profilePictureUrl(id)) .transform(FrostGlide.circleCrop).listener(object : RequestListener { override fun onResourceReady( resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean ): Boolean { cont.resume(true) return false } override fun onLoadFailed( e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean ): Boolean { e.logFrostEvent("Profile loading exception") cont.resume(false) return false } }).into(profile) } } private suspend fun loadUsername(cookie: CookieEntity): String? = withContext(Dispatchers.IO) { val result: String? = try { withTimeout(5000) { frostJsoup(cookie.cookie, FbItem.PROFILE.url).title() } } catch (e: Exception) { if (e !is UnknownHostException) e.logFrostEvent("Fetch username failed") null } if (result != null) { cookieDao.save(cookie.copy(name = result)) return@withContext result } return@withContext cookie.name } override fun backConsumer(): Boolean { if (web.canGoBack()) { web.goBack() return true } return false } override fun onResume() { super.onResume() web.resumeTimers() } override fun onPause() { web.pauseTimers() super.onPause() } }