1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-08 12:02:33 +01:00

Apply ktfmt

This commit is contained in:
Allan Wang 2022-09-15 15:17:07 -07:00
parent bc1e1bda4f
commit a319baa736
No known key found for this signature in database
GPG Key ID: C93E3F9C679D7A56
160 changed files with 9910 additions and 10751 deletions

95
.editorconfig Normal file
View File

@ -0,0 +1,95 @@
# https://raw.githubusercontent.com/facebookincubator/ktfmt/main/docs/editorconfig/.editorconfig-google
#
# This .editorconfig section approximates ktfmt's formatting rules. You can include it in an
# existing .editorconfig file or use it standalone by copying it to <project root>/.editorconfig
# and making sure your editor is set to read settings from .editorconfig files.
#
# It includes editor-specific config options for IntelliJ IDEA.
#
# If any option is wrong, PR are welcome
[{*.kt,*.kts}]
indent_style = space
insert_final_newline = true
max_line_length = 100
indent_size = 2
ij_continuation_indent_size = 2
ij_java_names_count_to_use_import_on_demand = 9999
ij_kotlin_align_in_columns_case_branch = false
ij_kotlin_align_multiline_binary_operation = false
ij_kotlin_align_multiline_extends_list = false
ij_kotlin_align_multiline_method_parentheses = false
ij_kotlin_align_multiline_parameters = true
ij_kotlin_align_multiline_parameters_in_calls = false
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_assignment_wrap = normal
ij_kotlin_blank_lines_after_class_header = 0
ij_kotlin_blank_lines_around_block_when_branches = 0
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
ij_kotlin_block_comment_at_first_column = true
ij_kotlin_call_parameters_new_line_after_left_paren = true
ij_kotlin_call_parameters_right_paren_on_new_line = false
ij_kotlin_call_parameters_wrap = on_every_item
ij_kotlin_catch_on_new_line = false
ij_kotlin_class_annotation_wrap = split_into_lines
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
ij_kotlin_continuation_indent_for_chained_calls = true
ij_kotlin_continuation_indent_for_expression_bodies = true
ij_kotlin_continuation_indent_in_argument_lists = true
ij_kotlin_continuation_indent_in_elvis = false
ij_kotlin_continuation_indent_in_if_conditions = false
ij_kotlin_continuation_indent_in_parameter_lists = false
ij_kotlin_continuation_indent_in_supertype_lists = false
ij_kotlin_else_on_new_line = false
ij_kotlin_enum_constants_wrap = off
ij_kotlin_extends_list_wrap = normal
ij_kotlin_field_annotation_wrap = split_into_lines
ij_kotlin_finally_on_new_line = false
ij_kotlin_if_rparen_on_new_line = false
ij_kotlin_import_nested_classes = false
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
ij_kotlin_keep_blank_lines_before_right_brace = 2
ij_kotlin_keep_blank_lines_in_code = 2
ij_kotlin_keep_blank_lines_in_declarations = 2
ij_kotlin_keep_first_column_comment = true
ij_kotlin_keep_indents_on_empty_lines = false
ij_kotlin_keep_line_breaks = true
ij_kotlin_lbrace_on_next_line = false
ij_kotlin_line_comment_add_space = false
ij_kotlin_line_comment_at_first_column = true
ij_kotlin_method_annotation_wrap = split_into_lines
ij_kotlin_method_call_chain_wrap = normal
ij_kotlin_method_parameters_new_line_after_left_paren = true
ij_kotlin_method_parameters_right_paren_on_new_line = true
ij_kotlin_method_parameters_wrap = on_every_item
ij_kotlin_name_count_to_use_star_import = 9999
ij_kotlin_name_count_to_use_star_import_for_members = 9999
ij_kotlin_parameter_annotation_wrap = off
ij_kotlin_space_after_comma = true
ij_kotlin_space_after_extend_colon = true
ij_kotlin_space_after_type_colon = true
ij_kotlin_space_before_catch_parentheses = true
ij_kotlin_space_before_comma = false
ij_kotlin_space_before_extend_colon = true
ij_kotlin_space_before_for_parentheses = true
ij_kotlin_space_before_if_parentheses = true
ij_kotlin_space_before_lambda_arrow = true
ij_kotlin_space_before_type_colon = false
ij_kotlin_space_before_when_parentheses = true
ij_kotlin_space_before_while_parentheses = true
ij_kotlin_spaces_around_additive_operators = true
ij_kotlin_spaces_around_assignment_operators = true
ij_kotlin_spaces_around_equality_operators = true
ij_kotlin_spaces_around_function_type_arrow = true
ij_kotlin_spaces_around_logical_operators = true
ij_kotlin_spaces_around_multiplicative_operators = true
ij_kotlin_spaces_around_range = false
ij_kotlin_spaces_around_relational_operators = true
ij_kotlin_spaces_around_unary_operator = false
ij_kotlin_spaces_around_when_arrow = true
ij_kotlin_variable_annotation_wrap = off
ij_kotlin_while_on_new_line = false
ij_kotlin_wrap_elvis_expressions = 1
ij_kotlin_wrap_expression_body_functions = 1
ij_kotlin_wrap_first_method_in_call_chain = false

View File

@ -27,15 +27,11 @@ import org.junit.Test
@HiltAndroidTest
class StartActivityTest {
@get:Rule(order = 0)
val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val activityRule = activityRule<StartActivity>(
intentAction = {
putExtra(ARG_URL, TEST_FORMATTED_URL)
}
)
val activityRule =
activityRule<StartActivity>(intentAction = { putExtra(ARG_URL, TEST_FORMATTED_URL) })
@Test
fun initializesSuccessfully() {

View File

@ -25,11 +25,7 @@ import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [PrefFactoryModule::class]
)
@TestInstallIn(components = [SingletonComponent::class], replaces = [PrefFactoryModule::class])
object PrefFactoryTestModule {
@Provides
fun factory(): KPrefFactory = KPrefFactoryInMemory
@Provides fun factory(): KPrefFactory = KPrefFactoryInMemory
}

View File

@ -25,11 +25,9 @@ import org.junit.Test
@HiltAndroidTest
class AboutActivityTest {
@get:Rule(order = 0)
val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val activityRule = activityRule<AboutActivity>()
@get:Rule(order = 1) val activityRule = activityRule<AboutActivity>()
@Test
fun initializesSuccessfully() {

View File

@ -25,11 +25,9 @@ import org.junit.Test
@HiltAndroidTest
class DebugActivityTest {
@get:Rule(order = 0)
val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val activityRule = activityRule<DebugActivity>()
@get:Rule(order = 1) val activityRule = activityRule<DebugActivity>()
@Test
fun initializesSuccessfully() {

View File

@ -27,15 +27,11 @@ import org.junit.Test
@HiltAndroidTest
class FrostWebActivityTest {
@get:Rule(order = 0)
val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val activityRule = activityRule<FrostWebActivity>(
intentAction = {
putExtra(ARG_URL, TEST_FORMATTED_URL)
}
)
val activityRule =
activityRule<FrostWebActivity>(intentAction = { putExtra(ARG_URL, TEST_FORMATTED_URL) })
@Test
fun initializesSuccessfully() {

View File

@ -29,6 +29,11 @@ import com.pitchedapps.frost.utils.ARG_TEXT
import com.pitchedapps.frost.utils.isIndirectImageUrl
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
import okhttp3.internal.closeQuietly
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
@ -42,27 +47,17 @@ import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.Timeout
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
@HiltAndroidTest
class ImageActivityTest {
@get:Rule(order = 0)
val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val activityRule = activityRule<ImageActivity>(
intentAction = {
putExtra(ARG_IMAGE_URL, TEST_FORMATTED_URL)
}
)
val activityRule =
activityRule<ImageActivity>(intentAction = { putExtra(ARG_IMAGE_URL, TEST_FORMATTED_URL) })
@get:Rule(order = 2)
val globalTimeout: Timeout = Timeout.seconds(15)
@get:Rule(order = 2) val globalTimeout: Timeout = Timeout.seconds(15)
lateinit var mockServer: MockWebServer
@ -77,12 +72,14 @@ class ImageActivityTest {
}
@Test
fun initializesSuccessfully() = launchScenario(mockServer.url("image").toString()) {
fun initializesSuccessfully() =
launchScenario(mockServer.url("image").toString()) {
// Verify no crash
}
@Test
fun validImageTest() = launchScenario(mockServer.url("image").toString()) {
fun validImageTest() =
launchScenario(mockServer.url("image").toString()) {
mockServer.takeRequest()
assertEquals(1, mockServer.requestCount, "One http request expected")
// assertEquals(
@ -103,7 +100,8 @@ class ImageActivityTest {
@Test
@Ignore("apparently this fails")
fun invalidImageTest() = launchScenario(mockServer.url("text").toString()) {
fun invalidImageTest() =
launchScenario(mockServer.url("text").toString()) {
mockServer.takeRequest()
assertEquals(1, mockServer.requestCount, "One http request expected")
assertTrue(binding.error.isVisible, "Error should be shown")
@ -113,20 +111,18 @@ class ImageActivityTest {
// fabAction,
// "Text should not be a valid image format, error state expected"
// )
assertEquals(
"Image format not supported",
errorRef?.message,
"Error message mismatch"
)
assertEquals("Image format not supported", errorRef?.message, "Error message mismatch")
assertFalse(tempFile?.exists() == true, "Temp file should have been removed")
}
@Test
fun errorTest() = launchScenario(mockServer.url("error").toString()) {
fun errorTest() =
launchScenario(mockServer.url("error").toString()) {
mockServer.takeRequest()
assertEquals(1, mockServer.requestCount, "One http request expected")
assertTrue(binding.error.isVisible, "Error should be shown")
// assertEquals(FabStates.ERROR, fabAction, "Error response code, error state expected")
// assertEquals(FabStates.ERROR, fabAction, "Error response code, error state
// expected")
assertEquals(
"Unsuccessful response for image: Error mock response",
errorRef?.message,
@ -151,27 +147,21 @@ class ImageActivityTest {
putExtra(ARG_TEXT, text)
putExtra(ARG_COOKIE, cookie)
}
ActivityScenario.launch<ImageActivity>(intent).use {
it.onActivity(action)
}
ActivityScenario.launch<ImageActivity>(intent).use { it.onActivity(action) }
}
private fun mockServer(): MockWebServer {
val img = Buffer()
img.writeAll(getResource("bayer-pattern.jpg").source())
return MockWebServer().apply {
dispatcher = object : Dispatcher() {
dispatcher =
object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse =
when {
request.path?.contains("text") == true -> MockResponse().setResponseCode(200)
.setBody(
"Valid mock text response"
)
request.path?.contains("image") == true -> MockResponse().setResponseCode(
200
).setBody(
img
)
request.path?.contains("text") == true ->
MockResponse().setResponseCode(200).setBody("Valid mock text response")
request.path?.contains("image") == true ->
MockResponse().setResponseCode(200).setBody(img)
else -> MockResponse().setResponseCode(404).setBody("Error mock response")
}
}

View File

@ -25,11 +25,9 @@ import org.junit.Test
@HiltAndroidTest
class IntroActivityTest {
@get:Rule(order = 0)
val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val activityRule = activityRule<IntroActivity>()
@get:Rule(order = 1) val activityRule = activityRule<IntroActivity>()
@Test
fun initializesSuccessfully() {

View File

@ -25,11 +25,9 @@ import org.junit.Test
@HiltAndroidTest
class LoginActivityTest {
@get:Rule(order = 0)
val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val activityRule = activityRule<LoginActivity>()
@get:Rule(order = 1) val activityRule = activityRule<LoginActivity>()
@Test
fun initializesSuccessfully() {

View File

@ -25,11 +25,9 @@ import org.junit.Test
@HiltAndroidTest
class MainActivityTest {
@get:Rule(order = 0)
val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val activityRule = activityRule<MainActivity>()
@get:Rule(order = 1) val activityRule = activityRule<MainActivity>()
@Test
fun initializesSuccessfully() {

View File

@ -25,11 +25,9 @@ import org.junit.Test
@HiltAndroidTest
class SelectorActivityTest {
@get:Rule(order = 0)
val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val activityRule = activityRule<SelectorActivity>()
@get:Rule(order = 1) val activityRule = activityRule<SelectorActivity>()
@Test
fun initializesSuccessfully() {

View File

@ -25,11 +25,9 @@ import org.junit.Test
@HiltAndroidTest
class SettingActivityTest {
@get:Rule(order = 0)
val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val activityRule = activityRule<SettingsActivity>()
@get:Rule(order = 1) val activityRule = activityRule<SettingsActivity>()
@Test
fun initializesSuccessfully() {

View File

@ -25,11 +25,9 @@ import org.junit.Test
@HiltAndroidTest
class TabCustomizerActivityTest {
@get:Rule(order = 0)
val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val activityRule = activityRule<TabCustomizerActivity>()
@get:Rule(order = 1) val activityRule = activityRule<TabCustomizerActivity>()
@Test
fun initializesSuccessfully() {

View File

@ -25,11 +25,9 @@ import org.junit.Test
@HiltAndroidTest
class WebOverlayActivityTest {
@get:Rule(order = 0)
val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val activityRule = activityRule<AboutActivity>()
@get:Rule(order = 1) val activityRule = activityRule<AboutActivity>()
@Test
fun initializesSuccessfully() {

View File

@ -20,9 +20,9 @@ import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
abstract class BaseDbTest {
@ -32,12 +32,8 @@ abstract class BaseDbTest {
@BeforeTest
fun before() {
val context = ApplicationProvider.getApplicationContext<Context>()
val privateDb = Room.inMemoryDatabaseBuilder(
context, FrostPrivateDatabase::class.java
).build()
val publicDb = Room.inMemoryDatabaseBuilder(
context, FrostPublicDatabase::class.java
).build()
val privateDb = Room.inMemoryDatabaseBuilder(context, FrostPrivateDatabase::class.java).build()
val publicDb = Room.inMemoryDatabaseBuilder(context, FrostPublicDatabase::class.java).build()
db = FrostDatabase(privateDb, publicDb)
}

View File

@ -16,16 +16,18 @@
*/
package com.pitchedapps.frost.db
import kotlinx.coroutines.runBlocking
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.fail
import kotlinx.coroutines.runBlocking
class CacheDbTest : BaseDbTest() {
private val dao get() = db.cacheDao()
private val cookieDao get() = db.cookieDao()
private val dao
get() = db.cacheDao()
private val cookieDao
get() = db.cookieDao()
private fun cookie(id: Long) = CookieEntity(id, "name$id", "cookie$id")

View File

@ -16,14 +16,15 @@
*/
package com.pitchedapps.frost.db
import kotlinx.coroutines.runBlocking
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlinx.coroutines.runBlocking
class CookieDbTest : BaseDbTest() {
private val dao get() = db.cookieDao()
private val dao
get() = db.cookieDao()
@Test
fun basicCookie() {

View File

@ -21,8 +21,8 @@ import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.runner.RunWith
import kotlin.test.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CookieMigrationTest {
@ -31,7 +31,8 @@ class CookieMigrationTest {
private val ALL_MIGRATIONS = arrayOf(COOKIES_MIGRATION_1_2)
val helper: MigrationTestHelper = MigrationTestHelper(
val helper: MigrationTestHelper =
MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
FrostPrivateDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
@ -40,9 +41,7 @@ class CookieMigrationTest {
@Test
fun migrateAll() {
// Create earliest version of the database.
helper.createDatabase(TEST_DB, 1).apply {
close()
}
helper.createDatabase(TEST_DB, 1).apply { close() }
// Open latest version of the database. Room will validate the schema
// once all migrations execute.
@ -50,7 +49,10 @@ class CookieMigrationTest {
InstrumentationRegistry.getInstrumentation().targetContext,
FrostPrivateDatabase::class.java,
TEST_DB
).addMigrations(*ALL_MIGRATIONS).build().apply {
)
.addMigrations(*ALL_MIGRATIONS)
.build()
.apply {
openHelper.writableDatabase
close()
}

View File

@ -18,20 +18,20 @@ package com.pitchedapps.frost.db
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.defaultTabs
import kotlinx.coroutines.runBlocking
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.runBlocking
class GenericDbTest : BaseDbTest() {
private val dao get() = db.genericDao()
private val dao
get() = db.genericDao()
/**
* Note that order is also preserved here
*/
/** Note that order is also preserved here */
@Test
fun save() {
val tabs = listOf(
val tabs =
listOf(
FbItem.ACTIVITY_LOG,
FbItem.BIRTHDAYS,
FbItem.EVENTS,
@ -49,9 +49,7 @@ class GenericDbTest : BaseDbTest() {
@Test
fun defaultRetrieve() {
runBlocking {
assertEquals(defaultTabs(), dao.getTabs(), "Default retrieval failed")
}
runBlocking { assertEquals(defaultTabs(), dao.getTabs(), "Default retrieval failed") }
}
@Test

View File

@ -19,19 +19,21 @@ package com.pitchedapps.frost.db
import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL
import com.pitchedapps.frost.services.NOTIF_CHANNEL_MESSAGES
import com.pitchedapps.frost.services.NotificationContent
import kotlinx.coroutines.runBlocking
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlinx.coroutines.runBlocking
class NotificationDbTest : BaseDbTest() {
private val dao get() = db.notifDao()
private val dao
get() = db.notifDao()
private fun cookie(id: Long) = CookieEntity(id, "name$id", "cookie$id")
private fun notifContent(id: Long, cookie: CookieEntity, time: Long = id) = NotificationContent(
private fun notifContent(id: Long, cookie: CookieEntity, time: Long = id) =
NotificationContent(
data = cookie,
id = id,
href = "",
@ -94,7 +96,8 @@ class NotificationDbTest : BaseDbTest() {
}
/**
* Primary key is both id and userId, in the event that the same notification to multiple users has the same id
* Primary key is both id and userId, in the event that the same notification to multiple users
* has the same id
*/
@Test
fun primaryKeyCheck() {
@ -130,18 +133,10 @@ class NotificationDbTest : BaseDbTest() {
// Unique unsorted ids
val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
runBlocking {
assertEquals(
-1L,
dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL),
"Default epoch failed"
)
assertEquals(-1L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Default epoch failed")
db.cookieDao().save(cookie)
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
assertEquals(
99L,
dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL),
"Latest epoch failed"
)
assertEquals(99L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Latest epoch failed")
}
}

View File

@ -17,8 +17,8 @@
package com.pitchedapps.frost.facebook
import android.webkit.CookieManager
import org.junit.Test
import kotlin.test.assertTrue
import org.junit.Test
class FbCookieTest {

View File

@ -28,8 +28,7 @@ import java.io.InputStream
val context: Context
get() = InstrumentationRegistry.getInstrumentation().targetContext
fun getAsset(asset: String): InputStream =
context.assets.open(asset)
fun getAsset(asset: String): InputStream = context.assets.open(asset)
private class Helper
@ -40,8 +39,7 @@ inline fun <reified A : Activity> activityRule(
intentAction: Intent.() -> Unit = {},
activityOptions: Bundle? = null
): ActivityScenarioRule<A> {
val intent =
Intent(ApplicationProvider.getApplicationContext(), A::class.java).also(intentAction)
val intent = Intent(ApplicationProvider.getApplicationContext(), A::class.java).also(intentAction)
return ActivityScenarioRule(intent, activityOptions)
}

View File

@ -37,23 +37,17 @@ import dagger.hilt.android.HiltAndroidApp
import java.util.Random
import javax.inject.Inject
/**
* Created by Allan Wang on 2017-05-28.
*/
/** Created by Allan Wang on 2017-05-28. */
@HiltAndroidApp
class FrostApp : Application() {
@Inject
lateinit var prefs: Prefs
@Inject lateinit var prefs: Prefs
@Inject
lateinit var themeProvider: ThemeProvider
@Inject lateinit var themeProvider: ThemeProvider
@Inject
lateinit var cookieDao: CookieDao
@Inject lateinit var cookieDao: CookieDao
@Inject
lateinit var notifDao: NotificationDao
@Inject lateinit var notifDao: NotificationDao
override fun onCreate() {
super.onCreate()
@ -72,7 +66,8 @@ class FrostApp : Application() {
BigImageViewer.initialize(GlideImageLoader.with(this, httpClient))
if (BuildConfig.DEBUG) {
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
registerActivityLifecycleCallbacks(
object : ActivityLifecycleCallbacks {
override fun onActivityPaused(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityStarted(activity: Activity) {}
@ -88,7 +83,8 @@ class FrostApp : Application() {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
L.d { "Activity ${activity.localClassName} created" }
}
})
}
)
}
}
@ -98,7 +94,8 @@ class FrostApp : Application() {
L.shouldLog = {
when (it) {
Log.VERBOSE -> BuildConfig.DEBUG
Log.INFO, Log.ERROR -> true
Log.INFO,
Log.ERROR -> true
else -> BuildConfig.DEBUG || prefs.verboseLogging
}
}

View File

@ -45,30 +45,23 @@ import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.launchNewTask
import com.pitchedapps.frost.utils.loadAssets
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import java.util.ArrayList
import javax.inject.Inject
import kotlinx.coroutines.launch
/**
* Created by Allan Wang on 2017-05-28.
*/
/** Created by Allan Wang on 2017-05-28. */
@AndroidEntryPoint
class StartActivity : KauBaseActivity() {
@Inject
lateinit var fbCookie: FbCookie
@Inject lateinit var fbCookie: FbCookie
@Inject
lateinit var prefs: Prefs
@Inject lateinit var prefs: Prefs
@Inject
lateinit var themeProvider: ThemeProvider
@Inject lateinit var themeProvider: ThemeProvider
@Inject
lateinit var cookieDao: CookieDao
@Inject lateinit var cookieDao: CookieDao
@Inject
lateinit var genericDao: GenericDao
@Inject lateinit var genericDao: GenericDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -106,10 +99,13 @@ class StartActivity : KauBaseActivity() {
cookies.isEmpty() -> launchNewTask<LoginActivity>()
// Has cookies but no selected account
prefs.userId == -1L -> launchNewTask<SelectorActivity>(cookies)
else -> startActivity<MainActivity>(
else ->
startActivity<MainActivity>(
intentBuilder = {
putParcelableArrayListExtra(EXTRA_COOKIES, cookies)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or
flags =
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
}
)
@ -121,21 +117,18 @@ class StartActivity : KauBaseActivity() {
}
}
private fun showInvalidWebView() =
showInvalidView(R.string.error_webview)
private fun showInvalidWebView() = showInvalidView(R.string.error_webview)
private fun showInvalidSdkView() {
val text = String.format(string(R.string.error_sdk), Build.VERSION.SDK_INT)
showInvalidView(text)
}
private fun showInvalidView(textRes: Int) =
showInvalidView(string(textRes))
private fun showInvalidView(textRes: Int) = showInvalidView(string(textRes))
private fun showInvalidView(text: String) {
setContentView(R.layout.activity_invalid)
findViewById<ImageView>(R.id.invalid_icon)
.setIcon(GoogleMaterial.Icon.gmd_adb, -1, Color.WHITE)
findViewById<ImageView>(R.id.invalid_icon).setIcon(GoogleMaterial.Icon.gmd_adb, -1, Color.WHITE)
findViewById<TextView>(R.id.invalid_text).text = text
}
}

View File

@ -52,17 +52,13 @@ import com.pitchedapps.frost.utils.L
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
/**
* Created by Allan Wang on 2017-06-26.
*/
/** Created by Allan Wang on 2017-06-26. */
@AndroidEntryPoint
class AboutActivity : AboutActivityBase(null) {
@Inject
lateinit var prefs: Prefs
@Inject lateinit var prefs: Prefs
@Inject
lateinit var themeProvider: ThemeProvider
@Inject lateinit var themeProvider: ThemeProvider
override fun Configs.buildConfigs() {
textColor = themeProvider.textColor
@ -79,19 +75,17 @@ class AboutActivity : AboutActivityBase(null) {
var clickCount = 0
override fun postInflateMainPage(adapter: FastItemThemedAdapter<GenericItem>) {
/**
* Frost may not be a library but we're conveying the same info
*/
val frost = Library(
/** Frost may not be a library but we're conveying the same info */
val frost =
Library(
uniqueId = "com.pitchedapps.frost",
name = string(R.string.frost_name),
developers = listOf(
Developer(name = string(R.string.dev_name), organisationUrl = null)
),
developers = listOf(Developer(name = string(R.string.dev_name), organisationUrl = null)),
website = string(R.string.github_url),
description = string(R.string.frost_description),
artifactVersion = BuildConfig.VERSION_NAME,
licenses = setOf(
licenses =
setOf(
License(
spdxId = "gplv3",
name = "GNU GPL v3",
@ -106,10 +100,7 @@ class AboutActivity : AboutActivityBase(null) {
adapter.onClickListener = { _, _, item, _ ->
if (item is LibraryIItem) {
val now = System.currentTimeMillis()
if (now - lastClick > 500)
clickCount = 1
else
clickCount++
if (now - lastClick > 500) clickCount = 1 else clickCount++
lastClick = now
if (clickCount == 8) {
if (!prefs.debugSettings) {
@ -126,8 +117,7 @@ class AboutActivity : AboutActivityBase(null) {
}
class AboutLinks :
AbstractItem<AboutLinks.ViewHolder>(),
ThemableIItem by ThemableIItemDelegate() {
AbstractItem<AboutLinks.ViewHolder>(), ThemableIItem by ThemableIItemDelegate() {
override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
override val layoutRes: Int
@ -151,9 +141,9 @@ class AboutActivity : AboutActivityBase(null) {
/**
* There are a lot of constraints to be added to each item just to have them chained properly
* My as well do it programmatically
* Initializing the viewholder will setup the icons, scale type and background of all icons,
* link their click listeners and chain them together via a horizontal spread
* My as well do it programmatically Initializing the viewholder will setup the icons, scale
* type and background of all icons, link their click listeners and chain them together via a
* horizontal spread
*/
init {
val c = itemView.context
@ -161,21 +151,17 @@ class AboutActivity : AboutActivityBase(null) {
val icons: Array<Pair<Int, () -> Unit>> =
arrayOf(R.drawable.ic_fdroid_24 to { c.startLink(R.string.fdroid_url) })
val iicons: Array<Pair<IIcon, () -> Unit>> = arrayOf(
val iicons: Array<Pair<IIcon, () -> Unit>> =
arrayOf(
GoogleMaterial.Icon.gmd_file_download to { c.startLink(R.string.github_downloads_url) },
CommunityMaterial.Icon3.cmd_reddit to { c.startLink(R.string.reddit_url) },
CommunityMaterial.Icon2.cmd_github to { c.startLink(R.string.github_url) }
)
images =
(
icons.map { (icon, onClick) -> c.drawable(icon) to onClick } + iicons.map { (icon, onClick) ->
icon.toDrawable(
c,
32
) to onClick
}
).mapIndexed { i, (icon, onClick) ->
(icons.map { (icon, onClick) -> c.drawable(icon) to onClick } +
iicons.map { (icon, onClick) -> icon.toDrawable(c, 32) to onClick })
.mapIndexed { i, (icon, onClick) ->
ImageView(c).apply {
layoutParams = ViewGroup.LayoutParams(size, size)
id = 109389 + i

View File

@ -28,27 +28,19 @@ import com.pitchedapps.frost.utils.ActivityThemer
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
/**
* Created by Allan Wang on 2017-06-12.
*/
/** Created by Allan Wang on 2017-06-12. */
@AndroidEntryPoint
abstract class BaseActivity : KauBaseActivity() {
@Inject
lateinit var fbCookie: FbCookie
@Inject lateinit var fbCookie: FbCookie
@Inject
lateinit var prefs: Prefs
@Inject lateinit var prefs: Prefs
@Inject
lateinit var themeProvider: ThemeProvider
@Inject lateinit var themeProvider: ThemeProvider
@Inject
lateinit var activityThemer: ActivityThemer
@Inject lateinit var activityThemer: ActivityThemer
/**
* Inherited consumer to customize back press
*/
/** Inherited consumer to customize back press */
protected open fun backConsumer(): Boolean = false
final override fun onBackPressed() {

View File

@ -130,9 +130,9 @@ import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.math.abs
import kotlinx.coroutines.launch
/**
* Created by Allan Wang on 20/12/17.
@ -141,27 +141,20 @@ import kotlin.math.abs
*/
@AndroidEntryPoint
abstract class BaseMainActivity :
BaseActivity(),
MainActivityContract,
VideoViewHolder,
SearchViewHolder {
BaseActivity(), MainActivityContract, VideoViewHolder, SearchViewHolder {
/**
* Note that tabs themselves are initialized through a coroutine during onCreate
*/
/** Note that tabs themselves are initialized through a coroutine during onCreate */
protected val adapter: SectionsPagerAdapter = SectionsPagerAdapter()
override val frameWrapper: FrameLayout get() = drawerWrapperBinding.mainContainer
override val frameWrapper: FrameLayout
get() = drawerWrapperBinding.mainContainer
lateinit var drawerWrapperBinding: ActivityMainDrawerWrapperBinding
lateinit var contentBinding: ActivityMainContentBinding
@Inject
lateinit var cookieDao: CookieDao
@Inject lateinit var cookieDao: CookieDao
@Inject
lateinit var genericDao: GenericDao
@Inject lateinit var genericDao: GenericDao
@Inject
lateinit var webFileChooser: WebFileChooser
@Inject lateinit var webFileChooser: WebFileChooser
interface ActivityMainContentBinding {
val root: View
@ -186,7 +179,8 @@ abstract class BaseMainActivity :
val start = System.currentTimeMillis()
drawerWrapperBinding = ActivityMainDrawerWrapperBinding.inflate(layoutInflater)
setContentView(drawerWrapperBinding.root)
contentBinding = when (prefs.mainActivityLayout) {
contentBinding =
when (prefs.mainActivityLayout) {
MainActivityLayout.TOP_BAR -> {
val binding = ActivityMainBinding.inflate(layoutInflater)
@SuppressLint("StaticFieldLeak")
@ -226,9 +220,7 @@ abstract class BaseMainActivity :
}
onNestedCreate(savedInstanceState)
L.i { "Main finished loading UI in ${System.currentTimeMillis() - start} ms" }
launch {
adapter.setPages(genericDao.getTabs())
}
launch { adapter.setPages(genericDao.getTabs()) }
controlWebview = WebView(this)
if (BuildConfig.VERSION_CODE > prefs.versionCode) {
prefs.prevVersionCode = prefs.versionCode
@ -251,9 +243,7 @@ abstract class BaseMainActivity :
lastAccessTime = System.currentTimeMillis()
}
/**
* Injector to handle creation for sub classes
*/
/** Injector to handle creation for sub classes */
protected abstract fun onNestedCreate(savedInstanceState: Bundle?)
private var hasFab = false
@ -291,8 +281,11 @@ abstract class BaseMainActivity :
private fun ActivityMainDrawerWrapperBinding.initDrawer() {
val toggle = ActionBarDrawerToggle(
this@BaseMainActivity, drawer, contentBinding.toolbar,
val toggle =
ActionBarDrawerToggle(
this@BaseMainActivity,
drawer,
contentBinding.toolbar,
R.string.open,
R.string.close
)
@ -364,9 +357,7 @@ abstract class BaseMainActivity :
fab.setOnClickListener { clickEvent() }
if (shouldShow) {
if (fab.isShown) {
fab.fadeScaleTransition {
setIcon(iicon, color = themeProvider.iconColor)
}
fab.fadeScaleTransition { setIcon(iicon, color = themeProvider.iconColor) }
return
}
}
@ -396,23 +387,24 @@ abstract class BaseMainActivity :
private var orderedAccounts: List<CookieEntity> = cookies()
private var pendingUpdate: Boolean = false
private val binding = ViewNavHeaderBinding.inflate(layoutInflater)
val root: View get() = binding.root
private val optionsBackground = themeProvider.bgColor.withMinAlpha(200).colorToForeground(
0.1f
)
val root: View
get() = binding.root
private val optionsBackground = themeProvider.bgColor.withMinAlpha(200).colorToForeground(0.1f)
init {
setPrimary(prefs.userId)
binding.updateAccounts()
with(drawerWrapperBinding) {
drawer.addDrawerListener(object : DrawerLayout.SimpleDrawerListener() {
drawer.addDrawerListener(
object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerClosed(drawerView: View) {
if (drawerView !== navigation) return
if (!pendingUpdate) return
pendingUpdate = false
binding.updateAccounts()
}
})
}
)
}
with(binding) {
optionsContainer.setBackgroundColor(optionsBackground)
@ -423,39 +415,24 @@ abstract class BaseMainActivity :
if (showOptions) {
animator.apply {
withAnimator(optionsContainer.height, 0) {
optionsContainer.updateLayoutParams {
height = it
}
}
withAnimator(arrow.rotation, 0f) {
arrow.rotation = it
}
withEndAction {
optionsContainer.gone()
optionsContainer.updateLayoutParams { height = it }
}
withAnimator(arrow.rotation, 0f) { arrow.rotation = it }
withEndAction { optionsContainer.gone() }
}
} else {
optionsContainer.visible()
animator.apply {
withAnimator(
optionsContainer.height,
optionsContainer.unboundedHeight
) {
optionsContainer.updateLayoutParams {
height = it
}
withAnimator(optionsContainer.height, optionsContainer.unboundedHeight) {
optionsContainer.updateLayoutParams { height = it }
}
withEndAction {
// Sometimes, height remains the same as measured during collapse
// if the animations are disabled.
// We will resolve this by always falling back to wrap content afterwards
optionsContainer.updateLayoutParams {
height = ViewGroup.LayoutParams.WRAP_CONTENT
}
}
withAnimator(arrow.rotation, 180f) {
arrow.rotation = it
optionsContainer.updateLayoutParams { height = ViewGroup.LayoutParams.WRAP_CONTENT }
}
withAnimator(arrow.rotation, 180f) { arrow.rotation = it }
}
}
showOptions = !showOptions
@ -496,10 +473,7 @@ abstract class BaseMainActivity :
)
positiveButton(R.string.kau_yes) {
this@BaseMainActivity.launch {
fbCookie.logout(
this@BaseMainActivity,
deleteCookie = true
)
fbCookie.logout(this@BaseMainActivity, deleteCookie = true)
}
}
negativeButton(R.string.kau_no)
@ -510,15 +484,11 @@ abstract class BaseMainActivity :
}
with(optionsAddAccount) {
setOptionsIcon(GoogleMaterial.Icon.gmd_add)
setOnClickListener {
launchNewTask<LoginActivity>(clearStack = false)
}
setOnClickListener { launchNewTask<LoginActivity>(clearStack = false) }
}
with(optionsManageAccount) {
setOptionsIcon(GoogleMaterial.Icon.gmd_settings)
setOnClickListener {
launchNewTask<SelectorActivity>(cookies(), false)
}
setOnClickListener { launchNewTask<SelectorActivity>(cookies(), false) }
}
arrow.setImageDrawable(
GoogleMaterial.Icon.gmd_arrow_drop_down.toDrawable(
@ -553,56 +523,37 @@ abstract class BaseMainActivity :
val accountSize = dimenPixelSize(R.dimen.drawer_account_avatar_size)
val textColor = themeProvider.textColor
orderedAccounts.forEach { cookie ->
val tv =
TextView(
this@BaseMainActivity,
null,
0,
R.style.Main_DrawerAccountUserOptions
)
glide.load(profilePictureUrl(cookie.id)).transform(FrostGlide.circleCrop)
.into(object : CustomTarget<Drawable>(accountSize, accountSize) {
val tv = TextView(this@BaseMainActivity, null, 0, R.style.Main_DrawerAccountUserOptions)
glide
.load(profilePictureUrl(cookie.id))
.transform(FrostGlide.circleCrop)
.into(
object : CustomTarget<Drawable>(accountSize, accountSize) {
override fun onLoadCleared(placeholder: Drawable?) {
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(
placeholder,
null,
null,
null
)
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(placeholder, null, null, null)
}
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(
resource,
null,
null,
null
)
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(resource, null, null, null)
}
})
}
)
tv.text = cookie.name
tv.setTextColor(textColor)
tv.background = createNavDrawable(themeProvider.accentColor, optionsBackground)
tv.setOnClickListener {
switchAccount(cookie.id)
}
tv.setOnClickListener { switchAccount(cookie.id) }
optionsAccountsContainer.addView(tv)
}
}
private fun closeDrawer() {
with(drawerWrapperBinding) {
drawer.closeDrawer(navigation)
}
with(drawerWrapperBinding) { drawer.closeDrawer(navigation) }
}
private fun ImageView.setAccount(
cookie: CookieEntity?,
primary: Boolean
) {
private fun ImageView.setAccount(cookie: CookieEntity?, primary: Boolean) {
if (cookie == null) {
invisible()
setOnClickListener(null)
@ -645,7 +596,8 @@ abstract class BaseMainActivity :
menuInflater.inflate(R.menu.menu_main, menu)
contentBinding.toolbar.tint(themeProvider.iconColor)
setMenuIcons(
menu, themeProvider.iconColor,
menu,
themeProvider.iconColor,
R.id.action_settings to GoogleMaterial.Icon.gmd_settings,
R.id.action_search to GoogleMaterial.Icon.gmd_search
)
@ -658,8 +610,7 @@ abstract class BaseMainActivity :
bindSearchView(menu, R.id.action_search, themeProvider.iconColor) {
textCallback = { query, searchView ->
val results = searchViewCache[query]
if (results != null)
searchView.results = results
if (results != null) searchView.results = results
else {
val data = SearchParser.query(fbCookie.webCookie, query)?.data?.results
if (data != null) {
@ -679,13 +630,9 @@ abstract class BaseMainActivity :
}
}
textDebounceInterval = 300
searchCallback =
{ query, _ ->
launchWebOverlay(
"${FbItem._SEARCH.url}/?q=${query.urlEncode()}",
fbCookie,
prefs
); true
searchCallback = { query, _ ->
launchWebOverlay("${FbItem._SEARCH.url}/?q=${query.urlEncode()}", fbCookie, prefs)
true
}
closeListener = { _ -> searchViewCache.clear() }
foregroundColor = themeProvider.textColor
@ -702,11 +649,8 @@ abstract class BaseMainActivity :
val intent = Intent(this, SettingsActivity::class.java)
intent.putParcelableArrayListExtra(EXTRA_COOKIES, cookies())
val bundle =
ActivityOptions.makeCustomAnimation(
this,
R.anim.kau_slide_in_right,
R.anim.kau_fade_out
).toBundle()
ActivityOptions.makeCustomAnimation(this, R.anim.kau_slide_in_right, R.anim.kau_fade_out)
.toBundle()
startActivityForResult(intent, ACTIVITY_SETTINGS, bundle)
}
else -> return super.onOptionsItemSelected(item)
@ -796,9 +740,7 @@ abstract class BaseMainActivity :
}
override fun collapseAppBar() {
with(contentBinding) {
appbar.post { appbar.setExpanded(false) }
}
with(contentBinding) { appbar.post { appbar.setExpanded(false) } }
}
override fun backConsumer(): Boolean {
@ -827,7 +769,9 @@ abstract class BaseMainActivity :
inline val currentFragment: BaseFragment?
get() {
val viewpager = contentBinding.viewpager
return supportFragmentManager.findFragmentByTag("android:switcher:${viewpager.id}:${viewpager.currentItem}") as BaseFragment?
return supportFragmentManager.findFragmentByTag(
"android:switcher:${viewpager.id}:${viewpager.currentItem}"
) as BaseFragment?
}
override fun reloadFragment(fragment: BaseFragment) {
@ -840,9 +784,7 @@ abstract class BaseMainActivity :
private val forcedFallbacks = mutableSetOf<String>()
/**
* Update page list and prompt reload
*/
/** Update page list and prompt reload */
fun setPages(pages: List<FbItem>) {
this.pages.clear()
this.pages.addAll(pages)
@ -851,11 +793,12 @@ abstract class BaseMainActivity :
tabs.removeAllTabs()
this@SectionsPagerAdapter.pages.forEachIndexed { index, fbItem ->
tabs.addTab(
tabs.newTab()
tabs
.newTab()
.setCustomView(
BadgedIcon(this@BaseMainActivity).apply {
iicon = fbItem.icon
}.also {
BadgedIcon(this@BaseMainActivity)
.apply { iicon = fbItem.icon }
.also {
it.setAllAlpha(if (index == 0) SELECTED_TAB_ALPHA else UNSELECTED_TAB_ALPHA)
}
)
@ -865,9 +808,7 @@ abstract class BaseMainActivity :
viewpager.setCurrentItem(0, false)
viewpager.offscreenPageLimit = pages.size
// todo check if post is necessary
viewpager.post {
fragmentEmit(0)
} // trigger hook so title is set
viewpager.post { fragmentEmit(0) } // trigger hook so title is set
}
}
@ -878,8 +819,7 @@ abstract class BaseMainActivity :
fun restoreInstanceState(savedInstanceState: Bundle) {
forcedFallbacks.clear()
forcedFallbacks.addAll(
savedInstanceState.getStringArrayList(STATE_FORCE_FALLBACK)
?: emptyList()
savedInstanceState.getStringArrayList(STATE_FORCE_FALLBACK) ?: emptyList()
)
}
@ -920,8 +860,7 @@ abstract class BaseMainActivity :
get() {
if (prefs.mainActivityLayout == MainActivityLayout.BOTTOM_BAR)
lowerVideoPaddingPointF.set(0f, contentBinding.toolbar.height.toFloat())
else
lowerVideoPaddingPointF.set(0f, 0f)
else lowerVideoPaddingPointF.set(0f, 0f)
return lowerVideoPaddingPointF
}
@ -939,5 +878,7 @@ object MainActivityModule {
@ActivityScoped
fun contract(@ActivityContext context: Context): MainActivityContract =
(context as? BaseMainActivity)
?: throw IllegalArgumentException("${context::class.java.simpleName} does not implement MainActivityContract")
?: throw IllegalArgumentException(
"${context::class.java.simpleName} does not implement MainActivityContract"
)
}

View File

@ -35,15 +35,13 @@ import com.pitchedapps.frost.utils.ActivityThemer
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.createFreshDir
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineExceptionHandler
import java.io.File
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.CoroutineExceptionHandler
/**
* Created by Allan Wang on 05/01/18.
*/
/** Created by Allan Wang on 05/01/18. */
@AndroidEntryPoint
class DebugActivity : KauBaseActivity() {
@ -54,11 +52,9 @@ class DebugActivity : KauBaseActivity() {
fun baseDir(context: Context) = File(context.externalCacheDir, "offline_debug")
}
@Inject
lateinit var activityThemer: ActivityThemer
@Inject lateinit var activityThemer: ActivityThemer
@Inject
lateinit var themeProvider: ThemeProvider
@Inject lateinit var themeProvider: ThemeProvider
lateinit var binding: ActivityDebugBinding
@ -77,9 +73,7 @@ class DebugActivity : KauBaseActivity() {
}
setTitle(R.string.debug_frost)
activityThemer.setFrostColors {
toolbar(toolbar)
}
activityThemer.setFrostColors { toolbar(toolbar) }
debugWebview.loadUrl(FbItem.FEED.url)
debugWebview.onPageFinished = { swipeRefresh.isRefreshing = false }
@ -101,19 +95,15 @@ class DebugActivity : KauBaseActivity() {
parent.createFreshDir()
val body: String? = suspendCoroutine { cont ->
debugWebview.evaluateJavascript(JsActions.RETURN_BODY.function) {
cont.resume(it)
}
debugWebview.evaluateJavascript(JsActions.RETURN_BODY.function) { cont.resume(it) }
}
val hasScreenshot: Boolean =
debugWebview.getScreenshot(File(parent, "screenshot.png"))
val hasScreenshot: Boolean = debugWebview.getScreenshot(File(parent, "screenshot.png"))
val intent = Intent()
intent.putExtra(RESULT_URL, debugWebview.url)
intent.putExtra(RESULT_SCREENSHOT, hasScreenshot)
if (body != null)
intent.putExtra(RESULT_BODY, body)
if (body != null) intent.putExtra(RESULT_BODY, body)
setResult(Activity.RESULT_OK, intent)
finish()
}
@ -136,9 +126,6 @@ class DebugActivity : KauBaseActivity() {
}
override fun onBackPressed() {
if (binding.debugWebview.canGoBack())
binding.debugWebview.goBack()
else
super.onBackPressed()
if (binding.debugWebview.canGoBack()) binding.debugWebview.goBack() else super.onBackPressed()
}
}

View File

@ -62,39 +62,32 @@ import com.pitchedapps.frost.utils.frostUriFromFile
import com.pitchedapps.frost.utils.isIndirectImageUrl
import com.pitchedapps.frost.utils.logFrostEvent
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileNotFoundException
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.max
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
/**
* Created by Allan Wang on 2017-07-15.
*/
/** Created by Allan Wang on 2017-07-15. */
@AndroidEntryPoint
class ImageActivity : KauBaseActivity() {
@Inject
lateinit var activityThemer: ActivityThemer
@Inject lateinit var activityThemer: ActivityThemer
@Inject
lateinit var prefs: Prefs
@Inject lateinit var prefs: Prefs
@Inject
lateinit var themeProvider: ThemeProvider
@Inject lateinit var themeProvider: ThemeProvider
@Volatile
internal var errorRef: Throwable? = null
@Volatile internal var errorRef: Throwable? = null
/**
* Reference to the temporary file path
*/
internal val tempFile: File? get() = binding.imagePhoto.currentImageFile
/** Reference to the temporary file path */
internal val tempFile: File?
get() = binding.imagePhoto.currentImageFile
private lateinit var dragHelper: ViewDragHelper
@ -110,8 +103,7 @@ class ImageActivity : KauBaseActivity() {
private var bottomBehavior: BottomSheetBehavior<View>? = null
private val baseBackgroundColor: Int
get() = if (prefs.blackMediaBg) Color.BLACK
else themeProvider.bgColor.withMinAlpha(235)
get() = if (prefs.blackMediaBg) Color.BLACK else themeProvider.bgColor.withMinAlpha(235)
private fun loadError(e: Throwable) {
if (e.message?.contains("<!DOCTYPE html>") == true) {
@ -121,10 +113,7 @@ class ImageActivity : KauBaseActivity() {
}
errorRef = e
e.logFrostEvent("Image load error")
with(binding) {
if (imageProgress.isVisible)
imageProgress.fadeOut()
}
with(binding) { if (imageProgress.isVisible) imageProgress.fadeOut() }
tempFile?.delete()
binding.error.fadeIn()
}
@ -135,13 +124,13 @@ class ImageActivity : KauBaseActivity() {
return finish()
}
L.i { "Displaying image" }
trueImageUrl = async(Dispatchers.IO) {
val result = if (!imageUrl.isIndirectImageUrl) imageUrl
trueImageUrl =
async(Dispatchers.IO) {
val result =
if (!imageUrl.isIndirectImageUrl) imageUrl
else cookie?.getFullSizedImageUrl(imageUrl) ?: imageUrl
if (result != imageUrl)
L.v { "Launching image with true url $result" }
else
L.v { "Launching image with url $result" }
if (result != imageUrl) L.v { "Launching image with true url $result" }
else L.v { "Launching image with url $result" }
result
}
binding = ActivityImageBinding.inflate(layoutInflater)
@ -154,14 +143,16 @@ class ImageActivity : KauBaseActivity() {
private fun ActivityImageBinding.showImage(url: String) {
imagePhoto.showImage(Uri.parse(url))
imagePhoto.setImageShownCallback(object : ImageShownCallback {
imagePhoto.setImageShownCallback(
object : ImageShownCallback {
override fun onThumbnailShown() {}
override fun onMainImageShown() {
imageProgress.fadeOut()
imagePhoto.animate().alpha(1f).scaleXY(1f).start()
}
})
}
)
}
private fun ActivityImageBinding.init() {
@ -172,12 +163,12 @@ class ImageActivity : KauBaseActivity() {
imageText.gone()
} else {
imageText.setTextColor(if (prefs.blackMediaBg) Color.WHITE else themeProvider.textColor)
imageText.setBackgroundColor(
baseBackgroundColor.colorToForeground(0.2f).withAlpha(255)
)
imageText.setBackgroundColor(baseBackgroundColor.colorToForeground(0.2f).withAlpha(255))
imageText.text = text
bottomBehavior = BottomSheetBehavior.from<View>(imageText).apply {
addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
bottomBehavior =
BottomSheetBehavior.from<View>(imageText).apply {
addBottomSheetCallback(
object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
imageText.alpha = slideOffset / 2 + 0.5f
}
@ -185,7 +176,8 @@ class ImageActivity : KauBaseActivity() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
// No op
}
})
}
)
}
imageText.bringToFront()
}
@ -202,14 +194,11 @@ class ImageActivity : KauBaseActivity() {
invisible()
setState(FabStates.ERROR)
}
download.apply {
setState(FabStates.DOWNLOAD)
}
share.apply {
setState(FabStates.SHARE)
}
download.apply { setState(FabStates.DOWNLOAD) }
share.apply { setState(FabStates.SHARE) }
imagePhoto.setImageLoaderCallback(object : ImageLoader.Callback {
imagePhoto.setImageLoaderCallback(
object : ImageLoader.Callback {
override fun onCacheHit(imageType: Int, image: File?) {}
override fun onCacheMiss(imageType: Int, image: File?) {}
@ -225,12 +214,12 @@ class ImageActivity : KauBaseActivity() {
override fun onFail(error: Exception) {
loadError(error)
}
})
activityThemer.setFrostColors {
themeWindow = false
}
dragHelper = ViewDragHelper.create(imageDrag, ViewDragCallback()).apply {
)
activityThemer.setFrostColors { themeWindow = false }
dragHelper =
ViewDragHelper.create(imageDrag, ViewDragCallback()).apply {
setEdgeTrackingEnabled(ViewDragHelper.EDGE_TOP or ViewDragHelper.EDGE_BOTTOM)
}
imageDrag.dragHelper = dragHelper
@ -251,13 +240,7 @@ class ImageActivity : KauBaseActivity() {
override fun getViewVerticalDragRange(child: View): Int = child.height
override fun onViewPositionChanged(
changedView: View,
left: Int,
top: Int,
dx: Int,
dy: Int
) {
override fun onViewPositionChanged(changedView: View, left: Int, top: Int, dx: Int, dy: Int) {
super.onViewPositionChanged(changedView, left, top, dx, dy)
with(binding) {
// make sure that we are using the proper axis
@ -284,7 +267,8 @@ class ImageActivity : KauBaseActivity() {
override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
val overScrolled = scrollPercent > scrollThreshold
val maxOffset = releasedChild.height + 10
val finalTop = when {
val finalTop =
when {
scrollToTop && (overScrolled || yvel < -dragHelper.minVelocity) -> -maxOffset
!scrollToTop && (overScrolled || yvel > dragHelper.minVelocity) -> maxOffset
else -> 0
@ -315,7 +299,9 @@ internal enum class FabStates(
ERROR(GoogleMaterial.Icon.gmd_error, { Color.WHITE }, Color.RED) {
override fun onClick(activity: ImageActivity) {
val err =
activity.errorRef?.takeIf { it !is FileNotFoundException && it.message != "Image failed to decode using JPEG decoder" }
activity.errorRef?.takeIf {
it !is FileNotFoundException && it.message != "Image failed to decode using JPEG decoder"
}
?: return
activity.materialDialog {
title(R.string.kau_error)
@ -339,7 +325,8 @@ internal enum class FabStates(
val file = activity.tempFile ?: return
try {
val photoURI = activity.frostUriFromFile(file)
val intent = Intent(Intent.ACTION_SEND).apply {
val intent =
Intent(Intent.ACTION_SEND).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
putExtra(Intent.EXTRA_STREAM, photoURI)
type = "image/png"
@ -354,29 +341,28 @@ internal enum class FabStates(
};
/**
* Change the fab look
* If it's in view, give it some animations
* Change the fab look If it's in view, give it some animations
*
* TODO investigate what is wrong with fadeScaleTransition
*
* https://github.com/AllanWang/KAU/issues/184
*
*/
fun update(fab: FloatingActionButton, themeProvider: ThemeProvider) {
val tint =
if (backgroundTint != Int.MAX_VALUE) backgroundTint else themeProvider.accentColor
val tint = if (backgroundTint != Int.MAX_VALUE) backgroundTint else themeProvider.accentColor
val iconColor = iconColorProvider(themeProvider)
if (fab.isHidden) {
fab.setIcon(iicon, color = iconColor)
fab.backgroundTintList = ColorStateList.valueOf(tint)
fab.show()
} else {
fab.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
fab.hide(
object : FloatingActionButton.OnVisibilityChangedListener() {
override fun onHidden(fab: FloatingActionButton) {
fab.setIcon(iicon, color = iconColor)
fab.show()
}
})
}
)
}
}

View File

@ -55,30 +55,23 @@ import com.pitchedapps.frost.utils.launchNewTask
import com.pitchedapps.frost.utils.loadAssets
import com.pitchedapps.frost.widgets.NotificationWidget
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Created by Allan Wang on 2017-07-25.
*
* A beautiful intro activity
* Phone showcases are drawn via layers
* A beautiful intro activity Phone showcases are drawn via layers
*/
@AndroidEntryPoint
class IntroActivity :
KauBaseActivity(),
ViewPager.PageTransformer,
ViewPager.OnPageChangeListener {
class IntroActivity : KauBaseActivity(), ViewPager.PageTransformer, ViewPager.OnPageChangeListener {
@Inject
lateinit var prefs: Prefs
@Inject lateinit var prefs: Prefs
@Inject
lateinit var themeProvider: ThemeProvider
@Inject lateinit var themeProvider: ThemeProvider
@Inject
lateinit var activityThemer: ActivityThemer
@Inject lateinit var activityThemer: ActivityThemer
lateinit var binding: ActivityIntroBinding
private var barHasNext = true
@ -132,9 +125,8 @@ class IntroActivity :
}
/**
* Transformations are mainly handled on a per view basis
* This makes the first fragment fade out as the second fragment comes in
* All fragments are locked in position
* Transformations are mainly handled on a per view basis This makes the first fragment fade out
* as the second fragment comes in All fragments are locked in position
*/
override fun transformPage(page: View, position: Float) {
// only apply to adjacent pages
@ -155,28 +147,22 @@ class IntroActivity :
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
)
binding.ripple.ripple(blue, x, y, 600) {
postDelayed(1000) { finish() }
}
binding.ripple.ripple(blue, x, y, 600) { postDelayed(1000) { finish() } }
val lastView: View? = fragments.last().view
arrayOf<View?>(
binding.skip, binding.indicator, binding.next,
binding.skip,
binding.indicator,
binding.next,
lastView?.findViewById(R.id.intro_title),
lastView?.findViewById(R.id.intro_desc)
).forEach {
it?.animate()?.alpha(0f)?.setDuration(600)?.start()
}
)
.forEach { it?.animate()?.alpha(0f)?.setDuration(600)?.start() }
if (themeProvider.textColor != Color.WHITE) {
val f = lastView?.findViewById<ImageView>(R.id.intro_image)?.drawable
if (f != null)
ValueAnimator.ofFloat(0f, 1f).apply {
addUpdateListener {
f.setTint(
themeProvider.textColor.blendWith(
Color.WHITE,
it.animatedValue as Float
)
)
f.setTint(themeProvider.textColor.blendWith(Color.WHITE, it.animatedValue as Float))
}
duration = 600
start()
@ -211,13 +197,11 @@ class IntroActivity :
}
}
override fun onPageScrollStateChanged(state: Int) {
}
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
fragments[position].onPageScrolled(positionOffset)
if (position + 1 < fragments.size)
fragments[position + 1].onPageScrolled(positionOffset - 1)
if (position + 1 < fragments.size) fragments[position + 1].onPageScrolled(positionOffset - 1)
}
override fun onPageSelected(position: Int) {

View File

@ -49,6 +49,9 @@ import com.pitchedapps.frost.web.FrostEmitter
import com.pitchedapps.frost.web.LoginWebView
import com.pitchedapps.frost.web.asFrostEmitter
import dagger.hilt.android.AndroidEntryPoint
import java.net.UnknownHostException
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.BufferOverflow
@ -63,18 +66,12 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import java.net.UnknownHostException
import javax.inject.Inject
import kotlin.coroutines.resume
/**
* Created by Allan Wang on 2017-06-01.
*/
/** Created by Allan Wang on 2017-06-01. */
@AndroidEntryPoint
class LoginActivity : BaseActivity() {
@Inject
lateinit var cookieDao: CookieDao
@Inject lateinit var cookieDao: CookieDao
private val toolbar: Toolbar by bindView(R.id.toolbar)
private val web: LoginWebView by bindView(R.id.login_webview)
@ -84,7 +81,8 @@ class LoginActivity : BaseActivity() {
private lateinit var profileLoader: RequestManager
private val refreshMutableFlow = MutableSharedFlow<Boolean>(
private val refreshMutableFlow =
MutableSharedFlow<Boolean>(
extraBufferCapacity = 10,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
@ -98,15 +96,10 @@ class LoginActivity : BaseActivity() {
setContentView(R.layout.activity_login)
setSupportActionBar(toolbar)
setTitle(R.string.kau_login)
activityThemer.setFrostColors {
toolbar(toolbar)
}
activityThemer.setFrostColors { toolbar(toolbar) }
profileLoader = GlideApp.with(profile)
refreshFlow
.distinctUntilChanged()
.onEach { swipeRefresh.isRefreshing = it }
.launchIn(this)
refreshFlow.distinctUntilChanged().onEach { swipeRefresh.isRefreshing = it }.launchIn(this)
launch {
val cookie = web.loadLogin { refresh(it != 100) }.await()
@ -153,16 +146,17 @@ class LoginActivity : BaseActivity() {
*/
val cookies = ArrayList(cookieDao.selectAll())
delay(1000)
if (prefs.intro)
launchNewTask<IntroActivity>(cookies, true)
else
launchNewTask<MainActivity>(cookies, true)
if (prefs.intro) launchNewTask<IntroActivity>(cookies, true)
else launchNewTask<MainActivity>(cookies, true)
}
private suspend fun loadProfile(id: Long): Boolean = withMainContext {
suspendCancellableCoroutine<Boolean> { cont ->
profileLoader.load(profilePictureUrl(id))
.transform(FrostGlide.circleCrop).listener(object : RequestListener<Drawable> {
profileLoader
.load(profilePictureUrl(id))
.transform(FrostGlide.circleCrop)
.listener(
object : RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable?,
model: Any?,
@ -184,18 +178,19 @@ class LoginActivity : BaseActivity() {
cont.resume(false)
return false
}
}).into(profile)
}
)
.into(profile)
}
}
private suspend fun loadUsername(cookie: CookieEntity): String? = withContext(Dispatchers.IO) {
val result: String? = try {
withTimeout(5000) {
frostJsoup(cookie.cookie, FbItem.PROFILE.url).title()
}
private suspend fun loadUsername(cookie: CookieEntity): String? =
withContext(Dispatchers.IO) {
val result: String? =
try {
withTimeout(5000) { frostJsoup(cookie.cookie, FbItem.PROFILE.url).title() }
} catch (e: Exception) {
if (e !is UnknownHostException)
e.logFrostEvent("Fetch username failed")
if (e !is UnknownHostException) e.logFrostEvent("Fetch username failed")
null
}

View File

@ -40,10 +40,8 @@ import kotlinx.coroutines.flow.onEach
class MainActivity : BaseMainActivity() {
private val fragmentMutableFlow = MutableSharedFlow<Int>(
extraBufferCapacity = 10,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
private val fragmentMutableFlow =
MutableSharedFlow<Int>(extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST)
override val fragmentFlow: SharedFlow<Int> = fragmentMutableFlow.asSharedFlow()
override val fragmentEmit: FrostEmitter<Int> = fragmentMutableFlow.asFrostEmitter()
@ -59,7 +57,8 @@ class MainActivity : BaseMainActivity() {
}
private fun ActivityMainContentBinding.setupViewPager() {
viewpager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
viewpager.addOnPageChangeListener(
object : ViewPager.SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if (lastPosition == position) {
@ -89,12 +88,14 @@ class MainActivity : BaseMainActivity() {
)
}
}
})
}
)
}
private fun ActivityMainContentBinding.setupTabs() {
viewpager.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabs))
tabs.addOnTabSelectedListener(object : TabLayout.ViewPagerOnTabSelectedListener(viewpager) {
tabs.addOnTabSelectedListener(
object : TabLayout.ViewPagerOnTabSelectedListener(viewpager) {
override fun onTabReselected(tab: TabLayout.Tab) {
super.onTabReselected(tab)
currentFragment?.onTabClick()
@ -104,14 +105,12 @@ class MainActivity : BaseMainActivity() {
super.onTabSelected(tab)
(tab.customView as BadgedIcon).badgeText = null
}
})
}
)
headerFlow
.filter { it.isNotBlank() }
.mapNotNull { html ->
BadgeParser.parseFromData(
cookie = fbCookie.webCookie,
text = html
)?.data
BadgeParser.parseFromData(cookie = fbCookie.webCookie, text = html)?.data
}
.distinctUntilChanged()
.flowOn(Dispatchers.IO)

View File

@ -32,9 +32,7 @@ import com.pitchedapps.frost.utils.launchNewTask
import com.pitchedapps.frost.views.AccountItem
import kotlinx.coroutines.launch
/**
* Created by Allan Wang on 2017-06-04.
*/
/** Created by Allan Wang on 2017-06-04. */
class SelectorActivity : BaseActivity() {
val recycler: RecyclerView by bindView(R.id.selector_recycler)
@ -49,7 +47,8 @@ class SelectorActivity : BaseActivity() {
recycler.adapter = adapter
adapter.add(cookies().map { AccountItem(it, themeProvider) })
adapter.add(AccountItem(null, themeProvider)) // add account
adapter.addEventHook(object : ClickEventHook<AccountItem>() {
adapter.addEventHook(
object : ClickEventHook<AccountItem>() {
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? =
(viewHolder as? AccountItem.ViewHolder)?.itemView
@ -60,12 +59,14 @@ class SelectorActivity : BaseActivity() {
item: AccountItem
) {
if (item.cookie == null) this@SelectorActivity.launchNewTask<LoginActivity>()
else launch {
else
launch {
fbCookie.switchUser(item.cookie)
launchNewTask<MainActivity>(cookies())
}
}
})
}
)
activityThemer.setFrostColors {
text(text)
background(container)

View File

@ -59,30 +59,23 @@ import com.pitchedapps.frost.utils.frostNavigationBar
import com.pitchedapps.frost.utils.launchNewTask
import com.pitchedapps.frost.utils.loadAssets
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Created by Allan Wang on 2017-06-06.
*/
/** Created by Allan Wang on 2017-06-06. */
@AndroidEntryPoint
class SettingsActivity : KPrefActivity() {
@Inject
lateinit var fbCookie: FbCookie
@Inject lateinit var fbCookie: FbCookie
@Inject
lateinit var prefs: Prefs
@Inject lateinit var prefs: Prefs
@Inject
lateinit var themeProvider: ThemeProvider
@Inject lateinit var themeProvider: ThemeProvider
@Inject
lateinit var notifDao: NotificationDao
@Inject lateinit var notifDao: NotificationDao
@Inject
lateinit var activityThemer: ActivityThemer
@Inject lateinit var activityThemer: ActivityThemer
private var resultFlag = Activity.RESULT_CANCELED
@ -99,8 +92,7 @@ class SettingsActivity : KPrefActivity() {
if (fetchRingtone(requestCode, resultCode, data)) return
when (requestCode) {
ACTIVITY_REQUEST_TABS -> {
if (resultCode == Activity.RESULT_OK)
shouldRestartMain()
if (resultCode == Activity.RESULT_OK) shouldRestartMain()
return
}
ACTIVITY_REQUEST_DEBUG -> {
@ -113,21 +105,15 @@ class SettingsActivity : KPrefActivity() {
reloadList()
}
/**
* Fetch ringtone and save uri
* Returns [true] if consumed, [false] otherwise
*/
/** Fetch ringtone and save uri Returns [true] if consumed, [false] otherwise */
private fun fetchRingtone(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode and REQUEST_RINGTONE != REQUEST_RINGTONE || resultCode != Activity.RESULT_OK) return false
if (requestCode and REQUEST_RINGTONE != REQUEST_RINGTONE || resultCode != Activity.RESULT_OK)
return false
val uri = data?.getParcelableExtra<Uri>(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
val uriString: String = uri?.toString() ?: ""
if (uri != null) {
try {
grantUriPermission(
"com.android.systemui",
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
grantUriPermission("com.android.systemui", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
} catch (e: Exception) {
L.e(e) { "grantUriPermission" }
}
@ -189,9 +175,7 @@ class SettingsActivity : KPrefActivity() {
onClick = {
startActivityForResult<AboutActivity>(
9,
bundleBuilder = {
withSceneTransitionAnimation(this@SettingsActivity)
}
bundleBuilder = { withSceneTransitionAnimation(this@SettingsActivity) }
)
}
}
@ -240,13 +224,9 @@ class SettingsActivity : KPrefActivity() {
}
fun themeExterior(animate: Boolean = true) {
if (animate) bgCanvas.fade(themeProvider.bgColor)
else bgCanvas.set(themeProvider.bgColor)
if (animate) toolbarCanvas.ripple(
themeProvider.headerColor,
RippleCanvas.MIDDLE,
RippleCanvas.END
)
if (animate) bgCanvas.fade(themeProvider.bgColor) else bgCanvas.set(themeProvider.bgColor)
if (animate)
toolbarCanvas.ripple(themeProvider.headerColor, RippleCanvas.MIDDLE, RippleCanvas.END)
else toolbarCanvas.set(themeProvider.headerColor)
frostNavigationBar(prefs, themeProvider)
}
@ -265,7 +245,8 @@ class SettingsActivity : KPrefActivity() {
menuInflater.inflate(R.menu.menu_settings, menu)
toolbar.tint(themeProvider.iconColor)
setMenuIcons(
menu, themeProvider.iconColor,
menu,
themeProvider.iconColor,
R.id.action_github to CommunityMaterial.Icon2.cmd_github,
R.id.action_changelog to GoogleMaterial.Icon.gmd_info
)

View File

@ -43,19 +43,16 @@ import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.iitems.TabIItem
import com.pitchedapps.frost.utils.L
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import java.util.Collections
import javax.inject.Inject
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
/**
* Created by Allan Wang on 26/11/17.
*/
/** Created by Allan Wang on 26/11/17. */
@AndroidEntryPoint
class TabCustomizerActivity : BaseActivity() {
@Inject
lateinit var genericDao: GenericDao
@Inject lateinit var genericDao: GenericDao
private val adapter = FastItemAdapter<TabIItem>()
@ -91,7 +88,10 @@ class TabCustomizerActivity : BaseActivity() {
bindSwapper(adapter, tabRecycler)
adapter.onClickListener = { view, _, _, _ -> view!!.wobble(); true }
adapter.onClickListener = { view, _, _, _ ->
view!!.wobble()
true
}
}
setResult(Activity.RESULT_CANCELED)
@ -109,9 +109,7 @@ class TabCustomizerActivity : BaseActivity() {
fabCancel.setIcon(GoogleMaterial.Icon.gmd_close, themeProvider.iconColor)
fabCancel.backgroundTintList = ColorStateList.valueOf(themeProvider.accentColor)
fabCancel.setOnClickListener { finish() }
activityThemer.setFrostColors {
themeWindow = true
}
activityThemer.setFrostColors { themeWindow = true }
}
private fun View.wobble() = startAnimation(wobble(context))
@ -121,7 +119,8 @@ class TabCustomizerActivity : BaseActivity() {
ItemTouchHelper(dragCallback).attachToRecyclerView(recycler)
}
private fun swapper(adapter: FastItemAdapter<*>) = object : ItemTouchCallback {
private fun swapper(adapter: FastItemAdapter<*>) =
object : ItemTouchCallback {
override fun itemTouchOnMove(oldPosition: Int, newPosition: Int): Boolean {
Collections.swap(adapter.adapterItems, oldPosition, newPosition)
adapter.notifyAdapterDataSetChanged()
@ -131,10 +130,8 @@ class TabCustomizerActivity : BaseActivity() {
override fun itemTouchDropped(oldPosition: Int, newPosition: Int) = Unit
}
private class TabDragCallback(
directions: Int,
itemTouchCallback: ItemTouchCallback
) : SimpleDragCallback(directions, itemTouchCallback) {
private class TabDragCallback(directions: Int, itemTouchCallback: ItemTouchCallback) :
SimpleDragCallback(directions, itemTouchCallback) {
private var draggingView: TabIItem.ViewHolder? = null

View File

@ -64,6 +64,7 @@ import com.pitchedapps.frost.views.FrostContentWeb
import com.pitchedapps.frost.views.FrostVideoViewer
import com.pitchedapps.frost.views.FrostWebView
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn
@ -71,7 +72,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import javax.inject.Inject
/**
* Created by Allan Wang on 2017-06-01.
@ -83,8 +83,8 @@ import javax.inject.Inject
*/
/**
* Used by notifications. Unlike the other overlays, this runs as a singleInstance
* Going back will bring you back to the previous app
* Used by notifications. Unlike the other overlays, this runs as a singleInstance Going back will
* bring you back to the previous app
*/
class FrostWebActivity : WebOverlayActivityBase() {
@ -110,8 +110,8 @@ class FrostWebActivity : WebOverlayActivityBase() {
}
/**
* Attempts to parse the action url
* Returns [true] if no action exists or if the action has been consumed, [false] if we need to notify the user of a bad action
* Attempts to parse the action url Returns [true] if no action exists or if the action has been
* consumed, [false] if we need to notify the user of a bad action
*/
private fun parseActionSend(): Boolean {
if (intent.action != Intent.ACTION_SEND || intent.type != "text/plain") return true
@ -133,27 +133,26 @@ class FrostWebActivity : WebOverlayActivityBase() {
}
/**
* Variant that forces a mobile user agent. This is largely internal,
* and is only necessary when we are launching from an existing [WebOverlayActivityBase]
* Variant that forces a mobile user agent. This is largely internal, and is only necessary when we
* are launching from an existing [WebOverlayActivityBase]
*/
class WebOverlayMobileActivity : WebOverlayActivityBase(USER_AGENT_MOBILE_CONST)
/**
* Variant that forces a desktop user agent. This is largely internal,
* and is only necessary when we are launching from an existing [WebOverlayActivityBase]
* Variant that forces a desktop user agent. This is largely internal, and is only necessary when we
* are launching from an existing [WebOverlayActivityBase]
*/
class WebOverlayDesktopActivity : WebOverlayActivityBase(USER_AGENT_DESKTOP_CONST)
/**
* Internal overlay for the app; this is tied with the main task and is singleTop as opposed to singleInstance
* Internal overlay for the app; this is tied with the main task and is singleTop as opposed to
* singleInstance
*/
class WebOverlayActivity : WebOverlayActivityBase()
@AndroidEntryPoint
abstract class WebOverlayActivityBase(private val userAgent: String = USER_AGENT) :
BaseActivity(),
FrostContentContainer,
VideoViewHolder {
BaseActivity(), FrostContentContainer, VideoViewHolder {
override val frameWrapper: FrameLayout by bindView(R.id.frame_wrapper)
val toolbar: Toolbar by bindView(R.id.overlay_toolbar)
@ -162,17 +161,14 @@ abstract class WebOverlayActivityBase(private val userAgent: String = USER_AGENT
get() = content.coreView
private val coordinator: CoordinatorLayout by bindView(R.id.overlay_main_content)
@Inject
lateinit var webFileChooser: WebFileChooser
@Inject lateinit var webFileChooser: WebFileChooser
private inline val urlTest: String?
get() = intent.getStringExtra(ARG_URL) ?: intent.dataString
lateinit var swipeBack: SwipeBackContract
/**
* Nonnull variant; verify by checking [urlTest]
*/
/** Nonnull variant; verify by checking [urlTest] */
override val baseUrl: String
get() = urlTest!!.formattedFbUrl
@ -240,9 +236,8 @@ abstract class WebOverlayActivityBase(private val userAgent: String = USER_AGENT
}
/**
* Manage url loadings
* This is usually only called when multiple listeners are added and inject the same url
* We will avoid reloading if the url is the same
* Manage url loadings This is usually only called when multiple listeners are added and inject
* the same url We will avoid reloading if the url is the same
*/
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
@ -256,14 +251,11 @@ abstract class WebOverlayActivityBase(private val userAgent: String = USER_AGENT
}
override fun backConsumer(): Boolean {
if (!web.onBackPressed())
finishSlideOut()
if (!web.onBackPressed()) finishSlideOut()
return true
}
/**
* Our theme for the overlay should be fully opaque
*/
/** Our theme for the overlay should be fully opaque */
fun theme() {
val opaqueAccent = themeProvider.headerColor.withAlpha(255)
statusBarColor = opaqueAccent.darken()
@ -309,7 +301,8 @@ abstract class WebOverlayActivityBase(private val userAgent: String = USER_AGENT
R.id.action_copy_link -> copyToClipboard(url)
R.id.action_share -> shareText(url)
R.id.action_open_in_browser -> startLink(url)
else -> if (!OverlayContext.onOptionsItemSelected(web, item.itemId))
else ->
if (!OverlayContext.onOptionsItemSelected(web, item.itemId))
return super.onOptionsItemSelected(item)
}
return true

View File

@ -31,9 +31,7 @@ interface MainActivityContract : MainFabContract {
fun setTitle(res: Int)
fun setTitle(text: CharSequence)
/**
* Available on all threads
*/
/** Available on all threads */
fun collapseAppBar()
fun reloadFragment(fragment: BaseFragment)

View File

@ -16,29 +16,18 @@
*/
package com.pitchedapps.frost.contracts
/**
* Functions that will modify the current ui
*/
/** Functions that will modify the current ui */
interface DynamicUiContract {
/**
* Change all necessary view components to the new theme
* Also propagate where applicable
*/
/** Change all necessary view components to the new theme Also propagate where applicable */
fun reloadTheme()
/**
* Change theme without propagation
*/
/** Change theme without propagation */
fun reloadThemeSelf()
/**
* Change text size & propagate
*/
/** Change text size & propagate */
fun reloadTextSize()
/**
* Change text size without propagation
*/
/** Change text size without propagation */
fun reloadTextSizeSelf()
}

View File

@ -35,9 +35,7 @@ import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped
import javax.inject.Inject
/**
* Created by Allan Wang on 2017-07-04.
*/
/** Created by Allan Wang on 2017-07-04. */
private const val MEDIA_CHOOSER_RESULT = 67
interface WebFileChooser {
@ -46,17 +44,13 @@ interface WebFileChooser {
fileChooserParams: WebChromeClient.FileChooserParams
)
fun onActivityResultWeb(
requestCode: Int,
resultCode: Int,
intent: Intent?
): Boolean
fun onActivityResultWeb(requestCode: Int, resultCode: Int, intent: Intent?): Boolean
}
class WebFileChooserImpl @Inject internal constructor(
private val activity: Activity,
private val themeProvider: ThemeProvider
) : WebFileChooser {
class WebFileChooserImpl
@Inject
internal constructor(private val activity: Activity, private val themeProvider: ThemeProvider) :
WebFileChooser {
private var filePathCallback: ValueCallback<Array<Uri>?>? = null
override fun openMediaPicker(
@ -81,11 +75,7 @@ class WebFileChooserImpl @Inject internal constructor(
}
}
override fun onActivityResultWeb(
requestCode: Int,
resultCode: Int,
intent: Intent?
): Boolean {
override fun onActivityResultWeb(requestCode: Int, resultCode: Int, intent: Intent?): Boolean {
L.d { "FileChooser On activity results web $requestCode" }
if (requestCode != MEDIA_CHOOSER_RESULT) return false
val data = intent?.data
@ -98,7 +88,5 @@ class WebFileChooserImpl @Inject internal constructor(
@Module
@InstallIn(ActivityComponent::class)
interface WebFileChooserModule {
@Binds
@ActivityScoped
fun webFileChooser(to: WebFileChooserImpl): WebFileChooser
@Binds @ActivityScoped fun webFileChooser(to: WebFileChooserImpl): WebFileChooser
}

View File

@ -22,53 +22,37 @@ import com.pitchedapps.frost.web.FrostEmitter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharedFlow
/**
* Created by Allan Wang on 20/12/17.
*/
/** Created by Allan Wang on 20/12/17. */
/**
* Contract for the underlying parent,
* binds to activities & fragments
*/
/** Contract for the underlying parent, binds to activities & fragments */
interface FrostContentContainer : CoroutineScope {
val baseUrl: String
val baseEnum: FbItem?
/**
* Update toolbar title
*/
/** Update toolbar title */
fun setTitle(title: String)
}
/**
* Contract for components shared among
* all content providers
*/
/** Contract for components shared among all content providers */
interface FrostContentParent : DynamicUiContract {
val scope: CoroutineScope
val core: FrostContentCore
/**
* Observable to get data on whether view is refreshing or not
*/
/** Observable to get data on whether view is refreshing or not */
val refreshFlow: SharedFlow<Boolean>
val refreshEmit: FrostEmitter<Boolean>
/**
* Observable to get data on refresh progress, with range [0, 100]
*/
/** Observable to get data on refresh progress, with range [0, 100] */
val progressFlow: SharedFlow<Int>
val progressEmit: FrostEmitter<Int>
/**
* Observable to get new title data (unique values only)
*/
/** Observable to get new title data (unique values only) */
val titleFlow: SharedFlow<String>
val titleEmit: FrostEmitter<String>
@ -77,104 +61,73 @@ interface FrostContentParent : DynamicUiContract {
var baseEnum: FbItem?
val swipeEnabled: Boolean get() = swipeAllowedByPage && !swipeDisabledByAction
val swipeEnabled: Boolean
get() = swipeAllowedByPage && !swipeDisabledByAction
/**
* Temporary disable swiping based on action
*/
/** Temporary disable swiping based on action */
var swipeDisabledByAction: Boolean
/**
* Decides if swipe should be allowed for the current page
*/
/** Decides if swipe should be allowed for the current page */
var swipeAllowedByPage: Boolean
/**
* Binds the container to self
* this will also handle all future bindings
* Must be called by container!
* Binds the container to self this will also handle all future bindings Must be called by
* container!
*/
fun bind(container: FrostContentContainer)
/**
* Signal that the contract will not be used again
* Clean up resources where applicable
*/
/** Signal that the contract will not be used again Clean up resources where applicable */
fun destroy()
/**
* Hook onto the refresh observable for one cycle
* Animate toggles between the fancy ripple and the basic fade
* The cycle only starts on the first load since
* there may have been another process when this is registered
* Hook onto the refresh observable for one cycle Animate toggles between the fancy ripple and the
* basic fade The cycle only starts on the first load since there may have been another process
* when this is registered
*
* Returns true to proceed with load
* In some cases when the url has not changed,
* it may not be advisable to proceed with the load
* For those cases, we will return false to stop it
* Returns true to proceed with load In some cases when the url has not changed, it may not be
* advisable to proceed with the load For those cases, we will return false to stop it
*/
fun registerTransition(urlChanged: Boolean, animate: Boolean): Boolean
}
/**
* Underlying contract for the content itself
*/
/** Underlying contract for the content itself */
interface FrostContentCore : DynamicUiContract {
val scope: CoroutineScope
get() = parent.scope
/**
* Reference to parent
* Bound through calling [FrostContentParent.bind]
*/
/** Reference to parent Bound through calling [FrostContentParent.bind] */
val parent: FrostContentParent
/**
* Initializes view through given [container]
*
* The content may be free to extract other data from
* the container if necessary
* The content may be free to extract other data from the container if necessary
*/
fun bind(parent: FrostContentParent, container: FrostContentContainer): View
/**
* Call to reload wrapped data
*/
/** Call to reload wrapped data */
fun reload(animate: Boolean)
/**
* Call to reload base data
*/
/** Call to reload base data */
fun reloadBase(animate: Boolean)
/**
* If possible, remove anything in the view stack
* Applies namely to webviews
*/
/** If possible, remove anything in the view stack Applies namely to webviews */
fun clearHistory()
/**
* Should be called when a back press is triggered
* Return [true] if consumed, [false] otherwise
* Should be called when a back press is triggered Return [true] if consumed, [false] otherwise
*/
fun onBackPressed(): Boolean
val currentUrl: String
/**
* Condition to help pause certain background resources
*/
/** Condition to help pause certain background resources */
var active: Boolean
/**
* Triggered when view is within viewpager
* and tab is clicked
*/
/** Triggered when view is within viewpager and tab is clicked */
fun onTabClicked()
/**
* Signal destruction to release some content manually
*/
/** Signal destruction to release some content manually */
fun destroy()
}

View File

@ -22,14 +22,14 @@ import android.widget.TextView
/**
* Created by Allan Wang on 2017-11-07.
*
* Should be implemented by all views in [com.pitchedapps.frost.activities.MainActivity]
* to allow for instant view reloading
* Should be implemented by all views in [com.pitchedapps.frost.activities.MainActivity] to allow
* for instant view reloading
*/
interface FrostThemable {
/**
* Change all necessary view components to the new theme
* and call whatever other children that also implement [FrostThemable]
* Change all necessary view components to the new theme and call whatever other children that
* also implement [FrostThemable]
*/
fun reloadTheme()

View File

@ -24,24 +24,17 @@ import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.views.FrostVideoContainerContract
import com.pitchedapps.frost.views.FrostVideoViewer
/**
* Created by Allan Wang on 2017-11-10.
*/
/** Created by Allan Wang on 2017-11-10. */
interface VideoViewHolder : FrameWrapper, FrostVideoContainerContract {
var videoViewer: FrostVideoViewer?
fun showVideo(url: String) = showVideo(url, false)
/**
* Create new viewer and reuse existing one
* The url will be formatted upon loading
*/
/** Create new viewer and reuse existing one The url will be formatted upon loading */
fun showVideo(url: String, repeat: Boolean) {
if (videoViewer != null)
videoViewer?.setVideo(url, repeat)
else
videoViewer = FrostVideoViewer.showVideo(url, repeat, this)
if (videoViewer != null) videoViewer?.setVideo(url, repeat)
else videoViewer = FrostVideoViewer.showVideo(url, repeat, this)
}
fun videoOnStop() = videoViewer?.pause()

View File

@ -26,24 +26,20 @@ import androidx.room.Query
import com.pitchedapps.frost.utils.L
import kotlinx.android.parcel.Parcelize
/**
* Created by Allan Wang on 2017-05-30.
*/
/** Created by Allan Wang on 2017-05-30. */
/**
* Generic cache to store serialized content
*/
/** Generic cache to store serialized content */
@Entity(
tableName = "frost_cache",
primaryKeys = ["id", "type"],
foreignKeys = [
foreignKeys =
[
ForeignKey(
entity = CookieEntity::class,
parentColumns = ["cookie_id"],
childColumns = ["id"],
onDelete = ForeignKey.CASCADE
)
]
)]
)
@Parcelize
data class CacheEntity(
@ -59,24 +55,17 @@ interface CacheDao {
@Query("SELECT * FROM frost_cache WHERE id = :id AND type = :type")
fun _select(id: Long, type: String): CacheEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun _insertCache(cache: CacheEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE) fun _insertCache(cache: CacheEntity)
@Query("DELETE FROM frost_cache WHERE id = :id AND type = :type")
fun _delete(id: Long, type: String)
}
suspend fun CacheDao.select(id: Long, type: String) = dao {
_select(id, type)
}
suspend fun CacheDao.select(id: Long, type: String) = dao { _select(id, type) }
suspend fun CacheDao.delete(id: Long, type: String) = dao {
_delete(id, type)
}
suspend fun CacheDao.delete(id: Long, type: String) = dao { _delete(id, type) }
/**
* Returns true if successful, given that there are constraints to the insertion
*/
/** Returns true if successful, given that there are constraints to the insertion */
suspend fun CacheDao.save(id: Long, type: String, contents: String): Boolean = dao {
try {
_insertCache(CacheEntity(id, type, System.currentTimeMillis(), contents))

View File

@ -28,16 +28,11 @@ import androidx.sqlite.db.SupportSQLiteDatabase
import com.pitchedapps.frost.prefs.Prefs
import kotlinx.android.parcel.Parcelize
/**
* Created by Allan Wang on 2017-05-30.
*/
/** Created by Allan Wang on 2017-05-30. */
@Entity(tableName = "cookies")
@Parcelize
data class CookieEntity(
@androidx.room.PrimaryKey
@ColumnInfo(name = "cookie_id")
val id: Long,
@androidx.room.PrimaryKey @ColumnInfo(name = "cookie_id") val id: Long,
val name: String?,
val cookie: String?,
val cookieMessenger: String? = null // Version 2
@ -51,35 +46,38 @@ data class CookieEntity(
@Dao
interface CookieDao {
@Query("SELECT * FROM cookies")
fun _selectAll(): List<CookieEntity>
@Query("SELECT * FROM cookies") fun _selectAll(): List<CookieEntity>
@Query("SELECT * FROM cookies WHERE cookie_id = :id")
fun _selectById(id: Long): CookieEntity?
@Query("SELECT * FROM cookies WHERE cookie_id = :id") fun _selectById(id: Long): CookieEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun _save(cookie: CookieEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE) fun _save(cookie: CookieEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun _save(cookies: List<CookieEntity>)
@Insert(onConflict = OnConflictStrategy.REPLACE) fun _save(cookies: List<CookieEntity>)
@Query("DELETE FROM cookies WHERE cookie_id = :id")
fun _deleteById(id: Long)
@Query("DELETE FROM cookies WHERE cookie_id = :id") fun _deleteById(id: Long)
@Query("UPDATE cookies SET cookieMessenger = :cookie WHERE cookie_id = :id")
fun _updateMessengerCookie(id: Long, cookie: String?)
}
suspend fun CookieDao.selectAll() = dao { _selectAll() }
suspend fun CookieDao.selectById(id: Long) = dao { _selectById(id) }
suspend fun CookieDao.save(cookie: CookieEntity) = dao { _save(cookie) }
suspend fun CookieDao.save(cookies: List<CookieEntity>) = dao { _save(cookies) }
suspend fun CookieDao.deleteById(id: Long) = dao { _deleteById(id) }
suspend fun CookieDao.currentCookie(prefs: Prefs) = selectById(prefs.userId)
suspend fun CookieDao.updateMessengerCookie(id: Long, cookie: String?) =
dao { _updateMessengerCookie(id, cookie) }
val COOKIES_MIGRATION_1_2 = object : Migration(1, 2) {
suspend fun CookieDao.selectById(id: Long) = dao { _selectById(id) }
suspend fun CookieDao.save(cookie: CookieEntity) = dao { _save(cookie) }
suspend fun CookieDao.save(cookies: List<CookieEntity>) = dao { _save(cookies) }
suspend fun CookieDao.deleteById(id: Long) = dao { _deleteById(id) }
suspend fun CookieDao.currentCookie(prefs: Prefs) = selectById(prefs.userId)
suspend fun CookieDao.updateMessengerCookie(id: Long, cookie: String?) = dao {
_updateMessengerCookie(id, cookie)
}
val COOKIES_MIGRATION_1_2 =
object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE cookies ADD COLUMN cookieMessenger TEXT")
}

View File

@ -20,9 +20,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Wraps dao calls to work with coroutines
* Non transactional queries were supposed to be fixed in https://issuetracker.google.com/issues/69474692,
* but it still requires dispatch from a non ui thread.
* This avoids that constraint
* Wraps dao calls to work with coroutines Non transactional queries were supposed to be fixed in
* https://issuetracker.google.com/issues/69474692, but it still requires dispatch from a non ui
* thread. This avoids that constraint
*/
suspend inline fun <T> dao(crossinline block: () -> T) = withContext(Dispatchers.IO) { block() }

View File

@ -60,16 +60,11 @@ interface FrostDao : FrostPrivateDao, FrostPublicDao {
fun close()
}
/**
* Composition of all database interfaces
*/
/** Composition of all database interfaces */
class FrostDatabase(
private val privateDb: FrostPrivateDatabase,
private val publicDb: FrostPublicDatabase
) :
FrostDao,
FrostPrivateDao by privateDb,
FrostPublicDao by publicDb {
) : FrostDao, FrostPrivateDao by privateDb, FrostPublicDao by publicDb {
override fun close() {
privateDb.close()
@ -86,14 +81,21 @@ class FrostDatabase(
}
fun create(context: Context): FrostDatabase {
val privateDb = Room.databaseBuilder(
context, FrostPrivateDatabase::class.java,
val privateDb =
Room.databaseBuilder(
context,
FrostPrivateDatabase::class.java,
FrostPrivateDatabase.DATABASE_NAME
).addMigrations(COOKIES_MIGRATION_1_2).frostBuild()
val publicDb = Room.databaseBuilder(
context, FrostPublicDatabase::class.java,
)
.addMigrations(COOKIES_MIGRATION_1_2)
.frostBuild()
val publicDb =
Room.databaseBuilder(
context,
FrostPublicDatabase::class.java,
FrostPublicDatabase.DATABASE_NAME
).frostBuild()
)
.frostBuild()
return FrostDatabase(privateDb, publicDb)
}
}

View File

@ -25,31 +25,20 @@ import androidx.room.Query
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.defaultTabs
/**
* Created by Allan Wang on 2017-05-30.
*/
/** Created by Allan Wang on 2017-05-30. */
/**
* Generic cache to store serialized content
*/
/** Generic cache to store serialized content */
@Entity(tableName = "frost_generic")
data class GenericEntity(
@PrimaryKey
val type: String,
val contents: String
)
data class GenericEntity(@PrimaryKey val type: String, val contents: String)
@Dao
interface GenericDao {
@Query("SELECT contents FROM frost_generic WHERE type = :type")
fun _select(type: String): String?
@Query("SELECT contents FROM frost_generic WHERE type = :type") fun _select(type: String): String?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun _save(entity: GenericEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE) fun _save(entity: GenericEntity)
@Query("DELETE FROM frost_generic WHERE type = :type")
fun _delete(type: String)
@Query("DELETE FROM frost_generic WHERE type = :type") fun _delete(type: String)
companion object {
const val TYPE_TABS = "generic_tabs"
@ -65,9 +54,6 @@ suspend fun GenericDao.saveTabs(tabs: List<FbItem>) = dao {
suspend fun GenericDao.getTabs(): List<FbItem> = dao {
val allTabs = FbItem.values.map { it.name to it }.toMap()
_select(GenericDao.TYPE_TABS)
?.split(",")
?.mapNotNull { allTabs[it] }
?.takeIf { it.isNotEmpty() }
_select(GenericDao.TYPE_TABS)?.split(",")?.mapNotNull { allTabs[it] }?.takeIf { it.isNotEmpty() }
?: defaultTabs()
}

View File

@ -32,19 +32,18 @@ import com.pitchedapps.frost.utils.L
@Entity(
tableName = "notifications",
primaryKeys = ["notif_id", "userId"],
foreignKeys = [
foreignKeys =
[
ForeignKey(
entity = CookieEntity::class,
parentColumns = ["cookie_id"],
childColumns = ["userId"],
onDelete = ForeignKey.CASCADE
)
],
)],
indices = [Index("notif_id"), Index("userId")]
)
data class NotificationEntity(
@ColumnInfo(name = "notif_id")
val id: Long,
@ColumnInfo(name = "notif_id") val id: Long,
val userId: Long,
val href: String,
val title: String?,
@ -72,12 +71,11 @@ data class NotificationEntity(
}
data class NotificationContentEntity(
@Embedded
val cookie: CookieEntity,
@Embedded
val notif: NotificationEntity
@Embedded val cookie: CookieEntity,
@Embedded val notif: NotificationEntity
) {
fun toNotifContent() = NotificationContent(
fun toNotifContent() =
NotificationContent(
data = cookie,
id = notif.id,
href = notif.href,
@ -92,14 +90,16 @@ data class NotificationContentEntity(
@Dao
interface NotificationDao {
/**
* Note that notifications are guaranteed to be ordered by descending timestamp
*/
/** Note that notifications are guaranteed to be ordered by descending timestamp */
@Transaction
@Query("SELECT * FROM cookies INNER JOIN notifications ON cookie_id = userId WHERE userId = :userId AND type = :type ORDER BY timestamp DESC")
@Query(
"SELECT * FROM cookies INNER JOIN notifications ON cookie_id = userId WHERE userId = :userId AND type = :type ORDER BY timestamp DESC"
)
fun _selectNotifications(userId: Long, type: String): List<NotificationContentEntity>
@Query("SELECT timestamp FROM notifications WHERE userId = :userId AND type = :type ORDER BY timestamp DESC LIMIT 1")
@Query(
"SELECT timestamp FROM notifications WHERE userId = :userId AND type = :type ORDER BY timestamp DESC LIMIT 1"
)
fun _selectEpoch(userId: Long, type: String): Long?
@Insert(onConflict = OnConflictStrategy.REPLACE)
@ -108,12 +108,9 @@ interface NotificationDao {
@Query("DELETE FROM notifications WHERE userId = :userId AND type = :type")
fun _deleteNotifications(userId: Long, type: String)
@Query("DELETE FROM notifications")
fun _deleteAll()
@Query("DELETE FROM notifications") fun _deleteAll()
/**
* It is assumed that the notification batch comes from the same user
*/
/** It is assumed that the notification batch comes from the same user */
@Transaction
fun _saveNotifications(type: String, notifs: List<NotificationContent>) {
val userId = notifs.firstOrNull()?.data?.id ?: return
@ -131,13 +128,9 @@ fun NotificationDao.selectNotificationsSync(userId: Long, type: String): List<No
suspend fun NotificationDao.selectNotifications(
userId: Long,
type: String
): List<NotificationContent> = dao {
selectNotificationsSync(userId, type)
}
): List<NotificationContent> = dao { selectNotificationsSync(userId, type) }
/**
* Returns true if successful, given that there are constraints to the insertion
*/
/** Returns true if successful, given that there are constraints to the insertion */
suspend fun NotificationDao.saveNotifications(
type: String,
notifs: List<NotificationContent>

View File

@ -26,6 +26,12 @@ import com.pitchedapps.frost.utils.createFreshDir
import com.pitchedapps.frost.utils.createFreshFile
import com.pitchedapps.frost.utils.frostJsoup
import com.pitchedapps.frost.utils.unescapeHtml
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
@ -37,12 +43,6 @@ import org.jsoup.Jsoup
import org.jsoup.nodes.Document
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.
@ -56,17 +56,15 @@ class OfflineWebsite(
private val cookie: String = "",
baseUrl: String? = null,
private val html: String? = null,
/**
* Directory that holds all the files
*/
/** Directory that holds all the files */
val baseDir: File,
private val userAgent: String = USER_AGENT
) {
/**
* Supplied url without the queries
*/
private val baseUrl: String = baseUrl ?: run {
/** Supplied url without the queries */
private val baseUrl: String =
baseUrl
?: run {
val url: HttpUrl = url.toHttpUrlOrNull() ?: throw IllegalArgumentException("Malformed url")
return@run "${url.scheme}://${url.host}"
}
@ -88,18 +86,15 @@ class OfflineWebsite(
private val cssQueue = mutableSetOf<String>()
private fun request(url: String) = Request.Builder()
.header("Cookie", cookie)
.header("User-Agent", userAgent)
.url(url)
.get()
.call()
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
* Caller to bind callbacks and start the load Callback is guaranteed to be called unless the load
* is cancelled
*/
suspend fun load(progress: (Int) -> Unit = {}): Boolean = withContext(Dispatchers.IO) {
suspend fun load(progress: (Int) -> Unit = {}): Boolean =
withContext(Dispatchers.IO) {
reset()
L.v { "Saving $url to ${baseDir.absolutePath}" }
@ -181,8 +176,7 @@ class OfflineWebsite(
fileQueue.clean().forEachIndexed { index, url ->
yield()
fileProgress(index)
if (!downloadFile(url))
return@withContext false
if (!downloadFile(url)) return@withContext false
}
yield()
@ -199,7 +193,6 @@ class OfflineWebsite(
}
ZipOutputStream(FileOutputStream(zip)).use { out ->
fun File.zip(name: String = this.name) {
if (!isFile) return
inputStream().use { file ->
@ -209,10 +202,8 @@ class OfflineWebsite(
out.closeEntry()
delete()
}
baseDir.listFiles { file -> file != zip }
?.forEach { it.zip() }
assetDir.listFiles()
?.forEach { it.zip("assets/${it.name}") }
baseDir.listFiles { file -> file != zip }?.forEach { it.zip() }
assetDir.listFiles()?.forEach { it.zip("assets/${it.name}") }
assetDir.delete()
}
@ -238,7 +229,8 @@ class OfflineWebsite(
return try {
val file = File(assetDir, fileName(url))
file.createNewFile()
val stream = request(url).execute().body?.byteStream()
val stream =
request(url).execute().body?.byteStream()
?: throw IllegalArgumentException("Response body not found for $url")
file.copyFromInputStream(stream)
true
@ -253,11 +245,15 @@ class OfflineWebsite(
val file = File(assetDir, fileName(url))
file.createNewFile()
var content = request(url).execute().body?.string()
var content =
request(url).execute().body?.string()
?: throw IllegalArgumentException("Response body not found for $url")
val links = FB_CSS_URL_MATCHER.findAll(content).mapNotNull { it[1] }
val absLinks = links.mapNotNull {
val newUrl = when {
val absLinks =
links
.mapNotNull {
val newUrl =
when {
it.startsWith("http") -> it
it.startsWith("/") -> "$baseUrl$it"
else -> return@mapNotNull null
@ -266,7 +262,8 @@ class OfflineWebsite(
// so the url does not point to another subfolder
content = content.replace(it, fileName(newUrl))
newUrl
}.toSet()
}
.toSet()
file.writeText(content)
absLinks
@ -290,35 +287,28 @@ class OfflineWebsite(
private inline val String.isValid
get() = startsWith("http")
/**
* Fetch the previously discovered filename
* or create a new one
* This is thread-safe
*/
/** Fetch the previously discovered filename or create a new one This is thread-safe */
private fun fileName(url: String): String {
val mapped = urlMapper[url]
if (mapped != null) return mapped
val candidate = url.substringBefore("?").trim('/')
.substringAfterLast("/").shorten()
val candidate = url.substringBefore("?").trim('/').substringAfterLast("/").shorten()
val index = atomicInt.getAndIncrement()
var newUrl = "a${index}_$candidate"
/**
* This is primarily for zipping up and sending via emails
* As .js files typically aren't allowed, we'll simply make everything txt files
* This is primarily for zipping up and sending via emails As .js files typically aren't
* allowed, we'll simply make everything txt files
*/
if (newUrl.endsWith(".js"))
newUrl = "$newUrl.txt"
if (newUrl.endsWith(".js")) newUrl = "$newUrl.txt"
urlMapper[url] = newUrl
return newUrl
}
private fun String.shorten() =
if (length <= 10) this else substring(length - 10)
private fun String.shorten() = if (length <= 10) this else substring(length - 10)
private fun Set<String>.clean(): List<String> =
filter(String::isNotBlank).filter { it.startsWith("http") }

View File

@ -20,9 +20,7 @@ import androidx.annotation.StringRes
import com.pitchedapps.frost.R
import com.pitchedapps.frost.facebook.FbItem
/**
* Created by Allan Wang on 2017-06-23.
*/
/** Created by Allan Wang on 2017-06-23. */
enum class FeedSort(@StringRes val textRes: Int, val item: FbItem) {
DEFAULT(R.string.kau_default, FbItem.FEED),
MOST_RECENT(R.string.most_recent, FbItem.FEED_MOST_RECENT),

View File

@ -19,26 +19,14 @@ package com.pitchedapps.frost.enums
import com.pitchedapps.frost.R
import com.pitchedapps.frost.injectors.ThemeProvider
/**
* Created by Allan Wang on 2017-08-19.
*/
/** Created by Allan Wang on 2017-08-19. */
enum class MainActivityLayout(
val titleRes: Int,
val backgroundColor: (ThemeProvider) -> Int,
val iconColor: (ThemeProvider) -> Int
) {
TOP_BAR(
R.string.top_bar,
{ it.headerColor },
{ it.iconColor }
),
BOTTOM_BAR(
R.string.bottom_bar,
{ it.bgColor },
{ it.textColor }
);
TOP_BAR(R.string.top_bar, { it.headerColor }, { it.iconColor }),
BOTTOM_BAR(R.string.bottom_bar, { it.bgColor }, { it.textColor });
companion object {
val values = values() // save one instance

View File

@ -30,19 +30,15 @@ import com.pitchedapps.frost.views.FrostWebView
/**
* Created by Allan Wang on 2017-09-16.
*
* Options for [WebOverlayActivityBase] to give more info as to what kind of
* overlay is present.
* Options for [WebOverlayActivityBase] to give more info as to what kind of overlay is present.
*
* For now, this is able to add new menu options upon first load
*/
enum class OverlayContext(private val menuItem: FrostMenuItem?) : EnumBundle<OverlayContext> {
NOTIFICATION(FrostMenuItem(R.id.action_notification, FbItem.NOTIFICATIONS)),
MESSAGE(FrostMenuItem(R.id.action_messages, FbItem.MESSAGES));
/**
* Inject the [menuItem] in the order that they are given at the front of the menu
*/
/** Inject the [menuItem] in the order that they are given at the front of the menu */
fun onMenuCreate(context: Context, menu: Menu) {
menuItem?.addToMenu(context, menu, 0)
}
@ -53,8 +49,8 @@ enum class OverlayContext(private val menuItem: FrostMenuItem?) : EnumBundle<Ove
companion object : EnumCompanion<OverlayContext>("frost_arg_overlay_context", values()) {
/**
* Execute selection call for an item by id
* Returns [true] if selection was consumed, [false] otherwise
* Execute selection call for an item by id Returns [true] if selection was consumed, [false]
* otherwise
*/
fun onOptionsItemSelected(web: FrostWebView, id: Int): Boolean {
val item = values.firstOrNull { id == it.menuItem?.id }?.menuItem ?: return false
@ -64,9 +60,7 @@ enum class OverlayContext(private val menuItem: FrostMenuItem?) : EnumBundle<Ove
}
}
/**
* Frame for an injectable menu item
*/
/** Frame for an injectable menu item */
class FrostMenuItem(
val id: Int,
val fbItem: FbItem,

View File

@ -23,9 +23,7 @@ import com.pitchedapps.frost.R
import com.pitchedapps.frost.prefs.sections.ThemePrefs
import java.util.Locale
/**
* Created by Allan Wang on 2017-06-14.
*/
/** Created by Allan Wang on 2017-06-14. */
const val FACEBOOK_BLUE = 0xff3b5998.toInt()
const val BLUE_LIGHT = 0xff5d86dd.toInt()
@ -38,7 +36,6 @@ enum class Theme(
val headerColorGetter: (ThemePrefs) -> Int,
val iconColorGetter: (ThemePrefs) -> Int
) {
DEFAULT(
R.string.kau_default,
"default",
@ -48,7 +45,6 @@ enum class Theme(
{ FACEBOOK_BLUE },
{ Color.WHITE }
),
LIGHT(
R.string.kau_light,
"material_light",
@ -58,7 +54,6 @@ enum class Theme(
{ FACEBOOK_BLUE },
{ Color.WHITE }
),
DARK(
R.string.kau_dark,
"material_dark",
@ -68,7 +63,6 @@ enum class Theme(
{ 0xff2e4b86.toInt() },
{ Color.WHITE }
),
AMOLED(
R.string.kau_amoled,
"material_amoled",
@ -78,7 +72,6 @@ enum class Theme(
{ Color.BLACK },
{ Color.WHITE }
),
GLASS(
R.string.kau_glass,
"material_glass",
@ -88,7 +81,6 @@ enum class Theme(
{ 0xb3000000.toInt() },
{ Color.WHITE }
),
CUSTOM(
R.string.kau_custom,
"custom",
@ -99,8 +91,7 @@ enum class Theme(
{ it.customIconColor }
);
@VisibleForTesting
internal val file = file?.let { "$it.css" }
@VisibleForTesting internal val file = file?.let { "$it.css" }
companion object {
val values = values() // save one instance
@ -109,9 +100,8 @@ enum class Theme(
}
enum class ThemeCategory {
FACEBOOK, MESSENGER
;
FACEBOOK,
MESSENGER;
@VisibleForTesting
internal val folder = name.toLowerCase(Locale.CANADA)
@VisibleForTesting internal val folder = name.toLowerCase(Locale.CANADA)
}

View File

@ -16,10 +16,7 @@
*/
package com.pitchedapps.frost.facebook
/**
* Created by Allan Wang on 2017-06-01.
*/
/** Created by Allan Wang on 2017-06-01. */
const val FACEBOOK_COM = "facebook.com"
const val MESSENGER_COM = "messenger.com"
const val FBCDN_NET = "fbcdn.net"
@ -31,7 +28,9 @@ const val FACEBOOK_BASE_COM = "m.$FACEBOOK_COM"
const val FB_URL_BASE = "https://$FACEBOOK_BASE_COM/"
const val FACEBOOK_MBASIC_COM = "mbasic.$FACEBOOK_COM"
const val FB_URL_MBASIC_BASE = "https://$FACEBOOK_MBASIC_COM/"
fun profilePictureUrl(id: Long) = "https://graph.facebook.com/$id/picture?type=large"
const val FB_LOGIN_URL = "${FB_URL_BASE}login"
const val FB_HOME_URL = "${FB_URL_BASE}home.php"
const val MESSENGER_THREAD_PREFIX = "$HTTPS_MESSENGER_COM/t/"
@ -53,15 +52,11 @@ const val USER_AGENT_DESKTOP_CONST =
const val USER_AGENT = USER_AGENT_DESKTOP_CONST
/**
* Animation transition delay, just to ensure that the styles
* have properly set in
*/
/** Animation transition delay, just to ensure that the styles have properly set in */
const val WEB_LOAD_DELAY = 50L
/**
* Additional delay for transition when called from commit.
* Note that transitions are also called from onFinish, so this value
* will never make a load slower than it is
* Additional delay for transition when called from commit. Note that transitions are also called
* from onFinish, so this value will never make a load slower than it is
*/
const val WEB_COMMIT_LOAD_DELAY = 200L

View File

@ -28,53 +28,43 @@ import com.pitchedapps.frost.prefs.Prefs
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.cookies
import com.pitchedapps.frost.utils.launchLogin
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* Created by Allan Wang on 2017-05-30.
*
* The following component manages all cookie transfers.
*/
class FbCookie @Inject internal constructor(
private val prefs: Prefs,
private val cookieDao: CookieDao
) {
class FbCookie
@Inject
internal constructor(private val prefs: Prefs, private val cookieDao: CookieDao) {
companion object {
/**
* Domain information. Dot prefix still matters for Android browsers.
*/
/** Domain information. Dot prefix still matters for Android browsers. */
private const val FB_COOKIE_DOMAIN = ".$FACEBOOK_COM"
private const val MESSENGER_COOKIE_DOMAIN = ".$MESSENGER_COM"
}
/**
* Retrieves the facebook cookie if it exists
* Note that this is a synchronized call
*/
/** Retrieves the facebook cookie if it exists Note that this is a synchronized call */
val webCookie: String?
get() = CookieManager.getInstance().getCookie(HTTPS_FACEBOOK_COM)
val messengerCookie: String?
get() = CookieManager.getInstance().getCookie(HTTPS_MESSENGER_COM)
private suspend fun CookieManager.suspendSetWebCookie(
domain: String,
cookie: String?
): Boolean {
private suspend fun CookieManager.suspendSetWebCookie(domain: String, cookie: String?): Boolean {
cookie ?: return true
return withContext(NonCancellable) {
// Save all cookies regardless of result, then check if all succeeded
val result = cookie.split(";")
.map { async { setSingleWebCookie(domain, it) } }
.awaitAll().all { it }
val result =
cookie.split(";").map { async { setSingleWebCookie(domain, it) } }.awaitAll().all { it }
L.d { "Cookies set" }
L._d { "Set $cookie\n\tResult $webCookie" }
result
@ -83,15 +73,11 @@ class FbCookie @Inject internal constructor(
private suspend fun CookieManager.setSingleWebCookie(domain: String, cookie: String): Boolean =
suspendCoroutine { cont ->
setCookie(domain, cookie.trim()) {
cont.resume(it)
}
setCookie(domain, cookie.trim()) { cont.resume(it) }
}
private suspend fun CookieManager.removeAllCookies(): Boolean = suspendCoroutine { cont ->
removeAllCookies {
cont.resume(it)
}
removeAllCookies { cont.resume(it) }
}
suspend fun save(id: Long) {
@ -134,21 +120,15 @@ class FbCookie @Inject internal constructor(
}
}
/**
* Helper function to remove the current cookies
* and launch the proper login page
*/
/** Helper function to remove the current cookies and launch the proper login page */
suspend fun logout(context: Context, deleteCookie: Boolean = true) {
val cookies = arrayListOf<CookieEntity>()
if (context is Activity)
cookies.addAll(context.cookies().filter { it.id != prefs.userId })
if (context is Activity) cookies.addAll(context.cookies().filter { it.id != prefs.userId })
logout(prefs.userId, deleteCookie)
context.launchLogin(cookies, true)
}
/**
* Clear the cookies of the given id
*/
/** Clear the cookies of the given id */
suspend fun logout(id: Long, deleteCookie: Boolean = true) {
L.d { "Logging out user" }
if (deleteCookie) {

View File

@ -35,7 +35,6 @@ enum class FbItem(
val fragmentCreator: () -> BaseFragment = ::WebFragment,
prefix: String = FB_URL_BASE
) : EnumBundle<FbItem> {
ACTIVITY_LOG(R.string.activity_log, GoogleMaterial.Icon.gmd_list, "me/allactivity"),
BIRTHDAYS(R.string.birthdays, GoogleMaterial.Icon.gmd_cake, "events/birthdays"),
CHAT(R.string.chat, GoogleMaterial.Icon.gmd_chat, "buddylist"),
@ -68,18 +67,10 @@ enum class FbItem(
PROFILE(R.string.profile, CommunityMaterial.Icon.cmd_account, "me"),
SAVED(R.string.saved, GoogleMaterial.Icon.gmd_bookmark, "saved"),
/**
* Note that this url only works if a query (?q=) is provided
*/
_SEARCH(
R.string.kau_search,
GoogleMaterial.Icon.gmd_search,
"search/top"
),
/** Note that this url only works if a query (?q=) is provided */
_SEARCH(R.string.kau_search, GoogleMaterial.Icon.gmd_search, "search/top"),
/**
* Non mbasic search cannot be parsed.
*/
/** Non mbasic search cannot be parsed. */
_SEARCH_PARSE(
R.string.kau_search,
GoogleMaterial.Icon.gmd_search,
@ -92,8 +83,11 @@ enum class FbItem(
val url = "$prefix$relativeUrl"
val isFeed: Boolean
get() = when (this) {
FEED, FEED_MOST_RECENT, FEED_TOP_STORIES -> true
get() =
when (this) {
FEED,
FEED_MOST_RECENT,
FEED_TOP_STORIES -> true
else -> false
}

View File

@ -19,21 +19,16 @@ package com.pitchedapps.frost.facebook
/**
* Created by Allan Wang on 21/12/17.
*
* Collection of regex matchers
* Input text must be properly unescaped
* Collection of regex matchers Input text must be properly unescaped
*
* See [StringEscapeUtils]
*/
/**
* Matches the fb_dtsg component of a page containing it as a hidden value
*/
/** Matches the fb_dtsg component of a page containing it as a hidden value */
val FB_DTSG_MATCHER: Regex by lazy { Regex("name=\"fb_dtsg\" value=\"(.*?)\"") }
val FB_REV_MATCHER: Regex by lazy { Regex("\"app_version\":\"(.*?)\"") }
/**
* Matches user id from cookie
*/
/** Matches user id from cookie */
val FB_USER_MATCHER: Regex = Regex("c_user=([0-9]*);")
val FB_EPOCH_MATCHER: Regex = Regex(":([0-9]+)")

View File

@ -82,7 +82,8 @@ class FbUrlFormatter(url: String) {
// Convert desktop urls to mobile ones
cleanedUrl = cleanedUrl.replace(WWW_FACEBOOK_COM, FACEBOOK_BASE_COM)
if (cleanedUrl.startsWith("/")) cleanedUrl = FB_URL_BASE + cleanedUrl.substring(1)
cleanedUrl = cleanedUrl.replaceFirst(
cleanedUrl =
cleanedUrl.replaceFirst(
".facebook.com//",
".facebook.com/"
) // sometimes we are given a bad url
@ -90,7 +91,8 @@ class FbUrlFormatter(url: String) {
return cleanedUrl
}
override fun toString(): String = buildString {
override fun toString(): String =
buildString {
append(cleaned)
if (queries.isNotEmpty()) {
append("?")
@ -102,7 +104,8 @@ class FbUrlFormatter(url: String) {
}
}
}
}.removeSuffix("&")
}
.removeSuffix("&")
fun toLogList(): List<String> {
val list = mutableListOf(cleaned)
@ -116,15 +119,15 @@ class FbUrlFormatter(url: String) {
const val VIDEO_REDIRECT = "/video_redirect/?src="
/**
* Items here are explicitly removed from the url
* Taken from FaceSlim
* Items here are explicitly removed from the url Taken from FaceSlim
* https://github.com/indywidualny/FaceSlim/blob/master/app/src/main/java/org/indywidualni/fblite/util/Miscellany.java
*
* Note: Typically, in this case, the redirect url should have all the necessary queries
* I am unsure how Facebook reacts in all cases, so the ones after the redirect are appended on afterwards
* That shouldn't break anything
* Note: Typically, in this case, the redirect url should have all the necessary queries I am
* unsure how Facebook reacts in all cases, so the ones after the redirect are appended on
* afterwards That shouldn't break anything
*/
val discardable = arrayOf(
val discardable =
arrayOf(
"http://lm.facebook.com/l.php?u=",
"https://lm.facebook.com/l.php?u=",
"http://m.facebook.com/l.php?u=",
@ -139,7 +142,8 @@ class FbUrlFormatter(url: String) {
*
* acontext is not required for "friends interested in" notifications
*/
val discardableQueries = arrayOf(
val discardableQueries =
arrayOf(
"ref",
"refid",
"SharedWith",
@ -155,13 +159,32 @@ class FbUrlFormatter(url: String) {
"pn_ref"
)
val converter = listOf(
"\\3C " to "%3C", "\\3E " to "%3E", "\\23 " to "%23", "\\25 " to "%25",
"\\7B " to "%7B", "\\7D " to "%7D", "\\7C " to "%7C", "\\5C " to "%5C",
"\\5E " to "%5E", "\\7E " to "%7E", "\\5B " to "%5B", "\\5D " to "%5D",
"\\60 " to "%60", "\\3B " to "%3B", "\\2F " to "%2F", "\\3F " to "%3F",
"\\3A " to "%3A", "\\40 " to "%40", "\\3D " to "%3D", "\\26 " to "%26",
"\\24 " to "%24", "\\2B " to "%2B", "\\22 " to "%22", "\\2C " to "%2C",
val converter =
listOf(
"\\3C " to "%3C",
"\\3E " to "%3E",
"\\23 " to "%23",
"\\25 " to "%25",
"\\7B " to "%7B",
"\\7D " to "%7D",
"\\7C " to "%7C",
"\\5C " to "%5C",
"\\5E " to "%5E",
"\\7E " to "%7E",
"\\5B " to "%5B",
"\\5D " to "%5D",
"\\60 " to "%60",
"\\3B " to "%3B",
"\\2F " to "%2F",
"\\3F " to "%3F",
"\\3A " to "%3A",
"\\40 " to "%40",
"\\3D " to "%3D",
"\\26 " to "%26",
"\\24 " to "%24",
"\\2B " to "%2B",
"\\22 " to "%22",
"\\2C " to "%2C",
"\\20 " to "%20"
)
}

View File

@ -29,7 +29,8 @@ data class FrostBadges(
val notifications: String?
) : ParseData {
override val isEmpty: Boolean
get() = feed.isNullOrEmpty() &&
get() =
feed.isNullOrEmpty() &&
friends.isNullOrEmpty() &&
messages.isNullOrEmpty() &&
notifications.isNullOrEmpty()
@ -43,14 +44,9 @@ private class BadgeParserImpl : FrostParserBase<FrostBadges>(false) {
override fun parseImpl(doc: Document): FrostBadges? {
val header = doc.getElementById("header") ?: return null
if (header.select("[data-sigil=count]").isEmpty())
return null
val (feed, requests, messages, notifications) = listOf(
"feed",
"requests",
"messages",
"notifications"
)
if (header.select("[data-sigil=count]").isEmpty()) return null
val (feed, requests, messages, notifications) =
listOf("feed", "requests", "messages", "notifications")
.map { "[data-sigil*=$it] [data-sigil=count]" }
.map { doc.select(it) }
.map { e -> e?.getOrNull(0)?.ownText() }

View File

@ -30,45 +30,32 @@ import org.jsoup.select.Elements
/**
* Created by Allan Wang on 2017-10-06.
*
* Interface for a given parser
* Use cases should be attached as delegates to objects that implement this interface
* Interface for a given parser Use cases should be attached as delegates to objects that implement
* this interface
*
* In all cases, parsing will be done from a JSoup document
* Variants accepting strings are also permitted, and they will be converted to documents accordingly
* The return type must be nonnull if no parsing errors occurred, as null signifies a parse error
* If null really must be allowed, use Optionals
* In all cases, parsing will be done from a JSoup document Variants accepting strings are also
* permitted, and they will be converted to documents accordingly The return type must be nonnull if
* no parsing errors occurred, as null signifies a parse error If null really must be allowed, use
* Optionals
*/
interface FrostParser<out T : ParseData> {
/**
* Name associated to parser
* Purely for display
*/
/** Name associated to parser Purely for display */
var nameRes: Int
/**
* Url to request from
*/
/** Url to request from */
val url: String
/**
* Call parsing with default implementation using cookie
*/
/** Call parsing with default implementation using cookie */
fun parse(cookie: String?): ParseResponse<T>?
/**
* Call parsing with given document
*/
/** Call parsing with given document */
fun parse(cookie: String?, document: Document): ParseResponse<T>?
/**
* Call parsing using jsoup to fetch from given url
*/
/** Call parsing using jsoup to fetch from given url */
fun parseFromUrl(cookie: String?, url: String): ParseResponse<T>?
/**
* Call parsing with given data
*/
/** Call parsing with given data */
fun parseFromData(cookie: String?, text: String): ParseResponse<T>?
}
@ -88,16 +75,19 @@ interface ParseNotification : ParseData {
fun getUnreadNotifications(data: CookieEntity): List<NotificationContent>
}
internal fun <T> List<T>.toJsonString(tag: String, indent: Int) = StringBuilder().apply {
internal fun <T> List<T>.toJsonString(tag: String, indent: Int) =
StringBuilder()
.apply {
val tabs = "\t".repeat(indent)
append("$tabs$tag: [\n\t$tabs")
append(this@toJsonString.joinToString("\n\t$tabs"))
append("\n$tabs]\n")
}.toString()
}
.toString()
/**
* T should have a readable toString() function
* [redirectToText] dictates whether all data should be converted to text then back to document before parsing
* T should have a readable toString() function [redirectToText] dictates whether all data should be
* converted to text then back to document before parsing
*/
internal abstract class FrostParserBase<out T : ParseData>(private val redirectToText: Boolean) :
FrostParser<T> {
@ -116,8 +106,7 @@ internal abstract class FrostParserBase<out T : ParseData>(private val redirectT
override fun parse(cookie: String?, document: Document): ParseResponse<T>? {
cookie ?: return null
if (redirectToText)
return parseFromData(cookie, document.toString())
if (redirectToText) return parseFromData(cookie, document.toString())
val data = parseImpl(document) ?: return null
return ParseResponse(cookie, data)
}
@ -125,18 +114,20 @@ internal abstract class FrostParserBase<out T : ParseData>(private val redirectT
protected abstract fun parseImpl(doc: Document): T?
/**
* Attempts to find inner <i> element with some style containing a url
* Returns the formatted url, or an empty string if nothing was found
* Attempts to find inner <i> element with some style containing a url Returns the formatted url,
* or an empty string if nothing was found
*/
protected fun Element.getInnerImgStyle(): String? =
select("i.img[style*=url]").getStyleUrl()
protected fun Element.getInnerImgStyle(): String? = select("i.img[style*=url]").getStyleUrl()
protected fun Elements.getStyleUrl(): String? =
FB_CSS_URL_MATCHER.find(attr("style"))[1]?.formattedFbUrl
protected open fun textToDoc(text: String): Document? =
if (!redirectToText) Jsoup.parse(text)
else throw RuntimeException("${this::class.java.simpleName} requires text redirect but did not implement textToDoc")
else
throw RuntimeException(
"${this::class.java.simpleName} requires text redirect but did not implement textToDoc"
)
protected fun parseLink(element: Element?): FrostLink? {
val a = element?.getElementsByTag("a")?.first() ?: return null

View File

@ -33,9 +33,8 @@ import org.jsoup.nodes.Element
/**
* Created by Allan Wang on 2017-10-06.
*
* In Facebook, messages are passed through scripts and loaded into view via react afterwards
* We can parse out the content we want directly and load it ourselves
*
* In Facebook, messages are passed through scripts and loaded into view via react afterwards We can
* parse out the content we want directly and load it ourselves
*/
object MessageParser : FrostParser<FrostMessages> by MessageParserImpl() {
@ -52,16 +51,22 @@ data class FrostMessages(
override val isEmpty: Boolean
get() = threads.isEmpty()
override fun toString() = StringBuilder().apply {
override fun toString() =
StringBuilder()
.apply {
append("FrostMessages {\n")
append(threads.toJsonString("threads", 1))
append("\tsee more: $seeMore\n")
append(extraLinks.toJsonString("extra links", 1))
append("}")
}.toString()
}
.toString()
override fun getUnreadNotifications(data: CookieEntity) =
threads.asSequence().filter(FrostThread::unread).map {
threads
.asSequence()
.filter(FrostThread::unread)
.map {
with(it) {
NotificationContent(
data = data,
@ -74,16 +79,14 @@ data class FrostMessages(
unread = unread
)
}
}.toList()
}
.toList()
}
/**
* [id] user/thread id, or current time fallback
* [img] parsed url for profile img
* [time] time of message
* [url] link to thread
* [unread] true if image is unread, false otherwise
* [content] optional string for thread
* [id] user/thread id, or current time fallback [img] parsed url for profile img [time] time of
* message [url] link to thread [unread] true if image is unread, false otherwise [content] optional
* string for thread
*/
data class FrostThread(
val id: Long,
@ -122,14 +125,12 @@ private class MessageParserImpl : FrostParserBase<FrostMessages>(true) {
override fun parseImpl(doc: Document): FrostMessages? {
val threadList = doc.getElementById("threadlist_rows") ?: return null
val threads: List<FrostThread> =
threadList.getElementsByAttributeValueMatching(
"id",
".*${FB_MESSAGE_NOTIF_ID_MATCHER.pattern}.*"
)
threadList
.getElementsByAttributeValueMatching("id", ".*${FB_MESSAGE_NOTIF_ID_MATCHER.pattern}.*")
.mapNotNull(this::parseMessage)
val seeMore = parseLink(doc.getElementById("see_older_threads"))
val extraLinks = threadList.nextElementSibling()?.select("a")
?.mapNotNull(this::parseLink) ?: emptyList()
val extraLinks =
threadList.nextElementSibling()?.select("a")?.mapNotNull(this::parseLink) ?: emptyList()
return FrostMessages(threads, seeMore, extraLinks)
}
@ -138,7 +139,8 @@ private class MessageParserImpl : FrostParserBase<FrostMessages>(true) {
val abbr = element.getElementsByTag("abbr")
val epoch = FB_EPOCH_MATCHER.find(abbr.attr("data-store"))[1]?.toLongOrNull() ?: -1L
// fetch id
val id = FB_MESSAGE_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull()
val id =
FB_MESSAGE_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull()
?: System.currentTimeMillis() % FALLBACK_TIME_MOD
val snippet = element.select("span.snippet").firstOrNull()
val content = snippet?.text()?.trim()

View File

@ -26,29 +26,29 @@ import com.pitchedapps.frost.services.NotificationContent
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
/**
* Created by Allan Wang on 2017-12-25.
*
*/
/** Created by Allan Wang on 2017-12-25. */
object NotifParser : FrostParser<FrostNotifs> by NotifParserImpl()
data class FrostNotifs(
val notifs: List<FrostNotif>,
val seeMore: FrostLink?
) : ParseNotification {
data class FrostNotifs(val notifs: List<FrostNotif>, val seeMore: FrostLink?) : ParseNotification {
override val isEmpty: Boolean
get() = notifs.isEmpty()
override fun toString() = StringBuilder().apply {
override fun toString() =
StringBuilder()
.apply {
append("FrostNotifs {\n")
append(notifs.toJsonString("notifs", 1))
append("\tsee more: $seeMore\n")
append("}")
}.toString()
}
.toString()
override fun getUnreadNotifications(data: CookieEntity) =
notifs.asSequence().filter(FrostNotif::unread).map {
notifs
.asSequence()
.filter(FrostNotif::unread)
.map {
with(it) {
NotificationContent(
data = data,
@ -61,18 +61,15 @@ data class FrostNotifs(
unread = unread
)
}
}.toList()
}
.toList()
}
/**
* [id] notif id, or current time fallback
* [img] parsed url for profile img
* [time] time of message
* [url] link to thread
* [unread] true if image is unread, false otherwise
* [content] optional string for thread
* [timeString] text version of time from Facebook
* [thumbnailUrl] optional thumbnail url if existent
* [id] notif id, or current time fallback [img] parsed url for profile img [time] time of message
* [url] link to thread [unread] true if image is unread, false otherwise [content] optional string
* for thread [timeString] text version of time from Facebook [thumbnailUrl] optional thumbnail url
* if existent
*/
data class FrostNotif(
val id: Long,
@ -93,7 +90,8 @@ private class NotifParserImpl : FrostParserBase<FrostNotifs>(false) {
override fun parseImpl(doc: Document): FrostNotifs? {
val notificationList = doc.getElementById("notifications_list") ?: return null
val notifications = notificationList
val notifications =
notificationList
.getElementsByAttributeValueMatching("id", ".*${FB_NOTIF_ID_MATCHER.pattern}.*")
.mapNotNull(this::parseNotif)
val seeMore =
@ -107,12 +105,12 @@ private class NotifParserImpl : FrostParserBase<FrostNotifs>(false) {
val abbr = element.getElementsByTag("abbr")
val epoch = FB_EPOCH_MATCHER.find(abbr.attr("data-store"))[1]?.toLongOrNull() ?: -1L
// fetch id
val id = FB_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull()
val id =
FB_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull()
?: System.currentTimeMillis() % FALLBACK_TIME_MOD
val img = element.getInnerImgStyle()
val timeString = abbr.text()
val content =
a.text().replace("\u00a0", " ").removeSuffix(timeString).trim() // remove &nbsp;
val content = a.text().replace("\u00a0", " ").removeSuffix(timeString).trim() // remove &nbsp;
val thumbnail = element.selectFirst("img.thumbnail")?.attr("src")
return FrostNotif(
id = id,

View File

@ -27,13 +27,10 @@ import com.pitchedapps.frost.utils.urlEncode
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
/**
* Created by Allan Wang on 2017-10-09.
*/
/** Created by Allan Wang on 2017-10-09. */
object SearchParser : FrostParser<FrostSearches> by SearchParserImpl() {
fun query(cookie: String?, input: String): ParseResponse<FrostSearches>? {
val url =
"${FbItem._SEARCH_PARSE.url}/?q=${if (input.isNotBlank()) input.urlEncode() else "a"}"
val url = "${FbItem._SEARCH_PARSE.url}/?q=${if (input.isNotBlank()) input.urlEncode() else "a"}"
L._i { "Search Query $url" }
return parseFromUrl(cookie, url)
}
@ -49,17 +46,19 @@ data class FrostSearches(val results: List<FrostSearch>) : ParseData {
override val isEmpty: Boolean
get() = results.isEmpty()
override fun toString() = StringBuilder().apply {
override fun toString() =
StringBuilder()
.apply {
append("FrostSearches {\n")
append(results.toJsonString("results", 1))
append("}")
}.toString()
}
.toString()
}
/**
* As far as I'm aware, all links are independent, so the queries don't matter
* A lot of it is tracking information, which I'll strip away
* Other text items are formatted for safety
* As far as I'm aware, all links are independent, so the queries don't matter A lot of it is
* tracking information, which I'll strip away Other text items are formatted for safety
*
* Note that it's best to create search results from [create]
*/
@ -68,7 +67,8 @@ data class FrostSearch(val href: String, val title: String, val description: Str
fun toSearchItem() = SearchItem(href, title, description)
companion object {
fun create(href: String, title: String, description: String?) = FrostSearch(
fun create(href: String, title: String, description: String?) =
FrostSearch(
with(href.indexOf("?")) { if (this == -1) href else href.substring(0, this) },
title.format(),
description?.format()
@ -86,38 +86,31 @@ private class SearchParserImpl : FrostParserBase<FrostSearches>(false) {
get() = replace(FACEBOOK_MBASIC_COM, FACEBOOK_BASE_COM)
override fun parseImpl(doc: Document): FrostSearches? {
val container: Element = doc.getElementById("BrowseResultsContainer")
?: doc.getElementById("root")
?: return null
val container: Element =
doc.getElementById("BrowseResultsContainer") ?: doc.getElementById("root") ?: return null
return FrostSearches(
container.select("table[role=presentation]").mapNotNull { el ->
// Our assumption is that search entries start with an image, followed by general info
// There may be other <td />s, but we will not be parsing them
// Furthermore, the <td /> entry wraps a link, containing all the necessary info
val a = el.select("td")
.getOrNull(1)
?.selectFirst("a")
?: return@mapNotNull null
val a = el.select("td").getOrNull(1)?.selectFirst("a") ?: return@mapNotNull null
val url =
a.attr("href").takeIf { it.isNotEmpty() }
?.formattedFbUrl?.formattedSearchUrl
a.attr("href").takeIf { it.isNotEmpty() }?.formattedFbUrl?.formattedSearchUrl
?: return@mapNotNull null
// Currently, children should all be <div /> elements, where the first entry is the name/title
// Currently, children should all be <div /> elements, where the first entry is the
// name/title
// And the other entries are additional info.
// There are also cases of nested tables, eg for the "join" button in groups.
// Those elements have <span /> texts, so we will filter by div to ignore those
val texts =
a.children()
.filter { childEl: Element -> childEl.tagName() == "div" && childEl.hasText() }
a.children().filter { childEl: Element ->
childEl.tagName() == "div" && childEl.hasText()
}
val title = texts.firstOrNull()?.text() ?: return@mapNotNull null
val info = texts.takeIf { it.size > 1 }?.last()?.text()
L.e { a }
create(
href = url,
title = title,
description = info
).also { L.e { it } }
create(href = url, title = title, description = info).also { L.e { it } }
}
)
}

View File

@ -26,18 +26,13 @@ import okhttp3.logging.HttpLoggingInterceptor
val httpClient: OkHttpClient by lazy {
val builder = OkHttpClient.Builder()
if (BuildConfig.DEBUG)
builder.addInterceptor(
HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BASIC)
)
builder.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC))
builder.build()
}
internal fun String?.requestBuilder(): Request.Builder {
val builder = Request.Builder()
.header("User-Agent", USER_AGENT)
if (this != null)
builder.header("Cookie", this)
val builder = Request.Builder().header("User-Agent", USER_AGENT)
if (this != null) builder.header("Cookie", this)
// .cacheControl(CacheControl.FORCE_NETWORK)
return builder
}

View File

@ -24,19 +24,15 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
/**
* Created by Allan Wang on 29/12/17.
*/
/** Created by Allan Wang on 29/12/17. */
/**
* Attempts to get the fbcdn url of the supplied image redirect url
*/
/** Attempts to get the fbcdn url of the supplied image redirect url */
suspend fun String.getFullSizedImageUrl(url: String, timeout: Long = 3000): String? =
withContext(Dispatchers.IO) {
try {
withTimeout(timeout) {
val redirect = requestBuilder().url(url).get().call()
.execute().body?.string() ?: return@withTimeout null
val redirect =
requestBuilder().url(url).get().call().execute().body?.string() ?: return@withTimeout null
FB_REDIRECT_URL_MATCHER.find(redirect)[1]?.formattedFbUrl
}
} catch (e: Exception) {

View File

@ -43,27 +43,23 @@ import com.pitchedapps.frost.utils.REQUEST_REFRESH
import com.pitchedapps.frost.utils.REQUEST_TEXT_ZOOM
import com.pitchedapps.frost.utils.frostEvent
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
/**
* Created by Allan Wang on 2017-11-07.
*
* All fragments pertaining to the main view
* Must be attached to activities implementing [MainActivityContract]
* All fragments pertaining to the main view Must be attached to activities implementing
* [MainActivityContract]
*/
@AndroidEntryPoint
abstract class BaseFragment :
Fragment(),
CoroutineScope,
FragmentContract,
DynamicUiContract {
abstract class BaseFragment : Fragment(), CoroutineScope, FragmentContract, DynamicUiContract {
companion object {
private const val ARG_POSITION = "arg_position"
@ -78,26 +74,19 @@ abstract class BaseFragment :
): BaseFragment {
val fragment = if (useFallback) WebFragment() else base()
val d = if (data == FbItem.FEED) FeedSort(prefs.feedSort).item else data
fragment.withArguments(
ARG_URL to d.url,
ARG_POSITION to position
)
fragment.withArguments(ARG_URL to d.url, ARG_POSITION to position)
d.put(fragment.requireArguments())
return fragment
}
}
@Inject
protected lateinit var mainContract: MainActivityContract
@Inject protected lateinit var mainContract: MainActivityContract
@Inject
protected lateinit var fbCookie: FbCookie
@Inject protected lateinit var fbCookie: FbCookie
@Inject
protected lateinit var prefs: Prefs
@Inject protected lateinit var prefs: Prefs
@Inject
protected lateinit var themeProvider: ThemeProvider
@Inject protected lateinit var themeProvider: ThemeProvider
open lateinit var job: Job
override val coroutineContext: CoroutineContext
@ -112,10 +101,7 @@ abstract class BaseFragment :
set(value) {
if (!isActive || value || this is WebFragment) return
requireArguments().putBoolean(ARG_VALID, value)
frostEvent(
"Native Fallback",
"Item" to baseEnum.name
)
frostEvent("Native Fallback", "Item" to baseEnum.name)
mainContract.reloadFragment(this)
}
@ -138,8 +124,11 @@ abstract class BaseFragment :
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(layoutRes, container, false)
val content = view as? FrostContentParent
?: throw IllegalArgumentException("layoutRes for fragment must return view implementing FrostContentParent")
val content =
view as? FrostContentParent
?: throw IllegalArgumentException(
"layoutRes for fragment must return view implementing FrostContentParent"
)
this.content = content
content.bind(this)
return view
@ -198,7 +187,8 @@ abstract class BaseFragment :
reloadTextSize()
}
}
}.launchIn(this)
}
.launchIn(this)
}
override fun updateFab(contract: MainFabContract) {
@ -207,9 +197,7 @@ abstract class BaseFragment :
protected fun FloatingActionButton.update(iicon: IIcon, click: () -> Unit) {
if (isShown) {
fadeScaleTransition {
setIcon(iicon, themeProvider.iconColor)
}
fadeScaleTransition { setIcon(iicon, themeProvider.iconColor) }
} else {
setIcon(iicon, themeProvider.iconColor)
show()

View File

@ -23,60 +23,44 @@ import com.pitchedapps.frost.contracts.MainActivityContract
import com.pitchedapps.frost.contracts.MainFabContract
import com.pitchedapps.frost.views.FrostRecyclerView
/**
* Created by Allan Wang on 2017-11-07.
*/
/** Created by Allan Wang on 2017-11-07. */
interface FragmentContract : FrostContentContainer {
val content: FrostContentParent?
/**
* Defines whether the fragment is valid in the viewpager
* or if it needs to be recreated
* May be called from any thread to toggle status.
* Note that calls beyond the fragment lifecycle will be ignored
* Defines whether the fragment is valid in the viewpager or if it needs to be recreated May be
* called from any thread to toggle status. Note that calls beyond the fragment lifecycle will be
* ignored
*/
var valid: Boolean
/**
* Helper to retrieve the core from [content]
*/
/** Helper to retrieve the core from [content] */
val core: FrostContentCore?
get() = content?.core
/**
* Specifies position in Activity's viewpager
*/
/** Specifies position in Activity's viewpager */
val position: Int
/**
* Specifies whether if current load
* will be fragment's first load
* Specifies whether if current load will be fragment's first load
*
* Defaults to true
*/
var firstLoad: Boolean
/**
* Called when the fragment is first visible
* Typically, if [firstLoad] is true,
* the fragment should call [reload] and make [firstLoad] false
* Called when the fragment is first visible Typically, if [firstLoad] is true, the fragment
* should call [reload] and make [firstLoad] false
*/
fun firstLoadRequest()
fun updateFab(contract: MainFabContract)
/**
* Single callable action to be executed upon creation
* Note that this call is not guaranteed
*/
/** Single callable action to be executed upon creation Note that this call is not guaranteed */
fun post(action: (fragment: FragmentContract) -> Unit)
/**
* Call whenever a fragment is attached so that it may listen
* to activity emissions.
*/
/** Call whenever a fragment is attached so that it may listen to activity emissions. */
fun attach(contract: MainActivityContract)
/*
@ -95,10 +79,9 @@ interface RecyclerContentContract {
fun bind(recyclerView: FrostRecyclerView)
/**
* Completely handle data reloading, within a non-ui thread
* The progress function allows optional emission of progress values (between 0 and 100)
* and can be called from any thread.
* Returns [true] for success, [false] otherwise
* Completely handle data reloading, within a non-ui thread The progress function allows optional
* emission of progress values (between 0 and 100) and can be called from any thread. Returns
* [true] for success, [false] otherwise
*/
suspend fun reload(progress: (Int) -> Unit): Boolean
}

View File

@ -32,9 +32,7 @@ import com.pitchedapps.frost.views.FrostRecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Created by Allan Wang on 27/12/17.
*/
/** Created by Allan Wang on 27/12/17. */
abstract class RecyclerFragment<T, Item : GenericItem> : BaseFragment(), RecyclerContentContract {
override val layoutRes: Int = R.layout.view_content_recycler
@ -51,7 +49,8 @@ abstract class RecyclerFragment<T, Item : GenericItem> : BaseFragment(), Recycle
final override suspend fun reload(progress: (Int) -> Unit): Boolean =
withContext(Dispatchers.IO) {
val data = try {
val data =
try {
reloadImpl(progress)
} catch (e: Exception) {
L.e(e) { "Recycler reload fail $baseUrl" }
@ -84,23 +83,19 @@ abstract class GenericRecyclerFragment<T, Item : GenericItem> : RecyclerFragment
}
/**
* Anything to call for one time bindings
* At this stage, all adapters will have FastAdapter references
* Anything to call for one time bindings At this stage, all adapters will have FastAdapter
* references
*/
open fun bindImpl(recyclerView: FrostRecyclerView) = Unit
/**
* Create the fast adapter to bind to the recyclerview
*/
/** Create the fast adapter to bind to the recyclerview */
open fun getAdapter(): GenericFastAdapter = fastAdapter(this.adapter)
}
abstract class FrostParserFragment<T : ParseData, Item : GenericItem> :
RecyclerFragment<Item, Item>() {
/**
* The parser to make this all happen
*/
/** The parser to make this all happen */
abstract val parser: FrostParser<T>
open fun getDoc(cookie: String?) = frostJsoup(cookie, parser.url)
@ -116,14 +111,12 @@ abstract class FrostParserFragment<T : ParseData, Item : GenericItem> :
}
/**
* Anything to call for one time bindings
* At this stage, all adapters will have FastAdapter references
* Anything to call for one time bindings At this stage, all adapters will have FastAdapter
* references
*/
open fun bindImpl(recyclerView: FrostRecyclerView) = Unit
/**
* Create the fast adapter to bind to the recyclerview
*/
/** Create the fast adapter to bind to the recyclerview */
open fun getAdapter(): GenericFastAdapter = fastAdapter(this.adapter)
override suspend fun reloadImpl(progress: (Int) -> Unit): List<Item>? =
@ -132,7 +125,8 @@ abstract class FrostParserFragment<T : ParseData, Item : GenericItem> :
val cookie = fbCookie.webCookie
val doc = getDoc(cookie)
progress(60)
val response = try {
val response =
try {
parser.parse(cookie, doc)
} catch (ignored: Exception) {
null

View File

@ -27,9 +27,12 @@ import com.pitchedapps.frost.views.FrostRecyclerView
/**
* Created by Allan Wang on 27/12/17.
*
* Retained as an example. Deletion made at https://github.com/AllanWang/Frost-for-Facebook/pull/1542
* Retained as an example. Deletion made at
* https://github.com/AllanWang/Frost-for-Facebook/pull/1542
*/
@Deprecated(message = "Retained as an example; currently does not support marking a notification as read")
@Deprecated(
message = "Retained as an example; currently does not support marking a notification as read"
)
class NotificationFragment : FrostParserFragment<FrostNotifs, NotificationIItem>() {
override val parser = NotifParser

View File

@ -31,17 +31,15 @@ import com.pitchedapps.frost.web.FrostWebViewClientMessenger
/**
* Created by Allan Wang on 27/12/17.
*
* Basic webfragment
* Do not extend as this is always a fallback
* Basic webfragment Do not extend as this is always a fallback
*/
class WebFragment : BaseFragment() {
override val layoutRes: Int = R.layout.view_content_web
/**
* Given a webview, output a client
*/
fun client(web: FrostWebView) = when (baseEnum) {
/** Given a webview, output a client */
fun client(web: FrostWebView) =
when (baseEnum) {
FbItem.MESSENGER -> FrostWebViewClientMessenger(web)
FbItem.MENU -> FrostWebViewClientMenu(web)
else -> FrostWebViewClient(web)
@ -54,9 +52,7 @@ class WebFragment : BaseFragment() {
return super.updateFab(contract)
}
if (baseEnum.isFeed && prefs.showCreateFab) {
contract.showFab(GoogleMaterial.Icon.gmd_edit) {
JsActions.CREATE_POST.inject(web, prefs)
}
contract.showFab(GoogleMaterial.Icon.gmd_edit) { JsActions.CREATE_POST.inject(web, prefs) }
return
}
super.updateFab(contract)

View File

@ -27,22 +27,23 @@ import com.bumptech.glide.load.resource.bitmap.CircleCrop
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import com.pitchedapps.frost.facebook.FbCookie
import javax.inject.Inject
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
/**
* Created by Allan Wang on 28/12/17.
*
* Collection of transformations
* Each caller will generate a new one upon request
* Collection of transformations Each caller will generate a new one upon request
*/
object FrostGlide {
val circleCrop
get() = CircleCrop()
}
fun <T> RequestBuilder<T>.transform(vararg transformation: BitmapTransformation): RequestBuilder<T> =
fun <T> RequestBuilder<T>.transform(
vararg transformation: BitmapTransformation
): RequestBuilder<T> =
when (transformation.size) {
0 -> this
1 -> apply(RequestOptions.bitmapTransform(transformation[0]))
@ -56,16 +57,16 @@ class FrostGlideModule : AppGlideModule() {
// registry.replace(GlideUrl::class.java,
// InputStream::class.java,
// OkHttpUrlLoader.Factory(getFrostHttpClient()))
// registry.prepend(HdImageMaybe::class.java, InputStream::class.java, HdImageLoadingFactory())
// registry.prepend(HdImageMaybe::class.java, InputStream::class.java,
// HdImageLoadingFactory())
}
}
// private fun getFrostHttpClient(): OkHttpClient =
// OkHttpClient.Builder().addInterceptor(FrostCookieInterceptor()).build()
class FrostCookieInterceptor @Inject internal constructor(
private val fbCookie: FbCookie
) : Interceptor {
class FrostCookieInterceptor @Inject internal constructor(private val fbCookie: FbCookie) :
Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val origRequest = chain.request()

View File

@ -32,13 +32,9 @@ import com.pitchedapps.frost.injectors.ThemeProvider
import com.pitchedapps.frost.prefs.Prefs
import com.pitchedapps.frost.utils.launchWebOverlay
/**
* Created by Allan Wang on 30/12/17.
*/
/** Created by Allan Wang on 30/12/17. */
/**
* Base contract for anything with a url that may be launched in a new overlay
*/
/** Base contract for anything with a url that may be launched in a new overlay */
interface ClickableIItemContract {
val url: String?
@ -51,39 +47,32 @@ interface ClickableIItemContract {
companion object {
fun bindEvents(adapter: IAdapter<GenericItem>, fbCookie: FbCookie, prefs: Prefs) {
adapter.fastAdapter?.apply {
selectExtension {
isSelectable = false
}
selectExtension { isSelectable = false }
onClickListener = { v, _, item, _ ->
if (item is ClickableIItemContract) {
item.click(v!!.context, fbCookie, prefs)
true
} else
false
} else false
}
}
}
}
}
/**
* Generic header item
* Not clickable with an accent color
*/
/** Generic header item Not clickable with an accent color */
open class HeaderIItem(
val text: String?,
itemId: Int = R.layout.iitem_header,
private val themeProvider: ThemeProvider
) : KauIItem<HeaderIItem.ViewHolder>(
) :
KauIItem<HeaderIItem.ViewHolder>(
R.layout.iitem_header,
{ ViewHolder(it, themeProvider) },
itemId
) {
class ViewHolder(
itemView: View,
private val themeProvider: ThemeProvider
) : FastAdapter.ViewHolder<HeaderIItem>(itemView) {
class ViewHolder(itemView: View, private val themeProvider: ThemeProvider) :
FastAdapter.ViewHolder<HeaderIItem>(itemView) {
val text: TextView by bindView(R.id.item_header_text)
@ -99,22 +88,18 @@ open class HeaderIItem(
}
}
/**
* Generic text item
* Clickable with text color
*/
/** Generic text item Clickable with text color */
open class TextIItem(
val text: String?,
override val url: String?,
itemId: Int = R.layout.iitem_text,
private val themeProvider: ThemeProvider
) : KauIItem<TextIItem.ViewHolder>(R.layout.iitem_text, { ViewHolder(it, themeProvider) }, itemId),
) :
KauIItem<TextIItem.ViewHolder>(R.layout.iitem_text, { ViewHolder(it, themeProvider) }, itemId),
ClickableIItemContract {
class ViewHolder(
itemView: View,
private val themeProvider: ThemeProvider
) : FastAdapter.ViewHolder<TextIItem>(itemView) {
class ViewHolder(itemView: View, private val themeProvider: ThemeProvider) :
FastAdapter.ViewHolder<TextIItem>(itemView) {
val text: TextView by bindView(R.id.item_text_view)

View File

@ -41,15 +41,15 @@ import com.pitchedapps.frost.prefs.Prefs
import com.pitchedapps.frost.utils.isIndependent
import com.pitchedapps.frost.utils.launchWebOverlay
/**
* Created by Allan Wang on 27/12/17.
*/
/** Created by Allan Wang on 27/12/17. */
class NotificationIItem(
val notification: FrostNotif,
val cookie: String,
private val themeProvider: ThemeProvider
) : KauIItem<NotificationIItem.ViewHolder>(
R.layout.iitem_notification, { ViewHolder(it, themeProvider) }
) :
KauIItem<NotificationIItem.ViewHolder>(
R.layout.iitem_notification,
{ ViewHolder(it, themeProvider) }
) {
companion object {
@ -60,23 +60,19 @@ class NotificationIItem(
themeProvider: ThemeProvider
) {
adapter.fastAdapter?.apply {
selectExtension {
isSelectable = false
}
selectExtension { isSelectable = false }
onClickListener = { v, _, item, position ->
val notif = item.notification
if (notif.unread) {
adapter.set(
position,
NotificationIItem(
notif.copy(unread = false),
item.cookie,
themeProvider
)
NotificationIItem(notif.copy(unread = false), item.cookie, themeProvider)
)
}
// TODO temp fix. If url is dependent, we cannot load it directly
v!!.context.launchWebOverlay(
v!!
.context
.launchWebOverlay(
if (notif.url.isIndependent) notif.url else FbItem.NOTIFICATIONS.url,
fbCookie,
prefs
@ -95,10 +91,7 @@ class NotificationIItem(
override fun areItemsTheSame(oldItem: NotificationIItem, newItem: NotificationIItem) =
oldItem.notification.id == newItem.notification.id
override fun areContentsTheSame(
oldItem: NotificationIItem,
newItem: NotificationIItem
) =
override fun areContentsTheSame(oldItem: NotificationIItem, newItem: NotificationIItem) =
oldItem.notification == newItem.notification
override fun getChangePayload(
@ -111,10 +104,8 @@ class NotificationIItem(
}
}
class ViewHolder(
itemView: View,
private val themeProvider: ThemeProvider
) : FastAdapter.ViewHolder<NotificationIItem>(itemView) {
class ViewHolder(itemView: View, private val themeProvider: ThemeProvider) :
FastAdapter.ViewHolder<NotificationIItem>(itemView) {
private val frame: ViewGroup by bindView(R.id.item_frame)
private val avatar: ImageView by bindView(R.id.item_avatar)
@ -127,7 +118,8 @@ class NotificationIItem(
override fun bindView(item: NotificationIItem, payloads: List<Any>) {
val notif = item.notification
frame.background = createSimpleRippleDrawable(
frame.background =
createSimpleRippleDrawable(
themeProvider.textColor,
themeProvider.nativeBgColor(notif.unread)
)
@ -135,11 +127,8 @@ class NotificationIItem(
date.setTextColor(themeProvider.textColor.withAlpha(150))
val glide = glide
glide.load(notif.img)
.transform(FrostGlide.circleCrop)
.into(avatar)
if (notif.thumbnailUrl != null)
glide.load(notif.thumbnailUrl).into(thumbnail.visible())
glide.load(notif.img).transform(FrostGlide.circleCrop).into(avatar)
if (notif.thumbnailUrl != null) glide.load(notif.thumbnailUrl).into(thumbnail.visible())
content.text = notif.content
date.text = notif.timeString

View File

@ -31,22 +31,15 @@ import com.pitchedapps.frost.R
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.injectors.ThemeProvider
/**
* Created by Allan Wang on 26/11/17.
*/
/** Created by Allan Wang on 26/11/17. */
class TabIItem(val item: FbItem, private val themeProvider: ThemeProvider) :
KauIItem<TabIItem.ViewHolder>(
R.layout.iitem_tab_preview,
{ ViewHolder(it, themeProvider) }
),
KauIItem<TabIItem.ViewHolder>(R.layout.iitem_tab_preview, { ViewHolder(it, themeProvider) }),
IDraggable {
override val isDraggable: Boolean = true
class ViewHolder(
itemView: View,
private val themeProvider: ThemeProvider
) : FastAdapter.ViewHolder<TabIItem>(itemView) {
class ViewHolder(itemView: View, private val themeProvider: ThemeProvider) :
FastAdapter.ViewHolder<TabIItem>(itemView) {
val image: ImageView by bindView(R.id.image)
val text: TextView by bindView(R.id.text)
@ -55,8 +48,7 @@ class TabIItem(val item: FbItem, private val themeProvider: ThemeProvider) :
val isInToolbar = adapterPosition < 4
val color = if (isInToolbar) themeProvider.iconColor else themeProvider.textColor
image.setIcon(item.item.icon, 20, color)
if (isInToolbar)
text.invisible()
if (isInToolbar) text.invisible()
else {
text.visible().setText(item.item.titleId)
text.setTextColor(color.withAlpha(200))

View File

@ -19,17 +19,18 @@ package com.pitchedapps.frost.injectors
import android.webkit.WebView
import com.pitchedapps.frost.prefs.Prefs
/**
* Small misc inline css assets
*/
/** Small misc inline css assets */
enum class CssAsset(private val content: String) : InjectorContract {
FullSizeImage("div._4prr[style*=\"max-width\"][style*=\"max-height\"]{max-width:none !important;max-height:none !important}"),
FullSizeImage(
"div._4prr[style*=\"max-width\"][style*=\"max-height\"]{max-width:none !important;max-height:none !important}"
),
/*
* Remove top margin and hide some contents from the top bar and home page (as it's our base url)
*/
Menu("#bookmarks_flyout{margin-top:0 !important}#m_news_feed_stream,#MComposer{display:none !important}")
;
Menu(
"#bookmarks_flyout{margin-top:0 !important}#m_news_feed_stream,#MComposer{display:none !important}"
);
val injector: JsInjector by lazy {
JsBuilder().css(content).single("css-small-assets-$name").build()

View File

@ -33,10 +33,7 @@ enum class CssHider(private vararg val items: String) : InjectorContract {
"#header-notices",
"[data-sigil*=m-promo-jewel-header]"
),
ADS(
"article[data-xt*=sponsor]",
"article[data-store*=sponsor]"
),
ADS("article[data-xt*=sponsor]", "article[data-store*=sponsor]"),
PEOPLE_YOU_MAY_KNOW("article._d2r"),
SUGGESTED_GROUPS("article[data-ft*=\"ei\":]"),
COMPOSER("#MComposer"),
@ -47,19 +44,15 @@ enum class CssHider(private vararg val items: String) : InjectorContract {
// Sub element with just the tray; title is not a part of this
"[data-testid=story_tray]"
),
POST_ACTIONS(
"footer [data-sigil=\"ufi-inline-actions\"]"
),
POST_REACTIONS(
"footer [data-sigil=\"reactions-bling-bar\"]"
)
;
POST_ACTIONS("footer [data-sigil=\"ufi-inline-actions\"]"),
POST_REACTIONS("footer [data-sigil=\"reactions-bling-bar\"]");
val injector: JsInjector by lazy {
JsBuilder().css("${items.joinToString(separator = ",")}{display:none !important}")
.single("css-hider-$name").build()
JsBuilder()
.css("${items.joinToString(separator = ",")}{display:none !important}")
.single("css-hider-$name")
.build()
}
override fun inject(webView: WebView, prefs: Prefs) =
injector.inject(webView, prefs)
override fun inject(webView: WebView, prefs: Prefs) = injector.inject(webView, prefs)
}

View File

@ -27,24 +27,23 @@ import com.pitchedapps.frost.prefs.Prefs
*/
enum class JsActions(body: String) : InjectorContract {
/**
* Redirects to login activity if create account is found
* see [com.pitchedapps.frost.web.FrostJSI.loadLogin]
* Redirects to login activity if create account is found see
* [com.pitchedapps.frost.web.FrostJSI.loadLogin]
*/
LOGIN_CHECK("document.getElementById('signup-button')&&Frost.loadLogin();"),
BASE_HREF("""document.write("<base href='$FB_URL_BASE'/>");"""),
FETCH_BODY("""setTimeout(function(){var e=document.querySelector("main");e||(e=document.querySelector("body")),Frost.handleHtml(e.outerHTML)},1e2);"""),
FETCH_BODY(
"""setTimeout(function(){var e=document.querySelector("main");e||(e=document.querySelector("body")),Frost.handleHtml(e.outerHTML)},1e2);"""
),
RETURN_BODY("return(document.getElementsByTagName('html')[0].innerHTML);"),
CREATE_POST(clickBySelector("#MComposer [onclick]")),
// CREATE_MSG(clickBySelector("a[rel=dialog]")),
/**
* Used as a pseudoinjector for maybe functions
*/
/** Used as a pseudoinjector for maybe functions */
EMPTY("");
val function = "(function(){$body})();"
override fun inject(webView: WebView, prefs: Prefs) =
JsInjector(function).inject(webView, prefs)
override fun inject(webView: WebView, prefs: Prefs) = JsInjector(function).inject(webView, prefs)
}
@Suppress("NOTHING_TO_INLINE")

View File

@ -22,24 +22,32 @@ import androidx.annotation.VisibleForTesting
import ca.allanwang.kau.kotlin.lazyContext
import com.pitchedapps.frost.prefs.Prefs
import com.pitchedapps.frost.utils.L
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.FileNotFoundException
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Created by Allan Wang on 2017-05-31.
* Mapping of the available assets
* The enum name must match the css file name
* Created by Allan Wang on 2017-05-31. Mapping of the available assets The enum name must match the
* css file name
*/
enum class JsAssets(private val singleLoad: Boolean = true) : InjectorContract {
MENU, MENU_QUICK(singleLoad = false), CLICK_A, CONTEXT_A, MEDIA, HEADER_BADGES, TEXTAREA_LISTENER, NOTIF_MSG,
DOCUMENT_WATCHER, HORIZONTAL_SCROLLING, AUTO_RESIZE_TEXTAREA(singleLoad = false), SCROLL_STOP,
MENU,
MENU_QUICK(singleLoad = false),
CLICK_A,
CONTEXT_A,
MEDIA,
HEADER_BADGES,
TEXTAREA_LISTENER,
NOTIF_MSG,
DOCUMENT_WATCHER,
HORIZONTAL_SCROLLING,
AUTO_RESIZE_TEXTAREA(singleLoad = false),
SCROLL_STOP,
;
@VisibleForTesting
internal val file = "${name.toLowerCase(Locale.CANADA)}.js"
@VisibleForTesting internal val file = "${name.toLowerCase(Locale.CANADA)}.js"
private val injector = lazyContext {
try {
val content = it.assets.open("js/$file").bufferedReader().use(BufferedReader::readText)
@ -56,9 +64,7 @@ enum class JsAssets(private val singleLoad: Boolean = true) : InjectorContract {
companion object {
// Ensures that all non themes and the selected theme are loaded
suspend fun load(context: Context) {
withContext(Dispatchers.IO) {
values().forEach { it.injector.invoke(context) }
}
withContext(Dispatchers.IO) { values().forEach { it.injector.invoke(context) } }
}
}
}

View File

@ -21,8 +21,8 @@ import androidx.annotation.VisibleForTesting
import com.pitchedapps.frost.prefs.Prefs
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.web.FrostWebViewClient
import org.apache.commons.text.StringEscapeUtils
import kotlin.random.Random
import org.apache.commons.text.StringEscapeUtils
class JsBuilder {
private val css = StringBuilder()
@ -49,7 +49,8 @@ class JsBuilder {
override fun toString(): String {
val tag = this.tag
val builder = StringBuilder().apply {
val builder =
StringBuilder().apply {
append("!function(){")
if (css.isNotBlank()) {
val cssMin = css.replace(Regex("\\s*\n\\s*"), "")
@ -71,51 +72,40 @@ class JsBuilder {
return content
}
private fun singleInjector(tag: String, content: String) = StringBuilder().apply {
private fun singleInjector(tag: String, content: String) =
StringBuilder()
.apply {
append("if (!window.hasOwnProperty(\"$tag\")) {")
append("console.log(\"Registering $tag\");")
append("window.$tag = true;")
append(content)
append("}")
}.toString()
}
.toString()
}
/**
* Contract for all injectors to allow it to interact properly with a webview
*/
/** Contract for all injectors to allow it to interact properly with a webview */
interface InjectorContract {
fun inject(webView: WebView, prefs: Prefs)
/**
* Toggle the injector (usually through Prefs
* If false, will fallback to an empty action
*/
/** Toggle the injector (usually through Prefs If false, will fallback to an empty action */
fun maybe(enable: Boolean): InjectorContract = if (enable) this else JsActions.EMPTY
}
/**
* Helper method to inject multiple functions simultaneously with a single callback
*/
/** Helper method to inject multiple functions simultaneously with a single callback */
fun WebView.jsInject(vararg injectors: InjectorContract, prefs: Prefs) {
injectors.asSequence().filter { it != JsActions.EMPTY }.forEach {
it.inject(this, prefs)
}
injectors.asSequence().filter { it != JsActions.EMPTY }.forEach { it.inject(this, prefs) }
}
fun FrostWebViewClient.jsInject(vararg injectors: InjectorContract, prefs: Prefs) =
web.jsInject(*injectors, prefs = prefs)
/**
* Wrapper class to convert a function into an injector
*/
/** Wrapper class to convert a function into an injector */
class JsInjector(val function: String) : InjectorContract {
override fun inject(webView: WebView, prefs: Prefs) =
webView.evaluateJavascript(function, null)
override fun inject(webView: WebView, prefs: Prefs) = webView.evaluateJavascript(function, null)
}
/**
* Helper object to obfuscate window tags for JS injection.
*/
/** Helper object to obfuscate window tags for JS injection. */
@VisibleForTesting
internal object TagObfuscator {

View File

@ -35,12 +35,12 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.FileNotFoundException
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
interface ThemeProvider {
val textColor: Int
@ -61,9 +61,7 @@ interface ThemeProvider {
val isCustomTheme: Boolean
/**
* Note that while this can be loaded from any thread, it is typically done through [preload]]
*/
/** Note that while this can be loaded from any thread, it is typically done through [preload]] */
fun injector(category: ThemeCategory): InjectorContract
fun setTheme(id: Int)
@ -74,13 +72,13 @@ interface ThemeProvider {
}
/**
* Provides [InjectorContract] for each [ThemeCategory].
* Can be reloaded to take in changes from [Prefs]
* Provides [InjectorContract] for each [ThemeCategory]. Can be reloaded to take in changes from
* [Prefs]
*/
class ThemeProviderImpl @Inject internal constructor(
@ApplicationContext private val context: Context,
private val prefs: Prefs
) : ThemeProvider {
class ThemeProviderImpl
@Inject
internal constructor(@ApplicationContext private val context: Context, private val prefs: Prefs) :
ThemeProvider {
private var theme: Theme = Theme.values[prefs.theme]
set(value) {
@ -97,7 +95,8 @@ class ThemeProviderImpl @Inject internal constructor(
get() = theme.accentColorGetter(prefs)
override val accentColorForWhite: Int
get() = when {
get() =
when {
accentColor.isColorVisibleOn(Color.WHITE) -> accentColor
textColor.isColorVisibleOn(Color.WHITE) -> textColor
else -> FACEBOOK_BLUE
@ -106,9 +105,8 @@ class ThemeProviderImpl @Inject internal constructor(
override val nativeBgColor: Int
get() = bgColor.withAlpha(30)
override fun nativeBgColor(unread: Boolean) = bgColor
.colorToForeground(if (unread) 0.7f else 0.0f)
.withAlpha(30)
override fun nativeBgColor(unread: Boolean) =
bgColor.colorToForeground(if (unread) 0.7f else 0.0f).withAlpha(30)
override val bgColor: Int
get() = theme.backgroundColorGetter(prefs)
@ -125,24 +123,22 @@ class ThemeProviderImpl @Inject internal constructor(
override fun injector(category: ThemeCategory): InjectorContract =
injectors.getOrPut(category) { createInjector(category) }
/**
* Note that while this can be loaded from any thread, it is typically done through [preload]
*/
/** Note that while this can be loaded from any thread, it is typically done through [preload] */
private fun createInjector(category: ThemeCategory): InjectorContract {
val file = theme.file ?: return JsActions.EMPTY
try {
var content =
context.assets.open("css/${category.folder}/themes/$file").bufferedReader()
context.assets
.open("css/${category.folder}/themes/$file")
.bufferedReader()
.use(BufferedReader::readText)
if (theme == Theme.CUSTOM) {
val bt = if (Color.alpha(bgColor) == 255)
bgColor.toRgbaString()
else
"transparent"
val bt = if (Color.alpha(bgColor) == 255) bgColor.toRgbaString() else "transparent"
val bb = bgColor.colorToForeground(0.35f)
content = content
content =
content
.replace("\$T\$", textColor.toRgbaString())
.replace("\$TT\$", textColor.colorToBackground(0.05f).toRgbaString())
.replace("\$TD\$", textColor.adjustAlpha(0.6f).toRgbaString())
@ -185,7 +181,5 @@ class ThemeProviderImpl @Inject internal constructor(
@Module
@InstallIn(SingletonComponent::class)
interface ThemeProviderModule {
@Binds
@Singleton
fun themeProvider(to: ThemeProviderImpl): ThemeProvider
@Binds @Singleton fun themeProvider(to: ThemeProviderImpl): ThemeProvider
}

View File

@ -24,19 +24,17 @@ import com.pitchedapps.frost.activities.IntroActivity
import com.pitchedapps.frost.databinding.IntroThemeBinding
import com.pitchedapps.frost.enums.Theme
/**
* Created by Allan Wang on 2017-07-28.
*/
/** Created by Allan Wang on 2017-07-28. */
class IntroFragmentTheme : BaseIntroFragment(R.layout.intro_theme) {
private lateinit var binding: IntroThemeBinding
val themeList
get() = with(binding) {
listOf(introThemeLight, introThemeDark, introThemeAmoled, introThemeGlass)
}
get() =
with(binding) { listOf(introThemeLight, introThemeDark, introThemeAmoled, introThemeGlass) }
override fun viewArray(): Array<Array<out View>> = with(binding) {
override fun viewArray(): Array<Array<out View>> =
with(binding) {
arrayOf(
arrayOf(title),
arrayOf(introThemeLight, introThemeDark),
@ -57,9 +55,7 @@ class IntroFragmentTheme : BaseIntroFragment(R.layout.intro_theme) {
introThemeGlass.setThemeClick(Theme.GLASS)
val currentTheme = prefs.theme - 1
if (currentTheme in 0..3)
themeList.forEachIndexed { index, v ->
v.scaleXY = if (index == currentTheme) 1.6f else 0.8f
}
themeList.forEachIndexed { index, v -> v.scaleXY = if (index == currentTheme) 1.6f else 0.8f }
}
private fun View.setThemeClick(theme: Theme) {

View File

@ -32,18 +32,17 @@ import com.pitchedapps.frost.R
import com.pitchedapps.frost.utils.launchTabCustomizerActivity
import kotlin.math.abs
/**
* Created by Allan Wang on 2017-07-28.
*/
abstract class BaseImageIntroFragment(
val titleRes: Int,
val imageRes: Int,
val descRes: Int
) : BaseIntroFragment(R.layout.intro_image) {
/** Created by Allan Wang on 2017-07-28. */
abstract class BaseImageIntroFragment(val titleRes: Int, val imageRes: Int, val descRes: Int) :
BaseIntroFragment(R.layout.intro_image) {
val imageDrawable: LayerDrawable by lazyResettableRegistered { image.drawable as LayerDrawable }
val phone: Drawable by lazyResettableRegistered { imageDrawable.findDrawableByLayerId(R.id.intro_phone) }
val screen: Drawable by lazyResettableRegistered { imageDrawable.findDrawableByLayerId(R.id.intro_phone_screen) }
val phone: Drawable by lazyResettableRegistered {
imageDrawable.findDrawableByLayerId(R.id.intro_phone)
}
val screen: Drawable by lazyResettableRegistered {
imageDrawable.findDrawableByLayerId(R.id.intro_phone_screen)
}
val icon: ImageView by bindViewResettable(R.id.intro_button)
override fun viewArray(): Array<Array<out View>> = arrayOf(arrayOf(title), arrayOf(desc))
@ -78,17 +77,16 @@ abstract class BaseImageIntroFragment(
}
fun firstImageFragmentTransition(offset: Float) {
if (offset < 0)
image.alpha = 1 + offset
if (offset < 0) image.alpha = 1 + offset
}
fun lastImageFragmentTransition(offset: Float) {
if (offset > 0)
image.alpha = 1 - offset
if (offset > 0) image.alpha = 1 - offset
}
}
class IntroAccountFragment : BaseImageIntroFragment(
class IntroAccountFragment :
BaseImageIntroFragment(
R.string.intro_multiple_accounts,
R.drawable.intro_phone_nav,
R.string.intro_multiple_accounts_desc
@ -96,7 +94,11 @@ class IntroAccountFragment : BaseImageIntroFragment(
override fun themeFragmentImpl() {
super.themeFragmentImpl()
themeImageComponent(themeProvider.iconColor, R.id.intro_phone_avatar_1, R.id.intro_phone_avatar_2)
themeImageComponent(
themeProvider.iconColor,
R.id.intro_phone_avatar_1,
R.id.intro_phone_avatar_2
)
themeImageComponent(themeProvider.bgColor.colorToForeground(), R.id.intro_phone_nav)
themeImageComponent(themeProvider.headerColor, R.id.intro_phone_header)
}
@ -107,16 +109,17 @@ class IntroAccountFragment : BaseImageIntroFragment(
}
}
class IntroTabTouchFragment : BaseImageIntroFragment(
R.string.intro_easy_navigation, R.drawable.intro_phone_tab, R.string.intro_easy_navigation_desc
class IntroTabTouchFragment :
BaseImageIntroFragment(
R.string.intro_easy_navigation,
R.drawable.intro_phone_tab,
R.string.intro_easy_navigation_desc
) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
icon.visible().setIcon(GoogleMaterial.Icon.gmd_edit, 24)
icon.setOnClickListener {
activity?.launchTabCustomizerActivity()
}
icon.setOnClickListener { activity?.launchTabCustomizerActivity() }
}
override fun themeFragmentImpl() {
@ -133,7 +136,8 @@ class IntroTabTouchFragment : BaseImageIntroFragment(
}
}
class IntroTabContextFragment : BaseImageIntroFragment(
class IntroTabContextFragment :
BaseImageIntroFragment(
R.string.intro_context_aware,
R.drawable.intro_phone_long_press,
R.string.intro_context_aware_desc

View File

@ -45,17 +45,13 @@ import kotlin.math.abs
* Contains the base, start, and end fragments
*/
/**
* The core intro fragment for all other fragments
*/
/** The core intro fragment for all other fragments */
@AndroidEntryPoint
abstract class BaseIntroFragment(val layoutRes: Int) : Fragment() {
@Inject
protected lateinit var prefs: Prefs
@Inject protected lateinit var prefs: Prefs
@Inject
protected lateinit var themeProvider: ThemeProvider
@Inject protected lateinit var themeProvider: ThemeProvider
val screenWidth
get() = resources.displayMetrics.widthPixels
@ -67,8 +63,7 @@ abstract class BaseIntroFragment(val layoutRes: Int) : Fragment() {
val increment = maxTranslation / views.size
views.forEachIndexed { i, group ->
group.forEach {
it.translationX =
if (offset > 0) -maxTranslation + i * increment else -(i + 1) * increment
it.translationX = if (offset > 0) -maxTranslation + i * increment else -(i + 1) * increment
it.alpha = 1 - abs(offset)
}
}
@ -112,7 +107,9 @@ abstract class BaseIntroFragment(val layoutRes: Int) : Fragment() {
}
protected open fun themeFragmentImpl() {
(view as? ViewGroup)?.children?.forEach { (it as? TextView)?.setTextColor(themeProvider.textColor) }
(view as? ViewGroup)?.children?.forEach {
(it as? TextView)?.setTextColor(themeProvider.textColor)
}
}
protected val viewArray: Array<Array<out View>> by lazyResettableRegistered { viewArray() }
@ -131,8 +128,7 @@ abstract class BaseIntroFragment(val layoutRes: Int) : Fragment() {
if (view != null) onPageSelectedImpl()
}
protected open fun onPageSelectedImpl() {
}
protected open fun onPageSelectedImpl() {}
}
class IntroFragmentWelcome : BaseIntroFragment(R.layout.intro_welcome) {

View File

@ -108,8 +108,8 @@ class OldPrefs @Inject internal constructor(factory: KPrefFactory) :
var enablePip: Boolean by kpref("enable_pip", true)
/**
* Despite the naming, this toggle currently only enables debug logging.
* Verbose is never logged in release builds.
* Despite the naming, this toggle currently only enables debug logging. Verbose is never logged
* in release builds.
*/
var verboseLogging: Boolean by kpref("verbose_logging", false)

View File

@ -41,7 +41,8 @@ import javax.inject.Inject
import javax.inject.Singleton
/**
* [Prefs] is no longer an actual pref, but we will expose the reset function as it is used elsewhere
* [Prefs] is no longer an actual pref, but we will expose the reset function as it is used
* elsewhere
*/
interface PrefsBase {
fun reset()
@ -49,22 +50,19 @@ interface PrefsBase {
}
interface Prefs :
BehaviourPrefs,
CorePrefs,
FeedPrefs,
NotifPrefs,
ThemePrefs,
ShowcasePrefs,
PrefsBase
BehaviourPrefs, CorePrefs, FeedPrefs, NotifPrefs, ThemePrefs, ShowcasePrefs, PrefsBase
class PrefsImpl @Inject internal constructor(
class PrefsImpl
@Inject
internal constructor(
private val behaviourPrefs: BehaviourPrefs,
private val corePrefs: CorePrefs,
private val feedPrefs: FeedPrefs,
private val notifPrefs: NotifPrefs,
private val themePrefs: ThemePrefs,
private val showcasePrefs: ShowcasePrefs
) : Prefs,
) :
Prefs,
BehaviourPrefs by behaviourPrefs,
CorePrefs by corePrefs,
FeedPrefs by feedPrefs,
@ -94,33 +92,19 @@ class PrefsImpl @Inject internal constructor(
@Module
@InstallIn(SingletonComponent::class)
interface PrefModule {
@Binds
@Singleton
fun behaviour(to: BehaviourPrefsImpl): BehaviourPrefs
@Binds @Singleton fun behaviour(to: BehaviourPrefsImpl): BehaviourPrefs
@Binds
@Singleton
fun core(to: CorePrefsImpl): CorePrefs
@Binds @Singleton fun core(to: CorePrefsImpl): CorePrefs
@Binds
@Singleton
fun feed(to: FeedPrefsImpl): FeedPrefs
@Binds @Singleton fun feed(to: FeedPrefsImpl): FeedPrefs
@Binds
@Singleton
fun notif(to: NotifPrefsImpl): NotifPrefs
@Binds @Singleton fun notif(to: NotifPrefsImpl): NotifPrefs
@Binds
@Singleton
fun theme(to: ThemePrefsImpl): ThemePrefs
@Binds @Singleton fun theme(to: ThemePrefsImpl): ThemePrefs
@Binds
@Singleton
fun showcase(to: ShowcasePrefsImpl): ShowcasePrefs
@Binds @Singleton fun showcase(to: ShowcasePrefsImpl): ShowcasePrefs
@Binds
@Singleton
fun prefs(to: PrefsImpl): Prefs
@Binds @Singleton fun prefs(to: PrefsImpl): Prefs
}
@Module

View File

@ -49,62 +49,42 @@ interface BehaviourPrefs : PrefsBase {
var autoExpandTextBox: Boolean
}
class BehaviourPrefsImpl @Inject internal constructor(
class BehaviourPrefsImpl
@Inject
internal constructor(
factory: KPrefFactory,
oldPrefs: OldPrefs,
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.behaviour", factory), BehaviourPrefs {
override var biometricsEnabled: Boolean by kpref(
"biometrics_enabled",
oldPrefs.biometricsEnabled /* false */
)
override var biometricsEnabled: Boolean by
kpref("biometrics_enabled", oldPrefs.biometricsEnabled /* false */)
override var overlayEnabled: Boolean by kpref(
"overlay_enabled",
oldPrefs.overlayEnabled /* true */
)
override var overlayEnabled: Boolean by
kpref("overlay_enabled", oldPrefs.overlayEnabled /* true */)
override var overlayFullScreenSwipe: Boolean by kpref(
"overlay_full_screen_swipe",
oldPrefs.overlayFullScreenSwipe /* true */
)
override var overlayFullScreenSwipe: Boolean by
kpref("overlay_full_screen_swipe", oldPrefs.overlayFullScreenSwipe /* true */)
override var viewpagerSwipe: Boolean by kpref(
"viewpager_swipe",
oldPrefs.viewpagerSwipe /* true */
)
override var viewpagerSwipe: Boolean by
kpref("viewpager_swipe", oldPrefs.viewpagerSwipe /* true */)
override var loadMediaOnMeteredNetwork: Boolean by kpref(
"media_on_metered_network",
oldPrefs.loadMediaOnMeteredNetwork /* true */
)
override var loadMediaOnMeteredNetwork: Boolean by
kpref("media_on_metered_network", oldPrefs.loadMediaOnMeteredNetwork /* true */)
override var debugSettings: Boolean by kpref(
"debug_settings",
oldPrefs.debugSettings /* false */
)
override var debugSettings: Boolean by kpref("debug_settings", oldPrefs.debugSettings /* false */)
override var linksInDefaultApp: Boolean by kpref(
"link_in_default_app",
oldPrefs.linksInDefaultApp /* false */
)
override var linksInDefaultApp: Boolean by
kpref("link_in_default_app", oldPrefs.linksInDefaultApp /* false */)
override var blackMediaBg: Boolean by kpref("black_media_bg", oldPrefs.blackMediaBg /* false */)
override var autoRefreshFeed: Boolean by kpref(
"auto_refresh_feed",
oldPrefs.autoRefreshFeed /* false */
)
override var autoRefreshFeed: Boolean by
kpref("auto_refresh_feed", oldPrefs.autoRefreshFeed /* false */)
override var showCreateFab: Boolean by kpref(
"show_create_fab",
oldPrefs.showCreateFab /* true */
)
override var showCreateFab: Boolean by kpref("show_create_fab", oldPrefs.showCreateFab /* true */)
override var fullSizeImage: Boolean by kpref(
"full_size_image",
oldPrefs.fullSizeImage /* false */
)
override var fullSizeImage: Boolean by
kpref("full_size_image", oldPrefs.fullSizeImage /* false */)
override var autoExpandTextBox: Boolean by kpref("auto_expand_text_box", true)
}

View File

@ -41,8 +41,8 @@ interface CorePrefs : PrefsBase {
var identifier: Int
/**
* Despite the naming, this toggle currently only enables debug logging.
* Verbose is never logged in release builds.
* Despite the naming, this toggle currently only enables debug logging. Verbose is never logged
* in release builds.
*/
var verboseLogging: Boolean
@ -55,7 +55,9 @@ interface CorePrefs : PrefsBase {
var messageScrollToBottom: Boolean
}
class CorePrefsImpl @Inject internal constructor(
class CorePrefsImpl
@Inject
internal constructor(
factory: KPrefFactory,
oldPrefs: OldPrefs,
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.core", factory), CorePrefs {
@ -71,31 +73,22 @@ class CorePrefsImpl @Inject internal constructor(
override var versionCode: Int by kpref("version_code", oldPrefs.versionCode /* -1 */)
override var prevVersionCode: Int by kpref(
"prev_version_code",
oldPrefs.prevVersionCode /* -1 */
)
override var prevVersionCode: Int by kpref("prev_version_code", oldPrefs.prevVersionCode /* -1 */)
override var installDate: Long by kpref("install_date", oldPrefs.installDate /* -1L */)
override var identifier: Int by kpref("identifier", oldPrefs.identifier /* -1 */)
override var verboseLogging: Boolean by kpref(
"verbose_logging",
oldPrefs.verboseLogging /* false */
)
override var verboseLogging: Boolean by
kpref("verbose_logging", oldPrefs.verboseLogging /* false */)
override var enablePip: Boolean by kpref("enable_pip", oldPrefs.enablePip /* true */)
override var exitConfirmation: Boolean by kpref(
"exit_confirmation",
oldPrefs.exitConfirmation /* true */
)
override var exitConfirmation: Boolean by
kpref("exit_confirmation", oldPrefs.exitConfirmation /* true */)
override var animate: Boolean by kpref("fancy_animations", oldPrefs.animate /* true */)
override var messageScrollToBottom: Boolean by kpref(
"message_scroll_to_bottom",
oldPrefs.messageScrollToBottom /* false */
)
override var messageScrollToBottom: Boolean by
kpref("message_scroll_to_bottom", oldPrefs.messageScrollToBottom /* false */)
}

View File

@ -50,49 +50,32 @@ interface FeedPrefs : PrefsBase {
var showPostReactions: Boolean
}
class FeedPrefsImpl @Inject internal constructor(
factory: KPrefFactory,
oldPrefs: OldPrefs
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.feed", factory), FeedPrefs {
class FeedPrefsImpl @Inject internal constructor(factory: KPrefFactory, oldPrefs: OldPrefs) :
KPref("${BuildConfig.APPLICATION_ID}.prefs.feed", factory), FeedPrefs {
override var webTextScaling: Int by kpref("web_text_scaling", oldPrefs.webTextScaling /* 100 */)
override var feedSort: Int by kpref(
"feed_sort",
oldPrefs.feedSort /* FeedSort.DEFAULT.ordinal */
)
override var feedSort: Int by kpref("feed_sort", oldPrefs.feedSort /* FeedSort.DEFAULT.ordinal */)
override var aggressiveRecents: Boolean by kpref(
"aggressive_recents",
oldPrefs.aggressiveRecents /* false */
)
override var aggressiveRecents: Boolean by
kpref("aggressive_recents", oldPrefs.aggressiveRecents /* false */)
override var showComposer: Boolean by kpref(
"status_composer_feed",
oldPrefs.showComposer /* true */
)
override var showComposer: Boolean by
kpref("status_composer_feed", oldPrefs.showComposer /* true */)
override var showSuggestedFriends: Boolean by kpref(
"suggested_friends_feed",
oldPrefs.showSuggestedFriends /* true */
)
override var showSuggestedFriends: Boolean by
kpref("suggested_friends_feed", oldPrefs.showSuggestedFriends /* true */)
override var showSuggestedGroups: Boolean by kpref(
"suggested_groups_feed",
oldPrefs.showSuggestedGroups /* true */
)
override var showSuggestedGroups: Boolean by
kpref("suggested_groups_feed", oldPrefs.showSuggestedGroups /* true */)
override var showFacebookAds: Boolean by kpref(
"facebook_ads",
oldPrefs.showFacebookAds /* false */
)
override var showFacebookAds: Boolean by
kpref("facebook_ads", oldPrefs.showFacebookAds /* false */)
override var showStories: Boolean by kpref("show_stories", oldPrefs.showStories /* true */)
override var mainActivityLayoutType: Int by kpref(
"main_activity_layout_type",
oldPrefs.mainActivityLayoutType /* 0 */
)
override var mainActivityLayoutType: Int by
kpref("main_activity_layout_type", oldPrefs.mainActivityLayoutType /* 0 */)
override val mainActivityLayout: MainActivityLayout
get() = MainActivityLayout(mainActivityLayoutType)

View File

@ -47,63 +47,43 @@ interface NotifPrefs : PrefsBase {
var notificationFreq: Long
}
class NotifPrefsImpl @Inject internal constructor(
class NotifPrefsImpl
@Inject
internal constructor(
factory: KPrefFactory,
oldPrefs: OldPrefs,
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.notif", factory), NotifPrefs {
override var notificationKeywords: Set<String> by kpref(
"notification_keywords",
oldPrefs.notificationKeywords /* mutableSetOf() */
)
override var notificationKeywords: Set<String> by
kpref("notification_keywords", oldPrefs.notificationKeywords /* mutableSetOf() */)
override var notificationsGeneral: Boolean by kpref(
"notification_general",
oldPrefs.notificationsGeneral /* true */
)
override var notificationsGeneral: Boolean by
kpref("notification_general", oldPrefs.notificationsGeneral /* true */)
override var notificationAllAccounts: Boolean by kpref(
"notification_all_accounts",
oldPrefs.notificationAllAccounts /* true */
)
override var notificationAllAccounts: Boolean by
kpref("notification_all_accounts", oldPrefs.notificationAllAccounts /* true */)
override var notificationsInstantMessages: Boolean by kpref(
"notification_im",
oldPrefs.notificationsInstantMessages /* true */
)
override var notificationsInstantMessages: Boolean by
kpref("notification_im", oldPrefs.notificationsInstantMessages /* true */)
override var notificationsImAllAccounts: Boolean by kpref(
"notification_im_all_accounts",
oldPrefs.notificationsImAllAccounts /* false */
)
override var notificationsImAllAccounts: Boolean by
kpref("notification_im_all_accounts", oldPrefs.notificationsImAllAccounts /* false */)
override var notificationVibrate: Boolean by kpref(
"notification_vibrate",
oldPrefs.notificationVibrate /* true */
)
override var notificationVibrate: Boolean by
kpref("notification_vibrate", oldPrefs.notificationVibrate /* true */)
override var notificationSound: Boolean by kpref(
"notification_sound",
oldPrefs.notificationSound /* true */
)
override var notificationSound: Boolean by
kpref("notification_sound", oldPrefs.notificationSound /* true */)
override var notificationRingtone: String by kpref(
"notification_ringtone",
oldPrefs.notificationRingtone /* "" */
)
override var notificationRingtone: String by
kpref("notification_ringtone", oldPrefs.notificationRingtone /* "" */)
override var messageRingtone: String by kpref(
"message_ringtone",
oldPrefs.messageRingtone /* "" */
)
override var messageRingtone: String by
kpref("message_ringtone", oldPrefs.messageRingtone /* "" */)
override var notificationLights: Boolean by kpref(
"notification_lights",
oldPrefs.notificationLights /* true */
)
override var notificationLights: Boolean by
kpref("notification_lights", oldPrefs.notificationLights /* true */)
override var notificationFreq: Long by kpref(
"notification_freq",
oldPrefs.notificationFreq /* 15L */
)
override var notificationFreq: Long by
kpref("notification_freq", oldPrefs.notificationFreq /* 15L */)
}

View File

@ -23,9 +23,7 @@ import com.pitchedapps.frost.prefs.PrefsBase
import javax.inject.Inject
interface ShowcasePrefs : PrefsBase {
/**
* Check if this is the first time launching the web overlay; show snackbar if true
*/
/** Check if this is the first time launching the web overlay; show snackbar if true */
val firstWebOverlay: Boolean
val intro: Boolean
@ -36,9 +34,8 @@ interface ShowcasePrefs : PrefsBase {
*
* Showcase prefs that offer one time helpers to guide new users
*/
class ShowcasePrefsImpl @Inject internal constructor(
factory: KPrefFactory
) : KPref("${BuildConfig.APPLICATION_ID}.showcase", factory), ShowcasePrefs {
class ShowcasePrefsImpl @Inject internal constructor(factory: KPrefFactory) :
KPref("${BuildConfig.APPLICATION_ID}.showcase", factory), ShowcasePrefs {
override val firstWebOverlay: Boolean by kprefSingle("first_web_overlay")

View File

@ -39,41 +39,30 @@ interface ThemePrefs : PrefsBase {
var tintNavBar: Boolean
}
class ThemePrefsImpl @Inject internal constructor(
class ThemePrefsImpl
@Inject
internal constructor(
factory: KPrefFactory,
oldPrefs: OldPrefs,
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.theme", factory), ThemePrefs {
/**
* Note that this is purely for the pref storage. Updating themes should use
* ThemeProvider
*/
/** Note that this is purely for the pref storage. Updating themes should use ThemeProvider */
override var theme: Int by kpref("theme", oldPrefs.theme /* 0 */)
override var customTextColor: Int by kpref(
"color_text",
oldPrefs.customTextColor /* 0xffeceff1.toInt() */
)
override var customTextColor: Int by
kpref("color_text", oldPrefs.customTextColor /* 0xffeceff1.toInt() */)
override var customAccentColor: Int by kpref(
"color_accent",
oldPrefs.customAccentColor /* 0xff0288d1.toInt() */
)
override var customAccentColor: Int by
kpref("color_accent", oldPrefs.customAccentColor /* 0xff0288d1.toInt() */)
override var customBackgroundColor: Int by kpref(
"color_bg",
oldPrefs.customBackgroundColor /* 0xff212121.toInt() */
)
override var customBackgroundColor: Int by
kpref("color_bg", oldPrefs.customBackgroundColor /* 0xff212121.toInt() */)
override var customHeaderColor: Int by kpref(
"color_header",
oldPrefs.customHeaderColor /* 0xff01579b.toInt() */
)
override var customHeaderColor: Int by
kpref("color_header", oldPrefs.customHeaderColor /* 0xff01579b.toInt() */)
override var customIconColor: Int by kpref(
"color_icons",
oldPrefs.customIconColor /* 0xffeceff1.toInt() */
)
override var customIconColor: Int by
kpref("color_icons", oldPrefs.customIconColor /* 0xffeceff1.toInt() */)
override var tintNavBar: Boolean by kpref("tint_nav_bar", oldPrefs.tintNavBar /* true */)
}

View File

@ -20,9 +20,9 @@ import android.app.job.JobParameters
import android.app.job.JobService
import androidx.annotation.CallSuper
import ca.allanwang.kau.utils.ContextHelper
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext
abstract class BaseJobService : JobService(), CoroutineScope {
@ -32,9 +32,7 @@ abstract class BaseJobService : JobService(), CoroutineScope {
protected val startTime = System.currentTimeMillis()
/**
* Note that if a job plans on running asynchronously, it should return true
*/
/** Note that if a job plans on running asynchronously, it should return true */
@CallSuper
override fun onStartJob(params: JobParameters?): Boolean {
job = Job()

View File

@ -60,12 +60,11 @@ import kotlin.math.abs
private val _40_DP = 40.dpToPx
private val pendingIntentFlagUpdateCurrent: Int
get() = PendingIntent.FLAG_UPDATE_CURRENT or
get() =
PendingIntent.FLAG_UPDATE_CURRENT or
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
/**
* Enum to handle notification creations
*/
/** Enum to handle notification creations */
enum class NotificationType(
val channelId: String,
private val overlayContext: OverlayContext,
@ -73,7 +72,6 @@ enum class NotificationType(
private val parser: FrostParser<ParseNotification>,
private val ringtoneProvider: (Prefs) -> String
) {
GENERAL(
NOTIF_CHANNEL_GENERAL,
OverlayContext.NOTIFICATION,
@ -81,7 +79,6 @@ enum class NotificationType(
NotifParser,
{ it.notificationRingtone }
),
MESSAGE(
NOTIF_CHANNEL_MESSAGES,
OverlayContext.MESSAGE,
@ -92,9 +89,7 @@ enum class NotificationType(
private val groupPrefix = "frost_${name.toLowerCase(Locale.CANADA)}"
/**
* Optional binder to return the request bundle builder
*/
/** Optional binder to return the request bundle builder */
internal open fun bindRequest(
content: NotificationContent,
cookie: String
@ -109,12 +104,10 @@ enum class NotificationType(
}
/**
* Get unread data from designated parser
* Display notifications for those after old epoch
* Save new epoch
* Get unread data from designated parser Display notifications for those after old epoch Save new
* epoch
*
* Returns the number of notifications generated,
* or -1 if an error occurred
* Returns the number of notifications generated, or -1 if an error occurred
*/
suspend fun fetch(
context: Context,
@ -122,7 +115,8 @@ enum class NotificationType(
prefs: Prefs,
notifDao: NotificationDao
): Int {
val response = try {
val response =
try {
parser.parse(data.cookie)
} catch (ignored: Exception) {
null
@ -132,17 +126,14 @@ enum class NotificationType(
return -1
}
/**
* Checks that the text doesn't contain any blacklisted keywords
*/
/** Checks that the text doesn't contain any blacklisted keywords */
fun validText(text: String?): Boolean {
val t = text ?: return true
return prefs.notificationKeywords.none {
t.contains(it, true)
}
return prefs.notificationKeywords.none { t.contains(it, true) }
}
val notifContents = response.data.getUnreadNotifications(data).filter { notif ->
val notifContents =
response.data.getUnreadNotifications(data).filter { notif ->
validText(notif.title) && validText(notif.text)
}
if (notifContents.isEmpty()) return 0
@ -173,8 +164,7 @@ enum class NotificationType(
val notifs = newNotifContents.map { createNotification(context, it) }
frostEvent("Notifications", "Type" to name, "Count" to notifs.size)
if (notifs.size > 1)
summaryNotification(context, userId, notifs.size).notify(context)
if (notifs.size > 1) summaryNotification(context, userId, notifs.size).notify(context)
val ringtone = ringtoneProvider(prefs)
notifs.forEachIndexed { i, notif ->
// Ring at most twice
@ -184,7 +174,8 @@ enum class NotificationType(
}
fun debugNotification(context: Context, data: CookieEntity) {
val content = NotificationContent(
val content =
NotificationContent(
data,
System.currentTimeMillis(),
"https://github.com/AllanWang/Frost-for-Facebook",
@ -197,9 +188,7 @@ enum class NotificationType(
createNotification(context, content).notify(context)
}
/**
* Attach content related data to an intent
*/
/** Attach content related data to an intent */
fun putContentExtra(intent: Intent, content: NotificationContent): Intent {
// We will show the notification page for dependent urls. We can trigger a click next time
intent.data =
@ -209,8 +198,7 @@ enum class NotificationType(
}
/**
* Create a generic content for the provided type and user id.
* No content related data is added
* Create a generic content for the provided type and user id. No content related data is added
*/
fun createCommonIntent(context: Context, userId: Long): Intent {
val intent = Intent(context, FrostWebActivity::class.java)
@ -219,9 +207,7 @@ enum class NotificationType(
return intent
}
/**
* Create and submit a new notification with the given [content]
*/
/** Create and submit a new notification with the given [content] */
private fun createNotification(
context: Context,
content: NotificationContent
@ -232,7 +218,9 @@ enum class NotificationType(
val group = "${groupPrefix}_${data.id}"
val pendingIntent =
PendingIntent.getActivity(context, 0, intent, pendingIntentFlagUpdateCurrent)
val notifBuilder = context.frostNotification(channelId)
val notifBuilder =
context
.frostNotification(channelId)
.setContentTitle(title ?: context.string(R.string.frost_name))
.setContentText(text)
.setContentIntent(pendingIntent)
@ -245,7 +233,8 @@ enum class NotificationType(
if (profileUrl != null) {
try {
val profileImg = GlideApp.with(context)
val profileImg =
GlideApp.with(context)
.asBitmap()
.load(profileUrl)
.transform(FrostGlide.circleCrop)
@ -261,9 +250,9 @@ enum class NotificationType(
}
/**
* Create a summary notification to wrap the previous ones
* This will always produce sound, vibration, and lights based on preferences
* and will only show if we have at least 2 notifications
* Create a summary notification to wrap the previous ones This will always produce sound,
* vibration, and lights based on preferences and will only show if we have at least 2
* notifications
*/
private fun summaryNotification(context: Context, userId: Long, count: Int): FrostNotification {
val intent = createCommonIntent(context, userId)
@ -271,7 +260,9 @@ enum class NotificationType(
val group = "${groupPrefix}_$userId"
val pendingIntent =
PendingIntent.getActivity(context, 0, intent, pendingIntentFlagUpdateCurrent)
val notifBuilder = context.frostNotification(channelId)
val notifBuilder =
context
.frostNotification(channelId)
.setContentTitle(context.string(R.string.frost_name))
.setContentText("$count ${context.string(fbItem.titleId)}")
.setGroup(group)
@ -287,9 +278,7 @@ enum class NotificationType(
}
}
/**
* Notification data holder
*/
/** Notification data holder */
data class NotificationContent(
// TODO replace data with userId?
val data: CookieEntity,
@ -306,8 +295,8 @@ data class NotificationContent(
}
/**
* Wrapper for a complete notification builder and identifier
* which can be immediately notified when given a [Context]
* Wrapper for a complete notification builder and identifier which can be immediately notified when
* given a [Context]
*/
data class FrostNotification(
private val tag: String,
@ -338,5 +327,4 @@ fun Context.scheduleNotificationsFromPrefs(prefs: Prefs): Boolean {
fun Context.scheduleNotifications(minutes: Long): Boolean =
scheduleJob<NotificationService>(NOTIFICATION_PERIODIC_JOB, minutes)
fun Context.fetchNotifications(): Boolean =
fetchJob<NotificationService>(NOTIFICATION_JOB_NOW)
fun Context.fetchNotifications(): Boolean = fetchJob<NotificationService>(NOTIFICATION_JOB_NOW)

View File

@ -30,32 +30,29 @@ import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.frostEvent
import com.pitchedapps.frost.widgets.NotificationWidget
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import javax.inject.Inject
/**
* Created by Allan Wang on 2017-06-14.
*
* Service to manage notifications
* Will periodically check through all accounts in the db and send notifications when appropriate
* Service to manage notifications Will periodically check through all accounts in the db and send
* notifications when appropriate
*
* All fetching is done through parsers
*/
@AndroidEntryPoint
class NotificationService : BaseJobService() {
@Inject
lateinit var prefs: Prefs
@Inject lateinit var prefs: Prefs
@Inject
lateinit var notifDao: NotificationDao
@Inject lateinit var notifDao: NotificationDao
@Inject
lateinit var cookieDao: CookieDao
@Inject lateinit var cookieDao: CookieDao
override fun onStopJob(params: JobParameters?): Boolean {
super.onStopJob(params)
@ -66,11 +63,12 @@ class NotificationService : BaseJobService() {
private var preparedFinish = false
private fun prepareFinish(abrupt: Boolean) {
if (preparedFinish)
return
if (preparedFinish) return
preparedFinish = true
val time = System.currentTimeMillis() - startTime
L.i { "Notification service has ${if (abrupt) "finished abruptly" else "finished"} in $time ms" }
L.i {
"Notification service has ${if (abrupt) "finished abruptly" else "finished"} in $time ms"
}
frostEvent(
"NotificationTime",
"Type" to (if (abrupt) "Service force stop" else "Service"),
@ -86,8 +84,7 @@ class NotificationService : BaseJobService() {
try {
sendNotifications(params)
} finally {
if (!isActive)
prepareFinish(false)
if (!isActive) prepareFinish(false)
jobFinished(params, false)
}
}
@ -104,13 +101,9 @@ class NotificationService : BaseJobService() {
for (cookie in cookies) {
yield()
val current = cookie.id == currentId
if (prefs.notificationsGeneral &&
(current || prefs.notificationAllAccounts)
)
if (prefs.notificationsGeneral && (current || prefs.notificationAllAccounts))
notifCount += fetch(jobId, NotificationType.GENERAL, cookie)
if (prefs.notificationsInstantMessages &&
(current || prefs.notificationsImAllAccounts)
)
if (prefs.notificationsInstantMessages && (current || prefs.notificationsImAllAccounts))
notifCount += fetch(jobId, NotificationType.MESSAGE, cookie)
}
@ -123,8 +116,8 @@ class NotificationService : BaseJobService() {
}
/**
* Implemented fetch to also notify when an error occurs
* Also normalized the output to return the number of notifications received
* Implemented fetch to also notify when an error occurs Also normalized the output to return the
* number of notifications received
*/
private suspend fun fetch(jobId: Int, type: NotificationType, cookie: CookieEntity): Int {
val count = type.fetch(this, cookie, prefs, notifDao)
@ -142,7 +135,8 @@ class NotificationService : BaseJobService() {
}
private fun generalNotification(id: Int, textRes: Int, withDefaults: Boolean) {
val notifBuilder = frostNotification(NOTIF_CHANNEL_GENERAL)
val notifBuilder =
frostNotification(NOTIF_CHANNEL_GENERAL)
.setFrostAlert(this, withDefaults, prefs.notificationRingtone, prefs)
.setContentTitle(string(R.string.frost_name))
.setContentText(string(textRes))

View File

@ -36,9 +36,7 @@ import com.pitchedapps.frost.prefs.Prefs
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.frostUri
/**
* Created by Allan Wang on 07/04/18.
*/
/** Created by Allan Wang on 07/04/18. */
const val NOTIF_CHANNEL_GENERAL = "general"
const val NOTIF_CHANNEL_MESSAGES = "messages"
@ -50,12 +48,11 @@ fun setupNotificationChannels(c: Context, themeProvider: ThemeProvider) {
manager.createNotificationChannel(NOTIF_CHANNEL_GENERAL, appName, themeProvider)
manager.createNotificationChannel(NOTIF_CHANNEL_MESSAGES, "$appName: $msg", themeProvider)
manager.notificationChannels
.filter {
it.id != NOTIF_CHANNEL_GENERAL &&
it.id != NOTIF_CHANNEL_MESSAGES
}
.filter { it.id != NOTIF_CHANNEL_GENERAL && it.id != NOTIF_CHANNEL_MESSAGES }
.forEach { manager.deleteNotificationChannel(it.id) }
L.d { "Created notification channels: ${manager.notificationChannels.size} channels, ${manager.notificationChannelGroups.size} groups" }
L.d {
"Created notification channels: ${manager.notificationChannels.size} channels, ${manager.notificationChannelGroups.size} groups"
}
}
@RequiresApi(Build.VERSION_CODES.O)
@ -64,10 +61,7 @@ private fun NotificationManager.createNotificationChannel(
name: String,
themeProvider: ThemeProvider
): NotificationChannel {
val channel = NotificationChannel(
id,
name, NotificationManager.IMPORTANCE_DEFAULT
)
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT)
channel.enableLights(true)
channel.lightColor = themeProvider.accentColor
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
@ -76,8 +70,7 @@ private fun NotificationManager.createNotificationChannel(
}
fun Context.frostNotification(id: String) =
NotificationCompat.Builder(this, id)
.apply {
NotificationCompat.Builder(this, id).apply {
setSmallIcon(R.drawable.frost_f_24)
setAutoCancel(true)
setOnlyAlertOnce(true)
@ -86,9 +79,8 @@ fun Context.frostNotification(id: String) =
}
/**
* Dictates whether a notification should have sound/vibration/lights or not
* Delegates to channels if Android O and up
* Otherwise uses our provided preferences
* Dictates whether a notification should have sound/vibration/lights or not Delegates to channels
* if Android O and up Otherwise uses our provided preferences
*/
fun NotificationCompat.Builder.setFrostAlert(
context: Context,
@ -131,15 +123,15 @@ fun JobInfo.Builder.setExtras(id: Int): JobInfo.Builder {
}
/**
* interval is # of min, which must be at least 15
* returns false if an error occurs; true otherwise
* interval is # of min, which must be at least 15 returns false if an error occurs; true otherwise
*/
inline fun <reified T : JobService> Context.scheduleJob(id: Int, minutes: Long): Boolean {
val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
scheduler.cancel(id)
if (minutes < 0L) return true
val serviceComponent = ComponentName(this, T::class.java)
val builder = JobInfo.Builder(id, serviceComponent)
val builder =
JobInfo.Builder(id, serviceComponent)
.setPeriodic(minutes * 60000)
.setExtras(id)
.setPersisted(true)
@ -152,13 +144,12 @@ inline fun <reified T : JobService> Context.scheduleJob(id: Int, minutes: Long):
return true
}
/**
* Run notification job right now
*/
/** Run notification job right now */
inline fun <reified T : JobService> Context.fetchJob(id: Int): Boolean {
val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val serviceComponent = ComponentName(this, T::class.java)
val builder = JobInfo.Builder(id, serviceComponent)
val builder =
JobInfo.Builder(id, serviceComponent)
.setMinimumLatency(0L)
.setExtras(id)
.setOverrideDeadline(2000L)

View File

@ -32,8 +32,7 @@ import javax.inject.Inject
@AndroidEntryPoint
class UpdateReceiver : BroadcastReceiver() {
@Inject
lateinit var prefs: Prefs
@Inject lateinit var prefs: Prefs
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return

View File

@ -35,11 +35,8 @@ import com.pitchedapps.frost.utils.frostSnackbar
import com.pitchedapps.frost.utils.launchTabCustomizerActivity
import com.pitchedapps.frost.views.KPrefTextSeekbar
/**
* Created by Allan Wang on 2017-06-29.
*/
/** Created by Allan Wang on 2017-06-29. */
fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
header(R.string.theme_customization)
text(R.string.theme, prefs::theme, { themeProvider.setTheme(it) }) {
@ -62,9 +59,7 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
}
}
}
textGetter = {
string(Theme(it).textRes)
}
textGetter = { string(Theme(it).textRes) }
}
fun KPrefColorPicker.KPrefColorContract.dependsOnCustom() {
@ -78,7 +73,8 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
}
colorPicker(
R.string.text_color, prefs::customTextColor,
R.string.text_color,
prefs::customTextColor,
{
prefs.customTextColor = it
reload()
@ -91,7 +87,8 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
}
colorPicker(
R.string.accent_color, prefs::customAccentColor,
R.string.accent_color,
prefs::customAccentColor,
{
prefs.customAccentColor = it
reload()
@ -104,7 +101,8 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
}
colorPicker(
R.string.background_color, prefs::customBackgroundColor,
R.string.background_color,
prefs::customBackgroundColor,
{
prefs.customBackgroundColor = it
bgCanvas.ripple(it, duration = 500L)
@ -118,7 +116,8 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
}
colorPicker(
R.string.header_color, prefs::customHeaderColor,
R.string.header_color,
prefs::customHeaderColor,
{
prefs.customHeaderColor = it
frostNavigationBar(prefs, themeProvider)
@ -132,7 +131,8 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
}
colorPicker(
R.string.icon_color, prefs::customIconColor,
R.string.icon_color,
prefs::customIconColor,
{
prefs.customIconColor = it
invalidateOptionsMenu()
@ -174,7 +174,8 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
}
checkbox(
R.string.tint_nav, prefs::tintNavBar,
R.string.tint_nav,
prefs::tintNavBar,
{
prefs.tintNavBar = it
frostNavigationBar(prefs, themeProvider)
@ -188,7 +189,8 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
KPrefTextSeekbar(
KPrefSeekbar.KPrefSeekbarBuilder(
globalOptions,
R.string.web_text_scaling, prefs::webTextScaling
R.string.web_text_scaling,
prefs::webTextScaling
) {
prefs.webTextScaling = it
setFrostResult(REQUEST_TEXT_ZOOM)
@ -196,12 +198,7 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
)
)
checkbox(
R.string.enforce_black_media_bg, prefs::blackMediaBg,
{
prefs.blackMediaBg = it
}
) {
checkbox(R.string.enforce_black_media_bg, prefs::blackMediaBg, { prefs.blackMediaBg = it }) {
descRes = R.string.enforce_black_media_bg_desc
}
}

View File

@ -20,23 +20,30 @@ import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.SettingsActivity
/**
* Created by Allan Wang on 2017-06-30.
*/
/** Created by Allan Wang on 2017-06-30. */
fun SettingsActivity.getBehaviourPrefs(): KPrefAdapterBuilder.() -> Unit = {
checkbox(R.string.auto_refresh_feed, prefs::autoRefreshFeed, { prefs.autoRefreshFeed = it }) {
descRes = R.string.auto_refresh_feed_desc
}
checkbox(R.string.fancy_animations, prefs::animate, { prefs.animate = it; animate = it }) {
checkbox(
R.string.fancy_animations,
prefs::animate,
{
prefs.animate = it
animate = it
}
) {
descRes = R.string.fancy_animations_desc
}
checkbox(
R.string.overlay_swipe,
prefs::overlayEnabled,
{ prefs.overlayEnabled = it; shouldRefreshMain() }
{
prefs.overlayEnabled = it
shouldRefreshMain()
}
) {
descRes = R.string.overlay_swipe_desc
}
@ -72,7 +79,10 @@ fun SettingsActivity.getBehaviourPrefs(): KPrefAdapterBuilder.() -> Unit = {
checkbox(
R.string.auto_expand_text_box,
prefs::autoExpandTextBox,
{ prefs.autoExpandTextBox = it; shouldRefreshMain() }
{
prefs.autoExpandTextBox = it
shouldRefreshMain()
}
) {
descRes = R.string.auto_expand_text_box_desc
}

View File

@ -40,34 +40,29 @@ import com.pitchedapps.frost.prefs.Prefs
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.frostUriFromFile
import com.pitchedapps.frost.utils.sendFrostEmail
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import java.io.File
/**
* Created by Allan Wang on 2017-06-30.
*
* A sub pref section that is enabled through a hidden preference
* Each category will load a page, extract the contents, remove private info, and create a report
* A sub pref section that is enabled through a hidden preference Each category will load a page,
* extract the contents, remove private info, and create a report
*/
fun SettingsActivity.getDebugPrefs(): KPrefAdapterBuilder.() -> Unit = {
plainText(R.string.disclaimer) {
descRes = R.string.debug_disclaimer_info
}
plainText(R.string.disclaimer) { descRes = R.string.debug_disclaimer_info }
plainText(R.string.debug_web) {
descRes = R.string.debug_web_desc
onClick =
{ this@getDebugPrefs.startActivityForResult<DebugActivity>(ACTIVITY_REQUEST_DEBUG) }
onClick = { this@getDebugPrefs.startActivityForResult<DebugActivity>(ACTIVITY_REQUEST_DEBUG) }
}
plainText(R.string.debug_parsers) {
descRes = R.string.debug_parsers_desc
onClick = {
val parsers = arrayOf(NotifParser, MessageParser, SearchParser)
materialDialog {
@ -86,7 +81,8 @@ fun SettingsActivity.getDebugPrefs(): KPrefAdapterBuilder.() -> Unit = {
cancelOnTouchOutside(false)
}
attempt = launch(Dispatchers.IO) {
attempt =
launch(Dispatchers.IO) {
try {
val data = parser.parse(fbCookie.webCookie)
withMainContext {
@ -116,7 +112,8 @@ private const val ZIP_NAME = "debug"
fun SettingsActivity.sendDebug(url: String, html: String?) {
val downloader = OfflineWebsite(
val downloader =
OfflineWebsite(
url,
cookie = fbCookie.webCookie ?: "",
baseUrl = FB_URL_BASE,
@ -139,21 +136,15 @@ fun SettingsActivity.sendDebug(url: String, html: String?) {
// progressFlow.onEach { md.setProgress(it) }.launchIn(this)
launchMain {
val success = downloader.loadAndZip(ZIP_NAME) {
progressFlow.tryEmit(it)
}
val success = downloader.loadAndZip(ZIP_NAME) { progressFlow.tryEmit(it) }
md.dismiss()
if (success) {
val zipUri = frostUriFromFile(
File(downloader.baseDir, "$ZIP_NAME.zip")
)
val zipUri = frostUriFromFile(File(downloader.baseDir, "$ZIP_NAME.zip"))
L.i { "Sending debug zip with uri $zipUri" }
sendFrostEmail(R.string.debug_report_email_title, prefs = prefs) {
addItem("Url", url)
addAttachment(zipUri)
extras = {
type = "application/zip"
}
extras = { type = "application/zip" }
}
} else {
toast(R.string.error_generic)

View File

@ -23,21 +23,17 @@ import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.SettingsActivity
import com.pitchedapps.frost.utils.REQUEST_RESTART_APPLICATION
/**
* Created by Allan Wang on 2017-06-29.
*/
/** Created by Allan Wang on 2017-06-29. */
fun SettingsActivity.getExperimentalPrefs(): KPrefAdapterBuilder.() -> Unit = {
plainText(R.string.disclaimer) {
descRes = R.string.experimental_disclaimer_info
}
plainText(R.string.disclaimer) { descRes = R.string.experimental_disclaimer_info }
// Experimental content starts here ------------------
// Experimental content ends here --------------------
checkbox(
R.string.verbose_logging, prefs::verboseLogging,
R.string.verbose_logging,
prefs::verboseLogging,
{
prefs.verboseLogging = it
KL.shouldLog = { it != Log.VERBOSE }

Some files were not shown because too many files have changed in this diff Show More