From ab7ec131b62ac1567e983c846c921bd3ada11dd4 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 7 Aug 2017 14:56:48 -0700 Subject: Fix/2FA (#115) * Create basis for downloading videos * Resolve some download errors and allow video to be opened in external apps * Remove url checks for loging * Update readme with build links * Allow for all apks to build * Fix travis apk uploads * Fix null mapping * Fix some notation * Add commit message to test builds * Remove faulty commit from test release * Add intent overriding to login web client * Add resource logging * Add intent verification without url check * Simplify login activity * Check start activity for result * Add check before resolving intent * Fix wrong index * Temporary fix for 2FA login with U2F (#116) * Clean up and add comments --- .../kotlin/com/pitchedapps/frost/StartActivity.kt | 2 +- .../pitchedapps/frost/activities/AboutActivity.kt | 2 +- .../pitchedapps/frost/activities/ImageActivity.kt | 18 +-- .../pitchedapps/frost/activities/LoginActivity.kt | 19 ++- .../pitchedapps/frost/activities/MainActivity.kt | 1 - .../com/pitchedapps/frost/injectors/JsInjector.kt | 1 + .../frost/services/FrostNotifications.kt | 8 +- .../frost/services/NotificationReceiver.kt | 34 +++++ .../frost/services/NotificationService.kt | 4 +- .../com/pitchedapps/frost/utils/Downloader.kt | 163 +++++++++++++++++++++ .../kotlin/com/pitchedapps/frost/utils/Utils.kt | 26 +++- .../com/pitchedapps/frost/views/AccountItem.kt | 9 +- .../com/pitchedapps/frost/views/FrostViewPager.kt | 2 + .../pitchedapps/frost/web/FrostChromeClients.kt | 2 + .../com/pitchedapps/frost/web/FrostWebView.kt | 12 +- .../pitchedapps/frost/web/FrostWebViewClients.kt | 8 +- .../com/pitchedapps/frost/web/FrostWebViewCore.kt | 2 + .../com/pitchedapps/frost/web/LoginWebView.kt | 78 +++++----- 18 files changed, 296 insertions(+), 95 deletions(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/services/NotificationReceiver.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt (limited to 'app/src/main/kotlin/com/pitchedapps') diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt index c2b3cc0f..5de07b7a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt @@ -21,7 +21,7 @@ class StartActivity : KauBaseActivity() { FbCookie.switchBackUser { loadFbCookiesAsync { cookies -> - L.d("Cookies loaded ${System.currentTimeMillis()}", cookies.toString()) + L.d("Cookies loaded at time ${System.currentTimeMillis()}", cookies.toString()) if (cookies.isNotEmpty()) launchNewTask(if (Prefs.userId != -1L) MainActivity::class.java else SelectorActivity::class.java, ArrayList(cookies)) else diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt index a1717de1..fbcd12cc 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt @@ -73,7 +73,7 @@ class AboutActivity : AboutActivityBase(null, { * Frost may not be a library but we're conveying the same info */ val frost = Library().apply { - libraryName = string(R.string.app_name) + libraryName = string(R.string.frost_name) author = "Pitched Apps" libraryWebsite = "https://github.com/AllanWang/Frost-for-Facebook" isOpenSource = true diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt index e419c21c..6a39b269 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt @@ -148,7 +148,7 @@ class ImageActivity : KauBaseActivity() { private fun saveTempImage(resource: Bitmap, callback: (uri: Uri?) -> Unit) { var photoFile: File? = null try { - photoFile = createImageFile() + photoFile = createPrivateMediaFile(".png") } catch (ignored: IOException) { } finally { if (photoFile == null) { @@ -166,27 +166,13 @@ class ImageActivity : KauBaseActivity() { } } - @Throws(IOException::class) - private fun createImageFile(): File { - // Create an image file name - val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) - val imageFileName = "Frost_" + timeStamp + "_" - val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES) - return File.createTempFile(imageFileName, ".png", storageDir) - } - internal fun downloadImage() { kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ -> L.d("Download image callback granted: $granted") if (granted) { doAsync { - val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) - val imageFileName = "Frost_" + timeStamp + "_" - val storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) - val frostDir = File(storageDir, "Frost") - if (!frostDir.exists()) frostDir.mkdirs() - val destination = File.createTempFile(imageFileName, ".png", frostDir) + val destination = createMediaFile(".png") downloadPath = destination.absolutePath var success = true try { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt index 8503145e..47c286fa 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -1,5 +1,6 @@ package com.pitchedapps.frost.activities +import android.content.Intent import android.graphics.drawable.Drawable import android.os.Bundle import android.os.Handler @@ -27,7 +28,6 @@ import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.functions.BiFunction import io.reactivex.internal.operators.single.SingleToObservable -import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.SingleSubject @@ -42,8 +42,6 @@ class LoginActivity : BaseActivity() { val textview: AppCompatTextView by bindView(R.id.textview) val profile: ImageView by bindView(R.id.profile) - val loginObservable = SingleSubject.create() - val progressObservable = BehaviorSubject.create()!! val profileObservable = SingleSubject.create() val usernameObservable = SingleSubject.create() @@ -62,17 +60,14 @@ class LoginActivity : BaseActivity() { setSupportActionBar(toolbar) setTitle(R.string.kau_login) setFrostColors(toolbar) - web.loginObservable = loginObservable - web.progressObservable = progressObservable - loginObservable.observeOn(AndroidSchedulers.mainThread()).subscribe { + web.loadLogin({ refresh = it != 100 }) { cookie -> + L.d("Login found") web.fadeOut(onFinish = { profile.fadeIn() loadInfo(cookie) }) } - progressObservable.observeOn(AndroidSchedulers.mainThread()).subscribe { refresh = it != 100 } - web.loadLogin() } fun loadInfo(cookie: CookieModel) { @@ -124,4 +119,12 @@ class LoginActivity : BaseActivity() { fun loadUsername(cookie: CookieModel) { cookie.fetchUsername { usernameObservable.onSuccess(it) } } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == 999) { + L.d("Result found for activity with result $resultCode") + L.d("Intent data ${data?.extras.toString()}") + } else + super.onActivityResult(requestCode, resultCode, data) + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt index df1228bd..e8148b55 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt @@ -350,7 +350,6 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract, hiddenSearchView?.dispose() hiddenSearchView = null searchView = null - //todo remove true searchview and add contract } override fun emitSearchResponse(items: List) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt index 7e9fdcad..0387bb99 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt @@ -52,6 +52,7 @@ interface InjectorContract { */ fun WebView.jsInject(vararg injectors: InjectorContract, callback: ((Array) -> Unit) = {}) { val validInjectors = injectors.filter { it != JsActions.EMPTY } + if (validInjectors.isEmpty()) return callback(emptyArray()) val observables = Array(validInjectors.size, { SingleSubject.create() }) Observable.zip>(observables.map { it.toObservable() }, { it.map { it.toString() }.toTypedArray() }).subscribeOn(AndroidSchedulers.mainThread()).subscribe({ callback(it) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt index d9b91225..2453d3b0 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -22,7 +22,7 @@ import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.FrostWebActivity import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.dbflow.fetchUsername -import com.pitchedapps.frost.facebook.FB_URL_BASE +import com.pitchedapps.frost.facebook.formattedFbUrl import com.pitchedapps.frost.utils.ARG_USER_ID import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs @@ -40,7 +40,7 @@ val Context.frostNotification: NotificationCompat.Builder } @Suppress("DEPRECATION") -//The update feature is for Android O and seems to still be in beta + //The update feature is for Android O and seems to still be in beta fun Notification.frostConfig() = apply { if (Prefs.notificationVibrate) defaults = defaults or Notification.DEFAULT_VIBRATE if (Prefs.notificationSound) defaults = defaults or Notification.DEFAULT_SOUND @@ -86,12 +86,12 @@ data class NotificationContent(val data: CookieModel, } } else { val intent = Intent(context, FrostWebActivity::class.java) - intent.data = Uri.parse("${FB_URL_BASE}$href") + intent.data = Uri.parse(href.formattedFbUrl) intent.putExtra(ARG_USER_ID, data.id) val group = "frost_${data.id}" val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0) val notifBuilder = context.frostNotification - .setContentTitle(title ?: context.string(R.string.app_name)) + .setContentTitle(title ?: context.string(R.string.frost_name)) .setContentText(text) .setContentIntent(pendingIntent) .setCategory(Notification.CATEGORY_SOCIAL) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationReceiver.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationReceiver.kt new file mode 100644 index 00000000..c903ff72 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationReceiver.kt @@ -0,0 +1,34 @@ +package com.pitchedapps.frost.services + +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.support.v4.app.NotificationManagerCompat +import com.pitchedapps.frost.utils.L + +/** + * Created by Allan Wang on 2017-08-04. + * + * Cancels a notification + */ +private const val NOTIF_TAG_TO_CANCEL = "notif_tag_to_cancel" +private const val NOTIF_ID_TO_CANCEL = "notif_id_to_cancel" + +class NotificationReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + L.d("NotificationReceiver triggered") + val notifTag = intent.getStringExtra(NOTIF_TAG_TO_CANCEL) + val notifId = intent.getIntExtra(NOTIF_ID_TO_CANCEL, -1) + if (notifId != -1) { + L.d("NotificationReceiver: Cancelling $notifTag $notifId") + NotificationManagerCompat.from(context).cancel(notifTag, notifId) + } + } +} + +fun Context.getNotificationPendingCancelIntent(tag: String?, notifId: Int): PendingIntent { + val cancelIntent = Intent(this, NotificationReceiver::class.java) + .putExtra(NOTIF_TAG_TO_CANCEL, tag).putExtra(NOTIF_ID_TO_CANCEL, notifId) + return PendingIntent.getBroadcast(this, 0, cancelIntent, 0) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt index 3ddad869..fe7758cc 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -182,7 +182,7 @@ class NotificationService : JobService() { private fun Context.debugNotification(text: String) { if (!BuildConfig.DEBUG) return val notifBuilder = frostNotification - .setContentTitle(string(R.string.app_name)) + .setContentTitle(string(R.string.frost_name)) .setContentText(text) NotificationManagerCompat.from(this).notify(999, notifBuilder.build().frostConfig()) } @@ -190,7 +190,7 @@ class NotificationService : JobService() { fun summaryNotification(userId: Long, count: Int) { if (count <= 1) return val notifBuilder = frostNotification - .setContentTitle(string(R.string.app_name)) + .setContentTitle(string(R.string.frost_name)) .setContentText("$count notifications") .setGroup("frost_$userId") .setGroupSummary(true) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt new file mode 100644 index 00000000..35f69bca --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt @@ -0,0 +1,163 @@ +package com.pitchedapps.frost.utils + +import android.app.Notification +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.support.v4.app.NotificationCompat +import android.support.v4.app.NotificationManagerCompat +import android.support.v4.content.FileProvider +import ca.allanwang.kau.permissions.PERMISSION_WRITE_EXTERNAL_STORAGE +import ca.allanwang.kau.permissions.kauRequestPermissions +import ca.allanwang.kau.utils.copyFromInputStream +import ca.allanwang.kau.utils.string +import com.pitchedapps.frost.BuildConfig +import com.pitchedapps.frost.R +import com.pitchedapps.frost.services.frostConfig +import com.pitchedapps.frost.services.frostNotification +import com.pitchedapps.frost.services.getNotificationPendingCancelIntent +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.ResponseBody +import okio.* +import org.jetbrains.anko.AnkoAsyncContext +import org.jetbrains.anko.doAsync +import java.io.File +import java.io.IOException +import java.lang.ref.WeakReference + +/** + * Created by Allan Wang on 2017-08-04. + * + * With reference to the OkHttp3 sample + */ +fun Context.frostDownload(url: String) { + L.d("Received download request", "Download $url") + val type = if (url.contains("video")) DownloadType.VIDEO + else return L.d("Download request does not match any type") + kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { + granted, _ -> + if (granted) doAsync { frostDownloadImpl(url, type) } + } +} + +private const val MAX_PROGRESS = 1000 +//private val DOWNLOAD_GROUP: String? = null +private const val DOWNLOAD_GROUP = "frost_downloads" + +private enum class DownloadType(val downloadingRes: Int, val downloadedRes: Int) { + VIDEO(R.string.downloading_video, R.string.downloaded_video), + FILE(R.string.downloading_file, R.string.downloaded_file); + + fun getPendingIntent(context: Context, file: File): PendingIntent { + val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file) + val type = context.contentResolver.getType(uri) + L.d("DownloadType: retrieved pending intent - $uri $type") + val intent = Intent(Intent.ACTION_VIEW, uri) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .setDataAndType(uri, type) + return PendingIntent.getActivity(context, 0, intent, 0) + } +} + +private fun AnkoAsyncContext.frostDownloadImpl(url: String, type: DownloadType) { + L.d("Starting download request") + val notifId = Math.abs(url.hashCode() + System.currentTimeMillis().toInt()) + var notifBuilderAttempt: NotificationCompat.Builder? = null + weakRef.get()?.apply { + notifBuilderAttempt = frostNotification + .setContentTitle(string(type.downloadingRes)) + .setCategory(Notification.CATEGORY_PROGRESS) + .setWhen(System.currentTimeMillis()) + .setProgress(MAX_PROGRESS, 0, false) + .setOngoing(true) + .addAction(R.drawable.ic_action_cancel, string(R.string.kau_cancel), getNotificationPendingCancelIntent(DOWNLOAD_GROUP, notifId)) + .setGroup(DOWNLOAD_GROUP) + } + val notifBuilder = notifBuilderAttempt ?: return + notifBuilder.show(weakRef, notifId) + + val request: Request = Request.Builder() + .url(url) + .tag(url) + .build() + + var client: OkHttpClient? = null + client = OkHttpClient.Builder() + .addNetworkInterceptor { + chain -> + val original = chain.proceed(chain.request()) + return@addNetworkInterceptor original.newBuilder().body(ProgressResponseBody(original.body()!!) { + bytesRead, contentLength, done -> + //cancel request if context reference is now invalid + if (weakRef.get() == null) { + client?.cancel(url) + } + val ctx = weakRef.get() ?: return@ProgressResponseBody client?.cancel(url) ?: Unit + val percentage = bytesRead.toFloat() / contentLength.toFloat() * MAX_PROGRESS + L.v("Download request progress: $percentage") + notifBuilder.setProgress(MAX_PROGRESS, percentage.toInt(), false) + if (done) { + notifBuilder.setFinished(ctx, type) + L.d("Download request finished") + } + notifBuilder.show(weakRef, notifId) + }).build() + } + .build() + client.newCall(request).execute().use { + response -> + if (!response.isSuccessful) throw IOException("Unexpected code $response") + val stream = response.body()?.byteStream() + if (stream != null) { + val destination = createMediaFile(".mp4") + destination.copyFromInputStream(stream) + weakRef.get()?.apply { + notifBuilder.setContentIntent(type.getPendingIntent(this, destination)) + notifBuilder.show(weakRef, notifId) + } + } + } +} + +private fun NotificationCompat.Builder.setFinished(context: Context, type: DownloadType) + = setContentTitle(context.string(type.downloadedRes)) + .setProgress(0, 0, false).setOngoing(false).setAutoCancel(true) + .apply { mActions.clear() } + +private fun OkHttpClient.cancel(url: String) { + val call = dispatcher().runningCalls().firstOrNull { it.request().tag() == url } + if (call != null && !call.isCanceled) call.cancel() +} + +private fun NotificationCompat.Builder.show(weakRef: WeakReference, notifId: Int) { + val c = weakRef.get() ?: return + NotificationManagerCompat.from(c).notify(DOWNLOAD_GROUP, notifId, build().frostConfig()) +} + +private class ProgressResponseBody( + val responseBody: ResponseBody, + val listener: (bytesRead: Long, contentLength: Long, done: Boolean) -> Unit) : ResponseBody() { + + private val bufferedSource: BufferedSource by lazy { Okio.buffer(source(responseBody.source())) } + + override fun contentLength(): Long = responseBody.contentLength() + + override fun contentType(): MediaType? = responseBody.contentType() + + override fun source(): BufferedSource = bufferedSource + + private fun source(source: Source): Source = object : ForwardingSource(source) { + + private var totalBytesRead = 0L + + override fun read(sink: Buffer?, byteCount: Long): Long { + val bytesRead = super.read(sink, byteCount) + // read() returns the number of bytes read, or -1 if this source is exhausted. + totalBytesRead += if (bytesRead != -1L) bytesRead else 0 + listener(totalBytesRead, responseBody.contentLength(), bytesRead == -1L) + return bytesRead + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt index cc3ea52e..496a6b5b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt @@ -2,8 +2,10 @@ package com.pitchedapps.frost.utils import android.app.Activity import android.content.Context +import android.content.Intent import android.graphics.Color import android.graphics.drawable.ColorDrawable +import android.net.Uri import android.support.annotation.StringRes import android.support.design.internal.SnackbarContentLayout import android.support.design.widget.Snackbar @@ -22,8 +24,11 @@ import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.* import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.facebook.FACEBOOK_COM import com.pitchedapps.frost.facebook.FbTab import com.pitchedapps.frost.facebook.formattedFbUrl +import java.io.IOException +import java.util.* /** * Created by Allan Wang on 2017-06-03. @@ -145,4 +150,23 @@ fun Activity.frostNavigationBar() { navigationBarColor = if (Prefs.tintNavBar) Prefs.headerColor else Color.BLACK } -fun RequestBuilder.withRoundIcon() = apply(RequestOptions().transform(CircleCrop()))!! \ No newline at end of file +fun RequestBuilder.withRoundIcon() = apply(RequestOptions().transform(CircleCrop()))!! + +@Throws(IOException::class) +fun createMediaFile(extension: String) = createMediaFile("Frost", extension) + +@Throws(IOException::class) +fun Context.createPrivateMediaFile(extension: String) = createPrivateMediaFile("Frost", extension) + +/** + * Tries to send the uri to the proper activity via an intent + * @returns {@code true} if activity is resolved, {@code false} otherwise + */ +fun Context.resolveActivityForUri(uri: Uri): Boolean { + if (uri.toString().contains(FACEBOOK_COM) && !uri.toString().contains("intent:")) return false //ignore response as we will be triggering ourself + val intent = Intent(Intent.ACTION_VIEW, uri) + if (intent.resolveActivity(packageManager) == null) return false + startActivity(intent) + return true +} + diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt index dc5ac6ac..4b6c9e4e 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt @@ -6,9 +6,7 @@ import android.support.v7.widget.RecyclerView import android.view.View import android.widget.ImageView import ca.allanwang.kau.iitems.KauIItem -import ca.allanwang.kau.utils.bindView -import ca.allanwang.kau.utils.fadeIn -import ca.allanwang.kau.utils.toDrawable +import ca.allanwang.kau.utils.* import com.bumptech.glide.Glide import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException @@ -30,7 +28,7 @@ class AccountItem(val cookie: CookieModel?) : KauIItem?) { super.bindView(viewHolder, payloads) with(viewHolder) { - text.visibility = View.INVISIBLE + text.invisible() text.setTextColor(Prefs.textColor) if (cookie != null) { text.text = cookie.name @@ -46,10 +44,9 @@ class AccountItem(val cookie: CookieModel?) : KauIItem= Build.VERSION_CODES.N) progress.setProgress(it, true) else progress.progress = it } @@ -62,7 +59,7 @@ class FrostWebView @JvmOverloads constructor( baseEnum = enum with(settings) { javaScriptEnabled = true - if (url.contains("/message")) + if (url.contains("facebook.com/message")) userAgentString = USER_AGENT_BASIC allowFileAccess = true textZoom = Prefs.webTextScaling @@ -73,6 +70,7 @@ class FrostWebView @JvmOverloads constructor( webChromeClient = FrostChromeClient(this) addJavascriptInterface(FrostJSI(this), "Frost") setBackgroundColor(Color.TRANSPARENT) + setDownloadListener { downloadUrl, _, _, _, _ -> context.frostDownload(downloadUrl) } } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt index 9f7dd916..94bff3c3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt @@ -132,13 +132,7 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient if (path.startsWith("/composer/")) return launchRequest(request) if (request.url.toString().contains("scontent-sea1-1.xx.fbcdn.net") && (path.endsWith(".jpg") || path.endsWith(".png"))) return launchImage(request) - if (!request.url.toString().contains(FACEBOOK_COM)) { - val intent = Intent(Intent.ACTION_VIEW, request.url) - if (intent.resolveActivity(view.context.packageManager) != null) { - view.context.startActivity(Intent(Intent.ACTION_VIEW, request.url)) - return true - } - } + if (view.context.resolveActivityForUri(request.url)) return true return super.shouldOverrideUrlLoading(view, request) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt index d96fba55..d8edc15c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt @@ -1,6 +1,7 @@ package com.pitchedapps.frost.web import android.animation.ValueAnimator +import android.annotation.SuppressLint import android.content.Context import android.support.v4.view.NestedScrollingChild import android.support.v4.view.NestedScrollingChildHelper @@ -94,6 +95,7 @@ class FrostWebViewCore @JvmOverloads constructor( * * https://github.com/takahirom/webview-in-coordinatorlayout/blob/master/app/src/main/java/com/github/takahirom/webview_in_coodinator_layout/NestedWebView.java */ + @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(ev: MotionEvent): Boolean { val event = MotionEvent.obtain(ev) val action = event.action diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt index b178f66c..31be4450 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt @@ -4,23 +4,19 @@ import android.annotation.SuppressLint import android.content.Context import android.util.AttributeSet import android.view.View -import android.webkit.ConsoleMessage -import android.webkit.CookieManager -import android.webkit.WebChromeClient -import android.webkit.WebView +import android.webkit.* import ca.allanwang.kau.utils.fadeIn -import com.pitchedapps.frost.R +import ca.allanwang.kau.utils.isVisible import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.facebook.FACEBOOK_COM +import com.pitchedapps.frost.facebook.FB_URL_BASE import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.injectors.CssHider import com.pitchedapps.frost.injectors.jsInject import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs -import com.pitchedapps.frost.utils.frostSnackbar -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.SingleSubject -import io.reactivex.subjects.Subject +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread /** * Created by Allan Wang on 2017-05-29. @@ -30,27 +26,15 @@ class LoginWebView @JvmOverloads constructor( ) : WebView(context, attrs, defStyleAttr) { companion object { - const val LOGIN_URL = "https://touch.facebook.com/login" - private val userMatcher: Regex by lazy { Regex("c_user=([0-9]*);") } + const val LOGIN_URL = "${FB_URL_BASE}login" + private val userMatcher: Regex = Regex("c_user=([0-9]*);") } - val cookieObservable = PublishSubject.create>() - lateinit var loginObservable: SingleSubject - lateinit var progressObservable: Subject + private lateinit var loginCallback: (CookieModel) -> Unit + private lateinit var progressCallback: (Int) -> Unit init { - FbCookie.reset({ - cookieObservable.filter { (_, cookie) -> cookie?.contains(userMatcher) ?: false } - .subscribe { - (url, cookie) -> - L.d("Checking cookie for login", "$url\n\t$cookie") - val id = userMatcher.find(cookie!!)?.groups?.get(1)?.value!! - FbCookie.save(id.toLong()) - cookieObservable.onComplete() - loginObservable.onSuccess(CookieModel(id.toLong(), "", cookie)) - } - setupWebview() - }) + FbCookie.reset { setupWebview() } } @SuppressLint("SetJavaScriptEnabled") @@ -61,27 +45,39 @@ class LoginWebView @JvmOverloads constructor( webChromeClient = LoginChromeClient() } - fun loadLogin() { + fun loadLogin(progressCallback: (Int) -> Unit, loginCallback: (CookieModel) -> Unit) { + this.progressCallback = progressCallback + this.loginCallback = loginCallback loadUrl(LOGIN_URL) } - - inner class LoginClient : BaseWebViewClient() { + private inner class LoginClient : BaseWebViewClient() { override fun onPageFinished(view: WebView, url: String?) { super.onPageFinished(view, url) - if (url == null || !url.contains(FACEBOOK_COM)) { - view.frostSnackbar(R.string.no_longer_facebook) - loadLogin() - return + val containsFacebook = url?.contains(FACEBOOK_COM) ?: false + checkForLogin(url) { id, cookie -> loginCallback(CookieModel(id, "", cookie)) } + view.jsInject(CssHider.HEADER.maybe(containsFacebook), + CssHider.CORE.maybe(containsFacebook), + Prefs.themeInjector.maybe(containsFacebook), + callback = { if (!view.isVisible) view.fadeIn(offset = 150L) }) + } + + fun checkForLogin(url: String?, onFound: (id: Long, cookie: String) -> Unit) { + doAsync { + if (url == null || !url.contains(FACEBOOK_COM)) return@doAsync + val cookie = CookieManager.getInstance().getCookie(url) ?: return@doAsync + L.d("Checking cookie for login", cookie) + val id = userMatcher.find(cookie)?.groups?.get(1)?.value?.toLong() ?: return@doAsync + uiThread { onFound(id, cookie) } } - cookieObservable.onNext(Pair(url, CookieManager.getInstance().getCookie(url))) - view.jsInject(CssHider.HEADER, CssHider.CORE, - Prefs.themeInjector, - callback = { - if (view.visibility != View.VISIBLE) - view.fadeIn(offset = 150L) - }) + } + + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { + //For now, we will ignore all attempts to launch external apps during login + if (request.url == null || request.url.scheme == "intent" || request.url.scheme == "android-app") + return true + return super.shouldOverrideUrlLoading(view, request) } } @@ -93,7 +89,7 @@ class LoginWebView @JvmOverloads constructor( override fun onProgressChanged(view: WebView, newProgress: Int) { super.onProgressChanged(view, newProgress) - progressObservable.onNext(newProgress) + progressCallback(newProgress) } } } \ No newline at end of file -- cgit v1.2.3