mirror of
https://github.com/AllanWang/Frost-for-Facebook.git
synced 2024-11-10 04:52:38 +01:00
Merge pull request #1260 from AllanWang/update/coroutines
Update/coroutines
This commit is contained in:
commit
9fb5d8a3d2
@ -156,6 +156,7 @@ dependencies {
|
||||
androidTestImplementation kauDependency.espresso
|
||||
androidTestImplementation kauDependency.testRules
|
||||
androidTestImplementation kauDependency.testRunner
|
||||
androidTestImplementation "com.squareup.okhttp3:mockwebserver:${OKHTTP}"
|
||||
|
||||
testImplementation kauDependency.kotlinTest
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-reflect:${KOTLIN}"
|
||||
@ -180,6 +181,17 @@ dependencies {
|
||||
//noinspection GradleDependency
|
||||
implementation "ca.allanwang.kau:core-ui:$KAU"
|
||||
|
||||
// TODO temp
|
||||
implementation "org.jetbrains.anko:anko-commons:0.10.8"
|
||||
|
||||
// implementation "org.koin:koin-android:${KOIN}"
|
||||
// testImplementation "org.koin:koin-test:${KOIN}"
|
||||
// androidTestImplementation "org.koin:koin-test:${KOIN}"
|
||||
|
||||
// androidTestImplementation "io.mockk:mockk:${MOCKK}"
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${COROUTINES}"
|
||||
|
||||
implementation "org.apache.commons:commons-text:${COMMONS_TEXT}"
|
||||
|
||||
implementation "com.devbrackets.android:exomedia:${EXOMEDIA}"
|
||||
@ -214,6 +226,8 @@ dependencies {
|
||||
|
||||
implementation "com.squareup.okhttp3:okhttp:${OKHTTP}"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:${OKHTTP}"
|
||||
androidTestImplementation "com.squareup.okhttp3:mockwebserver:${OKHTTP}"
|
||||
|
||||
|
||||
implementation "co.zsmb:materialdrawer-kt:${MATERIAL_DRAWER_KT}"
|
||||
|
||||
|
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2018 Allan Wang
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.pitchedapps.frost.activities
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import com.pitchedapps.frost.helper.getResource
|
||||
import com.pitchedapps.frost.utils.ARG_COOKIE
|
||||
import com.pitchedapps.frost.utils.ARG_IMAGE_URL
|
||||
import com.pitchedapps.frost.utils.ARG_TEXT
|
||||
import com.pitchedapps.frost.utils.isIndirectImageUrl
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
import okio.Buffer
|
||||
import okio.Okio
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.Timeout
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ImageActivityTest {
|
||||
|
||||
@get:Rule
|
||||
val activity: ActivityTestRule<ImageActivity> = ActivityTestRule(ImageActivity::class.java, true, false)
|
||||
|
||||
@get:Rule
|
||||
val globalTimeout: Timeout = Timeout.seconds(15)
|
||||
|
||||
private fun launchActivity(imageUrl: String, text: String? = null, cookie: String? = null) {
|
||||
assertFalse(
|
||||
imageUrl.isIndirectImageUrl,
|
||||
"For simplicity, urls that are direct will be used without modifications in the production code."
|
||||
)
|
||||
val intent = Intent().apply {
|
||||
putExtra(ARG_IMAGE_URL, imageUrl)
|
||||
putExtra(ARG_TEXT, text)
|
||||
putExtra(ARG_COOKIE, cookie)
|
||||
}
|
||||
activity.launchActivity(intent)
|
||||
}
|
||||
|
||||
private val mockServer: MockWebServer by lazy {
|
||||
val magentaImg = Buffer()
|
||||
magentaImg.writeAll(Okio.source(getResource("bayer-pattern.jpg")))
|
||||
MockWebServer().apply {
|
||||
setDispatcher(object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse =
|
||||
when {
|
||||
request.path.contains("text") -> MockResponse().setResponseCode(200).setBody("Valid mock text response")
|
||||
request.path.contains("image") -> MockResponse().setResponseCode(200).setBody(magentaImg)
|
||||
else -> MockResponse().setResponseCode(404).setBody("Error mock response")
|
||||
}
|
||||
})
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validImageTest() {
|
||||
launchActivity(mockServer.url("image").toString())
|
||||
mockServer.takeRequest()
|
||||
with(activity.activity) {
|
||||
assertEquals(1, mockServer.requestCount, "One http request expected")
|
||||
assertEquals(fabAction, FabStates.DOWNLOAD, "Image should be successful, image should be downloaded")
|
||||
assertTrue(tempFile.exists(), "Image should be located at temp file")
|
||||
assertTrue(
|
||||
System.currentTimeMillis() - tempFile.lastModified() < 2000L,
|
||||
"Image should have been modified within the last few seconds"
|
||||
)
|
||||
assertNull(errorRef, "No error should exist")
|
||||
tempFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun invalidImageTest() {
|
||||
launchActivity(mockServer.url("text").toString())
|
||||
mockServer.takeRequest()
|
||||
with(activity.activity) {
|
||||
assertEquals(1, mockServer.requestCount, "One http request expected")
|
||||
assertEquals(fabAction, FabStates.ERROR, "Text should not be a valid image format, error state expected")
|
||||
assertEquals("Image format not supported", errorRef?.message, "Error message mismatch")
|
||||
assertFalse(tempFile.exists(), "Temp file should have been removed")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun errorTest() {
|
||||
launchActivity(mockServer.url("error").toString())
|
||||
mockServer.takeRequest()
|
||||
with(activity.activity) {
|
||||
assertEquals(1, mockServer.requestCount, "One http request expected")
|
||||
assertEquals(fabAction, FabStates.ERROR, "Error response code, error state expected")
|
||||
assertEquals(
|
||||
"Unsuccessful response for image: Error mock response",
|
||||
errorRef?.message,
|
||||
"Error message mismatch"
|
||||
)
|
||||
assertFalse(tempFile.exists(), "Temp file should have been removed")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2018 Allan Wang
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.pitchedapps.frost.helper
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import java.io.InputStream
|
||||
|
||||
val context: Context
|
||||
get() = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
fun getAsset(asset: String): InputStream =
|
||||
context.assets.open(asset)
|
||||
|
||||
private class Helper
|
||||
|
||||
fun getResource(resource: String): InputStream =
|
||||
Helper::class.java.classLoader!!.getResource(resource).openStream()
|
BIN
app/src/androidTest/resources/bayer-pattern.jpg
Normal file
BIN
app/src/androidTest/resources/bayer-pattern.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.0 KiB |
BIN
app/src/androidTest/resources/magenta.png
Normal file
BIN
app/src/androidTest/resources/magenta.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 165 B |
@ -146,6 +146,11 @@
|
||||
android:enabled="true"
|
||||
android:label="@string/frost_requests"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
<service
|
||||
android:name=".services.LocalService"
|
||||
android:enabled="true"
|
||||
android:label="@string/local_service_name"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<receiver
|
||||
android:name=".services.UpdateReceiver"
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
package com.pitchedapps.frost.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
@ -28,13 +29,14 @@ 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.colorToForeground
|
||||
import ca.allanwang.kau.utils.copyFromInputStream
|
||||
import ca.allanwang.kau.utils.fadeOut
|
||||
import ca.allanwang.kau.utils.fadeScaleTransition
|
||||
import ca.allanwang.kau.utils.isHidden
|
||||
import ca.allanwang.kau.utils.isVisible
|
||||
import ca.allanwang.kau.utils.scaleXY
|
||||
import ca.allanwang.kau.utils.setIcon
|
||||
import ca.allanwang.kau.utils.tint
|
||||
import ca.allanwang.kau.utils.use
|
||||
import ca.allanwang.kau.utils.withAlpha
|
||||
import ca.allanwang.kau.utils.withMinAlpha
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
@ -48,12 +50,12 @@ import com.pitchedapps.frost.facebook.get
|
||||
import com.pitchedapps.frost.facebook.requests.call
|
||||
import com.pitchedapps.frost.facebook.requests.getFullSizedImageUrl
|
||||
import com.pitchedapps.frost.facebook.requests.requestBuilder
|
||||
import com.pitchedapps.frost.services.LocalService
|
||||
import com.pitchedapps.frost.utils.ARG_COOKIE
|
||||
import com.pitchedapps.frost.utils.ARG_IMAGE_URL
|
||||
import com.pitchedapps.frost.utils.ARG_TEXT
|
||||
import com.pitchedapps.frost.utils.L
|
||||
import com.pitchedapps.frost.utils.Prefs
|
||||
import com.pitchedapps.frost.utils.createFreshFile
|
||||
import com.pitchedapps.frost.utils.frostSnackbar
|
||||
import com.pitchedapps.frost.utils.frostUriFromFile
|
||||
import com.pitchedapps.frost.utils.isIndirectImageUrl
|
||||
@ -63,12 +65,13 @@ import com.pitchedapps.frost.utils.sendFrostEmail
|
||||
import com.pitchedapps.frost.utils.setFrostColors
|
||||
import com.sothree.slidinguppanel.SlidingUpPanelLayout
|
||||
import kotlinx.android.synthetic.main.activity_image.*
|
||||
import okhttp3.Response
|
||||
import org.jetbrains.anko.activityUiThreadWithContext
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
@ -79,14 +82,13 @@ import java.util.Locale
|
||||
*/
|
||||
class ImageActivity : KauBaseActivity() {
|
||||
|
||||
@Volatile
|
||||
internal var errorRef: Throwable? = null
|
||||
|
||||
private lateinit var tempDir: File
|
||||
|
||||
/**
|
||||
* Reference to the temporary file path
|
||||
*/
|
||||
private lateinit var tempFile: File
|
||||
internal lateinit var tempFile: File
|
||||
/**
|
||||
* Reference to path for downloaded image
|
||||
* Nonnull once the image is downloaded by the user
|
||||
@ -94,13 +96,12 @@ class ImageActivity : KauBaseActivity() {
|
||||
internal var savedFile: File? = null
|
||||
/**
|
||||
* Indicator for fab's click result
|
||||
* Can be called from any thread
|
||||
*/
|
||||
internal var fabAction: FabStates = FabStates.NOTHING
|
||||
set(value) {
|
||||
if (field == value) return
|
||||
field = value
|
||||
runOnUiThread { value.update(image_fab) }
|
||||
value.update(image_fab)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -112,21 +113,18 @@ class ImageActivity : KauBaseActivity() {
|
||||
private const val TIME_FORMAT = "yyyyMMdd_HHmmss"
|
||||
private const val IMG_TAG = "Frost"
|
||||
private const val IMG_EXTENSION = ".png"
|
||||
private const val PURGE_TIME: Long = 10 * 60 * 1000 // 10 min block
|
||||
const val PURGE_TIME: Long = 10 * 60 * 1000 // 10 min block
|
||||
private val L = KauLoggerExtension("Image", com.pitchedapps.frost.utils.L)
|
||||
|
||||
fun cacheDir(context: Context): File =
|
||||
File(context.cacheDir, IMAGE_FOLDER)
|
||||
}
|
||||
|
||||
private val cookie: String? by lazy { intent.getStringExtra(ARG_COOKIE) }
|
||||
|
||||
val imageUrl: String by lazy { intent.getStringExtra(ARG_IMAGE_URL).trim('"') }
|
||||
|
||||
private val trueImageUrl: String by lazy {
|
||||
val result = if (!imageUrl.isIndirectImageUrl) imageUrl
|
||||
else cookie?.getFullSizedImageUrl(imageUrl)?.blockingGet() ?: imageUrl
|
||||
if (result != imageUrl)
|
||||
L.v { "Launching with true url $result" }
|
||||
result
|
||||
}
|
||||
private lateinit var trueImageUrl: Deferred<String>
|
||||
|
||||
private val imageText: String? by lazy { intent.getStringExtra(ARG_TEXT) }
|
||||
|
||||
@ -138,11 +136,27 @@ class ImageActivity : KauBaseActivity() {
|
||||
)}_${Math.abs(imageUrl.hashCode())}"
|
||||
}
|
||||
|
||||
private fun loadError(e: Throwable) {
|
||||
errorRef = e
|
||||
e.logFrostEvent("Image load error")
|
||||
if (image_progress.isVisible)
|
||||
image_progress.fadeOut()
|
||||
tempFile.delete()
|
||||
fabAction = FabStates.ERROR
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
intent?.extras ?: return finish()
|
||||
L.i { "Displaying image" }
|
||||
L.v { "Displaying image $imageUrl" }
|
||||
trueImageUrl = async(Dispatchers.IO) {
|
||||
val result = if (!imageUrl.isIndirectImageUrl) imageUrl
|
||||
else cookie?.getFullSizedImageUrl(imageUrl)?.blockingGet() ?: imageUrl
|
||||
if (result != imageUrl)
|
||||
L.v { "Launching with true url $result" }
|
||||
result
|
||||
}
|
||||
|
||||
val layout = if (!imageText.isNullOrBlank()) R.layout.activity_image else R.layout.activity_image_textless
|
||||
setContentView(layout)
|
||||
image_container.setBackgroundColor(
|
||||
@ -165,82 +179,23 @@ class ImageActivity : KauBaseActivity() {
|
||||
})
|
||||
image_fab.setOnClickListener { fabAction.onClick(this) }
|
||||
image_photo.setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
|
||||
override fun onImageLoadError(e: Exception?) {
|
||||
errorRef = e
|
||||
e.logFrostEvent("Image load error")
|
||||
L.e { "Failed to load image $imageUrl" }
|
||||
tempFile?.delete()
|
||||
fabAction = FabStates.ERROR
|
||||
override fun onImageLoadError(e: Exception) {
|
||||
loadError(e)
|
||||
}
|
||||
})
|
||||
setFrostColors {
|
||||
themeWindow = false
|
||||
}
|
||||
tempDir = File(cacheDir, IMAGE_FOLDER)
|
||||
tempFile = File(tempDir, imageHash)
|
||||
doAsync({
|
||||
L.e(it) { "Failed to load image $imageHash" }
|
||||
errorRef = it
|
||||
runOnUiThread { image_progress.fadeOut() }
|
||||
tempFile.delete()
|
||||
fabAction = FabStates.ERROR
|
||||
}) {
|
||||
val loaded = loadImage(tempFile)
|
||||
uiThread {
|
||||
image_progress.fadeOut()
|
||||
if (!loaded) {
|
||||
fabAction = FabStates.ERROR
|
||||
} else {
|
||||
image_photo.setImage(ImageSource.uri(frostUriFromFile(tempFile)))
|
||||
fabAction = FabStates.DOWNLOAD
|
||||
image_photo.animate().alpha(1f).scaleXY(1f).start()
|
||||
}
|
||||
}
|
||||
tempFile = File(cacheDir(this), imageHash)
|
||||
launch(CoroutineExceptionHandler { _, throwable -> loadError(throwable) }) {
|
||||
downloadImageTo(tempFile)
|
||||
image_progress.fadeOut()
|
||||
image_photo.setImage(ImageSource.uri(frostUriFromFile(tempFile)))
|
||||
fabAction = FabStates.DOWNLOAD
|
||||
image_photo.animate().alpha(1f).scaleXY(1f).start()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to load the image to [file]
|
||||
* Returns true if successful
|
||||
* Note that this is a long execution and should not be done on the UI thread
|
||||
*/
|
||||
private fun loadImage(file: File): Boolean {
|
||||
if (file.exists() && file.length() > 1) {
|
||||
file.setLastModified(System.currentTimeMillis())
|
||||
L.d { "Loading from local cache ${file.absolutePath}" }
|
||||
return true
|
||||
}
|
||||
val response = getImageResponse()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
L.e { "Unsuccessful response for image" }
|
||||
errorRef = Throwable("Unsuccessful response for image")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!file.createFreshFile()) {
|
||||
L.e { "Could not create temp file" }
|
||||
return false
|
||||
}
|
||||
|
||||
var valid = false
|
||||
|
||||
response.body()?.byteStream()?.use { input ->
|
||||
file.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
L.e { "Failed to copy file" }
|
||||
file.delete()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun createPublicMediaFile(): File {
|
||||
val timeStamp = SimpleDateFormat(TIME_FORMAT, Locale.getDefault()).format(Date())
|
||||
@ -251,20 +206,56 @@ class ImageActivity : KauBaseActivity() {
|
||||
return File.createTempFile(imageFileName, IMG_EXTENSION, frostDir)
|
||||
}
|
||||
|
||||
private fun getImageResponse(): Response = cookie.requestBuilder()
|
||||
.url(trueImageUrl)
|
||||
.get()
|
||||
.call()
|
||||
.execute()
|
||||
|
||||
/**
|
||||
* Saves the image to the specified file, creating it if it doesn't exist.
|
||||
* Returns true if a change is made, false otherwise.
|
||||
* Throws an error if something goes wrong.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
private fun downloadImageTo(file: File) {
|
||||
val body = getImageResponse().body()
|
||||
?: throw IOException("Failed to retrieve image body")
|
||||
body.byteStream().use { input ->
|
||||
file.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
private suspend fun downloadImageTo(file: File): Boolean {
|
||||
val exceptionHandler = CoroutineExceptionHandler { _, err ->
|
||||
if (file.isFile && file.length() == 0L) {
|
||||
file.delete()
|
||||
}
|
||||
throw err
|
||||
}
|
||||
return withContext(Dispatchers.IO + exceptionHandler) {
|
||||
if (!file.isFile) {
|
||||
file.parentFile.mkdirs()
|
||||
file.createNewFile()
|
||||
} else {
|
||||
file.setLastModified(System.currentTimeMillis())
|
||||
}
|
||||
|
||||
// Forbid overwrites
|
||||
if (file.length() > 0) {
|
||||
L.i { "Forbid image overwrite" }
|
||||
return@withContext false
|
||||
}
|
||||
if (tempFile.isFile && tempFile.length() > 0) {
|
||||
if (tempFile == file) {
|
||||
return@withContext false
|
||||
}
|
||||
tempFile.copyTo(file)
|
||||
return@withContext true
|
||||
}
|
||||
|
||||
// No temp file, download ourselves
|
||||
val response = cookie.requestBuilder()
|
||||
.url(trueImageUrl.await())
|
||||
.get()
|
||||
.call()
|
||||
.execute()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Unsuccessful response for image: ${response.peekBody(128).string()}")
|
||||
}
|
||||
|
||||
val body = response.body() ?: throw IOException("Failed to retrieve image body")
|
||||
|
||||
file.copyFromInputStream(body.byteStream())
|
||||
|
||||
return@withContext true
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,45 +263,25 @@ class ImageActivity : KauBaseActivity() {
|
||||
kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ ->
|
||||
L.d { "Download image callback granted: $granted" }
|
||||
if (granted) {
|
||||
doAsync {
|
||||
val errorHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
loadError(throwable)
|
||||
frostSnackbar(R.string.image_download_fail)
|
||||
}
|
||||
launch(errorHandler) {
|
||||
val destination = createPublicMediaFile()
|
||||
var success = true
|
||||
try {
|
||||
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)
|
||||
if (success) fabAction = FabStates.SHARE
|
||||
}
|
||||
}
|
||||
downloadImageTo(destination)
|
||||
L.d { "Download image async finished" }
|
||||
scanMedia(destination)
|
||||
savedFile = destination
|
||||
frostSnackbar(R.string.image_download_success)
|
||||
fabAction = FabStates.SHARE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
val purge = System.currentTimeMillis() - PURGE_TIME
|
||||
tempDir.listFiles(FileFilter { it.isFile && it.lastModified() < purge })?.forEach {
|
||||
it.delete()
|
||||
}
|
||||
LocalService.schedule(this, LocalService.Flag.PURGE_IMAGE)
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
@ -30,50 +30,83 @@ import com.pitchedapps.frost.utils.launchLogin
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.subjects.SingleSubject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-05-30.
|
||||
*/
|
||||
object FbCookie {
|
||||
|
||||
const val COOKIE_DOMAIN = FACEBOOK_COM
|
||||
|
||||
/**
|
||||
* Retrieves the facebook cookie if it exists
|
||||
* Note that this is a synchronized call
|
||||
*/
|
||||
inline val webCookie: String?
|
||||
get() = CookieManager.getInstance().getCookie(FB_URL_BASE)
|
||||
get() = CookieManager.getInstance().getCookie(COOKIE_DOMAIN)
|
||||
|
||||
private fun setWebCookie(cookie: String?, callback: (() -> Unit)?) {
|
||||
with(CookieManager.getInstance()) {
|
||||
removeAllCookies { _ ->
|
||||
if (cookie == null) {
|
||||
callback?.invoke()
|
||||
return@removeAllCookies
|
||||
}
|
||||
L.d { "Setting cookie" }
|
||||
val cookies = cookie.split(";").map { Pair(it, SingleSubject.create<Boolean>()) }
|
||||
cookies.forEach { (cookie, callback) -> setCookie(FB_URL_BASE, cookie) { callback.onSuccess(it) } }
|
||||
Observable.zip<Boolean, Unit>(cookies.map { (_, callback) -> callback.toObservable() }) {}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
callback?.invoke()
|
||||
L.d { "Cookies set" }
|
||||
L._d { cookie }
|
||||
flush()
|
||||
}
|
||||
private fun CookieManager.setWebCookie(cookie: String?, callback: (() -> Unit)?) {
|
||||
removeAllCookies { _ ->
|
||||
if (cookie == null) {
|
||||
callback?.invoke()
|
||||
return@removeAllCookies
|
||||
}
|
||||
L.d { "Setting cookie" }
|
||||
val cookies = cookie.split(";").map { Pair(it, SingleSubject.create<Boolean>()) }
|
||||
cookies.forEach { (cookie, callback) -> setCookie(COOKIE_DOMAIN, cookie) { callback.onSuccess(it) } }
|
||||
Observable.zip<Boolean, Unit>(cookies.map { (_, callback) -> callback.toObservable() }) {}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
callback?.invoke()
|
||||
L.d { "Cookies set" }
|
||||
L._d { cookie }
|
||||
flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun CookieManager.suspendSetWebCookie(cookie: String?): Boolean {
|
||||
cookie ?: return true
|
||||
L.test { "Orig $webCookie" }
|
||||
removeAllCookies()
|
||||
L.test { "Save $cookie" }
|
||||
// Save all cookies regardless of result, then check if all succeeded
|
||||
val result = cookie.split(";").map { setSingleWebCookie(it) }.all { it }
|
||||
L.test { "AAAA $webCookie" }
|
||||
flush()
|
||||
L.test { "SSSS $webCookie" }
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun CookieManager.removeAllCookies(): Boolean = suspendCoroutine { cont ->
|
||||
removeAllCookies {
|
||||
L.test { "Removed all cookies $webCookie" }
|
||||
cont.resume(it)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun CookieManager.setSingleWebCookie(cookie: String): Boolean = suspendCoroutine { cont ->
|
||||
setCookie(COOKIE_DOMAIN, cookie.trim()) {
|
||||
L.test { "Save single $cookie\n\n\t$webCookie" }
|
||||
cont.resume(it)
|
||||
}
|
||||
}
|
||||
|
||||
operator fun invoke() {
|
||||
L.d { "FbCookie Invoke User" }
|
||||
with(CookieManager.getInstance()) {
|
||||
setAcceptCookie(true)
|
||||
}
|
||||
val manager = CookieManager.getInstance()
|
||||
manager.setAcceptCookie(true)
|
||||
val dbCookie = loadFbCookie(Prefs.userId)?.cookie
|
||||
if (dbCookie != null && webCookie == null) {
|
||||
L.d { "DbCookie found & WebCookie is null; setting webcookie" }
|
||||
setWebCookie(dbCookie, null)
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
manager.suspendSetWebCookie(dbCookie)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,7 +140,7 @@ object FbCookie {
|
||||
}
|
||||
L.d { "Switching User" }
|
||||
Prefs.userId = cookie.id
|
||||
setWebCookie(cookie.cookie, callback)
|
||||
CookieManager.getInstance().setWebCookie(cookie.cookie, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -41,6 +41,11 @@ import com.pitchedapps.frost.utils.REQUEST_TEXT_ZOOM
|
||||
import com.pitchedapps.frost.utils.frostEvent
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-11-07.
|
||||
@ -48,7 +53,7 @@ import io.reactivex.disposables.Disposable
|
||||
* All fragments pertaining to the main view
|
||||
* Must be attached to activities implementing [MainActivityContract]
|
||||
*/
|
||||
abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
|
||||
abstract class BaseFragment : Fragment(), CoroutineScope, FragmentContract, DynamicUiContract {
|
||||
|
||||
companion object {
|
||||
private const val ARG_POSITION = "arg_position"
|
||||
@ -71,6 +76,10 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
|
||||
}
|
||||
}
|
||||
|
||||
open lateinit var job: Job
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Main + job
|
||||
|
||||
override val baseUrl: String by lazy { arguments!!.getString(ARG_URL) }
|
||||
override val baseEnum: FbItem by lazy { FbItem[arguments]!! }
|
||||
override val position: Int by lazy { arguments!!.getInt(ARG_POSITION) }
|
||||
@ -98,6 +107,7 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
job = SupervisorJob()
|
||||
firstLoad = true
|
||||
if (context !is MainActivityContract)
|
||||
throw IllegalArgumentException("${this::class.java.simpleName} is not attached to a context implementing MainActivityContract")
|
||||
@ -207,6 +217,11 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
job.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun reloadTheme() {
|
||||
reloadThemeSelf()
|
||||
content?.reloadTextSize()
|
||||
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2018 Allan Wang
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.pitchedapps.frost.services
|
||||
|
||||
import android.app.job.JobParameters
|
||||
import android.app.job.JobService
|
||||
import androidx.annotation.CallSuper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
abstract class BaseJobService : JobService(), CoroutineScope {
|
||||
|
||||
private lateinit var job: Job
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Main + job
|
||||
|
||||
protected val startTime = System.currentTimeMillis()
|
||||
|
||||
/**
|
||||
* Note that if a job plans on running asynchronously, it should return true
|
||||
*/
|
||||
@CallSuper
|
||||
override fun onStartJob(params: JobParameters?): Boolean {
|
||||
job = Job()
|
||||
return false
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onStopJob(params: JobParameters?): Boolean {
|
||||
job.cancel()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Collection of ids for job services.
|
||||
* These should all be unique
|
||||
*/
|
||||
|
||||
const val NOTIFICATION_JOB_NOW = 6
|
||||
const val NOTIFICATION_PERIODIC_JOB = 7
|
||||
const val LOCAL_SERVICE_BASE = 110
|
||||
const val REQUEST_SERVICE_BASE = 220
|
@ -274,12 +274,8 @@ data class FrostNotification(
|
||||
NotificationManagerCompat.from(context).notify(tag, id, notif.build())
|
||||
}
|
||||
|
||||
const val NOTIFICATION_PERIODIC_JOB = 7
|
||||
|
||||
fun Context.scheduleNotifications(minutes: Long): Boolean =
|
||||
scheduleJob<NotificationService>(NOTIFICATION_PERIODIC_JOB, minutes)
|
||||
|
||||
const val NOTIFICATION_JOB_NOW = 6
|
||||
|
||||
fun Context.fetchNotifications(): Boolean =
|
||||
fetchJob<NotificationService>(NOTIFICATION_JOB_NOW)
|
||||
|
@ -82,7 +82,6 @@ private const val ARG_0 = "frost_request_arg_0"
|
||||
private const val ARG_1 = "frost_request_arg_1"
|
||||
private const val ARG_2 = "frost_request_arg_2"
|
||||
private const val ARG_3 = "frost_request_arg_3"
|
||||
private const val JOB_REQUEST_BASE = 928
|
||||
|
||||
private fun BaseBundle.getCookie() = getString(ARG_COOKIE)
|
||||
private fun BaseBundle.putCookie(cookie: String) = putString(ARG_COOKIE, cookie)
|
||||
@ -145,7 +144,7 @@ object FrostRunnable {
|
||||
return false
|
||||
}
|
||||
|
||||
val builder = JobInfo.Builder(JOB_REQUEST_BASE + command.ordinal, serviceComponent)
|
||||
val builder = JobInfo.Builder(REQUEST_SERVICE_BASE + command.ordinal, serviceComponent)
|
||||
.setMinimumLatency(0L)
|
||||
.setExtras(bundle)
|
||||
.setOverrideDeadline(2000L)
|
||||
|
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2018 Allan Wang
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.pitchedapps.frost.services
|
||||
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobParameters
|
||||
import android.app.job.JobScheduler
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.os.PersistableBundle
|
||||
import com.pitchedapps.frost.activities.ImageActivity
|
||||
import com.pitchedapps.frost.utils.L
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.FileFilter
|
||||
|
||||
class LocalService : BaseJobService() {
|
||||
|
||||
enum class Flag {
|
||||
PURGE_IMAGE
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FLAG = "extra_local_flag"
|
||||
|
||||
/**
|
||||
* Launches a local service with the provided flag
|
||||
*/
|
||||
fun schedule(context: Context, flag: Flag): Boolean {
|
||||
val scheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
||||
val serviceComponent = ComponentName(context, LocalService::class.java)
|
||||
val bundle = PersistableBundle()
|
||||
bundle.putString(FLAG, flag.name)
|
||||
|
||||
val builder = JobInfo.Builder(LOCAL_SERVICE_BASE + flag.ordinal, serviceComponent)
|
||||
.setMinimumLatency(0L)
|
||||
.setExtras(bundle)
|
||||
.setOverrideDeadline(2000L)
|
||||
|
||||
val result = scheduler.schedule(builder.build())
|
||||
if (result <= 0) {
|
||||
L.eThrow("FrostRequestService scheduler failed for ${flag.name}")
|
||||
return false
|
||||
}
|
||||
L.d { "Scheduled ${flag.name}" }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartJob(params: JobParameters?): Boolean {
|
||||
super.onStartJob(params)
|
||||
val flagString = params?.extras?.getString(FLAG)
|
||||
val flag: Flag = try {
|
||||
Flag.valueOf(flagString!!)
|
||||
} catch (e: Exception) {
|
||||
L.e { "Local service with invalid flag $flagString" }
|
||||
return true
|
||||
}
|
||||
launch {
|
||||
when (flag) {
|
||||
Flag.PURGE_IMAGE -> purgeImages()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun purgeImages() {
|
||||
withContext(Dispatchers.IO) {
|
||||
val purge = System.currentTimeMillis() - ImageActivity.PURGE_TIME
|
||||
ImageActivity.cacheDir(this@LocalService)
|
||||
.listFiles(FileFilter { it.isFile && it.lastModified() < purge })
|
||||
?.forEach { it.delete() }
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,6 @@
|
||||
package com.pitchedapps.frost.services
|
||||
|
||||
import android.app.job.JobParameters
|
||||
import android.app.job.JobService
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import ca.allanwang.kau.utils.string
|
||||
import com.pitchedapps.frost.BuildConfig
|
||||
@ -27,8 +26,10 @@ import com.pitchedapps.frost.dbflow.loadFbCookiesSync
|
||||
import com.pitchedapps.frost.utils.L
|
||||
import com.pitchedapps.frost.utils.Prefs
|
||||
import com.pitchedapps.frost.utils.frostEvent
|
||||
import org.jetbrains.anko.doAsync
|
||||
import java.util.concurrent.Future
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-06-14.
|
||||
@ -38,68 +39,69 @@ import java.util.concurrent.Future
|
||||
*
|
||||
* All fetching is done through parsers
|
||||
*/
|
||||
class NotificationService : JobService() {
|
||||
|
||||
private var future: Future<Unit>? = null
|
||||
|
||||
private val startTime = System.currentTimeMillis()
|
||||
class NotificationService : BaseJobService() {
|
||||
|
||||
override fun onStopJob(params: JobParameters?): Boolean {
|
||||
val time = System.currentTimeMillis() - startTime
|
||||
L.d { "Notification service has finished abruptly in $time ms" }
|
||||
frostEvent(
|
||||
"NotificationTime",
|
||||
"Type" to "Service force stop",
|
||||
"IM Included" to Prefs.notificationsInstantMessages,
|
||||
"Duration" to time
|
||||
)
|
||||
future?.cancel(true)
|
||||
future = null
|
||||
super.onStopJob(params)
|
||||
prepareFinish(true)
|
||||
return false
|
||||
}
|
||||
|
||||
fun finish(params: JobParameters?) {
|
||||
private var preparedFinish = false
|
||||
|
||||
private fun prepareFinish(abrupt: Boolean) {
|
||||
if (preparedFinish)
|
||||
return
|
||||
preparedFinish = true
|
||||
val time = System.currentTimeMillis() - startTime
|
||||
L.i { "Notification service has finished in $time ms" }
|
||||
L.i { "Notification service has ${if (abrupt) "finished abruptly" else "finished"} in $time ms" }
|
||||
frostEvent(
|
||||
"NotificationTime",
|
||||
"Type" to "Service",
|
||||
"Type" to (if (abrupt) "Service force stop" else "Service"),
|
||||
"IM Included" to Prefs.notificationsInstantMessages,
|
||||
"Duration" to time
|
||||
)
|
||||
jobFinished(params, false)
|
||||
future?.cancel(true)
|
||||
future = null
|
||||
}
|
||||
|
||||
override fun onStartJob(params: JobParameters?): Boolean {
|
||||
super.onStartJob(params)
|
||||
L.i { "Fetching notifications" }
|
||||
future = doAsync {
|
||||
val currentId = Prefs.userId
|
||||
val cookies = loadFbCookiesSync()
|
||||
val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1
|
||||
var notifCount = 0
|
||||
cookies.forEach {
|
||||
val current = it.id == currentId
|
||||
if (Prefs.notificationsGeneral &&
|
||||
(current || Prefs.notificationAllAccounts)
|
||||
)
|
||||
notifCount += fetch(jobId, NotificationType.GENERAL, it)
|
||||
if (Prefs.notificationsInstantMessages &&
|
||||
(current || Prefs.notificationsImAllAccounts)
|
||||
)
|
||||
notifCount += fetch(jobId, NotificationType.MESSAGE, it)
|
||||
launch {
|
||||
try {
|
||||
sendNotifications(params)
|
||||
} finally {
|
||||
if (!isActive)
|
||||
prepareFinish(false)
|
||||
jobFinished(params, false)
|
||||
}
|
||||
|
||||
L.i { "Sent $notifCount notifications" }
|
||||
if (notifCount == 0 && jobId == NOTIFICATION_JOB_NOW)
|
||||
generalNotification(665, R.string.no_new_notifications, BuildConfig.DEBUG)
|
||||
|
||||
finish(params)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun sendNotifications(params: JobParameters?): Unit = withContext(Dispatchers.Default) {
|
||||
val currentId = Prefs.userId
|
||||
val cookies = loadFbCookiesSync()
|
||||
if (!isActive) return@withContext
|
||||
val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1
|
||||
var notifCount = 0
|
||||
for (cookie in cookies) {
|
||||
if (!isActive) break
|
||||
val current = cookie.id == currentId
|
||||
if (Prefs.notificationsGeneral &&
|
||||
(current || Prefs.notificationAllAccounts)
|
||||
)
|
||||
notifCount += fetch(jobId, NotificationType.GENERAL, cookie)
|
||||
if (Prefs.notificationsInstantMessages &&
|
||||
(current || Prefs.notificationsImAllAccounts)
|
||||
)
|
||||
notifCount += fetch(jobId, NotificationType.MESSAGE, cookie)
|
||||
}
|
||||
|
||||
L.i { "Sent $notifCount notifications" }
|
||||
if (notifCount == 0 && jobId == NOTIFICATION_JOB_NOW)
|
||||
generalNotification(665, R.string.no_new_notifications, BuildConfig.DEBUG)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemented fetch to also notify when an error occurs
|
||||
* Also normalized the output to return the number of notifications received
|
||||
|
@ -19,8 +19,9 @@ package com.pitchedapps.frost.utils
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import ca.allanwang.kau.utils.use
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.HttpUrl
|
||||
import org.jetbrains.anko.doAsync
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-09-24.
|
||||
@ -38,7 +39,7 @@ open class AdBlocker(val assetPath: String) {
|
||||
val data: MutableSet<String> = mutableSetOf()
|
||||
|
||||
fun init(context: Context) {
|
||||
doAsync {
|
||||
GlobalScope.launch {
|
||||
val content = context.assets.open(assetPath).bufferedReader().use { f ->
|
||||
f.readLines().filter { !it.startsWith("#") }
|
||||
}
|
||||
@ -58,7 +59,7 @@ open class AdBlocker(val assetPath: String) {
|
||||
return false
|
||||
val index = host.indexOf(".")
|
||||
if (index < 0 || index + 1 < host.length) return false
|
||||
if (host.contains(host)) return true
|
||||
if (data.contains(host)) return true
|
||||
return isAdHost(host.substring(index + 1))
|
||||
}
|
||||
}
|
||||
|
@ -39,8 +39,6 @@ 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 2017-05-29.
|
||||
@ -76,18 +74,18 @@ class LoginWebView @JvmOverloads constructor(
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
checkForLogin(url) { id, cookie -> loginCallback(CookieModel(id, "", cookie)) }
|
||||
val cookieModel = checkForLogin(url)
|
||||
if (cookieModel != null)
|
||||
loginCallback(cookieModel)
|
||||
if (!view.isVisible) view.fadeIn()
|
||||
}
|
||||
|
||||
fun checkForLogin(url: String?, onFound: (id: Long, cookie: String) -> Unit) {
|
||||
doAsync {
|
||||
if (!url.isFacebookUrl) return@doAsync
|
||||
val cookie = CookieManager.getInstance().getCookie(url) ?: return@doAsync
|
||||
L.d { "Checking cookie for login" }
|
||||
val id = FB_USER_MATCHER.find(cookie)[1]?.toLong() ?: return@doAsync
|
||||
uiThread { onFound(id, cookie) }
|
||||
}
|
||||
fun checkForLogin(url: String?): CookieModel? {
|
||||
if (!url.isFacebookUrl) return null
|
||||
val cookie = CookieManager.getInstance().getCookie(url) ?: return null
|
||||
L.d { "Checking cookie for login" }
|
||||
val id = FB_USER_MATCHER.find(cookie)[1]?.toLong() ?: return null
|
||||
return CookieModel(id, "", cookie)
|
||||
}
|
||||
|
||||
override fun onPageCommitVisible(view: WebView, url: String?) {
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
<string name="share_link">Share Link</string>
|
||||
<string name="debug_link">Debug Link</string>
|
||||
<string name="local_service_name" translatable="false">Local Frost Service</string>
|
||||
<string name="debug_link_subject" translatable="false">Frost for Facebook: Link Debug</string>
|
||||
<string name="debug_link_content" translatable="false">Write here. Note that your link may contain private information, but I won\'t be able to see it as the post isn\'t public. The url will still help with debugging though.</string>
|
||||
<string name="debug_link_desc">If a link isn\'t loading properly, you can email me so I can help debug it. Clicking okay will open an email request</string>
|
||||
|
@ -16,7 +16,9 @@
|
||||
*/
|
||||
package com.pitchedapps.frost
|
||||
|
||||
import com.pitchedapps.frost.facebook.requests.call
|
||||
import com.pitchedapps.frost.facebook.requests.zip
|
||||
import okhttp3.Request
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@ -45,4 +47,15 @@ class MiscTest {
|
||||
"zip did not seem to work on different threads"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun a() {
|
||||
val s = Request.Builder()
|
||||
.url("https://www.allanwang.ca/ecse429/magenta.png")
|
||||
.get()
|
||||
.call().execute().body()!!.string()
|
||||
"<EFBFBD>PNG\n\u001A\nIDA<EFBFBD>c<EFBFBD><EFBFBD><EFBFBD><EFBFBD>?\u0000\u0006<EFBFBD>\u0002<EFBFBD><EFBFBD>p<EFBFBD>\u0000\u0000\u0000\u0000IEND<EFBFBD>B`<60>"
|
||||
println("Hello")
|
||||
println(s)
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryErro
|
||||
APP_ID=Frost
|
||||
APP_GROUP=com.pitchedapps
|
||||
|
||||
KAU=b4a2ded
|
||||
KAU=d850474
|
||||
KOTLIN=1.3.11
|
||||
|
||||
# https://mvnrepository.com/artifact/com.android.tools.build/gradle?repo=google
|
||||
@ -23,6 +23,8 @@ ANDROID_GRADLE=3.2.1
|
||||
# https://github.com/diffplug/spotless/blob/master/plugin-gradle/CHANGES.md
|
||||
SPOTLESS=3.17.0
|
||||
|
||||
# https://github.com/Kotlin/kotlinx.coroutines/releases
|
||||
COROUTINES=1.0.1
|
||||
# https://github.com/bugsnag/bugsnag-android/releases
|
||||
BUGSNAG=4.9.3
|
||||
# https://github.com/bugsnag/bugsnag-android-gradle-plugin/releases
|
||||
@ -38,6 +40,10 @@ COMMONS_TEXT=1.4
|
||||
DBFLOW=4.2.4
|
||||
# https://github.com/brianwernick/ExoMedia/releases
|
||||
EXOMEDIA=4.3.0
|
||||
# https://github.com/InsertKoinIO/koin/blob/master/CHANGELOG.md
|
||||
KOIN=1.0.2
|
||||
# https://github.com/mockk/mockk/releases
|
||||
MOCKK=1.8.13.kotlin13
|
||||
|
||||
# https://github.com/FasterXML/jackson-core/releases
|
||||
JACKSON=2.9.8
|
||||
|
Loading…
Reference in New Issue
Block a user