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 --- README.md | 5 + app/src/main/AndroidManifest.xml | 5 + app/src/main/assets/css/core/main.compact.css | 6 +- app/src/main/assets/css/core/main.scss | 8 +- .../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 +++++----- app/src/main/res/drawable/ic_action_cancel.xml | 9 ++ app/src/main/res/values/strings.xml | 8 +- app/src/main/res/xml/frost_changelog.xml | 9 +- docs/Changelog.md | 8 +- generate-apk-release.sh | 15 +- gradle.properties | 2 +- 28 files changed, 353 insertions(+), 113 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 create mode 100644 app/src/main/res/drawable/ic_action_cancel.xml diff --git a/README.md b/README.md index cf2a0fb4..17e27b64 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,8 @@ It contains many features, including: * Native image viewer and downloader via long press * Reactive based loading * The transparency of open sourced development + +For testers and users without a play store account, test builds can be found [here](https://github.com/AllanWang/Frost-for-Facebook-APK-Builder/releases). +Note that these builds occur for every commit, including unstable ones. +You can find the release numbers for the master branch under the [Travis](https://travis-ci.org/AllanWang/Frost-for-Facebook/branches). +Those builds are likely more stable as they are pushed out to the alpha stream on the play store. \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c4b5b9dc..769dd14d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -137,6 +137,7 @@ android:enabled="true" android:label="@string/frost_notifications" android:permission="android.permission.BIND_JOB_SERVICE" /> + @@ -144,6 +145,10 @@ + + *::after, ._32qk, ._d00, ._d01, ._38o9, ._2u4w, ._3u9t, ._55fj, ._52x1, ._3wjp, ._usq, ._2cul:before, ._13e_, .jewel .flyout, ._3bg5 ._52x6, ._3on6, ._2om3, ._2ol-, ._56d8, .al, ._1gkq, ._5fjv, ._5fjw, ._4z83 { border-top: 1px solid rgba(215, 176, 215, 0.3) !important; } -._15ny::after, ._z-w, ._8i2, ._2nk0, ._2u4w, ._577z:not(:last-child) ._ygd, ._3u9u, ._3mgz, ._52x6, ._2066, ._5luf, .mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .jx-result, ._2om3, ._2ol-, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._1gkq, ._5rgs, ._5xuj, ._1sv1, ._idb, ._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc, ._5pz4, ._5lp4, ._5lp5, ._3on6, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child, ._5fjw > :first-child, ._5fjv, ._5fjw, ._4z83 { border-bottom: 1px solid rgba(215, 176, 215, 0.3) !important; } +._15ny::after, ._z-w, ._8i2, ._2nk0, ._22_8, ._2u4w, ._577z:not(:last-child) ._ygd, ._3u9u, ._3mgz, ._52x6, ._2066, ._5luf, .mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .jx-result, ._2om3, ._2ol-, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._1gkq, ._5rgs, ._5xuj, ._1sv1, ._idb, ._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc, ._5pz4, ._5lp4, ._5lp5, ._3on6, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child, ._5fjw > :first-child, ._5fjv, ._5fjw, ._4z83 { border-bottom: 1px solid rgba(215, 176, 215, 0.3) !important; } ._d4i, ._f6s, .mentions-suggest-item, .mentions-suggest, ._1_y5, ._lr0, ._5hgt, ._2cpp, ._4e8n, ._uww, .mentions-placeholder, .mentions-shadow, .mentions-measurer, ._5whq, ._59tt, ._41ft::after, .jx-tokenizer, ._3uqf, ._4756, ._1rrd, ._5n_f { border: 1px solid rgba(215, 176, 215, 0.3) !important; } diff --git a/app/src/main/assets/css/core/main.scss b/app/src/main/assets/css/core/main.scss index 5f532cf4..85ce793f 100644 --- a/app/src/main/assets/css/core/main.scss +++ b/app/src/main/assets/css/core/main.scss @@ -5,8 +5,8 @@ background: $background !important; } -body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, -.tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, +body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, +.tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, .btnD, .btnI, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._10c_, ._2jl2, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._d4i, ._577z, ._2u4w, ._3u9p, ._3u9t, ._2v9s, ._cw4, ._5_y-, ._5_y_, ._5_z3, ._cwy, ._5_z0, ._5_z1, ._5_z2, ._2mtc, ._206a, ._1_-1, ._1ybg, .appCenterCategorySelectorButton, ._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5, ._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, @@ -41,7 +41,7 @@ button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS background: $background2 !important; } -[style*="color"], body, input, ._42rv, ._4qau, +[style*="color"], body, input, ._42rv, ._4qau, ._dwm .descArea, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782, ._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, @@ -68,7 +68,7 @@ h1, h2, h3, h4, h5, h6 { border-top: 1px solid $divider !important; } -._15ny::after, ._z-w, ._8i2, ._2nk0, +._15ny::after, ._z-w, ._8i2, ._2nk0, ._22_8, ._2u4w, ._577z:not(:last-child) ._ygd, ._3u9u, ._3mgz, ._52x6, ._2066, ._5luf, .mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .jx-result, ._2om3, ._2ol-, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._1gkq, ._5rgs, ._5xuj, ._1sv1, ._idb, ._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc, 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 diff --git a/app/src/main/res/drawable/ic_action_cancel.xml b/app/src/main/res/drawable/ic_action_cancel.xml new file mode 100644 index 00000000..e349d8c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_cancel.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d4a7c9dc..412d5f34 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -48,9 +48,6 @@ Copy Text Frost for Facebook: Image Link Debug - - No longer under facebook; refreshing… - Swipe right to go back to the previous window. Profile Picture @@ -79,4 +76,9 @@ Image downloaded Image failed to download Failed to share image + + Downloading Video + Video Downloaded + Downloading File + File Downloaded diff --git a/app/src/main/res/xml/frost_changelog.xml b/app/src/main/res/xml/frost_changelog.xml index 8f68de9f..e7681107 100644 --- a/app/src/main/res/xml/frost_changelog.xml +++ b/app/src/main/res/xml/frost_changelog.xml @@ -10,7 +10,14 @@ - + + + + + + + + diff --git a/docs/Changelog.md b/docs/Changelog.md index c4abb9ae..c36d2b7f 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,10 +1,16 @@ # Changelog -## v1.4.2 +## Beta Updates +* Fixed notification titles +* Added support for downloading videos + +## v1.5.0 * Experimental: Add notifications for messages; report to me if this drains your battery * Add FAQ in the about section * Add video uploading * Add open link option in context menu +* Add geolocation +* Update theme ## v1.4.1 * Add intro pages diff --git a/generate-apk-release.sh b/generate-apk-release.sh index 830fa07d..6d1c868c 100644 --- a/generate-apk-release.sh +++ b/generate-apk-release.sh @@ -3,11 +3,11 @@ # config # make sure the GITHUB_API_KEY is encrypted and inside the travis file # travis encrypt GITHUB_API_KEY=super_secret --add env.global +# Note - gradle 3.0.0 generates outputs in their own folders - ctrl + f > releaseTest RELEASE_REPO=AllanWang/Frost-for-Facebook-APK-Builder USER_AUTH=AllanWang EMAIL=me@allanwang.ca -APK_NAME=Frost-releaseTest MODULE_NAME=app VERSION_KEY=Frost # Make version key different from module name @@ -15,7 +15,9 @@ VERSION_KEY=Frost # create a new directory that will contain our generated apk mkdir $HOME/$VERSION_KEY/ # copy generated apk from build folder to the folder just created -cp -R $MODULE_NAME/build/outputs/apk/$APK_NAME.apk $HOME/$VERSION_KEY/ +cp -a $MODULE_NAME/build/outputs/apk/releaseTest/. $HOME/$VERSION_KEY/ +printf "Moved apks\n" +ls -a $HOME/${VERSION_KEY} # go to home and setup git echo "Clone Git" @@ -41,9 +43,12 @@ API_JSON="$(printf '{"tag_name": "v%s","target_commitish": "master","name": "v%s newRelease="$(curl --data "$API_JSON" https://api.github.com/repos/$RELEASE_REPO/releases?access_token=$GITHUB_API_KEY)" rID="$(echo "$newRelease" | jq ".id")" -cd $HOME +cd $HOME/${VERSION_KEY} echo "Push apk to $rID" -curl "https://uploads.github.com/repos/${RELEASE_REPO}/releases/${rID}/assets?access_token=${GITHUB_API_KEY}&name=${APK_NAME}-v${TRAVIS_BUILD_NUMBER}.apk" --header 'Content-Type: application/zip' --upload-file $VERSION_KEY/$APK_NAME.apk -X POST - +for apk in $(find *.apk -type f); do + apkName="${apk::-4}" + printf "Apk $apkName\n" + curl "https://uploads.github.com/repos/${RELEASE_REPO}/releases/${rID}/assets?access_token=${GITHUB_API_KEY}&name=${apkName}-v${TRAVIS_BUILD_NUMBER}.apk" --header 'Content-Type: application/zip' --upload-file $apkName.apk -X POST +done echo -e "Done\n" \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 589a03a0..0a6e4ac0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ MIN_SDK=21 TARGET_SDK=26 BUILD_TOOLS=26.0.1 -KAU=ca2cda0 +KAU=9d3169f KOTLIN=1.1.3-2 CRASHLYTICS=2.6.8 DBFLOW=4.0.5 -- cgit v1.2.3