From 8aece5e3f9209d7c161410c304655f0aec2d6054 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sun, 7 Jan 2018 02:43:57 -0500 Subject: Feature/website debug (#603) * Create beginning of web downloader * Clean up * Update KAU for reified activity launching * Update web attachments and setFrostColor * Test other zipper * Test simpler image saving model * Finish up image activity * Restore aggressive overlays * Try new zipper * Test again * Fix tests * Add working build * Rename * Support cancellation --- app/src/debug/res/xml/file_paths.xml | 9 - app/src/main/AndroidManifest.xml | 3 + .../kotlin/com/pitchedapps/frost/StartActivity.kt | 14 +- .../frost/activities/BaseMainActivity.kt | 11 +- .../pitchedapps/frost/activities/DebugActivity.kt | 75 +++++ .../pitchedapps/frost/activities/ImageActivity.kt | 136 ++++++--- .../pitchedapps/frost/activities/IntroActivity.kt | 2 +- .../pitchedapps/frost/activities/LoginActivity.kt | 13 +- .../frost/activities/SelectorActivity.kt | 9 +- .../frost/activities/SettingsActivity.kt | 21 +- .../frost/activities/TabCustomizerActivity.kt | 4 +- .../frost/activities/WebOverlayActivity.kt | 5 +- .../pitchedapps/frost/debugger/OfflineWebsite.kt | 307 +++++++++++++++++++++ .../pitchedapps/frost/services/DownloadService.kt | 5 +- .../kotlin/com/pitchedapps/frost/settings/Debug.kt | 171 +++++------- .../kotlin/com/pitchedapps/frost/utils/Utils.kt | 94 +++++-- .../com/pitchedapps/frost/web/DebugWebView.kt | 54 ++++ .../frost/web/FrostUrlOverlayValidator.kt | 2 +- .../pitchedapps/frost/web/FrostWebViewClients.kt | 4 +- app/src/main/res/layout/activity_debug.xml | 36 +++ app/src/main/res/layout/activity_login.xml | 1 - app/src/main/res/values/strings_pref_debug.xml | 5 + app/src/main/res/xml/file_paths.xml | 13 +- app/src/main/res/xml/frost_changelog.xml | 10 +- app/src/releaseTest/res/xml/file_paths.xml | 9 - .../frost/debugger/OfflineWebsiteTest.kt | 25 ++ .../kotlin/com/pitchedapps/frost/utils/UrlTests.kt | 2 +- docs/Changelog.md | 4 + gradle.properties | 2 +- 29 files changed, 823 insertions(+), 223 deletions(-) delete mode 100644 app/src/debug/res/xml/file_paths.xml create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/activities/DebugActivity.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt create mode 100644 app/src/main/res/layout/activity_debug.xml delete mode 100644 app/src/releaseTest/res/xml/file_paths.xml create mode 100644 app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt diff --git a/app/src/debug/res/xml/file_paths.xml b/app/src/debug/res/xml/file_paths.xml deleted file mode 100644 index 087f23c9..00000000 --- a/app/src/debug/res/xml/file_paths.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c6414c51..12b8c029 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -142,6 +142,9 @@ + + loadFbCookiesAsync { + val cookies = ArrayList(it) L.i { "Cookies loaded at time ${System.currentTimeMillis()}" } L._d { "Cookies: ${cookies.joinToString("\t")}" } - if (cookies.isNotEmpty()) - launchNewTask(if (Prefs.userId != -1L) MainActivity::class.java else SelectorActivity::class.java, ArrayList(cookies)) - else - launchNewTask(LoginActivity::class.java) + if (cookies.isNotEmpty()) { + if (Prefs.userId != -1L) + launchNewTask(cookies) + else + launchNewTask(cookies) + } else + launchNewTask() } } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt index 1e17f06a..ffcbadab 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -123,7 +123,12 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, // Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) // .setAction("Action", null).show() // } - setFrostColors(toolbar, themeWindow = false, headers = arrayOf(appBar), backgrounds = arrayOf(viewPager)) + setFrostColors { + toolbar(toolbar) + themeWindow = false + header(appBar) + background(viewPager) + } tabs.setBackgroundColor(Prefs.mainActivityLayout.backgroundColor()) onCreateBilling() } @@ -192,8 +197,8 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, } } } - -3L -> launchNewTask(LoginActivity::class.java, clearStack = false) - -4L -> launchNewTask(SelectorActivity::class.java, cookies(), false) + -3L -> launchNewTask(clearStack = false) + -4L -> launchNewTask( cookies(), false) else -> { FbCookie.switchUser(profile.identifier, this@BaseMainActivity::refreshAll) tabsForEachView { _, view -> view.badgeText = null } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/DebugActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/DebugActivity.kt new file mode 100644 index 00000000..b6becf90 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/DebugActivity.kt @@ -0,0 +1,75 @@ +package com.pitchedapps.frost.activities + +import android.app.Activity +import android.content.Intent +import android.content.res.ColorStateList +import android.os.Bundle +import android.support.design.widget.FloatingActionButton +import android.support.v4.widget.SwipeRefreshLayout +import android.support.v7.widget.Toolbar +import ca.allanwang.kau.internal.KauBaseActivity +import ca.allanwang.kau.utils.bindView +import ca.allanwang.kau.utils.setIcon +import ca.allanwang.kau.utils.visible +import com.mikepenz.google_material_typeface_library.GoogleMaterial +import com.pitchedapps.frost.R +import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.utils.setFrostColors +import com.pitchedapps.frost.web.DebugWebView + +/** + * Created by Allan Wang on 05/01/18. + */ +class DebugActivity : KauBaseActivity(), SwipeRefreshLayout.OnRefreshListener { + + private val toolbar: Toolbar by bindView(R.id.toolbar) + private val web: DebugWebView by bindView(R.id.debug_webview) + private val swipeRefresh: SwipeRefreshLayout by bindView(R.id.swipe_refresh) + private val fab: FloatingActionButton by bindView(R.id.fab) + + companion object { + const val RESULT_URL = "extra_result_url" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_debug) + setSupportActionBar(toolbar) + setTitle(R.string.debug_frost) + setFrostColors { + toolbar(toolbar) + } + web.loadUrl(FbItem.FEED.url) + web.onPageFinished = { swipeRefresh.isRefreshing = false } + fab.visible().setIcon(GoogleMaterial.Icon.gmd_bug_report, Prefs.iconColor) + fab.backgroundTintList = ColorStateList.valueOf(Prefs.accentColor) + fab.setOnClickListener { + val intent = Intent() + intent.putExtra(RESULT_URL, web.url) + setResult(Activity.RESULT_OK, intent) + finish() + } + } + + override fun onRefresh() { + web.reload() + } + + override fun onResume() { + super.onResume() + web.resumeTimers() + } + + override fun onPause() { + web.pauseTimers() + super.onPause() + } + + override fun onBackPressed() { + if (web.canGoBack()) + web.goBack() + else + super.onBackPressed() + } +} \ No newline at end of file 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 d63fa25e..99fa6eee 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt @@ -1,5 +1,6 @@ package com.pitchedapps.frost.activities +import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.graphics.Bitmap @@ -7,18 +8,20 @@ import android.graphics.Color import android.graphics.drawable.Drawable import android.net.Uri import android.os.Bundle +import android.os.Environment import android.support.design.widget.FloatingActionButton -import android.support.v4.content.FileProvider import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import android.widget.TextView import ca.allanwang.kau.internal.KauBaseActivity +import ca.allanwang.kau.logging.KauLoggerExtension import ca.allanwang.kau.mediapicker.scanMedia import ca.allanwang.kau.permissions.PERMISSION_WRITE_EXTERNAL_STORAGE import ca.allanwang.kau.permissions.kauRequestPermissions import ca.allanwang.kau.utils.* import com.bumptech.glide.Glide +import com.bumptech.glide.RequestManager import com.bumptech.glide.request.target.BaseTarget import com.bumptech.glide.request.target.SizeReadyCallback import com.bumptech.glide.request.target.Target @@ -27,14 +30,17 @@ import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.mikepenz.iconics.typeface.IIcon -import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R +import com.pitchedapps.frost.facebook.requests.call import com.pitchedapps.frost.utils.* import com.sothree.slidinguppanel.SlidingUpPanelLayout +import okhttp3.Request import org.jetbrains.anko.activityUiThreadWithContext import org.jetbrains.anko.doAsync import java.io.File import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* /** * Created by Allan Wang on 2017-07-15. @@ -49,17 +55,19 @@ class ImageActivity : KauBaseActivity() { val fab: FloatingActionButton by bindView(R.id.image_fab) var errorRef: Throwable? = null + private val tempDir: File by lazy { File(cacheDir, IMAGE_FOLDER) } + /** * Reference to the temporary file path * Should be nonnull if the image is successfully loaded * As this is temporary, the image is deleted upon exit */ - private var tempFilePath: String? = null + internal var tempFile: File? = null /** * Reference to path for downloaded image * Nonnull once the image is downloaded by the user */ - internal var downloadPath: String? = null + internal var savedFile: File? = null /** * Indicator for fab's click result */ @@ -70,15 +78,28 @@ class ImageActivity : KauBaseActivity() { value.update(fab) } + companion object { + /** + * Cache folder to store images + * Linked to the uri provider + */ + private const val IMAGE_FOLDER = "images" + private const val TIME_FORMAT = "yyyyMMdd_HHmmss" + private const val IMG_TAG = "Frost" + private const val IMG_EXTENSION = ".png" + private val L = KauLoggerExtension("Image", com.pitchedapps.frost.utils.L) + } + val imageUrl: String by lazy { intent.getStringExtra(ARG_IMAGE_URL).trim('"') } val text: String? by lazy { intent.getStringExtra(ARG_TEXT) } + private val glide: RequestManager by lazy { Glide.with(this) } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) intent?.extras ?: return finish() L.i { "Displaying image" } - L._i { imageUrl } val layout = if (!text.isNullOrBlank()) R.layout.activity_image else R.layout.activity_image_textless setContentView(layout) container.setBackgroundColor(Prefs.bgColor.withMinAlpha(222)) @@ -101,8 +122,10 @@ class ImageActivity : KauBaseActivity() { imageCallback(null, false) } }) - Glide.with(this).asBitmap().load(imageUrl).into(PhotoTarget(this::imageCallback)) - setFrostColors(themeWindow = false) + glide.asBitmap().load(imageUrl).into(PhotoTarget(this::imageCallback)) + setFrostColors { + themeWindow = false + } } /** @@ -119,7 +142,7 @@ class ImageActivity : KauBaseActivity() { } else { photo.setImage(ImageSource.uri(it)) fabAction = FabStates.DOWNLOAD - photo.animate().alpha(1f).scaleXY(1f).withEndAction { fab.show() }.start() + photo.animate().alpha(1f).scaleXY(1f).withEndAction(fab::show).start() } }) } else { @@ -135,52 +158,105 @@ class ImageActivity : KauBaseActivity() { override fun removeCallback(cb: SizeReadyCallback?) {} - override fun onResourceReady(resource: Bitmap, transition: Transition?) = callback(resource, true) + override fun onResourceReady(resource: Bitmap, transition: Transition?) = + callback(resource, true) - override fun onLoadFailed(errorDrawable: Drawable?) = callback(null, false) + override fun onLoadFailed(errorDrawable: Drawable?) = + callback(null, false) - override fun getSize(cb: SizeReadyCallback) = cb.onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + override fun getSize(cb: SizeReadyCallback) = + cb.onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) } private fun saveTempImage(resource: Bitmap, callback: (uri: Uri?) -> Unit) { var photoFile: File? = null try { - photoFile = createPrivateMediaFile(".png") + photoFile = createPrivateMediaFile() } catch (e: IOException) { errorRef = e + logImage(e) } finally { if (photoFile == null) { callback(null) } else { - tempFilePath = photoFile.absolutePath - L.d { "Temp image path $tempFilePath" } + tempFile = photoFile + L.d { "Temp image path ${tempFile?.absolutePath}" } // File created; proceed with request - val photoURI = FileProvider.getUriForFile(this, - BuildConfig.APPLICATION_ID + ".provider", - photoFile) + val photoURI = frostUriFromFile(photoFile) photoFile.outputStream().use { resource.compress(Bitmap.CompressFormat.PNG, 100, it) } callback(photoURI) } } } - internal fun downloadImage() { + private fun logImage(e: Exception?) { + if (!Prefs.analytics) return + val error = e ?: IOException("$imageUrl failed to load") + L.e(error) { "$imageUrl failed to load" } + } + + @Throws(IOException::class) + private fun createPrivateMediaFile(): File { + val timeStamp = SimpleDateFormat(TIME_FORMAT, Locale.getDefault()).format(Date()) + val imageFileName = "${IMG_TAG}_${timeStamp}_" + if (!tempDir.exists()) + tempDir.mkdirs() + return File.createTempFile(imageFileName, IMG_EXTENSION, tempDir) + } + + @Throws(IOException::class) + private fun createPublicMediaFile(): File { + val timeStamp = SimpleDateFormat(TIME_FORMAT, Locale.getDefault()).format(Date()) + val imageFileName = "${IMG_TAG}_${timeStamp}_" + val storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + val frostDir = File(storageDir, IMG_TAG) + if (!frostDir.exists()) frostDir.mkdirs() + return File.createTempFile(imageFileName, IMG_EXTENSION, frostDir) + } + + @Throws(IOException::class) + private fun downloadImageTo(file: File) { + val body = Request.Builder() + .url(imageUrl) + .get() + .call() + .execute() + .body() ?: throw IOException("Failed to retrieve image body") + body.byteStream().use { input -> + file.outputStream().use { output -> + input.copyTo(output) + } + } + } + + internal fun saveImage() { kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ -> L.d { "Download image callback granted: $granted" } if (granted) { doAsync { - val destination = createMediaFile(".png") - downloadPath = destination.absolutePath + val destination = createPublicMediaFile() var success = true try { - File(tempFilePath).copyTo(destination, true) - scanMedia(destination) + val temp = tempFile + if (temp != null) + temp.copyTo(destination, true) + else + downloadImageTo(destination) } catch (e: Exception) { errorRef = e success = false } finally { L.d { "Download image async finished: $success" } + if (success) { + scanMedia(destination) + savedFile = destination + } else { + try { + destination.delete() + } catch (ignore: Exception) { + } + } activityUiThreadWithContext { val text = if (success) R.string.image_download_success else R.string.image_download_fail frostSnackbar(text) @@ -192,15 +268,9 @@ class ImageActivity : KauBaseActivity() { } } - internal fun deleteTempFile() { - if (tempFilePath != null) { - File(tempFilePath!!).delete() - tempFilePath = null - } - } - override fun onDestroy() { - deleteTempFile() + tempFile = null + tempDir.deleteRecursively() L.d { "Closing $localClassName" } super.onDestroy() } @@ -229,14 +299,12 @@ internal enum class FabStates(val iicon: IIcon, val iconColor: Int = Prefs.iconC override fun onClick(activity: ImageActivity) {} }, DOWNLOAD(GoogleMaterial.Icon.gmd_file_download) { - override fun onClick(activity: ImageActivity) = activity.downloadImage() + override fun onClick(activity: ImageActivity) = activity.saveImage() }, SHARE(GoogleMaterial.Icon.gmd_share) { override fun onClick(activity: ImageActivity) { try { - val photoURI = FileProvider.getUriForFile(activity, - BuildConfig.APPLICATION_ID + ".provider", - File(activity.downloadPath)) + val photoURI = activity.frostUriFromFile(activity.savedFile!!) val intent = Intent(Intent.ACTION_SEND).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK putExtra(Intent.EXTRA_STREAM, photoURI) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/IntroActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/IntroActivity.kt index 2321a936..9babd431 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/IntroActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/IntroActivity.kt @@ -134,7 +134,7 @@ class IntroActivity : KauBaseActivity(), ViewPager.PageTransformer, ViewPager.On } override fun finish() { - launchNewTask(MainActivity::class.java, cookies()) + launchNewTask(cookies()) super.finish() } 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 e9657934..2434c8c2 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -62,7 +62,9 @@ class LoginActivity : BaseActivity() { setContentView(R.layout.activity_login) setSupportActionBar(toolbar) setTitle(R.string.kau_login) - setFrostColors(toolbar) + setFrostColors{ + toolbar(toolbar) + } web.loadLogin({ refresh = it != 100 }) { cookie -> L.d { "Login found" } FbCookie.save(cookie.id) @@ -97,10 +99,13 @@ class LoginActivity : BaseActivity() { * 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 */ - loadFbCookiesAsync { cookies -> + loadFbCookiesAsync { + val cookies = ArrayList(it) Handler().postDelayed({ - launchNewTask(if (Showcase.intro) IntroActivity::class.java else MainActivity::class.java, - ArrayList(cookies), clearStack = true) + if (Showcase.intro) + launchNewTask(cookies, true) + else + launchNewTask(cookies, true) }, 1000) } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/SelectorActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/SelectorActivity.kt index ff87f448..a9658eb1 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/SelectorActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/SelectorActivity.kt @@ -38,10 +38,13 @@ class SelectorActivity : BaseActivity() { override fun onBind(viewHolder: RecyclerView.ViewHolder): View? = (viewHolder as? AccountItem.ViewHolder)?.v override fun onClick(v: View, position: Int, fastAdapter: FastAdapter, item: AccountItem) { - if (item.cookie == null) this@SelectorActivity.launchNewTask(LoginActivity::class.java) - else FbCookie.switchUser(item.cookie, { launchNewTask(MainActivity::class.java, cookies()) }) + if (item.cookie == null) this@SelectorActivity.launchNewTask() + else FbCookie.switchUser(item.cookie, { launchNewTask(cookies()) }) } }) - setFrostColors(texts = arrayOf(text), backgrounds = arrayOf(container)) + setFrostColors { + text(text) + background(container) + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt index 58e73530..93d303ab 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt @@ -38,14 +38,23 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IabSettings() { const val REQUEST_NOTIFICATION_RINGTONE = REQUEST_RINGTONE or 1 const val REQUEST_MESSAGE_RINGTONE = REQUEST_RINGTONE or 2 const val ACTIVITY_REQUEST_TABS = 29 + const val ACTIVITY_REQUEST_DEBUG = 53 } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (fetchRingtone(requestCode, resultCode, data)) return - if (requestCode == ACTIVITY_REQUEST_TABS) { - if (resultCode == Activity.RESULT_OK) - shouldRestartMain() - return + when (requestCode) { + ACTIVITY_REQUEST_TABS -> { + if (resultCode == Activity.RESULT_OK) + shouldRestartMain() + return + } + ACTIVITY_REQUEST_DEBUG -> { + val url = data?.extras?.getString(DebugActivity.RESULT_URL) + if (url?.isNotBlank() == true) + sendDebug(url) + return + } } if (!onActivityResultBilling(requestCode, resultCode, data)) super.onActivityResult(requestCode, resultCode, data) @@ -127,7 +136,7 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IabSettings() { descRes = R.string.about_frost_desc iicon = GoogleMaterial.Icon.gmd_info onClick = { - startActivityForResult(AboutActivity::class.java, 9, bundleBuilder = { + startActivityForResult(9, bundleBuilder = { withSceneTransitionAnimation(this@SettingsActivity) }) } @@ -141,7 +150,7 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IabSettings() { plainText(R.string.replay_intro) { iicon = GoogleMaterial.Icon.gmd_replay - onClick = { launchIntroActivity(cookies()) } + onClick = { launchNewTask(cookies(), true) } } subItems(R.string.debug_frost, getDebugPrefs()) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt index ca7a231d..554eaa00 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt @@ -79,7 +79,9 @@ class TabCustomizerActivity : BaseActivity() { fabCancel.setIcon(GoogleMaterial.Icon.gmd_close, Prefs.iconColor) fabCancel.backgroundTintList = ColorStateList.valueOf(Prefs.accentColor) fabCancel.setOnClickListener { finish() } - setFrostColors(themeWindow = true) + setFrostColors { + themeWindow = true + } } private fun View.wobble() = startAnimation(wobble(context)) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt index 853ade72..1bd3ede2 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt @@ -147,7 +147,10 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseAc toolbar.navigationIcon = GoogleMaterial.Icon.gmd_close.toDrawable(this, 16, Prefs.iconColor) toolbar.setNavigationOnClickListener { finishSlideOut() } - setFrostColors(toolbar, themeWindow = false) + setFrostColors { + toolbar(toolbar) + themeWindow = false + } coordinator.setBackgroundColor(Prefs.bgColor.withAlpha(255)) content.bind(this) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt b/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt new file mode 100644 index 00000000..434f1bae --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt @@ -0,0 +1,307 @@ +package com.pitchedapps.frost.debugger + +import ca.allanwang.kau.logging.KauLoggerExtension +import com.pitchedapps.frost.facebook.FB_CSS_URL_MATCHER +import com.pitchedapps.frost.facebook.USER_AGENT_BASIC +import com.pitchedapps.frost.facebook.get +import com.pitchedapps.frost.facebook.requests.call +import com.pitchedapps.frost.facebook.requests.zip +import com.pitchedapps.frost.utils.frostJsoup +import okhttp3.Request +import okhttp3.ResponseBody +import org.jsoup.nodes.Element +import org.jsoup.nodes.Entities +import java.io.File +import java.io.FileOutputStream +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +/** + * Created by Allan Wang on 04/01/18. + * + * Helper to download html files and assets for offline viewing + * + * Inspired by Save for Offline + */ +class OfflineWebsite(private val url: String, + private val cookie: String = "", + /** + * Directory that holds all the files + */ + val baseDir: File, + private val userAgent: String = USER_AGENT_BASIC) { + + /** + * Supplied url without the queries + */ + val baseUrl = url.substringBefore("?").trim('/') + + private val mainFile = File(baseDir, "index.html") + private val assetDir = File(baseDir, "assets") + + private var cancelled = false + private val urlMapper = ConcurrentHashMap() + private val atomicInt = AtomicInteger() + + private val L = KauLoggerExtension("Offline", com.pitchedapps.frost.utils.L) + + init { + if (!baseUrl.startsWith("http")) + throw IllegalArgumentException("Base Url must start with http") + } + + private val fileQueue = mutableSetOf() + + private val cssQueue = mutableSetOf() + + private fun request(url: String) = Request.Builder() + .header("Cookie", cookie) + .header("User-Agent", userAgent) + .url(url) + .get() + .call() + + /** + * Caller to bind callbacks and start the load + * Callback is guaranteed to be called unless the load is cancelled + */ + fun load(progress: (Int) -> Unit = {}, callback: (Boolean) -> Unit) { + reset() + + L.v { "Saving $url to ${baseDir.absolutePath}" } + if (baseDir.exists() && !baseDir.deleteRecursively()) { + L.e { "Could not clean directory" } + return callback(false) + } + + if (!baseDir.mkdirs()) { + L.e { "Could not make directory" } + return callback(false) + } + + + if (!mainFile.createNewFile()) { + L.e { "Could not create ${mainFile.absolutePath}" } + return callback(false) + } + + + if (!assetDir.mkdirs()) { + L.e { "Could not create ${assetDir.absolutePath}" } + return callback(false) + } + + progress(10) + + if (cancelled) return + + val doc = frostJsoup(cookie, url) + doc.setBaseUri(baseUrl) + doc.outputSettings().escapeMode(Entities.EscapeMode.extended) + if (doc.childNodeSize() == 0) { + L.e { "No content found" } + return callback(false) + } + + if (cancelled) return + + progress(35) + + doc.collect("link[href][rel=stylesheet]", "href", cssQueue) + doc.collect("link[href]:not([rel=stylesheet])", "href", fileQueue) + doc.collect("img[src]", "src", fileQueue) + doc.collect("img[data-canonical-src]", "data-canonical-src", fileQueue) + doc.collect("script[src]", "src", fileQueue) + + // make links absolute + doc.select("a[href]").forEach { + val absLink = it.attr("abs:href") + it.attr("href", absLink) + } + + if (cancelled) return + + mainFile.writeText(doc.html()) + + progress(50) + + downloadCss().subscribe { cssLinks, cssThrowable -> + if (cssThrowable != null) { + L.e { "CSS parsing failed" } + } + + progress(70) + + fileQueue.addAll(cssLinks) + + if (cancelled) return@subscribe + + downloadFiles().subscribe { success, throwable -> + L.v { "All files downloaded: $success with throwable $throwable" } + progress(100) + callback(true) + } + } + } + + fun zip(name: String): Boolean { + try { + val zip = File(baseDir, "$name.zip") + if (zip.exists() && (!zip.delete() || !zip.createNewFile())) { + L.e { "Failed to create zip at ${zip.absolutePath}" } + return false + } + + ZipOutputStream(FileOutputStream(zip)).use { out -> + + fun File.zip(name: String = this.name) { + inputStream().use { file -> + out.putNextEntry(ZipEntry(name)) + file.copyTo(out) + } + out.closeEntry() + delete() + } + + mainFile.zip() + assetDir.listFiles().forEach { + it.zip("assets/${it.name}") + } + } + return true + } catch (e: Exception) { + return false + } + } + + fun loadAndZip(name: String, progress: (Int) -> Unit = {}, callback: (Boolean) -> Unit) { + + load({ progress((it * 0.85f).toInt()) }) { + if (cancelled) return@load + if (!it) callback(false) + else { + val result = zip(name) + progress(100) + callback(result) + } + } + } + + private fun downloadFiles() = fileQueue.clean().toTypedArray().zip({ + it.all { it } + }, { + it.downloadUrl({ false }) { file, body -> + body.byteStream().use { input -> + file.outputStream().use { output -> + input.copyTo(output) + return@downloadUrl true + } + } + } + }) + + private fun downloadCss() = cssQueue.clean().toTypedArray().zip, Set>({ + it.flatMap { it }.toSet() + }, { + it.downloadUrl({ emptySet() }) { file, body -> + var content = body.string() + val links = FB_CSS_URL_MATCHER.findAll(content).mapNotNull { it[1] } + val absLinks = links.mapNotNull { + val url = when { + it.startsWith("http") -> it + it.startsWith("/") -> "$baseUrl$it" + else -> return@mapNotNull null + } + // css files are already in the asset folder, + // so the url does not point to another subfolder + content = content.replace(it, url.fileName()) + url + }.toSet() + + L.v { "Abs links $absLinks" } + + file.writeText(content) + return@downloadUrl absLinks + } + }) + + private inline fun String.downloadUrl(fallback: () -> T, + action: (file: File, body: ResponseBody) -> T): T { + + val file = File(assetDir, fileName()) + if (!file.createNewFile()) { + L.e { "Could not create path for ${file.absolutePath}" } + return fallback() + } + + val body = request(this).execute().body() ?: return fallback() + + try { + body.use { + return action(file, it) + } + } catch (e: Exception) { + return fallback() + } + } + + private fun Element.collect(query: String, key: String, collector: MutableSet) { + val data = select(query) + L.v { "Found ${data.size} elements with $query" } + data.forEach { + val absLink = it.attr("abs:$key") + if (!absLink.isValid) return@forEach + collector.add(absLink) + it.attr(key, "assets/${absLink.fileName()}") + } + } + + private inline val String.isValid + get() = startsWith("http") + + /** + * Fetch the previously discovered filename + * or create a new one + * This is thread-safe + */ + private fun String.fileName(): String { + val mapped = urlMapper[this] + if (mapped != null) return mapped + + val candidate = substringBefore("?").trim('/') + .substringAfterLast("/").shorten() + + val index = atomicInt.getAndIncrement() + + /** + * This is primarily for zipping up and sending via emails + * As .js files typically aren't allowed, we'll simply make everything txt files + */ + val newUrl = "a${index}_$candidate.txt" + urlMapper.put(this, newUrl) + return newUrl + } + + private fun String.shorten() = + if (length <= 10) this else substring(length - 10) + + private fun Set.clean() + = filter(String::isNotBlank).filter { it.startsWith("http") } + + private fun reset() { + cancelled = false + urlMapper.clear() + atomicInt.set(0) + fileQueue.clear() + cssQueue.clear() + baseDir.deleteRecursively() + } + + fun cancel() { + cancelled = true + L.v { "Request cancelled" } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/DownloadService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/DownloadService.kt index bf6e1329..25f10398 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/DownloadService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/DownloadService.kt @@ -9,13 +9,12 @@ 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.utils.copyFromInputStream import ca.allanwang.kau.utils.string -import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.createMediaFile +import com.pitchedapps.frost.utils.frostUriFromFile import okhttp3.MediaType import okhttp3.OkHttpClient import okhttp3.Request @@ -105,7 +104,7 @@ class DownloadService : IntentService("FrostVideoDownloader") { private fun getPendingIntent(context: Context, file: File): PendingIntent { - val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file) + val uri = context.frostUriFromFile(file) val type = context.contentResolver.getType(uri) L.i { "DownloadType: retrieved pending intent" } L._i { "Contents: $uri $type" } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt index 269b5a95..76c6e5a4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt @@ -1,8 +1,22 @@ package com.pitchedapps.frost.settings import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder +import ca.allanwang.kau.utils.materialDialog +import ca.allanwang.kau.utils.startActivityForResult import com.pitchedapps.frost.R +import com.pitchedapps.frost.activities.DebugActivity import com.pitchedapps.frost.activities.SettingsActivity +import com.pitchedapps.frost.activities.SettingsActivity.Companion.ACTIVITY_REQUEST_DEBUG +import com.pitchedapps.frost.debugger.OfflineWebsite +import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.utils.frostUriFromFile +import com.pitchedapps.frost.utils.sendFrostEmail +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.toast +import org.jetbrains.anko.uiThread +import java.io.File /** * Created by Allan Wang on 2017-06-30. @@ -16,108 +30,59 @@ fun SettingsActivity.getDebugPrefs(): KPrefAdapterBuilder.() -> Unit = { descRes = R.string.debug_disclaimer_info } -// Debugger.values().forEach { -// plainText(it.data.titleId) { -// iicon = it.data.icon -// onClick = { it.debug(itemView.context) } -// } -// } + plainText(R.string.debug_web) { + descRes = R.string.debug_web_desc + onClick = { this@getDebugPrefs.startActivityForResult(ACTIVITY_REQUEST_DEBUG) } + } +} + +private const val ZIP_NAME = "debug" + +fun SettingsActivity.sendDebug(urlOrig: String) { + + val url = when { + urlOrig.endsWith("soft=requests") -> FbItem.FRIENDS.url + urlOrig.endsWith("soft=messages") -> FbItem.MESSAGES.url + urlOrig.endsWith("soft=notifications") -> FbItem.NOTIFICATIONS.url + urlOrig.endsWith("soft=search") -> "${FbItem._SEARCH.url}?q=a" + else -> urlOrig + } + + val downloader = OfflineWebsite(url, FbCookie.webCookie ?: "", + File(externalCacheDir, "offline_debug")) + + val md = materialDialog { + title(R.string.parsing_data) + progress(false, 100) + negativeText(R.string.kau_cancel) + onNegative { dialog, _ -> dialog.dismiss() } + canceledOnTouchOutside(false) + dismissListener { downloader.cancel() } + } + + md.doAsync { + downloader.loadAndZip(ZIP_NAME, { progress -> + uiThread { it.setProgress(progress) } + }) { success -> + uiThread { + it.dismiss() + if (success) { + val zipUri = it.context.frostUriFromFile( + File(downloader.baseDir, "$ZIP_NAME.zip")) + L.i { "Sending debug zip with uri $zipUri" } + sendFrostEmail(R.string.debug_report_email_title) { + addItem("Url", url) + addAttachment(zipUri) + extras = { + type = "application/zip" + } + } + } else { + toast(R.string.error_generic) + } + } + } + + } } -// -//private enum class Debugger(val data: FbItem, val injector: InjectorContract?, vararg query: String) { -// MENU(FbItem.MENU, JsAssets.MENU_DEBUG, "#viewport"), //todo modify menu js for debugging -// NOTIFICATIONS(FbItem.NOTIFICATIONS, null, "#notifications_list"); -//// SEARCH(FbItem.SEARCH, JsActions.FETCH_BODY); -// -// val query = if (query.isNotEmpty()) arrayOf(*query, "#root", "main", "body") else emptyArray() -// -// fun debug(context: Context) { -// val dialog = context.materialDialogThemed { -// title("Debugging") -// progress(true, 0) -// canceledOnTouchOutside(false) -// positiveText(R.string.kau_cancel) -// onPositive { dialog, _ -> dialog.cancel() } -// } -// if (injector != null) dialog.extractHtml(injector) -// else dialog.debugAsync { -// loadJsoup() -// } -// } -// -// fun MaterialDialog.debugAsync(task: AnkoAsyncContext.() -> Unit) { -// doAsync({ t: Throwable -> -// val msg = t.message -// L.e{"Debugger failed: $msg"} -// context.runOnUiThread { -// cancel() -// context.materialDialogThemed { -// title(R.string.debug_incomplete) -// if (msg != null) content(msg) -// } -// } -// }, task) -// } -// -// /** -// * Wait for html to be returned from headless webview -// * -// * from [debug] to [simplifyJsoup] if [query] is not empty, or [createReport] otherwise -// */ -// @UiThread -// private fun MaterialDialog.extractHtml(injector: InjectorContract) { -// setContent("Fetching webpage") -// var disposable: Disposable? = null -// setOnCancelListener { disposable?.dispose() } -// context.launchHeadlessHtmlExtractor(data.url, injector) { -// disposable = it.subscribe { (html, errorRes) -> -// debugAsync { -// if (errorRes == -1) { -// L.i("Debug report successful", html) -// if (query.isNotEmpty()) simplifyJsoup(Jsoup.parseBodyFragment(html)) -// else createReport(html) -// } else { -// throw Throwable(context.string(errorRes)) -// } -// } -// } -// } -// } -// -// /** -// * Get data directly from the link and search for our queries, returning the outerHTML -// * of the first query found -// * -// * from [debug] to [simplifyJsoup] -// */ -// private fun AnkoAsyncContext.loadJsoup() { -// uiThread { -// it.setContent("Load Jsoup") -// it.setOnCancelListener(null) -// it.debugAsync { simplifyJsoup(frostJsoup(data.url)) } -// } -// } -// -// /** -// * Takes snippet of given document that matches the first query in the [query] items -// * before sending it to [createReport] -// */ -// private fun AnkoAsyncContext.simplifyJsoup(doc: Document) { -// weakRef.get() ?: return -// val q = query.first { doc.select(it).isNotEmpty() } -// createReport(doc.select(q).outerHtml()) -// } -// -// private fun AnkoAsyncContext.createReport(html: String) { -// val cleanHtml = html.cleanHtml() -// uiThread { -// val c = it.context -// it.dismiss() -// c.sendFrostEmail("${c.string(R.string.debug_report_email_title)} $name") { -// addItem("Query List", query.contentToString()) -// footer = cleanHtml -// } -// } -// } -//} \ 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 92e1bd05..bd9c1f99 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt @@ -11,6 +11,7 @@ import android.net.Uri import android.support.annotation.StringRes import android.support.design.internal.SnackbarContentLayout import android.support.design.widget.Snackbar +import android.support.v4.content.FileProvider import android.support.v7.widget.Toolbar import android.view.View import android.widget.FrameLayout @@ -33,6 +34,7 @@ import com.pitchedapps.frost.facebook.FbUrlFormatter.Companion.VIDEO_REDIRECT import com.pitchedapps.frost.utils.iab.IS_FROST_PRO import org.jsoup.Jsoup import org.jsoup.nodes.Element +import java.io.File import java.io.IOException import java.util.* @@ -45,15 +47,15 @@ const val ARG_USER_ID = "arg_user_id" const val ARG_IMAGE_URL = "arg_image_url" const val ARG_TEXT = "arg_text" -fun Context.launchNewTask(clazz: Class, cookieList: ArrayList = arrayListOf(), clearStack: Boolean = false) { - startActivity(clazz, clearStack, intentBuilder = { +inline fun Context.launchNewTask(cookieList: ArrayList = arrayListOf(), clearStack: Boolean = false) { + startActivity(clearStack, intentBuilder = { putParcelableArrayListExtra(EXTRA_COOKIES, cookieList) }) } fun Context.launchLogin(cookieList: ArrayList, clearStack: Boolean = true) { - if (cookieList.isNotEmpty()) launchNewTask(SelectorActivity::class.java, cookieList, clearStack) - else launchNewTask(LoginActivity::class.java, clearStack = clearStack) + if (cookieList.isNotEmpty()) launchNewTask(cookieList, clearStack) + else launchNewTask(clearStack = clearStack) } fun Activity.cookies(): ArrayList { @@ -65,22 +67,26 @@ fun Activity.cookies(): ArrayList { * Note that most requests may need to first check if the url can be launched as an overlay * See [requestWebOverlay] to verify the launch */ -fun Context.launchWebOverlay(url: String, clazz: Class = WebOverlayActivity::class.java) { +private inline fun Context.launchWebOverlayImpl(url: String) { val argUrl = url.formattedFbUrl L.v { "Launch received: $url\nLaunch web overlay: $argUrl" } if (argUrl.isFacebookUrl && argUrl.contains("/logout.php")) FbCookie.logout(this) else if (!(Prefs.linksInDefaultApp && resolveActivityForUri(Uri.parse(argUrl)))) - startActivity(clazz, false, intentBuilder = { + startActivity(false, intentBuilder = { putExtra(ARG_URL, argUrl) }) } +fun Context.launchWebOverlay(url: String) = launchWebOverlayImpl(url) + +fun Context.launchWebOverlayBasic(url: String) = launchWebOverlayImpl(url) + private fun Context.fadeBundle() = ActivityOptions.makeCustomAnimation(this, android.R.anim.fade_in, android.R.anim.fade_out).toBundle() fun Context.launchImageActivity(imageUrl: String, text: String?) { - startActivity(ImageActivity::class.java, intentBuilder = { + startActivity(intentBuilder = { putExtras(fadeBundle()) putExtra(ARG_IMAGE_URL, imageUrl) putExtra(ARG_TEXT, text) @@ -88,15 +94,11 @@ fun Context.launchImageActivity(imageUrl: String, text: String?) { } fun Activity.launchTabCustomizerActivity() { - startActivityForResult(TabCustomizerActivity::class.java, - SettingsActivity.ACTIVITY_REQUEST_TABS, bundleBuilder = { + startActivityForResult(SettingsActivity.ACTIVITY_REQUEST_TABS, bundleBuilder = { with(fadeBundle()) }) } -fun Activity.launchIntroActivity(cookieList: ArrayList) - = launchNewTask(IntroActivity::class.java, cookieList, true) - fun WebOverlayActivity.url(): String { return intent.getStringExtra(ARG_URL) ?: FbItem.FEED.url } @@ -127,17 +129,49 @@ fun Activity.setFrostTheme(forceTransparent: Boolean = false) { setTheme(if (isTransparent) R.style.FrostTheme_Light_Transparent else R.style.FrostTheme_Light) } -fun Activity.setFrostColors(toolbar: Toolbar? = null, themeWindow: Boolean = true, - texts: Array = arrayOf(), headers: Array = arrayOf(), backgrounds: Array = arrayOf()) { - statusBarColor = Prefs.headerColor.darken(0.1f).withAlpha(255) - if (Prefs.tintNavBar) navigationBarColor = Prefs.headerColor - if (themeWindow) window.setBackgroundDrawable(ColorDrawable(Prefs.bgColor)) - toolbar?.setBackgroundColor(Prefs.headerColor) - toolbar?.setTitleTextColor(Prefs.iconColor) - toolbar?.overflowIcon?.setTint(Prefs.iconColor) - texts.forEach { it.setTextColor(Prefs.textColor) } - headers.forEach { it.setBackgroundColor(Prefs.headerColor) } - backgrounds.forEach { it.setBackgroundColor(Prefs.bgColor) } +class ActivityThemeUtils { + + private var toolbar: Toolbar? = null + var themeWindow = true + private var texts = mutableListOf() + private var headers = mutableListOf() + private var backgrounds = mutableListOf() + + fun toolbar(toolbar: Toolbar) { + this.toolbar = toolbar + } + + fun text(vararg views: TextView) { + texts.addAll(views) + } + + fun header(vararg views: View) { + headers.addAll(views) + } + + fun background(vararg views: View) { + backgrounds.addAll(views) + } + + fun theme(activity: Activity) { + with(activity) { + statusBarColor = Prefs.headerColor.darken(0.1f).withAlpha(255) + if (Prefs.tintNavBar) navigationBarColor = Prefs.headerColor + if (themeWindow) window.setBackgroundDrawable(ColorDrawable(Prefs.bgColor)) + toolbar?.setBackgroundColor(Prefs.headerColor) + toolbar?.setTitleTextColor(Prefs.iconColor) + toolbar?.overflowIcon?.setTint(Prefs.iconColor) + texts.forEach { it.setTextColor(Prefs.textColor) } + headers.forEach { it.setBackgroundColor(Prefs.headerColor) } + backgrounds.forEach { it.setBackgroundColor(Prefs.bgColor) } + } + } +} + +inline fun Activity.setFrostColors(builder: ActivityThemeUtils.() -> Unit) { + val themer = ActivityThemeUtils() + themer.builder() + themer.theme(this) } fun frostAnswers(action: Answers.() -> Unit) { @@ -227,16 +261,10 @@ inline val String?.isIndependent: Boolean if (this == null || length < 5) return false // ignore short queries if (this[0] == '#' && !contains('/')) return false // ignore element values if (startsWith("http") && !isFacebookUrl) return true // ignore non facebook urls - if (independentSegments.any { contains(it) }) return true // known independent segments - if (this.startsWith("#!/")) return false // ignore these links for now if (dependentSegments.any { contains(it) }) return false // ignore known dependent segments return true } -val independentSegments = arrayOf( - "messages/read/?tid=cid" -) - val dependentSegments = arrayOf( "photoset_token", "direct_action_execute", "messages/?pageNum", "sharer.php", /** @@ -262,13 +290,21 @@ fun Context.frostChangelog() = showChangelog(R.xml.frost_changelog, Prefs.textCo } } +fun Context.frostUriFromFile(file: File): Uri = + FileProvider.getUriForFile(this, + BuildConfig.APPLICATION_ID + ".provider", + file) + inline fun Context.sendFrostEmail(@StringRes subjectId: Int, crossinline builder: EmailBuilder.() -> Unit) = sendFrostEmail(string(subjectId), builder) inline fun Context.sendFrostEmail(subjectId: String, crossinline builder: EmailBuilder.() -> Unit) = sendEmail(string(R.string.dev_email), subjectId) { builder() + addFrostDetails() +} +fun EmailBuilder.addFrostDetails() { addItem("Prev version", Prefs.prevVersionCode.toString()) val proTag = if (IS_FROST_PRO) "TY" else "FP" addItem("Random Frost ID", "${Prefs.frostId}-$proTag") diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt new file mode 100644 index 00000000..3cc10236 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt @@ -0,0 +1,54 @@ +package com.pitchedapps.frost.web + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.View +import android.webkit.CookieManager +import android.webkit.WebResourceRequest +import android.webkit.WebView +import com.pitchedapps.frost.facebook.FB_USER_MATCHER +import com.pitchedapps.frost.facebook.USER_AGENT_BASIC +import com.pitchedapps.frost.facebook.get +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.isFacebookUrl +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread + +/** + * Created by Allan Wang on 2018-01-05. + * + * A barebone webview with a refresh listener + */ +class DebugWebView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : WebView(context, attrs, defStyleAttr) { + + var onPageFinished: (String?) -> Unit = {} + + init { + setupWebview() + } + + @SuppressLint("SetJavaScriptEnabled") + fun setupWebview() { + settings.javaScriptEnabled = true + settings.userAgentString = USER_AGENT_BASIC + setLayerType(View.LAYER_TYPE_HARDWARE, null) + webViewClient = DebugClient() + } + + private inner class DebugClient : BaseWebViewClient() { + + override fun onPageFinished(view: WebView, url: String?) { + super.onPageFinished(view, url) + onPageFinished(url) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt index 8eec3402..9a3dc331 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt @@ -46,7 +46,7 @@ fun FrostWebView.requestWebOverlay(url: String): Boolean { //already overlay; manage user agent if (userAgentString != USER_AGENT_BASIC && url.formattedFbUrl.shouldUseBasicAgent) { L.i { "Switch to basic agent overlay" } - context.launchWebOverlay(url, WebOverlayBasicActivity::class.java) + context.launchWebOverlayBasic(url) return true } if (context is WebOverlayBasicActivity && !url.formattedFbUrl.shouldUseBasicAgent) { 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 a826066d..9e5f4996 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt @@ -55,9 +55,9 @@ open class FrostWebViewClient(val web: FrostWebView) : BaseWebViewClient() { fun launchLogin(c: Context) { if (c is MainActivity && c.cookies().isNotEmpty()) - c.launchNewTask(SelectorActivity::class.java, c.cookies()) + c.launchNewTask(c.cookies()) else - c.launchNewTask(LoginActivity::class.java) + c.launchNewTask() } private fun injectBackgroundColor() { diff --git a/app/src/main/res/layout/activity_debug.xml b/app/src/main/res/layout/activity_debug.xml new file mode 100644 index 00000000..42428e49 --- /dev/null +++ b/app/src/main/res/layout/activity_debug.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index cd5eef08..8b3ca157 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -2,7 +2,6 @@ Incomplete report Frost for Facebook: Debug Report + + Debug from the Web + Navigate to the page with an issue and send the resources for debugging. + + Parsing Data \ No newline at end of file diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml index 334ea0d7..568043d6 100644 --- a/app/src/main/res/xml/file_paths.xml +++ b/app/src/main/res/xml/file_paths.xml @@ -1,9 +1,12 @@ - + name="external" + path="/" /> + + \ No newline at end of file diff --git a/app/src/main/res/xml/frost_changelog.xml b/app/src/main/res/xml/frost_changelog.xml index 7e9612b4..28978193 100644 --- a/app/src/main/res/xml/frost_changelog.xml +++ b/app/src/main/res/xml/frost_changelog.xml @@ -6,13 +6,21 @@ --> + + + + + + + + + - diff --git a/app/src/releaseTest/res/xml/file_paths.xml b/app/src/releaseTest/res/xml/file_paths.xml deleted file mode 100644 index 139a1972..00000000 --- a/app/src/releaseTest/res/xml/file_paths.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt new file mode 100644 index 00000000..63c11bac --- /dev/null +++ b/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt @@ -0,0 +1,25 @@ +package com.pitchedapps.frost.debugger + +import com.pitchedapps.frost.facebook.FB_URL_BASE +import com.pitchedapps.frost.internal.COOKIE +import org.junit.Test +import java.io.File +import java.util.concurrent.CountDownLatch + +/** + * Created by Allan Wang on 05/01/18. + */ +class OfflineWebsiteTest { + + @Test + fun basic() { + val countdown = CountDownLatch(1) + OfflineWebsite(FB_URL_BASE, COOKIE, File("app/build/offline_test")) + .loadAndZip("test") { + println("Outcome $it") + countdown.countDown() + } + countdown.await() + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/pitchedapps/frost/utils/UrlTests.kt b/app/src/test/kotlin/com/pitchedapps/frost/utils/UrlTests.kt index c400c0f7..5dbcad65 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/utils/UrlTests.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/utils/UrlTests.kt @@ -22,7 +22,7 @@ class UrlTests { assertFalse("#!".isIndependent, "#!") assertFalse("#!/".isIndependent, "#!/") assertTrue("/this/is/valid".isIndependent, "url segments") -// assertTrue("#!/facebook/segment".isIndependent, "facebook segments") + assertTrue("#!/facebook/segment".isIndependent, "facebook segments") } @Test diff --git a/docs/Changelog.md b/docs/Changelog.md index f258a5dc..06452550 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,5 +1,9 @@ # Changelog +## v1.7.7 +* Fix overlay loading +* Improve image loading + ## v1.7.5 * Mark notifications as read when clicked! * Create menu parser diff --git a/gradle.properties b/gradle.properties index 41d97ba8..e36e8b40 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ MIN_SDK=21 TARGET_SDK=27 BUILD_TOOLS=27.0.2 -KAU=5a29ae9 +KAU=f145f6a KOTLIN=1.2.10 ANDROID_SUPPORT_LIBS=27.0.2 -- cgit v1.2.3