aboutsummaryrefslogtreecommitdiff
path: root/app/src
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2018-01-07 02:43:57 -0500
committerGitHub <noreply@github.com>2018-01-07 02:43:57 -0500
commit8aece5e3f9209d7c161410c304655f0aec2d6054 (patch)
tree1a68d7289a7e67dfaba37a9152fc3942f944bc5e /app/src
parent726d2a4dc3d3158490ca94b660b195898becb30a (diff)
downloadfrost-8aece5e3f9209d7c161410c304655f0aec2d6054.tar.gz
frost-8aece5e3f9209d7c161410c304655f0aec2d6054.tar.bz2
frost-8aece5e3f9209d7c161410c304655f0aec2d6054.zip
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
Diffstat (limited to 'app/src')
-rw-r--r--app/src/debug/res/xml/file_paths.xml9
-rw-r--r--app/src/main/AndroidManifest.xml3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt14
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt11
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/DebugActivity.kt75
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt136
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/IntroActivity.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt13
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/SelectorActivity.kt9
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt21
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt5
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt307
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/DownloadService.kt5
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt171
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt94
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt54
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt4
-rw-r--r--app/src/main/res/layout/activity_debug.xml36
-rw-r--r--app/src/main/res/layout/activity_login.xml1
-rw-r--r--app/src/main/res/values/strings_pref_debug.xml5
-rw-r--r--app/src/main/res/xml/file_paths.xml13
-rw-r--r--app/src/main/res/xml/frost_changelog.xml10
-rw-r--r--app/src/releaseTest/res/xml/file_paths.xml9
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt25
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/utils/UrlTests.kt2
27 files changed, 818 insertions, 222 deletions
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 @@
-<?xml version="1.0" encoding="utf-8"?>
-<paths>
- <external-path
- name="Frost_images"
- path="Android/data/com.pitchedapps.frost.debug/files/Pictures" />
- <external-path
- name="Frost_public_images"
- path="Pictures/Frost" />
-</paths> \ 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 @@
<activity
android:name=".activities.ImageActivity"
android:theme="@style/FrostTheme.Transparent" />
+ <activity
+ android:name=".activities.DebugActivity"
+ android:theme="@style/FrostTheme" />
<service
android:name=".services.NotificationService"
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt
index 2107f31f..f9da7073 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt
@@ -19,13 +19,17 @@ class StartActivity : KauBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
FbCookie.switchBackUser {
- loadFbCookiesAsync { cookies ->
+ 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<MainActivity>(cookies)
+ else
+ launchNewTask<SelectorActivity>(cookies)
+ } else
+ launchNewTask<LoginActivity>()
}
}
}
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<LoginActivity>(clearStack = false)
+ -4L -> launchNewTask<SelectorActivity>( 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<in Bitmap>?) = callback(resource, true)
+ override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) =
+ 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<MainActivity>(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<IntroActivity>(cookies, true)
+ else
+ launchNewTask<MainActivity>(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<AccountItem>, 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<LoginActivity>()
+ else FbCookie.switchUser(item.cookie, { launchNewTask<MainActivity>(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<AboutActivity>(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<IntroActivity>(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 <a href="https://github.com/JonasCz/save-for-offline">Save for Offline</a>
+ */
+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<String, String>()
+ 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<String>()
+
+ private val cssQueue = mutableSetOf<String>()
+
+ 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<String, Boolean, Boolean>({
+ 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<String, Set<String>, Set<String>>({
+ 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 <T> 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<String>) {
+ 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<String>.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<DebugActivity>(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<MaterialDialog>.() -> 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<MaterialDialog>.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<MaterialDialog>.simplifyJsoup(doc: Document) {
-// weakRef.get() ?: return
-// val q = query.first { doc.select(it).isNotEmpty() }
-// createReport(doc.select(q).outerHtml())
-// }
-//
-// private fun AnkoAsyncContext<MaterialDialog>.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<out Activity>, cookieList: ArrayList<CookieModel> = arrayListOf(), clearStack: Boolean = false) {
- startActivity(clazz, clearStack, intentBuilder = {
+inline fun <reified T : Activity> Context.launchNewTask(cookieList: ArrayList<CookieModel> = arrayListOf(), clearStack: Boolean = false) {
+ startActivity<T>(clearStack, intentBuilder = {
putParcelableArrayListExtra(EXTRA_COOKIES, cookieList)
})
}
fun Context.launchLogin(cookieList: ArrayList<CookieModel>, clearStack: Boolean = true) {
- if (cookieList.isNotEmpty()) launchNewTask(SelectorActivity::class.java, cookieList, clearStack)
- else launchNewTask(LoginActivity::class.java, clearStack = clearStack)
+ if (cookieList.isNotEmpty()) launchNewTask<SelectorActivity>(cookieList, clearStack)
+ else launchNewTask<LoginActivity>(clearStack = clearStack)
}
fun Activity.cookies(): ArrayList<CookieModel> {
@@ -65,22 +67,26 @@ fun Activity.cookies(): ArrayList<CookieModel> {
* 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<out WebOverlayActivityBase> = WebOverlayActivity::class.java) {
+private inline fun <reified T : WebOverlayActivityBase> 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<T>(false, intentBuilder = {
putExtra(ARG_URL, argUrl)
})
}
+fun Context.launchWebOverlay(url: String) = launchWebOverlayImpl<WebOverlayActivity>(url)
+
+fun Context.launchWebOverlayBasic(url: String) = launchWebOverlayImpl<WebOverlayBasicActivity>(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<ImageActivity>(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<TabCustomizerActivity>(SettingsActivity.ACTIVITY_REQUEST_TABS, bundleBuilder = {
with(fadeBundle())
})
}
-fun Activity.launchIntroActivity(cookieList: ArrayList<CookieModel>)
- = 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<TextView> = arrayOf(), headers: Array<View> = arrayOf(), backgrounds: Array<View> = 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<TextView>()
+ private var headers = mutableListOf<View>()
+ private var backgrounds = mutableListOf<View>()
+
+ 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<SelectorActivity>(c.cookies())
else
- c.launchNewTask(LoginActivity::class.java)
+ c.launchNewTask<LoginActivity>()
}
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ tools:context=".activities.DebugActivity">
+
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:background="?attr/colorPrimary"
+ android:theme="@style/AppTheme.AppBarOverlay"
+ app:layout_scrollFlags="scroll|enterAlways"
+ app:popupTheme="@style/AppTheme.PopupOverlay" />
+
+ <android.support.v4.widget.SwipeRefreshLayout
+ android:id="@+id/swipe_refresh"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="?attr/actionBarSize">
+
+ <com.pitchedapps.frost.web.DebugWebView
+ android:id="@+id/debug_webview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:focusable="true"
+ android:focusableInTouchMode="true" />
+
+ </android.support.v4.widget.SwipeRefreshLayout>
+
+ <include layout="@layout/view_main_fab" />
+
+</android.support.design.widget.CoordinatorLayout>
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 @@
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
diff --git a/app/src/main/res/values/strings_pref_debug.xml b/app/src/main/res/values/strings_pref_debug.xml
index 975c626d..8e8c051c 100644
--- a/app/src/main/res/values/strings_pref_debug.xml
+++ b/app/src/main/res/values/strings_pref_debug.xml
@@ -11,4 +11,9 @@
<string name="debug_incomplete">Incomplete report</string>
<string name="debug_report_email_title" translatable="false">Frost for Facebook: Debug Report</string>
+
+ <string name="debug_web">Debug from the Web</string>
+ <string name="debug_web_desc">Navigate to the page with an issue and send the resources for debugging.</string>
+
+ <string name="parsing_data">Parsing Data</string>
</resources> \ 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 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
- name="Frost_images"
- path="Android/data/com.pitchedapps.frost/files/Pictures" />
- <external-path
- name="Frost_public_images"
- path="Pictures/Frost" />
+ name="external"
+ path="/" />
+ <cache-path
+ name="cache"
+ path="/" />
+ <files-path
+ name="files"
+ path="/" />
</paths> \ 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 @@
<item text="" />
-->
+ <version title="v1.7.7" />
+ <item text="Fix overlay loading" />
+ <item text="Improve image loading" />
+ <item text="" />
+ <item text="" />
+ <item text="" />
+ <item text="" />
+ <item text="" />
+
<version title="v1.7.5" />
<item text="Mark notifications as read when clicked!" />
<item text="Create menu parser" />
<item text="Implement automatic web fallback" />
<item text="Optimize logging" />
<item text="Fix link loading for some locations (eg changing profile pictures)" />
- <item text="" />
<version title="v1.7.2" />
<item text="Optimize login view" />
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 @@
-<?xml version="1.0" encoding="utf-8"?>
-<paths>
- <external-path
- name="Frost_images"
- path="Android/data/com.pitchedapps.frost.test/files/Pictures" />
- <external-path
- name="Frost_public_images"
- path="Pictures/Frost" />
-</paths> \ 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