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:
parent
bc1e1bda4f
commit
a319baa736
95
.editorconfig
Normal file
95
.editorconfig
Normal 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
|
@ -22,11 +22,11 @@ import androidx.test.runner.AndroidJUnitRunner
|
||||
import dagger.hilt.android.testing.HiltTestApplication
|
||||
|
||||
class FrostTestRunner : AndroidJUnitRunner() {
|
||||
override fun newApplication(
|
||||
cl: ClassLoader?,
|
||||
className: String?,
|
||||
context: Context?
|
||||
): Application {
|
||||
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
|
||||
}
|
||||
override fun newApplication(
|
||||
cl: ClassLoader?,
|
||||
className: String?,
|
||||
context: Context?
|
||||
): Application {
|
||||
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
|
||||
}
|
||||
}
|
||||
|
@ -27,22 +27,18 @@ 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)
|
||||
}
|
||||
)
|
||||
@get:Rule(order = 1)
|
||||
val activityRule =
|
||||
activityRule<StartActivity>(intentAction = { putExtra(ARG_URL, TEST_FORMATTED_URL) })
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -25,18 +25,16 @@ 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() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,18 +25,16 @@ 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() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,22 +27,18 @@ 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)
|
||||
}
|
||||
)
|
||||
@get:Rule(order = 1)
|
||||
val activityRule =
|
||||
activityRule<FrostWebActivity>(intentAction = { putExtra(ARG_URL, TEST_FORMATTED_URL) })
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,140 +47,125 @@ 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)
|
||||
}
|
||||
@get:Rule(order = 1)
|
||||
val activityRule =
|
||||
activityRule<ImageActivity>(intentAction = { putExtra(ARG_IMAGE_URL, TEST_FORMATTED_URL) })
|
||||
|
||||
@get:Rule(order = 2) val globalTimeout: Timeout = Timeout.seconds(15)
|
||||
|
||||
lateinit var mockServer: MockWebServer
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
mockServer = mockServer()
|
||||
}
|
||||
|
||||
@After
|
||||
fun after() {
|
||||
mockServer.closeQuietly()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() =
|
||||
launchScenario(mockServer.url("image").toString()) {
|
||||
// Verify no crash
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validImageTest() =
|
||||
launchScenario(mockServer.url("image").toString()) {
|
||||
mockServer.takeRequest()
|
||||
assertEquals(1, mockServer.requestCount, "One http request expected")
|
||||
// assertEquals(
|
||||
// FabStates.DOWNLOAD,
|
||||
// fabAction,
|
||||
// "Image should be successful, image should be downloaded"
|
||||
// )
|
||||
assertFalse(binding.error.isVisible, "Error should not be shown")
|
||||
val tempFile = assertNotNull(tempFile, "Temp file not created")
|
||||
assertTrue(tempFile.exists(), "Image should be located at temp file")
|
||||
assertTrue(
|
||||
System.currentTimeMillis() - tempFile.lastModified() < 2000L,
|
||||
"Image should have been modified within the last few seconds"
|
||||
)
|
||||
assertNull(errorRef, "No error should exist")
|
||||
tempFile.delete()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("apparently this fails")
|
||||
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")
|
||||
|
||||
// assertEquals(
|
||||
// FabStates.ERROR,
|
||||
// fabAction,
|
||||
// "Text should not be a valid image format, error state expected"
|
||||
// )
|
||||
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()) {
|
||||
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(
|
||||
"Unsuccessful response for image: Error mock response",
|
||||
errorRef?.message,
|
||||
"Error message mismatch"
|
||||
)
|
||||
assertFalse(tempFile?.exists() == true, "Temp file should have been removed")
|
||||
}
|
||||
|
||||
private fun launchScenario(
|
||||
imageUrl: String,
|
||||
text: String? = null,
|
||||
cookie: String? = null,
|
||||
action: ImageActivity.() -> Unit
|
||||
) {
|
||||
assertFalse(
|
||||
imageUrl.isIndirectImageUrl,
|
||||
"For simplicity, urls that are direct will be used without modifications in the production code."
|
||||
)
|
||||
val intent =
|
||||
Intent(ApplicationProvider.getApplicationContext(), ImageActivity::class.java).apply {
|
||||
putExtra(ARG_IMAGE_URL, imageUrl)
|
||||
putExtra(ARG_TEXT, text)
|
||||
putExtra(ARG_COOKIE, cookie)
|
||||
}
|
||||
ActivityScenario.launch<ImageActivity>(intent).use { it.onActivity(action) }
|
||||
}
|
||||
|
||||
@get:Rule(order = 2)
|
||||
val globalTimeout: Timeout = Timeout.seconds(15)
|
||||
|
||||
lateinit var mockServer: MockWebServer
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
mockServer = mockServer()
|
||||
}
|
||||
|
||||
@After
|
||||
fun after() {
|
||||
mockServer.closeQuietly()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() = launchScenario(mockServer.url("image").toString()) {
|
||||
// Verify no crash
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validImageTest() = launchScenario(mockServer.url("image").toString()) {
|
||||
mockServer.takeRequest()
|
||||
assertEquals(1, mockServer.requestCount, "One http request expected")
|
||||
// assertEquals(
|
||||
// FabStates.DOWNLOAD,
|
||||
// fabAction,
|
||||
// "Image should be successful, image should be downloaded"
|
||||
// )
|
||||
assertFalse(binding.error.isVisible, "Error should not be shown")
|
||||
val tempFile = assertNotNull(tempFile, "Temp file not created")
|
||||
assertTrue(tempFile.exists(), "Image should be located at temp file")
|
||||
assertTrue(
|
||||
System.currentTimeMillis() - tempFile.lastModified() < 2000L,
|
||||
"Image should have been modified within the last few seconds"
|
||||
)
|
||||
assertNull(errorRef, "No error should exist")
|
||||
tempFile.delete()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("apparently this fails")
|
||||
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")
|
||||
|
||||
// assertEquals(
|
||||
// FabStates.ERROR,
|
||||
// fabAction,
|
||||
// "Text should not be a valid image format, error state expected"
|
||||
// )
|
||||
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()) {
|
||||
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(
|
||||
"Unsuccessful response for image: Error mock response",
|
||||
errorRef?.message,
|
||||
"Error message mismatch"
|
||||
)
|
||||
assertFalse(tempFile?.exists() == true, "Temp file should have been removed")
|
||||
}
|
||||
|
||||
private fun launchScenario(
|
||||
imageUrl: String,
|
||||
text: String? = null,
|
||||
cookie: String? = null,
|
||||
action: ImageActivity.() -> Unit
|
||||
) {
|
||||
assertFalse(
|
||||
imageUrl.isIndirectImageUrl,
|
||||
"For simplicity, urls that are direct will be used without modifications in the production code."
|
||||
)
|
||||
val intent =
|
||||
Intent(ApplicationProvider.getApplicationContext(), ImageActivity::class.java).apply {
|
||||
putExtra(ARG_IMAGE_URL, imageUrl)
|
||||
putExtra(ARG_TEXT, text)
|
||||
putExtra(ARG_COOKIE, cookie)
|
||||
private fun mockServer(): MockWebServer {
|
||||
val img = Buffer()
|
||||
img.writeAll(getResource("bayer-pattern.jpg").source())
|
||||
return MockWebServer().apply {
|
||||
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)
|
||||
else -> MockResponse().setResponseCode(404).setBody("Error mock response")
|
||||
}
|
||||
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() {
|
||||
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
|
||||
)
|
||||
else -> MockResponse().setResponseCode(404).setBody("Error mock response")
|
||||
}
|
||||
}
|
||||
start()
|
||||
}
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,18 +25,16 @@ 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() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,18 +25,16 @@ 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() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,18 +25,16 @@ 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() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,18 +25,16 @@ 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() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,18 +25,16 @@ 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() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,18 +25,16 @@ 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() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,18 +25,16 @@ 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() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
activityRule.scenario.use {
|
||||
it.onActivity {
|
||||
// Verify no crash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,29 +20,25 @@ 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 {
|
||||
|
||||
protected lateinit var db: FrostDatabase
|
||||
protected lateinit var db: FrostDatabase
|
||||
|
||||
@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()
|
||||
db = FrostDatabase(privateDb, publicDb)
|
||||
}
|
||||
@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()
|
||||
db = FrostDatabase(privateDb, publicDb)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun after() {
|
||||
db.close()
|
||||
}
|
||||
@AfterTest
|
||||
fun after() {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
@ -16,33 +16,35 @@
|
||||
*/
|
||||
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")
|
||||
private fun cookie(id: Long) = CookieEntity(id, "name$id", "cookie$id")
|
||||
|
||||
@Test
|
||||
fun save() {
|
||||
val cookie = cookie(1L)
|
||||
val type = "test"
|
||||
val content = "long test".repeat(10000)
|
||||
runBlocking {
|
||||
cookieDao.save(cookie)
|
||||
dao.save(cookie.id, type, content)
|
||||
val cache = dao.select(cookie.id, type) ?: fail("Cache not found")
|
||||
assertEquals(content, cache.contents, "Content mismatch")
|
||||
assertTrue(
|
||||
System.currentTimeMillis() - cache.lastUpdated < 500,
|
||||
"Cache retrieval took over 500ms (${System.currentTimeMillis() - cache.lastUpdated})"
|
||||
)
|
||||
}
|
||||
@Test
|
||||
fun save() {
|
||||
val cookie = cookie(1L)
|
||||
val type = "test"
|
||||
val content = "long test".repeat(10000)
|
||||
runBlocking {
|
||||
cookieDao.save(cookie)
|
||||
dao.save(cookie.id, type, content)
|
||||
val cache = dao.select(cookie.id, type) ?: fail("Cache not found")
|
||||
assertEquals(content, cache.contents, "Content mismatch")
|
||||
assertTrue(
|
||||
System.currentTimeMillis() - cache.lastUpdated < 500,
|
||||
"Cache retrieval took over 500ms (${System.currentTimeMillis() - cache.lastUpdated})"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,70 +16,71 @@
|
||||
*/
|
||||
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() {
|
||||
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||
runBlocking {
|
||||
dao.save(cookie)
|
||||
val cookies = dao.selectAll()
|
||||
assertEquals(listOf(cookie), cookies, "Cookie mismatch")
|
||||
}
|
||||
@Test
|
||||
fun basicCookie() {
|
||||
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||
runBlocking {
|
||||
dao.save(cookie)
|
||||
val cookies = dao.selectAll()
|
||||
assertEquals(listOf(cookie), cookies, "Cookie mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteCookie() {
|
||||
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||
@Test
|
||||
fun deleteCookie() {
|
||||
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||
|
||||
runBlocking {
|
||||
dao.save(cookie)
|
||||
dao.deleteById(cookie.id + 1)
|
||||
assertEquals(
|
||||
listOf(cookie),
|
||||
dao.selectAll(),
|
||||
"Cookie list should be the same after inexistent deletion"
|
||||
)
|
||||
dao.deleteById(cookie.id)
|
||||
assertEquals(emptyList(), dao.selectAll(), "Cookie list should be empty after deletion")
|
||||
}
|
||||
runBlocking {
|
||||
dao.save(cookie)
|
||||
dao.deleteById(cookie.id + 1)
|
||||
assertEquals(
|
||||
listOf(cookie),
|
||||
dao.selectAll(),
|
||||
"Cookie list should be the same after inexistent deletion"
|
||||
)
|
||||
dao.deleteById(cookie.id)
|
||||
assertEquals(emptyList(), dao.selectAll(), "Cookie list should be empty after deletion")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertReplaceCookie() {
|
||||
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||
runBlocking {
|
||||
dao.save(cookie)
|
||||
assertEquals(listOf(cookie), dao.selectAll(), "Cookie insertion failed")
|
||||
dao.save(cookie.copy(name = "testName2"))
|
||||
assertEquals(
|
||||
listOf(cookie.copy(name = "testName2")),
|
||||
dao.selectAll(),
|
||||
"Cookie replacement failed"
|
||||
)
|
||||
dao.save(cookie.copy(id = 123L))
|
||||
assertEquals(
|
||||
setOf(cookie.copy(id = 123L), cookie.copy(name = "testName2")),
|
||||
dao.selectAll().toSet(),
|
||||
"New cookie insertion failed"
|
||||
)
|
||||
}
|
||||
@Test
|
||||
fun insertReplaceCookie() {
|
||||
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||
runBlocking {
|
||||
dao.save(cookie)
|
||||
assertEquals(listOf(cookie), dao.selectAll(), "Cookie insertion failed")
|
||||
dao.save(cookie.copy(name = "testName2"))
|
||||
assertEquals(
|
||||
listOf(cookie.copy(name = "testName2")),
|
||||
dao.selectAll(),
|
||||
"Cookie replacement failed"
|
||||
)
|
||||
dao.save(cookie.copy(id = 123L))
|
||||
assertEquals(
|
||||
setOf(cookie.copy(id = 123L), cookie.copy(name = "testName2")),
|
||||
dao.selectAll().toSet(),
|
||||
"New cookie insertion failed"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectCookie() {
|
||||
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||
runBlocking {
|
||||
dao.save(cookie)
|
||||
assertEquals(cookie, dao.selectById(cookie.id), "Cookie selection failed")
|
||||
assertNull(dao.selectById(cookie.id + 1), "Inexistent cookie selection failed")
|
||||
}
|
||||
@Test
|
||||
fun selectCookie() {
|
||||
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||
runBlocking {
|
||||
dao.save(cookie)
|
||||
assertEquals(cookie, dao.selectById(cookie.id), "Cookie selection failed")
|
||||
assertNull(dao.selectById(cookie.id + 1), "Inexistent cookie selection failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,38 +21,40 @@ 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 {
|
||||
|
||||
private val TEST_DB = "cookie_migration_test"
|
||||
private val TEST_DB = "cookie_migration_test"
|
||||
|
||||
private val ALL_MIGRATIONS = arrayOf(COOKIES_MIGRATION_1_2)
|
||||
private val ALL_MIGRATIONS = arrayOf(COOKIES_MIGRATION_1_2)
|
||||
|
||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
FrostPrivateDatabase::class.java.canonicalName,
|
||||
FrameworkSQLiteOpenHelperFactory()
|
||||
val helper: MigrationTestHelper =
|
||||
MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
FrostPrivateDatabase::class.java.canonicalName,
|
||||
FrameworkSQLiteOpenHelperFactory()
|
||||
)
|
||||
|
||||
@Test
|
||||
fun migrateAll() {
|
||||
// Create earliest version of the database.
|
||||
helper.createDatabase(TEST_DB, 1).apply {
|
||||
close()
|
||||
}
|
||||
@Test
|
||||
fun migrateAll() {
|
||||
// Create earliest version of the database.
|
||||
helper.createDatabase(TEST_DB, 1).apply { close() }
|
||||
|
||||
// Open latest version of the database. Room will validate the schema
|
||||
// once all migrations execute.
|
||||
Room.databaseBuilder(
|
||||
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
FrostPrivateDatabase::class.java,
|
||||
TEST_DB
|
||||
).addMigrations(*ALL_MIGRATIONS).build().apply {
|
||||
openHelper.writableDatabase
|
||||
close()
|
||||
}
|
||||
}
|
||||
// Open latest version of the database. Room will validate the schema
|
||||
// once all migrations execute.
|
||||
Room.databaseBuilder(
|
||||
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
FrostPrivateDatabase::class.java,
|
||||
TEST_DB
|
||||
)
|
||||
.addMigrations(*ALL_MIGRATIONS)
|
||||
.build()
|
||||
.apply {
|
||||
openHelper.writableDatabase
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,56 +18,54 @@ 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
|
||||
*/
|
||||
@Test
|
||||
fun save() {
|
||||
val tabs = listOf(
|
||||
FbItem.ACTIVITY_LOG,
|
||||
FbItem.BIRTHDAYS,
|
||||
FbItem.EVENTS,
|
||||
FbItem.MARKETPLACE,
|
||||
FbItem.ACTIVITY_LOG
|
||||
/** Note that order is also preserved here */
|
||||
@Test
|
||||
fun save() {
|
||||
val tabs =
|
||||
listOf(
|
||||
FbItem.ACTIVITY_LOG,
|
||||
FbItem.BIRTHDAYS,
|
||||
FbItem.EVENTS,
|
||||
FbItem.MARKETPLACE,
|
||||
FbItem.ACTIVITY_LOG
|
||||
)
|
||||
runBlocking {
|
||||
dao.saveTabs(tabs)
|
||||
assertEquals(tabs, dao.getTabs(), "Tab saving failed")
|
||||
val newTabs = listOf(FbItem.PAGES, FbItem.MENU)
|
||||
dao.saveTabs(newTabs)
|
||||
assertEquals(newTabs, dao.getTabs(), "Tab overwrite failed")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultRetrieve() {
|
||||
runBlocking { assertEquals(defaultTabs(), dao.getTabs(), "Default retrieval failed") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ignoreErrors() {
|
||||
runBlocking {
|
||||
dao._save(
|
||||
GenericEntity(
|
||||
GenericDao.TYPE_TABS,
|
||||
"${FbItem.ACTIVITY_LOG.name},unknown,${FbItem.EVENTS.name}"
|
||||
)
|
||||
runBlocking {
|
||||
dao.saveTabs(tabs)
|
||||
assertEquals(tabs, dao.getTabs(), "Tab saving failed")
|
||||
val newTabs = listOf(FbItem.PAGES, FbItem.MENU)
|
||||
dao.saveTabs(newTabs)
|
||||
assertEquals(newTabs, dao.getTabs(), "Tab overwrite failed")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultRetrieve() {
|
||||
runBlocking {
|
||||
assertEquals(defaultTabs(), dao.getTabs(), "Default retrieval failed")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ignoreErrors() {
|
||||
runBlocking {
|
||||
dao._save(
|
||||
GenericEntity(
|
||||
GenericDao.TYPE_TABS,
|
||||
"${FbItem.ACTIVITY_LOG.name},unknown,${FbItem.EVENTS.name}"
|
||||
)
|
||||
)
|
||||
assertEquals(
|
||||
listOf(FbItem.ACTIVITY_LOG, FbItem.EVENTS),
|
||||
dao.getTabs(),
|
||||
"Tab fetching does not ignore unknown names"
|
||||
)
|
||||
}
|
||||
)
|
||||
assertEquals(
|
||||
listOf(FbItem.ACTIVITY_LOG, FbItem.EVENTS),
|
||||
dao.getTabs(),
|
||||
"Tab fetching does not ignore unknown names"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,139 +19,134 @@ 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 cookie(id: Long) = CookieEntity(id, "name$id", "cookie$id")
|
||||
|
||||
private fun notifContent(id: Long, cookie: CookieEntity, time: Long = id) = NotificationContent(
|
||||
data = cookie,
|
||||
id = id,
|
||||
href = "",
|
||||
title = null,
|
||||
text = "",
|
||||
timestamp = time,
|
||||
profileUrl = null,
|
||||
unread = true
|
||||
private fun notifContent(id: Long, cookie: CookieEntity, time: Long = id) =
|
||||
NotificationContent(
|
||||
data = cookie,
|
||||
id = id,
|
||||
href = "",
|
||||
title = null,
|
||||
text = "",
|
||||
timestamp = time,
|
||||
profileUrl = null,
|
||||
unread = true
|
||||
)
|
||||
|
||||
@Test
|
||||
fun saveAndRetrieve() {
|
||||
val cookie = cookie(12345L)
|
||||
// Unique unsorted ids
|
||||
val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
|
||||
runBlocking {
|
||||
db.cookieDao().save(cookie)
|
||||
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
|
||||
val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL)
|
||||
assertEquals(
|
||||
notifs.sortedByDescending { it.timestamp },
|
||||
dbNotifs,
|
||||
"Incorrect notification list received"
|
||||
)
|
||||
}
|
||||
@Test
|
||||
fun saveAndRetrieve() {
|
||||
val cookie = cookie(12345L)
|
||||
// Unique unsorted ids
|
||||
val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
|
||||
runBlocking {
|
||||
db.cookieDao().save(cookie)
|
||||
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
|
||||
val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL)
|
||||
assertEquals(
|
||||
notifs.sortedByDescending { it.timestamp },
|
||||
dbNotifs,
|
||||
"Incorrect notification list received"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectConditions() {
|
||||
runBlocking {
|
||||
val cookie1 = cookie(12345L)
|
||||
val cookie2 = cookie(12L)
|
||||
val notifs1 = (0L..2L).map { notifContent(it, cookie1) }
|
||||
val notifs2 = (5L..10L).map { notifContent(it, cookie2) }
|
||||
db.cookieDao().save(cookie1)
|
||||
db.cookieDao().save(cookie2)
|
||||
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1)
|
||||
dao.saveNotifications(NOTIF_CHANNEL_MESSAGES, notifs2)
|
||||
assertEquals(
|
||||
emptyList(),
|
||||
dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_MESSAGES),
|
||||
"Filtering by type did not work for cookie1"
|
||||
)
|
||||
assertEquals(
|
||||
notifs1.sortedByDescending { it.timestamp },
|
||||
dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_GENERAL),
|
||||
"Selection for cookie1 failed"
|
||||
)
|
||||
assertEquals(
|
||||
emptyList(),
|
||||
dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_GENERAL),
|
||||
"Filtering by type did not work for cookie2"
|
||||
)
|
||||
assertEquals(
|
||||
notifs2.sortedByDescending { it.timestamp },
|
||||
dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_MESSAGES),
|
||||
"Selection for cookie2 failed"
|
||||
)
|
||||
}
|
||||
@Test
|
||||
fun selectConditions() {
|
||||
runBlocking {
|
||||
val cookie1 = cookie(12345L)
|
||||
val cookie2 = cookie(12L)
|
||||
val notifs1 = (0L..2L).map { notifContent(it, cookie1) }
|
||||
val notifs2 = (5L..10L).map { notifContent(it, cookie2) }
|
||||
db.cookieDao().save(cookie1)
|
||||
db.cookieDao().save(cookie2)
|
||||
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1)
|
||||
dao.saveNotifications(NOTIF_CHANNEL_MESSAGES, notifs2)
|
||||
assertEquals(
|
||||
emptyList(),
|
||||
dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_MESSAGES),
|
||||
"Filtering by type did not work for cookie1"
|
||||
)
|
||||
assertEquals(
|
||||
notifs1.sortedByDescending { it.timestamp },
|
||||
dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_GENERAL),
|
||||
"Selection for cookie1 failed"
|
||||
)
|
||||
assertEquals(
|
||||
emptyList(),
|
||||
dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_GENERAL),
|
||||
"Filtering by type did not work for cookie2"
|
||||
)
|
||||
assertEquals(
|
||||
notifs2.sortedByDescending { it.timestamp },
|
||||
dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_MESSAGES),
|
||||
"Selection for cookie2 failed"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary key is both id and userId, in the event that the same notification to multiple users has the same id
|
||||
*/
|
||||
@Test
|
||||
fun primaryKeyCheck() {
|
||||
runBlocking {
|
||||
val cookie1 = cookie(12345L)
|
||||
val cookie2 = cookie(12L)
|
||||
val notifs1 = (0L..2L).map { notifContent(it, cookie1) }
|
||||
val notifs2 = notifs1.map { it.copy(data = cookie2) }
|
||||
db.cookieDao().save(cookie1)
|
||||
db.cookieDao().save(cookie2)
|
||||
assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1), "Notif1 save failed")
|
||||
assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs2), "Notif2 save failed")
|
||||
}
|
||||
/**
|
||||
* Primary key is both id and userId, in the event that the same notification to multiple users
|
||||
* has the same id
|
||||
*/
|
||||
@Test
|
||||
fun primaryKeyCheck() {
|
||||
runBlocking {
|
||||
val cookie1 = cookie(12345L)
|
||||
val cookie2 = cookie(12L)
|
||||
val notifs1 = (0L..2L).map { notifContent(it, cookie1) }
|
||||
val notifs2 = notifs1.map { it.copy(data = cookie2) }
|
||||
db.cookieDao().save(cookie1)
|
||||
db.cookieDao().save(cookie2)
|
||||
assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1), "Notif1 save failed")
|
||||
assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs2), "Notif2 save failed")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cascadeDeletion() {
|
||||
val cookie = cookie(12345L)
|
||||
// Unique unsorted ids
|
||||
val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
|
||||
runBlocking {
|
||||
db.cookieDao().save(cookie)
|
||||
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
|
||||
db.cookieDao().deleteById(cookie.id)
|
||||
val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL)
|
||||
assertTrue(dbNotifs.isEmpty(), "Cascade deletion failed")
|
||||
}
|
||||
@Test
|
||||
fun cascadeDeletion() {
|
||||
val cookie = cookie(12345L)
|
||||
// Unique unsorted ids
|
||||
val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
|
||||
runBlocking {
|
||||
db.cookieDao().save(cookie)
|
||||
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
|
||||
db.cookieDao().deleteById(cookie.id)
|
||||
val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL)
|
||||
assertTrue(dbNotifs.isEmpty(), "Cascade deletion failed")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun latestEpoch() {
|
||||
val cookie = cookie(12345L)
|
||||
// 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"
|
||||
)
|
||||
db.cookieDao().save(cookie)
|
||||
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
|
||||
assertEquals(
|
||||
99L,
|
||||
dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL),
|
||||
"Latest epoch failed"
|
||||
)
|
||||
}
|
||||
@Test
|
||||
fun latestEpoch() {
|
||||
val cookie = cookie(12345L)
|
||||
// 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")
|
||||
db.cookieDao().save(cookie)
|
||||
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
|
||||
assertEquals(99L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Latest epoch failed")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertionWithInvalidCookies() {
|
||||
runBlocking {
|
||||
assertFalse(
|
||||
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, listOf(notifContent(1L, cookie(2L)))),
|
||||
"Notif save should not have passed without relevant cookie entries"
|
||||
)
|
||||
}
|
||||
@Test
|
||||
fun insertionWithInvalidCookies() {
|
||||
runBlocking {
|
||||
assertFalse(
|
||||
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, listOf(notifContent(1L, cookie(2L)))),
|
||||
"Notif save should not have passed without relevant cookie entries"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,16 +17,16 @@
|
||||
package com.pitchedapps.frost.facebook
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class FbCookieTest {
|
||||
|
||||
@Test
|
||||
fun managerAcceptsCookie() {
|
||||
assertTrue(
|
||||
CookieManager.getInstance().acceptCookie(),
|
||||
"Cookie manager should accept cookie by default"
|
||||
)
|
||||
}
|
||||
@Test
|
||||
fun managerAcceptsCookie() {
|
||||
assertTrue(
|
||||
CookieManager.getInstance().acceptCookie(),
|
||||
"Cookie manager should accept cookie by default"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -26,23 +26,21 @@ import androidx.test.platform.app.InstrumentationRegistry
|
||||
import java.io.InputStream
|
||||
|
||||
val context: Context
|
||||
get() = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
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
|
||||
|
||||
fun getResource(resource: String): InputStream =
|
||||
Helper::class.java.classLoader!!.getResource(resource).openStream()
|
||||
Helper::class.java.classLoader!!.getResource(resource).openStream()
|
||||
|
||||
inline fun <reified A : Activity> activityRule(
|
||||
intentAction: Intent.() -> Unit = {},
|
||||
activityOptions: Bundle? = null
|
||||
intentAction: Intent.() -> Unit = {},
|
||||
activityOptions: Bundle? = null
|
||||
): ActivityScenarioRule<A> {
|
||||
val intent =
|
||||
Intent(ApplicationProvider.getApplicationContext(), A::class.java).also(intentAction)
|
||||
return ActivityScenarioRule(intent, activityOptions)
|
||||
val intent = Intent(ApplicationProvider.getApplicationContext(), A::class.java).also(intentAction)
|
||||
return ActivityScenarioRule(intent, activityOptions)
|
||||
}
|
||||
|
||||
const val TEST_FORMATTED_URL = "https://www.google.com"
|
||||
|
@ -37,78 +37,75 @@ 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()
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
if (!buildIsLollipopAndUp) return // not supported
|
||||
if (!buildIsLollipopAndUp) return // not supported
|
||||
|
||||
initPrefs()
|
||||
initPrefs()
|
||||
|
||||
L.i { "Begin Frost for Facebook" }
|
||||
FrostPglAdBlock.init(this)
|
||||
L.i { "Begin Frost for Facebook" }
|
||||
FrostPglAdBlock.init(this)
|
||||
|
||||
setupNotificationChannels(this, themeProvider)
|
||||
setupNotificationChannels(this, themeProvider)
|
||||
|
||||
scheduleNotificationsFromPrefs(prefs)
|
||||
scheduleNotificationsFromPrefs(prefs)
|
||||
|
||||
BigImageViewer.initialize(GlideImageLoader.with(this, httpClient))
|
||||
BigImageViewer.initialize(GlideImageLoader.with(this, httpClient))
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
|
||||
override fun onActivityPaused(activity: Activity) {}
|
||||
override fun onActivityResumed(activity: Activity) {}
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
if (BuildConfig.DEBUG) {
|
||||
registerActivityLifecycleCallbacks(
|
||||
object : ActivityLifecycleCallbacks {
|
||||
override fun onActivityPaused(activity: Activity) {}
|
||||
override fun onActivityResumed(activity: Activity) {}
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
L.d { "Activity ${activity.localClassName} destroyed" }
|
||||
}
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
L.d { "Activity ${activity.localClassName} destroyed" }
|
||||
}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
L.d { "Activity ${activity.localClassName} created" }
|
||||
}
|
||||
})
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
L.d { "Activity ${activity.localClassName} created" }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initPrefs() {
|
||||
prefs.deleteKeys("search_bar", "shown_release", "experimental_by_default")
|
||||
KL.shouldLog = { BuildConfig.DEBUG }
|
||||
L.shouldLog = {
|
||||
when (it) {
|
||||
Log.VERBOSE -> BuildConfig.DEBUG
|
||||
Log.INFO, Log.ERROR -> true
|
||||
else -> BuildConfig.DEBUG || prefs.verboseLogging
|
||||
}
|
||||
}
|
||||
prefs.verboseLogging = false
|
||||
if (prefs.installDate == -1L) {
|
||||
prefs.installDate = System.currentTimeMillis()
|
||||
}
|
||||
if (prefs.identifier == -1) {
|
||||
prefs.identifier = Random().nextInt(Int.MAX_VALUE)
|
||||
}
|
||||
prefs.lastLaunch = System.currentTimeMillis()
|
||||
private fun initPrefs() {
|
||||
prefs.deleteKeys("search_bar", "shown_release", "experimental_by_default")
|
||||
KL.shouldLog = { BuildConfig.DEBUG }
|
||||
L.shouldLog = {
|
||||
when (it) {
|
||||
Log.VERBOSE -> BuildConfig.DEBUG
|
||||
Log.INFO,
|
||||
Log.ERROR -> true
|
||||
else -> BuildConfig.DEBUG || prefs.verboseLogging
|
||||
}
|
||||
}
|
||||
prefs.verboseLogging = false
|
||||
if (prefs.installDate == -1L) {
|
||||
prefs.installDate = System.currentTimeMillis()
|
||||
}
|
||||
if (prefs.identifier == -1) {
|
||||
prefs.identifier = Random().nextInt(Int.MAX_VALUE)
|
||||
}
|
||||
prefs.lastLaunch = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
@ -45,97 +45,90 @@ 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)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (!buildIsLollipopAndUp) { // not supported
|
||||
showInvalidSdkView()
|
||||
return
|
||||
}
|
||||
if (!buildIsLollipopAndUp) { // not supported
|
||||
showInvalidSdkView()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO add better descriptions
|
||||
CookieManager.getInstance()
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "No cookiemanager instance" }
|
||||
showInvalidWebView()
|
||||
}
|
||||
try {
|
||||
// TODO add better descriptions
|
||||
CookieManager.getInstance()
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "No cookiemanager instance" }
|
||||
showInvalidWebView()
|
||||
}
|
||||
|
||||
launch {
|
||||
try {
|
||||
val authDefer = BiometricUtils.authenticate(this@StartActivity, prefs)
|
||||
fbCookie.switchBackUser()
|
||||
val cookies = ArrayList(cookieDao.selectAll())
|
||||
L.i { "Cookies loaded at time ${System.currentTimeMillis()}" }
|
||||
L._d {
|
||||
"Cookies: ${
|
||||
launch {
|
||||
try {
|
||||
val authDefer = BiometricUtils.authenticate(this@StartActivity, prefs)
|
||||
fbCookie.switchBackUser()
|
||||
val cookies = ArrayList(cookieDao.selectAll())
|
||||
L.i { "Cookies loaded at time ${System.currentTimeMillis()}" }
|
||||
L._d {
|
||||
"Cookies: ${
|
||||
cookies.joinToString(
|
||||
"\t",
|
||||
transform = CookieEntity::toSensitiveString
|
||||
)
|
||||
}"
|
||||
}
|
||||
loadAssets(themeProvider)
|
||||
authDefer.await()
|
||||
when {
|
||||
cookies.isEmpty() -> launchNewTask<LoginActivity>()
|
||||
// Has cookies but no selected account
|
||||
prefs.userId == -1L -> launchNewTask<SelectorActivity>(cookies)
|
||||
else -> startActivity<MainActivity>(
|
||||
intentBuilder = {
|
||||
putParcelableArrayListExtra(EXTRA_COOKIES, cookies)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
L._e(e) { "Load start failed" }
|
||||
showInvalidWebView()
|
||||
}
|
||||
}
|
||||
loadAssets(themeProvider)
|
||||
authDefer.await()
|
||||
when {
|
||||
cookies.isEmpty() -> launchNewTask<LoginActivity>()
|
||||
// Has cookies but no selected account
|
||||
prefs.userId == -1L -> launchNewTask<SelectorActivity>(cookies)
|
||||
else ->
|
||||
startActivity<MainActivity>(
|
||||
intentBuilder = {
|
||||
putParcelableArrayListExtra(EXTRA_COOKIES, cookies)
|
||||
flags =
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
L._e(e) { "Load start failed" }
|
||||
showInvalidWebView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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<TextView>(R.id.invalid_text).text = text
|
||||
}
|
||||
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<TextView>(R.id.invalid_text).text = text
|
||||
}
|
||||
}
|
||||
|
@ -52,154 +52,140 @@ 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
|
||||
accentColor = themeProvider.accentColor
|
||||
backgroundColor = themeProvider.bgColor.withMinAlpha(200)
|
||||
cutoutForeground = themeProvider.accentColor
|
||||
cutoutDrawableRes = R.drawable.frost_f_200
|
||||
faqPageTitleRes = R.string.faq_title
|
||||
faqXmlRes = R.xml.frost_faq
|
||||
faqParseNewLine = false
|
||||
override fun Configs.buildConfigs() {
|
||||
textColor = themeProvider.textColor
|
||||
accentColor = themeProvider.accentColor
|
||||
backgroundColor = themeProvider.bgColor.withMinAlpha(200)
|
||||
cutoutForeground = themeProvider.accentColor
|
||||
cutoutDrawableRes = R.drawable.frost_f_200
|
||||
faqPageTitleRes = R.string.faq_title
|
||||
faqXmlRes = R.xml.frost_faq
|
||||
faqParseNewLine = false
|
||||
}
|
||||
|
||||
var lastClick = -1L
|
||||
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(
|
||||
uniqueId = "com.pitchedapps.frost",
|
||||
name = string(R.string.frost_name),
|
||||
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(
|
||||
License(
|
||||
spdxId = "gplv3",
|
||||
name = "GNU GPL v3",
|
||||
url = "https://www.gnu.org/licenses/gpl-3.0.en.html",
|
||||
hash = "gplv3"
|
||||
)
|
||||
),
|
||||
scm = null,
|
||||
organization = null,
|
||||
)
|
||||
adapter.add(LibraryIItem(frost)).add(AboutLinks())
|
||||
adapter.onClickListener = { _, _, item, _ ->
|
||||
if (item is LibraryIItem) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastClick > 500) clickCount = 1 else clickCount++
|
||||
lastClick = now
|
||||
if (clickCount == 8) {
|
||||
if (!prefs.debugSettings) {
|
||||
prefs.debugSettings = true
|
||||
L.d { "Debugging section enabled" }
|
||||
toast(R.string.debug_toast_enabled)
|
||||
} else {
|
||||
toast(R.string.debug_toast_already_enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
class AboutLinks :
|
||||
AbstractItem<AboutLinks.ViewHolder>(), ThemableIItem by ThemableIItemDelegate() {
|
||||
override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
|
||||
|
||||
override val layoutRes: Int
|
||||
get() = R.layout.item_about_links
|
||||
|
||||
override val type: Int
|
||||
get() = R.id.item_about_links
|
||||
|
||||
override fun bindView(holder: ViewHolder, payloads: List<Any>) {
|
||||
super.bindView(holder, payloads)
|
||||
with(holder) {
|
||||
bindIconColor(*images.toTypedArray())
|
||||
bindBackgroundColor(container)
|
||||
}
|
||||
}
|
||||
|
||||
var lastClick = -1L
|
||||
var clickCount = 0
|
||||
class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
|
||||
|
||||
override fun postInflateMainPage(adapter: FastItemThemedAdapter<GenericItem>) {
|
||||
/**
|
||||
* 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)
|
||||
),
|
||||
website = string(R.string.github_url),
|
||||
description = string(R.string.frost_description),
|
||||
artifactVersion = BuildConfig.VERSION_NAME,
|
||||
licenses = setOf(
|
||||
License(
|
||||
spdxId = "gplv3",
|
||||
name = "GNU GPL v3",
|
||||
url = "https://www.gnu.org/licenses/gpl-3.0.en.html",
|
||||
hash = "gplv3"
|
||||
)
|
||||
),
|
||||
scm = null,
|
||||
organization = null,
|
||||
val container: ConstraintLayout by bindView(R.id.about_icons_container)
|
||||
val images: List<ImageView>
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
init {
|
||||
val c = itemView.context
|
||||
val size = c.dimenPixelSize(R.dimen.kau_avatar_bounds)
|
||||
|
||||
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(
|
||||
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) ->
|
||||
ImageView(c).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(size, size)
|
||||
id = 109389 + i
|
||||
setImageDrawable(icon)
|
||||
scaleType = ImageView.ScaleType.CENTER
|
||||
background =
|
||||
context.resolveDrawable(android.R.attr.selectableItemBackgroundBorderless)
|
||||
setOnClickListener { onClick() }
|
||||
container.addView(this)
|
||||
}
|
||||
}
|
||||
val set = ConstraintSet()
|
||||
set.clone(container)
|
||||
set.createHorizontalChain(
|
||||
ConstraintSet.PARENT_ID,
|
||||
ConstraintSet.LEFT,
|
||||
ConstraintSet.PARENT_ID,
|
||||
ConstraintSet.RIGHT,
|
||||
images.map { it.id }.toIntArray(),
|
||||
null,
|
||||
ConstraintSet.CHAIN_SPREAD_INSIDE
|
||||
)
|
||||
adapter.add(LibraryIItem(frost)).add(AboutLinks())
|
||||
adapter.onClickListener = { _, _, item, _ ->
|
||||
if (item is LibraryIItem) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastClick > 500)
|
||||
clickCount = 1
|
||||
else
|
||||
clickCount++
|
||||
lastClick = now
|
||||
if (clickCount == 8) {
|
||||
if (!prefs.debugSettings) {
|
||||
prefs.debugSettings = true
|
||||
L.d { "Debugging section enabled" }
|
||||
toast(R.string.debug_toast_enabled)
|
||||
} else {
|
||||
toast(R.string.debug_toast_already_enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
class AboutLinks :
|
||||
AbstractItem<AboutLinks.ViewHolder>(),
|
||||
ThemableIItem by ThemableIItemDelegate() {
|
||||
override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
|
||||
|
||||
override val layoutRes: Int
|
||||
get() = R.layout.item_about_links
|
||||
|
||||
override val type: Int
|
||||
get() = R.id.item_about_links
|
||||
|
||||
override fun bindView(holder: ViewHolder, payloads: List<Any>) {
|
||||
super.bindView(holder, payloads)
|
||||
with(holder) {
|
||||
bindIconColor(*images.toTypedArray())
|
||||
bindBackgroundColor(container)
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
|
||||
|
||||
val container: ConstraintLayout by bindView(R.id.about_icons_container)
|
||||
val images: List<ImageView>
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
init {
|
||||
val c = itemView.context
|
||||
val size = c.dimenPixelSize(R.dimen.kau_avatar_bounds)
|
||||
|
||||
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(
|
||||
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) ->
|
||||
ImageView(c).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(size, size)
|
||||
id = 109389 + i
|
||||
setImageDrawable(icon)
|
||||
scaleType = ImageView.ScaleType.CENTER
|
||||
background =
|
||||
context.resolveDrawable(android.R.attr.selectableItemBackgroundBorderless)
|
||||
setOnClickListener { onClick() }
|
||||
container.addView(this)
|
||||
}
|
||||
}
|
||||
val set = ConstraintSet()
|
||||
set.clone(container)
|
||||
set.createHorizontalChain(
|
||||
ConstraintSet.PARENT_ID,
|
||||
ConstraintSet.LEFT,
|
||||
ConstraintSet.PARENT_ID,
|
||||
ConstraintSet.RIGHT,
|
||||
images.map { it.id }.toIntArray(),
|
||||
null,
|
||||
ConstraintSet.CHAIN_SPREAD_INSIDE
|
||||
)
|
||||
set.applyTo(container)
|
||||
}
|
||||
}
|
||||
set.applyTo(container)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,48 +28,40 @@ 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
|
||||
*/
|
||||
protected open fun backConsumer(): Boolean = false
|
||||
/** Inherited consumer to customize back press */
|
||||
protected open fun backConsumer(): Boolean = false
|
||||
|
||||
final override fun onBackPressed() {
|
||||
if (this is SearchViewHolder && searchViewOnBackPress()) return
|
||||
if (this is VideoViewHolder && videoOnBackPress()) return
|
||||
if (backConsumer()) return
|
||||
super.onBackPressed()
|
||||
}
|
||||
final override fun onBackPressed() {
|
||||
if (this is SearchViewHolder && searchViewOnBackPress()) return
|
||||
if (this is VideoViewHolder && videoOnBackPress()) return
|
||||
if (backConsumer()) return
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (this !is WebOverlayActivityBase) activityThemer.setFrostTheme()
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (this !is WebOverlayActivityBase) activityThemer.setFrostTheme()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
if (this is VideoViewHolder) videoOnStop()
|
||||
super.onStop()
|
||||
}
|
||||
override fun onStop() {
|
||||
if (this is VideoViewHolder) videoOnStop()
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
if (this is VideoViewHolder) videoViewer?.updateLocation()
|
||||
}
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
if (this is VideoViewHolder) videoViewer?.updateLocation()
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -35,110 +35,97 @@ 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() {
|
||||
|
||||
companion object {
|
||||
const val RESULT_URL = "extra_result_url"
|
||||
const val RESULT_SCREENSHOT = "extra_result_screenshot"
|
||||
const val RESULT_BODY = "extra_result_body"
|
||||
fun baseDir(context: Context) = File(context.externalCacheDir, "offline_debug")
|
||||
companion object {
|
||||
const val RESULT_URL = "extra_result_url"
|
||||
const val RESULT_SCREENSHOT = "extra_result_screenshot"
|
||||
const val RESULT_BODY = "extra_result_body"
|
||||
fun baseDir(context: Context) = File(context.externalCacheDir, "offline_debug")
|
||||
}
|
||||
|
||||
@Inject lateinit var activityThemer: ActivityThemer
|
||||
|
||||
@Inject lateinit var themeProvider: ThemeProvider
|
||||
|
||||
lateinit var binding: ActivityDebugBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityDebugBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.init()
|
||||
}
|
||||
|
||||
fun ActivityDebugBinding.init() {
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.apply {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
setTitle(R.string.debug_frost)
|
||||
|
||||
@Inject
|
||||
lateinit var activityThemer: ActivityThemer
|
||||
activityThemer.setFrostColors { toolbar(toolbar) }
|
||||
debugWebview.loadUrl(FbItem.FEED.url)
|
||||
debugWebview.onPageFinished = { swipeRefresh.isRefreshing = false }
|
||||
|
||||
@Inject
|
||||
lateinit var themeProvider: ThemeProvider
|
||||
swipeRefresh.setOnRefreshListener(debugWebview::reload)
|
||||
|
||||
lateinit var binding: ActivityDebugBinding
|
||||
fab.visible().setIcon(GoogleMaterial.Icon.gmd_bug_report, themeProvider.iconColor)
|
||||
fab.backgroundTintList = ColorStateList.valueOf(themeProvider.accentColor)
|
||||
fab.setOnClickListener { _ ->
|
||||
fab.hide()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityDebugBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.init()
|
||||
}
|
||||
|
||||
fun ActivityDebugBinding.init() {
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.apply {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
setTitle(R.string.debug_frost)
|
||||
|
||||
activityThemer.setFrostColors {
|
||||
toolbar(toolbar)
|
||||
}
|
||||
debugWebview.loadUrl(FbItem.FEED.url)
|
||||
debugWebview.onPageFinished = { swipeRefresh.isRefreshing = false }
|
||||
|
||||
swipeRefresh.setOnRefreshListener(debugWebview::reload)
|
||||
|
||||
fab.visible().setIcon(GoogleMaterial.Icon.gmd_bug_report, themeProvider.iconColor)
|
||||
fab.backgroundTintList = ColorStateList.valueOf(themeProvider.accentColor)
|
||||
fab.setOnClickListener { _ ->
|
||||
fab.hide()
|
||||
|
||||
val errorHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
L.e { "DebugActivity error ${throwable.message}" }
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
|
||||
launchMain(errorHandler) {
|
||||
val parent = baseDir(this@DebugActivity)
|
||||
parent.createFreshDir()
|
||||
|
||||
val body: String? = suspendCoroutine { cont ->
|
||||
debugWebview.evaluateJavascript(JsActions.RETURN_BODY.function) {
|
||||
cont.resume(it)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
setResult(Activity.RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
val errorHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
L.e { "DebugActivity error ${throwable.message}" }
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.debugWebview.resumeTimers()
|
||||
}
|
||||
launchMain(errorHandler) {
|
||||
val parent = baseDir(this@DebugActivity)
|
||||
parent.createFreshDir()
|
||||
|
||||
override fun onPause() {
|
||||
binding.debugWebview.pauseTimers()
|
||||
super.onPause()
|
||||
}
|
||||
val body: String? = suspendCoroutine { cont ->
|
||||
debugWebview.evaluateJavascript(JsActions.RETURN_BODY.function) { cont.resume(it) }
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (binding.debugWebview.canGoBack())
|
||||
binding.debugWebview.goBack()
|
||||
else
|
||||
super.onBackPressed()
|
||||
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)
|
||||
setResult(Activity.RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.debugWebview.resumeTimers()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
binding.debugWebview.pauseTimers()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (binding.debugWebview.canGoBack()) binding.debugWebview.goBack() else super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
@ -62,323 +62,309 @@ 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
|
||||
private lateinit var dragHelper: ViewDragHelper
|
||||
|
||||
private val cookie: String? by lazy { intent.getStringExtra(ARG_COOKIE) }
|
||||
private val cookie: String? by lazy { intent.getStringExtra(ARG_COOKIE) }
|
||||
|
||||
val imageUrl: String by lazy { intent.getStringExtra(ARG_IMAGE_URL)?.trim('"') ?: "" }
|
||||
val imageUrl: String by lazy { intent.getStringExtra(ARG_IMAGE_URL)?.trim('"') ?: "" }
|
||||
|
||||
private lateinit var trueImageUrl: Deferred<String>
|
||||
private lateinit var trueImageUrl: Deferred<String>
|
||||
|
||||
private val imageText: String? by lazy { intent.getStringExtra(ARG_TEXT) }
|
||||
private val imageText: String? by lazy { intent.getStringExtra(ARG_TEXT) }
|
||||
|
||||
lateinit var binding: ActivityImageBinding
|
||||
private var bottomBehavior: BottomSheetBehavior<View>? = null
|
||||
lateinit var binding: ActivityImageBinding
|
||||
private var bottomBehavior: BottomSheetBehavior<View>? = null
|
||||
|
||||
private val baseBackgroundColor: Int
|
||||
get() = if (prefs.blackMediaBg) Color.BLACK
|
||||
else themeProvider.bgColor.withMinAlpha(235)
|
||||
private val baseBackgroundColor: Int
|
||||
get() = if (prefs.blackMediaBg) Color.BLACK else themeProvider.bgColor.withMinAlpha(235)
|
||||
|
||||
private fun loadError(e: Throwable) {
|
||||
if (e.message?.contains("<!DOCTYPE html>") == true) {
|
||||
applicationContext.toast(R.string.image_not_found)
|
||||
private fun loadError(e: Throwable) {
|
||||
if (e.message?.contains("<!DOCTYPE html>") == true) {
|
||||
applicationContext.toast(R.string.image_not_found)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
errorRef = e
|
||||
e.logFrostEvent("Image load error")
|
||||
with(binding) { if (imageProgress.isVisible) imageProgress.fadeOut() }
|
||||
tempFile?.delete()
|
||||
binding.error.fadeIn()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (imageUrl.isEmpty()) {
|
||||
return finish()
|
||||
}
|
||||
L.i { "Displaying image" }
|
||||
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" }
|
||||
result
|
||||
}
|
||||
binding = ActivityImageBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.init()
|
||||
launch(CoroutineExceptionHandler { _, throwable -> loadError(throwable) }) {
|
||||
binding.showImage(trueImageUrl.await())
|
||||
}
|
||||
}
|
||||
|
||||
private fun ActivityImageBinding.showImage(url: String) {
|
||||
imagePhoto.showImage(Uri.parse(url))
|
||||
imagePhoto.setImageShownCallback(
|
||||
object : ImageShownCallback {
|
||||
override fun onThumbnailShown() {}
|
||||
|
||||
override fun onMainImageShown() {
|
||||
imageProgress.fadeOut()
|
||||
imagePhoto.animate().alpha(1f).scaleXY(1f).start()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun ActivityImageBinding.init() {
|
||||
imageContainer.setBackgroundColor(baseBackgroundColor)
|
||||
toolbar.setBackgroundColor(baseBackgroundColor)
|
||||
this@ImageActivity.imageText.also { text ->
|
||||
if (text.isNullOrBlank()) {
|
||||
imageText.gone()
|
||||
} else {
|
||||
imageText.setTextColor(if (prefs.blackMediaBg) Color.WHITE else themeProvider.textColor)
|
||||
imageText.setBackgroundColor(baseBackgroundColor.colorToForeground(0.2f).withAlpha(255))
|
||||
imageText.text = text
|
||||
bottomBehavior =
|
||||
BottomSheetBehavior.from<View>(imageText).apply {
|
||||
addBottomSheetCallback(
|
||||
object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
imageText.alpha = slideOffset / 2 + 0.5f
|
||||
}
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
// No op
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
imageText.bringToFront()
|
||||
}
|
||||
}
|
||||
val foregroundTint = if (prefs.blackMediaBg) Color.WHITE else themeProvider.accentColor
|
||||
|
||||
fun ImageView.setState(state: FabStates) {
|
||||
setIcon(state.iicon, color = foregroundTint, sizeDp = 24)
|
||||
setOnClickListener { state.onClick(this@ImageActivity) }
|
||||
}
|
||||
|
||||
imageProgress.tint(foregroundTint)
|
||||
error.apply {
|
||||
invisible()
|
||||
setState(FabStates.ERROR)
|
||||
}
|
||||
download.apply { setState(FabStates.DOWNLOAD) }
|
||||
share.apply { setState(FabStates.SHARE) }
|
||||
|
||||
imagePhoto.setImageLoaderCallback(
|
||||
object : ImageLoader.Callback {
|
||||
override fun onCacheHit(imageType: Int, image: File?) {}
|
||||
|
||||
override fun onCacheMiss(imageType: Int, image: File?) {}
|
||||
|
||||
override fun onStart() {}
|
||||
|
||||
override fun onProgress(progress: Int) {}
|
||||
|
||||
override fun onFinish() {}
|
||||
|
||||
override fun onSuccess(image: File) {}
|
||||
|
||||
override fun onFail(error: Exception) {
|
||||
loadError(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
activityThemer.setFrostColors { themeWindow = false }
|
||||
dragHelper =
|
||||
ViewDragHelper.create(imageDrag, ViewDragCallback()).apply {
|
||||
setEdgeTrackingEnabled(ViewDragHelper.EDGE_TOP or ViewDragHelper.EDGE_BOTTOM)
|
||||
}
|
||||
imageDrag.dragHelper = dragHelper
|
||||
imageDrag.viewToIgnore = imageText
|
||||
}
|
||||
|
||||
private inner class ViewDragCallback : ViewDragHelper.Callback() {
|
||||
private var scrollPercent: Float = 0f
|
||||
private var scrollThreshold = 0.5f
|
||||
private var scrollToTop = false
|
||||
|
||||
override fun tryCaptureView(view: View, i: Int): Boolean {
|
||||
L.d { "Try capture ${view.id} $i ${binding.imagePhoto.id} ${binding.imageText.id}" }
|
||||
return view === binding.imagePhoto
|
||||
}
|
||||
|
||||
override fun getViewHorizontalDragRange(child: View): Int = 0
|
||||
|
||||
override fun getViewVerticalDragRange(child: View): Int = child.height
|
||||
|
||||
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
|
||||
scrollPercent = abs(top.toFloat() / imageContainer.height)
|
||||
scrollToTop = top < 0
|
||||
val multiplier = max(1f - scrollPercent, 0f)
|
||||
|
||||
toolbar.alpha = multiplier
|
||||
bottomBehavior?.also {
|
||||
imageText.alpha =
|
||||
multiplier * (if (it.state == BottomSheetBehavior.STATE_COLLAPSED) 0.5f else 1f)
|
||||
}
|
||||
imageContainer.setBackgroundColor(baseBackgroundColor.adjustAlpha(multiplier))
|
||||
|
||||
if (scrollPercent >= 1) {
|
||||
if (!isFinishing) {
|
||||
finish()
|
||||
return
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
}
|
||||
errorRef = e
|
||||
e.logFrostEvent("Image load error")
|
||||
with(binding) {
|
||||
if (imageProgress.isVisible)
|
||||
imageProgress.fadeOut()
|
||||
}
|
||||
tempFile?.delete()
|
||||
binding.error.fadeIn()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (imageUrl.isEmpty()) {
|
||||
return finish()
|
||||
}
|
||||
L.i { "Displaying image" }
|
||||
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" }
|
||||
result
|
||||
}
|
||||
binding = ActivityImageBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.init()
|
||||
launch(CoroutineExceptionHandler { _, throwable -> loadError(throwable) }) {
|
||||
binding.showImage(trueImageUrl.await())
|
||||
override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
|
||||
val overScrolled = scrollPercent > scrollThreshold
|
||||
val maxOffset = releasedChild.height + 10
|
||||
val finalTop =
|
||||
when {
|
||||
scrollToTop && (overScrolled || yvel < -dragHelper.minVelocity) -> -maxOffset
|
||||
!scrollToTop && (overScrolled || yvel > dragHelper.minVelocity) -> maxOffset
|
||||
else -> 0
|
||||
}
|
||||
dragHelper.settleCapturedViewAt(0, finalTop)
|
||||
binding.imageDrag.invalidate()
|
||||
}
|
||||
|
||||
private fun ActivityImageBinding.showImage(url: String) {
|
||||
imagePhoto.showImage(Uri.parse(url))
|
||||
imagePhoto.setImageShownCallback(object : ImageShownCallback {
|
||||
override fun onThumbnailShown() {}
|
||||
override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int = 0
|
||||
|
||||
override fun onMainImageShown() {
|
||||
imageProgress.fadeOut()
|
||||
imagePhoto.animate().alpha(1f).scaleXY(1f).start()
|
||||
}
|
||||
})
|
||||
}
|
||||
override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int = top
|
||||
}
|
||||
|
||||
private fun ActivityImageBinding.init() {
|
||||
imageContainer.setBackgroundColor(baseBackgroundColor)
|
||||
toolbar.setBackgroundColor(baseBackgroundColor)
|
||||
this@ImageActivity.imageText.also { text ->
|
||||
if (text.isNullOrBlank()) {
|
||||
imageText.gone()
|
||||
} else {
|
||||
imageText.setTextColor(if (prefs.blackMediaBg) Color.WHITE else themeProvider.textColor)
|
||||
imageText.setBackgroundColor(
|
||||
baseBackgroundColor.colorToForeground(0.2f).withAlpha(255)
|
||||
)
|
||||
imageText.text = text
|
||||
bottomBehavior = BottomSheetBehavior.from<View>(imageText).apply {
|
||||
addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
imageText.alpha = slideOffset / 2 + 0.5f
|
||||
}
|
||||
internal suspend fun saveImage() {
|
||||
frostDownload(cookie = cookie, url = trueImageUrl.await())
|
||||
}
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
// No op
|
||||
}
|
||||
})
|
||||
}
|
||||
imageText.bringToFront()
|
||||
}
|
||||
}
|
||||
val foregroundTint = if (prefs.blackMediaBg) Color.WHITE else themeProvider.accentColor
|
||||
|
||||
fun ImageView.setState(state: FabStates) {
|
||||
setIcon(state.iicon, color = foregroundTint, sizeDp = 24)
|
||||
setOnClickListener { state.onClick(this@ImageActivity) }
|
||||
}
|
||||
|
||||
imageProgress.tint(foregroundTint)
|
||||
error.apply {
|
||||
invisible()
|
||||
setState(FabStates.ERROR)
|
||||
}
|
||||
download.apply {
|
||||
setState(FabStates.DOWNLOAD)
|
||||
}
|
||||
share.apply {
|
||||
setState(FabStates.SHARE)
|
||||
}
|
||||
|
||||
imagePhoto.setImageLoaderCallback(object : ImageLoader.Callback {
|
||||
override fun onCacheHit(imageType: Int, image: File?) {}
|
||||
|
||||
override fun onCacheMiss(imageType: Int, image: File?) {}
|
||||
|
||||
override fun onStart() {}
|
||||
|
||||
override fun onProgress(progress: Int) {}
|
||||
|
||||
override fun onFinish() {}
|
||||
|
||||
override fun onSuccess(image: File) {}
|
||||
|
||||
override fun onFail(error: Exception) {
|
||||
loadError(error)
|
||||
}
|
||||
})
|
||||
|
||||
activityThemer.setFrostColors {
|
||||
themeWindow = false
|
||||
}
|
||||
dragHelper = ViewDragHelper.create(imageDrag, ViewDragCallback()).apply {
|
||||
setEdgeTrackingEnabled(ViewDragHelper.EDGE_TOP or ViewDragHelper.EDGE_BOTTOM)
|
||||
}
|
||||
imageDrag.dragHelper = dragHelper
|
||||
imageDrag.viewToIgnore = imageText
|
||||
}
|
||||
|
||||
private inner class ViewDragCallback : ViewDragHelper.Callback() {
|
||||
private var scrollPercent: Float = 0f
|
||||
private var scrollThreshold = 0.5f
|
||||
private var scrollToTop = false
|
||||
|
||||
override fun tryCaptureView(view: View, i: Int): Boolean {
|
||||
L.d { "Try capture ${view.id} $i ${binding.imagePhoto.id} ${binding.imageText.id}" }
|
||||
return view === binding.imagePhoto
|
||||
}
|
||||
|
||||
override fun getViewHorizontalDragRange(child: View): Int = 0
|
||||
|
||||
override fun getViewVerticalDragRange(child: View): Int = child.height
|
||||
|
||||
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
|
||||
scrollPercent = abs(top.toFloat() / imageContainer.height)
|
||||
scrollToTop = top < 0
|
||||
val multiplier = max(1f - scrollPercent, 0f)
|
||||
|
||||
toolbar.alpha = multiplier
|
||||
bottomBehavior?.also {
|
||||
imageText.alpha =
|
||||
multiplier * (if (it.state == BottomSheetBehavior.STATE_COLLAPSED) 0.5f else 1f)
|
||||
}
|
||||
imageContainer.setBackgroundColor(baseBackgroundColor.adjustAlpha(multiplier))
|
||||
|
||||
if (scrollPercent >= 1) {
|
||||
if (!isFinishing) {
|
||||
finish()
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
|
||||
val overScrolled = scrollPercent > scrollThreshold
|
||||
val maxOffset = releasedChild.height + 10
|
||||
val finalTop = when {
|
||||
scrollToTop && (overScrolled || yvel < -dragHelper.minVelocity) -> -maxOffset
|
||||
!scrollToTop && (overScrolled || yvel > dragHelper.minVelocity) -> maxOffset
|
||||
else -> 0
|
||||
}
|
||||
dragHelper.settleCapturedViewAt(0, finalTop)
|
||||
binding.imageDrag.invalidate()
|
||||
}
|
||||
|
||||
override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int = 0
|
||||
|
||||
override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int = top
|
||||
}
|
||||
|
||||
internal suspend fun saveImage() {
|
||||
frostDownload(cookie = cookie, url = trueImageUrl.await())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val L = KauLoggerExtension("Image", com.pitchedapps.frost.utils.L)
|
||||
}
|
||||
companion object {
|
||||
private val L = KauLoggerExtension("Image", com.pitchedapps.frost.utils.L)
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class FabStates(
|
||||
val iicon: IIcon,
|
||||
val iconColorProvider: (ThemeProvider) -> Int = { it.iconColor },
|
||||
val backgroundTint: Int = Int.MAX_VALUE
|
||||
val iicon: IIcon,
|
||||
val iconColorProvider: (ThemeProvider) -> Int = { it.iconColor },
|
||||
val backgroundTint: Int = Int.MAX_VALUE
|
||||
) {
|
||||
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" }
|
||||
?: return
|
||||
activity.materialDialog {
|
||||
title(R.string.kau_error)
|
||||
message(text = err.message ?: err.javaClass.name)
|
||||
}
|
||||
}
|
||||
},
|
||||
NOTHING(GoogleMaterial.Icon.gmd_adjust) {
|
||||
override fun onClick(activity: ImageActivity) {}
|
||||
},
|
||||
DOWNLOAD(GoogleMaterial.Icon.gmd_file_download) {
|
||||
override fun onClick(activity: ImageActivity) {
|
||||
activity.launch {
|
||||
activity.binding.download.fadeOut()
|
||||
activity.saveImage()
|
||||
}
|
||||
}
|
||||
},
|
||||
SHARE(GoogleMaterial.Icon.gmd_share) {
|
||||
override fun onClick(activity: ImageActivity) {
|
||||
val file = activity.tempFile ?: return
|
||||
try {
|
||||
val photoURI = activity.frostUriFromFile(file)
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
putExtra(Intent.EXTRA_STREAM, photoURI)
|
||||
type = "image/png"
|
||||
}
|
||||
activity.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
activity.errorRef = e
|
||||
e.logFrostEvent("Image share failed")
|
||||
activity.frostSnackbar(R.string.image_share_failed, activity.themeProvider)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 iconColor = iconColorProvider(themeProvider)
|
||||
if (fab.isHidden) {
|
||||
fab.setIcon(iicon, color = iconColor)
|
||||
fab.backgroundTintList = ColorStateList.valueOf(tint)
|
||||
fab.show()
|
||||
} else {
|
||||
fab.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
|
||||
override fun onHidden(fab: FloatingActionButton) {
|
||||
fab.setIcon(iicon, color = iconColor)
|
||||
fab.show()
|
||||
}
|
||||
})
|
||||
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"
|
||||
}
|
||||
?: return
|
||||
activity.materialDialog {
|
||||
title(R.string.kau_error)
|
||||
message(text = err.message ?: err.javaClass.name)
|
||||
}
|
||||
}
|
||||
},
|
||||
NOTHING(GoogleMaterial.Icon.gmd_adjust) {
|
||||
override fun onClick(activity: ImageActivity) {}
|
||||
},
|
||||
DOWNLOAD(GoogleMaterial.Icon.gmd_file_download) {
|
||||
override fun onClick(activity: ImageActivity) {
|
||||
activity.launch {
|
||||
activity.binding.download.fadeOut()
|
||||
activity.saveImage()
|
||||
}
|
||||
}
|
||||
},
|
||||
SHARE(GoogleMaterial.Icon.gmd_share) {
|
||||
override fun onClick(activity: ImageActivity) {
|
||||
val file = activity.tempFile ?: return
|
||||
try {
|
||||
val photoURI = activity.frostUriFromFile(file)
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SEND).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
putExtra(Intent.EXTRA_STREAM, photoURI)
|
||||
type = "image/png"
|
||||
}
|
||||
activity.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
activity.errorRef = e
|
||||
e.logFrostEvent("Image share failed")
|
||||
activity.frostSnackbar(R.string.image_share_failed, activity.themeProvider)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
abstract fun onClick(activity: ImageActivity)
|
||||
/**
|
||||
* 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 iconColor = iconColorProvider(themeProvider)
|
||||
if (fab.isHidden) {
|
||||
fab.setIcon(iicon, color = iconColor)
|
||||
fab.backgroundTintList = ColorStateList.valueOf(tint)
|
||||
fab.show()
|
||||
} else {
|
||||
fab.hide(
|
||||
object : FloatingActionButton.OnVisibilityChangedListener() {
|
||||
override fun onHidden(fab: FloatingActionButton) {
|
||||
fab.setIcon(iicon, color = iconColor)
|
||||
fab.show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun onClick(activity: ImageActivity)
|
||||
}
|
||||
|
@ -55,190 +55,174 @@ 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
|
||||
lateinit var binding: ActivityIntroBinding
|
||||
private var barHasNext = true
|
||||
|
||||
private val fragments by lazyUi {
|
||||
listOf(
|
||||
IntroFragmentWelcome(),
|
||||
IntroFragmentTheme(),
|
||||
IntroAccountFragment(),
|
||||
IntroTabTouchFragment(),
|
||||
IntroTabContextFragment(),
|
||||
IntroFragmentEnd()
|
||||
)
|
||||
private val fragments by lazyUi {
|
||||
listOf(
|
||||
IntroFragmentWelcome(),
|
||||
IntroFragmentTheme(),
|
||||
IntroAccountFragment(),
|
||||
IntroTabTouchFragment(),
|
||||
IntroTabContextFragment(),
|
||||
IntroFragmentEnd()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityIntroBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.init()
|
||||
}
|
||||
|
||||
private fun ActivityIntroBinding.init() {
|
||||
viewpager.apply {
|
||||
setPageTransformer(true, this@IntroActivity)
|
||||
addOnPageChangeListener(this@IntroActivity)
|
||||
adapter = IntroPageAdapter(supportFragmentManager, fragments)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityIntroBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.init()
|
||||
indicator.setViewPager(viewpager)
|
||||
next.setIcon(GoogleMaterial.Icon.gmd_navigate_next)
|
||||
next.setOnClickListener {
|
||||
if (barHasNext) viewpager.setCurrentItem(viewpager.currentItem + 1, true)
|
||||
else finish(next.x + next.pivotX, next.y + next.pivotY)
|
||||
}
|
||||
skip.setOnClickListener { finish() }
|
||||
ripple.set(themeProvider.bgColor)
|
||||
theme()
|
||||
}
|
||||
|
||||
private fun ActivityIntroBinding.init() {
|
||||
viewpager.apply {
|
||||
setPageTransformer(true, this@IntroActivity)
|
||||
addOnPageChangeListener(this@IntroActivity)
|
||||
adapter = IntroPageAdapter(supportFragmentManager, fragments)
|
||||
}
|
||||
indicator.setViewPager(viewpager)
|
||||
next.setIcon(GoogleMaterial.Icon.gmd_navigate_next)
|
||||
next.setOnClickListener {
|
||||
if (barHasNext) viewpager.setCurrentItem(viewpager.currentItem + 1, true)
|
||||
else finish(next.x + next.pivotX, next.y + next.pivotY)
|
||||
}
|
||||
skip.setOnClickListener { finish() }
|
||||
ripple.set(themeProvider.bgColor)
|
||||
theme()
|
||||
fun theme() {
|
||||
statusBarColor = themeProvider.headerColor
|
||||
navigationBarColor = themeProvider.headerColor
|
||||
with(binding) {
|
||||
skip.setTextColor(themeProvider.textColor)
|
||||
next.imageTintList = ColorStateList.valueOf(themeProvider.textColor)
|
||||
indicator.setColour(themeProvider.textColor)
|
||||
indicator.invalidate()
|
||||
}
|
||||
fragments.forEach { it.themeFragment() }
|
||||
activityThemer.setFrostTheme(forceTransparent = true)
|
||||
}
|
||||
|
||||
fun theme() {
|
||||
statusBarColor = themeProvider.headerColor
|
||||
navigationBarColor = themeProvider.headerColor
|
||||
with(binding) {
|
||||
skip.setTextColor(themeProvider.textColor)
|
||||
next.imageTintList = ColorStateList.valueOf(themeProvider.textColor)
|
||||
indicator.setColour(themeProvider.textColor)
|
||||
indicator.invalidate()
|
||||
}
|
||||
fragments.forEach { it.themeFragment() }
|
||||
activityThemer.setFrostTheme(forceTransparent = true)
|
||||
/**
|
||||
* 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
|
||||
if ((position < 0 && position > -1) || (position > 0 && position < 1)) {
|
||||
val pageWidth = page.width
|
||||
val translateValue = position * -pageWidth
|
||||
page.translationX = (if (translateValue > -pageWidth) translateValue else 0f)
|
||||
page.alpha = if (position < 0) 1 + position else 1f
|
||||
} else {
|
||||
page.alpha = 1f
|
||||
page.translationX = 0f
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
if ((position < 0 && position > -1) || (position > 0 && position < 1)) {
|
||||
val pageWidth = page.width
|
||||
val translateValue = position * -pageWidth
|
||||
page.translationX = (if (translateValue > -pageWidth) translateValue else 0f)
|
||||
page.alpha = if (position < 0) 1 + position else 1f
|
||||
} else {
|
||||
page.alpha = 1f
|
||||
page.translationX = 0f
|
||||
fun finish(x: Float, y: Float) {
|
||||
val blue = color(R.color.facebook_blue)
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||
)
|
||||
binding.ripple.ripple(blue, x, y, 600) { postDelayed(1000) { finish() } }
|
||||
val lastView: View? = fragments.last().view
|
||||
arrayOf<View?>(
|
||||
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() }
|
||||
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))
|
||||
}
|
||||
duration = 600
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
fun finish(x: Float, y: Float) {
|
||||
val blue = color(R.color.facebook_blue)
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||
)
|
||||
binding.ripple.ripple(blue, x, y, 600) {
|
||||
postDelayed(1000) { finish() }
|
||||
}
|
||||
val lastView: View? = fragments.last().view
|
||||
arrayOf<View?>(
|
||||
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()
|
||||
}
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
duration = 600
|
||||
start()
|
||||
}
|
||||
}
|
||||
if (themeProvider.headerColor != blue) {
|
||||
ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
addUpdateListener {
|
||||
val c = themeProvider.headerColor.blendWith(blue, it.animatedValue as Float)
|
||||
statusBarColor = c
|
||||
navigationBarColor = c
|
||||
}
|
||||
duration = 600
|
||||
start()
|
||||
}
|
||||
if (themeProvider.headerColor != blue) {
|
||||
ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
addUpdateListener {
|
||||
val c = themeProvider.headerColor.blendWith(blue, it.animatedValue as Float)
|
||||
statusBarColor = c
|
||||
navigationBarColor = c
|
||||
}
|
||||
duration = 600
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
launch(NonCancellable) {
|
||||
loadAssets(themeProvider)
|
||||
NotificationWidget.forceUpdate(this@IntroActivity)
|
||||
launchNewTask<MainActivity>(cookies(), false)
|
||||
super.finish()
|
||||
}
|
||||
override fun finish() {
|
||||
launch(NonCancellable) {
|
||||
loadAssets(themeProvider)
|
||||
NotificationWidget.forceUpdate(this@IntroActivity)
|
||||
launchNewTask<MainActivity>(cookies(), false)
|
||||
super.finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
with(binding) {
|
||||
if (viewpager.currentItem > 0) viewpager.setCurrentItem(viewpager.currentItem - 1, true)
|
||||
else finish()
|
||||
}
|
||||
override fun onBackPressed() {
|
||||
with(binding) {
|
||||
if (viewpager.currentItem > 0) viewpager.setCurrentItem(viewpager.currentItem - 1, true)
|
||||
else finish()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
fragments[position].onPageSelected()
|
||||
val hasNext = position != fragments.size - 1
|
||||
if (barHasNext == hasNext) return
|
||||
barHasNext = hasNext
|
||||
binding.next.fadeScaleTransition {
|
||||
setIcon(
|
||||
if (barHasNext) GoogleMaterial.Icon.gmd_navigate_next else GoogleMaterial.Icon.gmd_done,
|
||||
color = themeProvider.textColor
|
||||
)
|
||||
}
|
||||
binding.skip.animate().scaleXY(if (barHasNext) 1f else 0f)
|
||||
}
|
||||
|
||||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
||||
fragments[position].onPageScrolled(positionOffset)
|
||||
if (position + 1 < fragments.size)
|
||||
fragments[position + 1].onPageScrolled(positionOffset - 1)
|
||||
}
|
||||
class IntroPageAdapter(fm: FragmentManager, private val fragments: List<BaseIntroFragment>) :
|
||||
FragmentPagerAdapter(fm) {
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
fragments[position].onPageSelected()
|
||||
val hasNext = position != fragments.size - 1
|
||||
if (barHasNext == hasNext) return
|
||||
barHasNext = hasNext
|
||||
binding.next.fadeScaleTransition {
|
||||
setIcon(
|
||||
if (barHasNext) GoogleMaterial.Icon.gmd_navigate_next else GoogleMaterial.Icon.gmd_done,
|
||||
color = themeProvider.textColor
|
||||
)
|
||||
}
|
||||
binding.skip.animate().scaleXY(if (barHasNext) 1f else 0f)
|
||||
}
|
||||
override fun getItem(position: Int): Fragment = fragments[position]
|
||||
|
||||
class IntroPageAdapter(fm: FragmentManager, private val fragments: List<BaseIntroFragment>) :
|
||||
FragmentPagerAdapter(fm) {
|
||||
|
||||
override fun getItem(position: Int): Fragment = fragments[position]
|
||||
|
||||
override fun getCount(): Int = fragments.size
|
||||
}
|
||||
override fun getCount(): Int = fragments.size
|
||||
}
|
||||
}
|
||||
|
@ -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,165 +66,157 @@ 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)
|
||||
private val swipeRefresh: SwipeRefreshLayout by bindView(R.id.swipe_refresh)
|
||||
private val textview: AppCompatTextView by bindView(R.id.textview)
|
||||
private val profile: ImageView by bindView(R.id.profile)
|
||||
private val toolbar: Toolbar by bindView(R.id.toolbar)
|
||||
private val web: LoginWebView by bindView(R.id.login_webview)
|
||||
private val swipeRefresh: SwipeRefreshLayout by bindView(R.id.swipe_refresh)
|
||||
private val textview: AppCompatTextView by bindView(R.id.textview)
|
||||
private val profile: ImageView by bindView(R.id.profile)
|
||||
|
||||
private lateinit var profileLoader: RequestManager
|
||||
private lateinit var profileLoader: RequestManager
|
||||
|
||||
private val refreshMutableFlow = MutableSharedFlow<Boolean>(
|
||||
extraBufferCapacity = 10,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
private val refreshMutableFlow =
|
||||
MutableSharedFlow<Boolean>(
|
||||
extraBufferCapacity = 10,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
|
||||
private val refreshFlow: SharedFlow<Boolean> = refreshMutableFlow.asSharedFlow()
|
||||
private val refreshFlow: SharedFlow<Boolean> = refreshMutableFlow.asSharedFlow()
|
||||
|
||||
private val refreshEmit: FrostEmitter<Boolean> = refreshMutableFlow.asFrostEmitter()
|
||||
private val refreshEmit: FrostEmitter<Boolean> = refreshMutableFlow.asFrostEmitter()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_login)
|
||||
setSupportActionBar(toolbar)
|
||||
setTitle(R.string.kau_login)
|
||||
activityThemer.setFrostColors {
|
||||
toolbar(toolbar)
|
||||
}
|
||||
profileLoader = GlideApp.with(profile)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_login)
|
||||
setSupportActionBar(toolbar)
|
||||
setTitle(R.string.kau_login)
|
||||
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()
|
||||
L.d { "Login found" }
|
||||
fbCookie.save(cookie.id)
|
||||
webFadeOut()
|
||||
profile.fadeIn()
|
||||
loadInfo(cookie)
|
||||
}
|
||||
launch {
|
||||
val cookie = web.loadLogin { refresh(it != 100) }.await()
|
||||
L.d { "Login found" }
|
||||
fbCookie.save(cookie.id)
|
||||
webFadeOut()
|
||||
profile.fadeIn()
|
||||
loadInfo(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun webFadeOut(): Unit = suspendCancellableCoroutine { cont ->
|
||||
web.fadeOut { cont.resume(Unit) }
|
||||
}
|
||||
|
||||
private fun refresh(refreshing: Boolean) {
|
||||
refreshEmit(refreshing)
|
||||
}
|
||||
|
||||
private suspend fun loadInfo(cookie: CookieEntity): Unit = withMainContext {
|
||||
refresh(true)
|
||||
|
||||
val imageDeferred = async { loadProfile(cookie.id) }
|
||||
val nameDeferred = async { loadUsername(cookie) }
|
||||
|
||||
val name: String? = nameDeferred.await()
|
||||
val foundImage: Boolean = imageDeferred.await()
|
||||
|
||||
L._d { "Logged in and received data" }
|
||||
refresh(false)
|
||||
|
||||
if (!foundImage) {
|
||||
L.e { "Could not get profile photo; Invalid userId?" }
|
||||
L._i { cookie }
|
||||
}
|
||||
|
||||
private suspend fun webFadeOut(): Unit = suspendCancellableCoroutine { cont ->
|
||||
web.fadeOut { cont.resume(Unit) }
|
||||
}
|
||||
textview.text = String.format(getString(R.string.welcome), name ?: "")
|
||||
textview.fadeIn()
|
||||
frostEvent("Login", "success" to true)
|
||||
|
||||
private fun refresh(refreshing: Boolean) {
|
||||
refreshEmit(refreshing)
|
||||
}
|
||||
/*
|
||||
* The user may have logged into an account that is already in the database
|
||||
* We will let the db handle duplicates and load it now after the new account has been saved
|
||||
*/
|
||||
val cookies = ArrayList(cookieDao.selectAll())
|
||||
delay(1000)
|
||||
if (prefs.intro) launchNewTask<IntroActivity>(cookies, true)
|
||||
else launchNewTask<MainActivity>(cookies, true)
|
||||
}
|
||||
|
||||
private suspend fun loadInfo(cookie: CookieEntity): Unit = withMainContext {
|
||||
refresh(true)
|
||||
|
||||
val imageDeferred = async { loadProfile(cookie.id) }
|
||||
val nameDeferred = async { loadUsername(cookie) }
|
||||
|
||||
val name: String? = nameDeferred.await()
|
||||
val foundImage: Boolean = imageDeferred.await()
|
||||
|
||||
L._d { "Logged in and received data" }
|
||||
refresh(false)
|
||||
|
||||
if (!foundImage) {
|
||||
L.e { "Could not get profile photo; Invalid userId?" }
|
||||
L._i { cookie }
|
||||
}
|
||||
|
||||
textview.text = String.format(getString(R.string.welcome), name ?: "")
|
||||
textview.fadeIn()
|
||||
frostEvent("Login", "success" to true)
|
||||
|
||||
/*
|
||||
* The user may have logged into an account that is already in the database
|
||||
* We will let the db handle duplicates and load it now after the new account has been saved
|
||||
*/
|
||||
val cookies = ArrayList(cookieDao.selectAll())
|
||||
delay(1000)
|
||||
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> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
cont.resume(true)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
e.logFrostEvent("Profile loading exception")
|
||||
cont.resume(false)
|
||||
return false
|
||||
}
|
||||
}).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 loadProfile(id: Long): Boolean = withMainContext {
|
||||
suspendCancellableCoroutine<Boolean> { cont ->
|
||||
profileLoader
|
||||
.load(profilePictureUrl(id))
|
||||
.transform(FrostGlide.circleCrop)
|
||||
.listener(
|
||||
object : RequestListener<Drawable> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
cont.resume(true)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
e.logFrostEvent("Profile loading exception")
|
||||
cont.resume(false)
|
||||
return false
|
||||
}
|
||||
}
|
||||
)
|
||||
.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() }
|
||||
} catch (e: Exception) {
|
||||
if (e !is UnknownHostException)
|
||||
e.logFrostEvent("Fetch username failed")
|
||||
null
|
||||
if (e !is UnknownHostException) e.logFrostEvent("Fetch username failed")
|
||||
null
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
cookieDao.save(cookie.copy(name = result))
|
||||
return@withContext result
|
||||
}
|
||||
if (result != null) {
|
||||
cookieDao.save(cookie.copy(name = result))
|
||||
return@withContext result
|
||||
}
|
||||
|
||||
return@withContext cookie.name
|
||||
return@withContext cookie.name
|
||||
}
|
||||
|
||||
override fun backConsumer(): Boolean {
|
||||
if (web.canGoBack()) {
|
||||
web.goBack()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
override fun backConsumer(): Boolean {
|
||||
if (web.canGoBack()) {
|
||||
web.goBack()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
web.resumeTimers()
|
||||
}
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
web.resumeTimers()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
web.pauseTimers()
|
||||
super.onPause()
|
||||
}
|
||||
override fun onPause() {
|
||||
web.pauseTimers()
|
||||
super.onPause()
|
||||
}
|
||||
}
|
||||
|
@ -40,93 +40,92 @@ import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class MainActivity : BaseMainActivity() {
|
||||
|
||||
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()
|
||||
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()
|
||||
|
||||
private val headerMutableFlow = MutableStateFlow("")
|
||||
override val headerFlow: SharedFlow<String> = headerMutableFlow.asSharedFlow()
|
||||
override val headerEmit: FrostEmitter<String> = headerMutableFlow.asFrostEmitter()
|
||||
private val headerMutableFlow = MutableStateFlow("")
|
||||
override val headerFlow: SharedFlow<String> = headerMutableFlow.asSharedFlow()
|
||||
override val headerEmit: FrostEmitter<String> = headerMutableFlow.asFrostEmitter()
|
||||
|
||||
override fun onNestedCreate(savedInstanceState: Bundle?) {
|
||||
with(contentBinding) {
|
||||
setupTabs()
|
||||
setupViewPager()
|
||||
override fun onNestedCreate(savedInstanceState: Bundle?) {
|
||||
with(contentBinding) {
|
||||
setupTabs()
|
||||
setupViewPager()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ActivityMainContentBinding.setupViewPager() {
|
||||
viewpager.addOnPageChangeListener(
|
||||
object : ViewPager.SimpleOnPageChangeListener() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
if (lastPosition == position) {
|
||||
return
|
||||
}
|
||||
if (lastPosition != -1) {
|
||||
fragmentEmit(-(lastPosition + 1))
|
||||
}
|
||||
fragmentEmit(position)
|
||||
lastPosition = position
|
||||
}
|
||||
}
|
||||
|
||||
private fun ActivityMainContentBinding.setupViewPager() {
|
||||
viewpager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
if (lastPosition == position) {
|
||||
return
|
||||
}
|
||||
if (lastPosition != -1) {
|
||||
fragmentEmit(-(lastPosition + 1))
|
||||
}
|
||||
fragmentEmit(position)
|
||||
lastPosition = position
|
||||
}
|
||||
override fun onPageScrolled(
|
||||
position: Int,
|
||||
positionOffset: Float,
|
||||
positionOffsetPixels: Int
|
||||
) {
|
||||
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
|
||||
val delta = positionOffset * (SELECTED_TAB_ALPHA - UNSELECTED_TAB_ALPHA)
|
||||
tabsForEachView { tabPosition, view ->
|
||||
view.setAllAlpha(
|
||||
when (tabPosition) {
|
||||
position -> SELECTED_TAB_ALPHA - delta
|
||||
position + 1 -> UNSELECTED_TAB_ALPHA + delta
|
||||
else -> UNSELECTED_TAB_ALPHA
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPageScrolled(
|
||||
position: Int,
|
||||
positionOffset: Float,
|
||||
positionOffsetPixels: Int
|
||||
) {
|
||||
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
|
||||
val delta = positionOffset * (SELECTED_TAB_ALPHA - UNSELECTED_TAB_ALPHA)
|
||||
tabsForEachView { tabPosition, view ->
|
||||
view.setAllAlpha(
|
||||
when (tabPosition) {
|
||||
position -> SELECTED_TAB_ALPHA - delta
|
||||
position + 1 -> UNSELECTED_TAB_ALPHA + delta
|
||||
else -> UNSELECTED_TAB_ALPHA
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
private fun ActivityMainContentBinding.setupTabs() {
|
||||
viewpager.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabs))
|
||||
tabs.addOnTabSelectedListener(
|
||||
object : TabLayout.ViewPagerOnTabSelectedListener(viewpager) {
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
super.onTabReselected(tab)
|
||||
currentFragment?.onTabClick()
|
||||
}
|
||||
|
||||
private fun ActivityMainContentBinding.setupTabs() {
|
||||
viewpager.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabs))
|
||||
tabs.addOnTabSelectedListener(object : TabLayout.ViewPagerOnTabSelectedListener(viewpager) {
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
super.onTabReselected(tab)
|
||||
currentFragment?.onTabClick()
|
||||
}
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
super.onTabSelected(tab)
|
||||
(tab.customView as BadgedIcon).badgeText = null
|
||||
}
|
||||
})
|
||||
headerFlow
|
||||
.filter { it.isNotBlank() }
|
||||
.mapNotNull { html ->
|
||||
BadgeParser.parseFromData(
|
||||
cookie = fbCookie.webCookie,
|
||||
text = html
|
||||
)?.data
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
.onEach { data ->
|
||||
L.v { "Badges $data" }
|
||||
tabsForEachView { _, view ->
|
||||
when (view.iicon) {
|
||||
FbItem.FEED.icon -> view.badgeText = data.feed
|
||||
FbItem.FRIENDS.icon -> view.badgeText = data.friends
|
||||
FbItem.MESSAGES.icon -> view.badgeText = data.messages
|
||||
FbItem.NOTIFICATIONS.icon -> view.badgeText = data.notifications
|
||||
}
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.Main)
|
||||
.launchIn(this@MainActivity)
|
||||
}
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
super.onTabSelected(tab)
|
||||
(tab.customView as BadgedIcon).badgeText = null
|
||||
}
|
||||
}
|
||||
)
|
||||
headerFlow
|
||||
.filter { it.isNotBlank() }
|
||||
.mapNotNull { html ->
|
||||
BadgeParser.parseFromData(cookie = fbCookie.webCookie, text = html)?.data
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
.onEach { data ->
|
||||
L.v { "Badges $data" }
|
||||
tabsForEachView { _, view ->
|
||||
when (view.iicon) {
|
||||
FbItem.FEED.icon -> view.badgeText = data.feed
|
||||
FbItem.FRIENDS.icon -> view.badgeText = data.friends
|
||||
FbItem.MESSAGES.icon -> view.badgeText = data.messages
|
||||
FbItem.NOTIFICATIONS.icon -> view.badgeText = data.notifications
|
||||
}
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.Main)
|
||||
.launchIn(this@MainActivity)
|
||||
}
|
||||
}
|
||||
|
@ -32,43 +32,44 @@ 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)
|
||||
val adapter = FastItemAdapter<AccountItem>()
|
||||
val text: AppCompatTextView by bindView(R.id.text_select_account)
|
||||
val container: ConstraintLayout by bindView(R.id.container)
|
||||
val recycler: RecyclerView by bindView(R.id.selector_recycler)
|
||||
val adapter = FastItemAdapter<AccountItem>()
|
||||
val text: AppCompatTextView by bindView(R.id.text_select_account)
|
||||
val container: ConstraintLayout by bindView(R.id.container)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_selector)
|
||||
recycler.layoutManager = GridLayoutManager(this, 2)
|
||||
recycler.adapter = adapter
|
||||
adapter.add(cookies().map { AccountItem(it, themeProvider) })
|
||||
adapter.add(AccountItem(null, themeProvider)) // add account
|
||||
adapter.addEventHook(object : ClickEventHook<AccountItem>() {
|
||||
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? =
|
||||
(viewHolder as? AccountItem.ViewHolder)?.itemView
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_selector)
|
||||
recycler.layoutManager = GridLayoutManager(this, 2)
|
||||
recycler.adapter = adapter
|
||||
adapter.add(cookies().map { AccountItem(it, themeProvider) })
|
||||
adapter.add(AccountItem(null, themeProvider)) // add account
|
||||
adapter.addEventHook(
|
||||
object : ClickEventHook<AccountItem>() {
|
||||
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? =
|
||||
(viewHolder as? AccountItem.ViewHolder)?.itemView
|
||||
|
||||
override fun onClick(
|
||||
v: View,
|
||||
position: Int,
|
||||
fastAdapter: FastAdapter<AccountItem>,
|
||||
item: AccountItem
|
||||
) {
|
||||
if (item.cookie == null) this@SelectorActivity.launchNewTask<LoginActivity>()
|
||||
else launch {
|
||||
fbCookie.switchUser(item.cookie)
|
||||
launchNewTask<MainActivity>(cookies())
|
||||
}
|
||||
override fun onClick(
|
||||
v: View,
|
||||
position: Int,
|
||||
fastAdapter: FastAdapter<AccountItem>,
|
||||
item: AccountItem
|
||||
) {
|
||||
if (item.cookie == null) this@SelectorActivity.launchNewTask<LoginActivity>()
|
||||
else
|
||||
launch {
|
||||
fbCookie.switchUser(item.cookie)
|
||||
launchNewTask<MainActivity>(cookies())
|
||||
}
|
||||
})
|
||||
activityThemer.setFrostColors {
|
||||
text(text)
|
||||
background(container)
|
||||
}
|
||||
}
|
||||
)
|
||||
activityThemer.setFrostColors {
|
||||
text(text)
|
||||
background(container)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,225 +59,206 @@ 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
|
||||
private var resultFlag = Activity.RESULT_CANCELED
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_RINGTONE = 0b10111 shl 5
|
||||
const val REQUEST_NOTIFICATION_RINGTONE = REQUEST_RINGTONE or 1
|
||||
const val REQUEST_MESSAGE_RINGTONE = REQUEST_RINGTONE or 2
|
||||
const val ACTIVITY_REQUEST_TABS = 29
|
||||
const val ACTIVITY_REQUEST_DEBUG = 53
|
||||
companion object {
|
||||
private const val REQUEST_RINGTONE = 0b10111 shl 5
|
||||
const val REQUEST_NOTIFICATION_RINGTONE = REQUEST_RINGTONE or 1
|
||||
const val REQUEST_MESSAGE_RINGTONE = REQUEST_RINGTONE or 2
|
||||
const val ACTIVITY_REQUEST_TABS = 29
|
||||
const val ACTIVITY_REQUEST_DEBUG = 53
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (fetchRingtone(requestCode, resultCode, data)) return
|
||||
when (requestCode) {
|
||||
ACTIVITY_REQUEST_TABS -> {
|
||||
if (resultCode == Activity.RESULT_OK) shouldRestartMain()
|
||||
return
|
||||
}
|
||||
ACTIVITY_REQUEST_DEBUG -> {
|
||||
val url = data?.extras?.getString(DebugActivity.RESULT_URL)
|
||||
if (resultCode == Activity.RESULT_OK && url?.isNotBlank() == true)
|
||||
sendDebug(url, data.getStringExtra(DebugActivity.RESULT_BODY))
|
||||
return
|
||||
}
|
||||
}
|
||||
reloadList()
|
||||
}
|
||||
|
||||
/** 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
|
||||
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)
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "grantUriPermission" }
|
||||
}
|
||||
}
|
||||
when (requestCode) {
|
||||
REQUEST_NOTIFICATION_RINGTONE -> {
|
||||
prefs.notificationRingtone = uriString
|
||||
reloadByTitle(R.string.notification_ringtone)
|
||||
}
|
||||
REQUEST_MESSAGE_RINGTONE -> {
|
||||
prefs.messageRingtone = uriString
|
||||
reloadByTitle(R.string.message_ringtone)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun kPrefCoreAttributes(): CoreAttributeContract.() -> Unit = {
|
||||
textColor = { themeProvider.textColor }
|
||||
accentColor = { themeProvider.accentColor }
|
||||
}
|
||||
|
||||
override fun onCreateKPrefs(savedInstanceState: Bundle?): KPrefAdapterBuilder.() -> Unit = {
|
||||
subItems(R.string.appearance, getAppearancePrefs()) {
|
||||
descRes = R.string.appearance_desc
|
||||
iicon = GoogleMaterial.Icon.gmd_palette
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (fetchRingtone(requestCode, resultCode, data)) return
|
||||
when (requestCode) {
|
||||
ACTIVITY_REQUEST_TABS -> {
|
||||
if (resultCode == Activity.RESULT_OK)
|
||||
shouldRestartMain()
|
||||
return
|
||||
}
|
||||
ACTIVITY_REQUEST_DEBUG -> {
|
||||
val url = data?.extras?.getString(DebugActivity.RESULT_URL)
|
||||
if (resultCode == Activity.RESULT_OK && url?.isNotBlank() == true)
|
||||
sendDebug(url, data.getStringExtra(DebugActivity.RESULT_BODY))
|
||||
return
|
||||
}
|
||||
}
|
||||
reloadList()
|
||||
subItems(R.string.behaviour, getBehaviourPrefs()) {
|
||||
descRes = R.string.behaviour_desc
|
||||
iicon = GoogleMaterial.Icon.gmd_trending_up
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
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
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "grantUriPermission" }
|
||||
}
|
||||
}
|
||||
when (requestCode) {
|
||||
REQUEST_NOTIFICATION_RINGTONE -> {
|
||||
prefs.notificationRingtone = uriString
|
||||
reloadByTitle(R.string.notification_ringtone)
|
||||
}
|
||||
REQUEST_MESSAGE_RINGTONE -> {
|
||||
prefs.messageRingtone = uriString
|
||||
reloadByTitle(R.string.message_ringtone)
|
||||
}
|
||||
}
|
||||
return true
|
||||
subItems(R.string.newsfeed, getFeedPrefs()) {
|
||||
descRes = R.string.newsfeed_desc
|
||||
iicon = CommunityMaterial.Icon3.cmd_newspaper
|
||||
}
|
||||
|
||||
override fun kPrefCoreAttributes(): CoreAttributeContract.() -> Unit = {
|
||||
textColor = { themeProvider.textColor }
|
||||
accentColor = { themeProvider.accentColor }
|
||||
subItems(R.string.notifications, getNotificationPrefs()) {
|
||||
descRes = R.string.notifications_desc
|
||||
iicon = GoogleMaterial.Icon.gmd_notifications
|
||||
}
|
||||
|
||||
override fun onCreateKPrefs(savedInstanceState: Bundle?): KPrefAdapterBuilder.() -> Unit = {
|
||||
subItems(R.string.appearance, getAppearancePrefs()) {
|
||||
descRes = R.string.appearance_desc
|
||||
iicon = GoogleMaterial.Icon.gmd_palette
|
||||
}
|
||||
|
||||
subItems(R.string.behaviour, getBehaviourPrefs()) {
|
||||
descRes = R.string.behaviour_desc
|
||||
iicon = GoogleMaterial.Icon.gmd_trending_up
|
||||
}
|
||||
|
||||
subItems(R.string.newsfeed, getFeedPrefs()) {
|
||||
descRes = R.string.newsfeed_desc
|
||||
iicon = CommunityMaterial.Icon3.cmd_newspaper
|
||||
}
|
||||
|
||||
subItems(R.string.notifications, getNotificationPrefs()) {
|
||||
descRes = R.string.notifications_desc
|
||||
iicon = GoogleMaterial.Icon.gmd_notifications
|
||||
}
|
||||
|
||||
subItems(R.string.security, getSecurityPrefs()) {
|
||||
descRes = R.string.security_desc
|
||||
iicon = GoogleMaterial.Icon.gmd_lock
|
||||
}
|
||||
|
||||
// subItems(R.string.network, getNetworkPrefs()) {
|
||||
// descRes = R.string.network_desc
|
||||
// iicon = GoogleMaterial.Icon.gmd_network_cell
|
||||
// }
|
||||
|
||||
// todo add donation?
|
||||
|
||||
plainText(R.string.about_frost) {
|
||||
descRes = R.string.about_frost_desc
|
||||
iicon = GoogleMaterial.Icon.gmd_info
|
||||
onClick = {
|
||||
startActivityForResult<AboutActivity>(
|
||||
9,
|
||||
bundleBuilder = {
|
||||
withSceneTransitionAnimation(this@SettingsActivity)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
plainText(R.string.help_translate) {
|
||||
descRes = R.string.help_translate_desc
|
||||
iicon = GoogleMaterial.Icon.gmd_translate
|
||||
onClick = { startLink(R.string.translation_url) }
|
||||
}
|
||||
|
||||
plainText(R.string.replay_intro) {
|
||||
iicon = GoogleMaterial.Icon.gmd_replay
|
||||
onClick = { launchNewTask<IntroActivity>(cookies(), true) }
|
||||
}
|
||||
|
||||
subItems(R.string.experimental, getExperimentalPrefs()) {
|
||||
descRes = R.string.experimental_desc
|
||||
iicon = CommunityMaterial.Icon2.cmd_flask_outline
|
||||
}
|
||||
|
||||
subItems(R.string.debug_frost, getDebugPrefs()) {
|
||||
descRes = R.string.debug_frost_desc
|
||||
iicon = CommunityMaterial.Icon.cmd_bug
|
||||
visible = { prefs.debugSettings }
|
||||
}
|
||||
subItems(R.string.security, getSecurityPrefs()) {
|
||||
descRes = R.string.security_desc
|
||||
iicon = GoogleMaterial.Icon.gmd_lock
|
||||
}
|
||||
|
||||
fun setFrostResult(flag: Int) {
|
||||
resultFlag = resultFlag or flag
|
||||
}
|
||||
// subItems(R.string.network, getNetworkPrefs()) {
|
||||
// descRes = R.string.network_desc
|
||||
// iicon = GoogleMaterial.Icon.gmd_network_cell
|
||||
// }
|
||||
|
||||
fun shouldRestartMain() {
|
||||
setFrostResult(REQUEST_RESTART)
|
||||
}
|
||||
// todo add donation?
|
||||
|
||||
fun shouldRefreshMain() {
|
||||
setFrostResult(REQUEST_REFRESH)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
activityThemer.setFrostTheme(forceTransparent = true)
|
||||
animate = prefs.animate
|
||||
themeExterior(false)
|
||||
}
|
||||
|
||||
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
|
||||
plainText(R.string.about_frost) {
|
||||
descRes = R.string.about_frost_desc
|
||||
iicon = GoogleMaterial.Icon.gmd_info
|
||||
onClick = {
|
||||
startActivityForResult<AboutActivity>(
|
||||
9,
|
||||
bundleBuilder = { withSceneTransitionAnimation(this@SettingsActivity) }
|
||||
)
|
||||
else toolbarCanvas.set(themeProvider.headerColor)
|
||||
frostNavigationBar(prefs, themeProvider)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (!super.backPress()) {
|
||||
setResult(resultFlag)
|
||||
launch(NonCancellable) {
|
||||
loadAssets(themeProvider)
|
||||
finishSlideOut()
|
||||
}
|
||||
}
|
||||
plainText(R.string.help_translate) {
|
||||
descRes = R.string.help_translate_desc
|
||||
iicon = GoogleMaterial.Icon.gmd_translate
|
||||
onClick = { startLink(R.string.translation_url) }
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_settings, menu)
|
||||
toolbar.tint(themeProvider.iconColor)
|
||||
setMenuIcons(
|
||||
menu, themeProvider.iconColor,
|
||||
R.id.action_github to CommunityMaterial.Icon2.cmd_github,
|
||||
R.id.action_changelog to GoogleMaterial.Icon.gmd_info
|
||||
)
|
||||
return true
|
||||
plainText(R.string.replay_intro) {
|
||||
iicon = GoogleMaterial.Icon.gmd_replay
|
||||
onClick = { launchNewTask<IntroActivity>(cookies(), true) }
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_github -> startLink(R.string.github_url)
|
||||
R.id.action_changelog -> frostChangelog()
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
subItems(R.string.experimental, getExperimentalPrefs()) {
|
||||
descRes = R.string.experimental_desc
|
||||
iicon = CommunityMaterial.Icon2.cmd_flask_outline
|
||||
}
|
||||
|
||||
subItems(R.string.debug_frost, getDebugPrefs()) {
|
||||
descRes = R.string.debug_frost_desc
|
||||
iicon = CommunityMaterial.Icon.cmd_bug
|
||||
visible = { prefs.debugSettings }
|
||||
}
|
||||
}
|
||||
|
||||
fun setFrostResult(flag: Int) {
|
||||
resultFlag = resultFlag or flag
|
||||
}
|
||||
|
||||
fun shouldRestartMain() {
|
||||
setFrostResult(REQUEST_RESTART)
|
||||
}
|
||||
|
||||
fun shouldRefreshMain() {
|
||||
setFrostResult(REQUEST_REFRESH)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
activityThemer.setFrostTheme(forceTransparent = true)
|
||||
animate = prefs.animate
|
||||
themeExterior(false)
|
||||
}
|
||||
|
||||
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)
|
||||
else toolbarCanvas.set(themeProvider.headerColor)
|
||||
frostNavigationBar(prefs, themeProvider)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (!super.backPress()) {
|
||||
setResult(resultFlag)
|
||||
launch(NonCancellable) {
|
||||
loadAssets(themeProvider)
|
||||
finishSlideOut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_settings, menu)
|
||||
toolbar.tint(themeProvider.iconColor)
|
||||
setMenuIcons(
|
||||
menu,
|
||||
themeProvider.iconColor,
|
||||
R.id.action_github to CommunityMaterial.Icon2.cmd_github,
|
||||
R.id.action_changelog to GoogleMaterial.Icon.gmd_info
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_github -> startLink(R.string.github_url)
|
||||
R.id.action_changelog -> frostChangelog()
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -43,119 +43,116 @@ 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>()
|
||||
private val adapter = FastItemAdapter<TabIItem>()
|
||||
|
||||
private val wobble = lazyContext { AnimationUtils.loadAnimation(it, R.anim.rotate_delta) }
|
||||
private val wobble = lazyContext { AnimationUtils.loadAnimation(it, R.anim.rotate_delta) }
|
||||
|
||||
private lateinit var binding: ActivityTabCustomizerBinding
|
||||
private lateinit var binding: ActivityTabCustomizerBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityTabCustomizerBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.init()
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityTabCustomizerBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.init()
|
||||
}
|
||||
|
||||
fun ActivityTabCustomizerBinding.init() {
|
||||
pseudoToolbar.setBackgroundColor(themeProvider.headerColor)
|
||||
|
||||
tabRecycler.layoutManager =
|
||||
GridLayoutManager(this@TabCustomizerActivity, TAB_COUNT, RecyclerView.VERTICAL, false)
|
||||
tabRecycler.adapter = adapter
|
||||
tabRecycler.setHasFixedSize(true)
|
||||
|
||||
divider.setBackgroundColor(themeProvider.textColor.withAlpha(30))
|
||||
instructions.setTextColor(themeProvider.textColor)
|
||||
|
||||
launch {
|
||||
val tabs = genericDao.getTabs().toMutableList()
|
||||
L.d { "Tabs $tabs" }
|
||||
val remaining = FbItem.values().filter { it.name[0] != '_' }.toMutableList()
|
||||
remaining.removeAll(tabs)
|
||||
tabs.addAll(remaining)
|
||||
adapter.set(tabs.map { TabIItem(it, themeProvider) })
|
||||
|
||||
bindSwapper(adapter, tabRecycler)
|
||||
|
||||
adapter.onClickListener = { view, _, _, _ ->
|
||||
view!!.wobble()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun ActivityTabCustomizerBinding.init() {
|
||||
pseudoToolbar.setBackgroundColor(themeProvider.headerColor)
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
|
||||
tabRecycler.layoutManager =
|
||||
GridLayoutManager(this@TabCustomizerActivity, TAB_COUNT, RecyclerView.VERTICAL, false)
|
||||
tabRecycler.adapter = adapter
|
||||
tabRecycler.setHasFixedSize(true)
|
||||
fabSave.setIcon(GoogleMaterial.Icon.gmd_check, themeProvider.iconColor)
|
||||
fabSave.backgroundTintList = ColorStateList.valueOf(themeProvider.accentColor)
|
||||
fabSave.setOnClickListener {
|
||||
launchMain(NonCancellable) {
|
||||
val tabs = adapter.adapterItems.subList(0, TAB_COUNT).map(TabIItem::item)
|
||||
genericDao.saveTabs(tabs)
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
fabCancel.setIcon(GoogleMaterial.Icon.gmd_close, themeProvider.iconColor)
|
||||
fabCancel.backgroundTintList = ColorStateList.valueOf(themeProvider.accentColor)
|
||||
fabCancel.setOnClickListener { finish() }
|
||||
activityThemer.setFrostColors { themeWindow = true }
|
||||
}
|
||||
|
||||
divider.setBackgroundColor(themeProvider.textColor.withAlpha(30))
|
||||
instructions.setTextColor(themeProvider.textColor)
|
||||
private fun View.wobble() = startAnimation(wobble(context))
|
||||
|
||||
launch {
|
||||
val tabs = genericDao.getTabs().toMutableList()
|
||||
L.d { "Tabs $tabs" }
|
||||
val remaining = FbItem.values().filter { it.name[0] != '_' }.toMutableList()
|
||||
remaining.removeAll(tabs)
|
||||
tabs.addAll(remaining)
|
||||
adapter.set(tabs.map { TabIItem(it, themeProvider) })
|
||||
private fun bindSwapper(adapter: FastItemAdapter<*>, recycler: RecyclerView) {
|
||||
val dragCallback = TabDragCallback(SimpleDragCallback.ALL, swapper(adapter))
|
||||
ItemTouchHelper(dragCallback).attachToRecyclerView(recycler)
|
||||
}
|
||||
|
||||
bindSwapper(adapter, tabRecycler)
|
||||
private fun swapper(adapter: FastItemAdapter<*>) =
|
||||
object : ItemTouchCallback {
|
||||
override fun itemTouchOnMove(oldPosition: Int, newPosition: Int): Boolean {
|
||||
Collections.swap(adapter.adapterItems, oldPosition, newPosition)
|
||||
adapter.notifyAdapterDataSetChanged()
|
||||
return true
|
||||
}
|
||||
|
||||
adapter.onClickListener = { view, _, _, _ -> view!!.wobble(); true }
|
||||
}
|
||||
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
|
||||
fabSave.setIcon(GoogleMaterial.Icon.gmd_check, themeProvider.iconColor)
|
||||
fabSave.backgroundTintList = ColorStateList.valueOf(themeProvider.accentColor)
|
||||
fabSave.setOnClickListener {
|
||||
launchMain(NonCancellable) {
|
||||
val tabs = adapter.adapterItems.subList(0, TAB_COUNT).map(TabIItem::item)
|
||||
genericDao.saveTabs(tabs)
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
fabCancel.setIcon(GoogleMaterial.Icon.gmd_close, themeProvider.iconColor)
|
||||
fabCancel.backgroundTintList = ColorStateList.valueOf(themeProvider.accentColor)
|
||||
fabCancel.setOnClickListener { finish() }
|
||||
activityThemer.setFrostColors {
|
||||
themeWindow = true
|
||||
}
|
||||
override fun itemTouchDropped(oldPosition: Int, newPosition: Int) = Unit
|
||||
}
|
||||
|
||||
private fun View.wobble() = startAnimation(wobble(context))
|
||||
private class TabDragCallback(directions: Int, itemTouchCallback: ItemTouchCallback) :
|
||||
SimpleDragCallback(directions, itemTouchCallback) {
|
||||
|
||||
private fun bindSwapper(adapter: FastItemAdapter<*>, recycler: RecyclerView) {
|
||||
val dragCallback = TabDragCallback(SimpleDragCallback.ALL, swapper(adapter))
|
||||
ItemTouchHelper(dragCallback).attachToRecyclerView(recycler)
|
||||
}
|
||||
private var draggingView: TabIItem.ViewHolder? = null
|
||||
|
||||
private fun swapper(adapter: FastItemAdapter<*>) = object : ItemTouchCallback {
|
||||
override fun itemTouchOnMove(oldPosition: Int, newPosition: Int): Boolean {
|
||||
Collections.swap(adapter.adapterItems, oldPosition, newPosition)
|
||||
adapter.notifyAdapterDataSetChanged()
|
||||
return true
|
||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
super.onSelectedChanged(viewHolder, actionState)
|
||||
when (actionState) {
|
||||
ItemTouchHelper.ACTION_STATE_DRAG -> {
|
||||
(viewHolder as? TabIItem.ViewHolder)?.apply {
|
||||
draggingView = this
|
||||
itemView.animate().scaleXY(1.3f)
|
||||
text.animate().alpha(0f)
|
||||
}
|
||||
}
|
||||
|
||||
override fun itemTouchDropped(oldPosition: Int, newPosition: Int) = Unit
|
||||
}
|
||||
|
||||
private class TabDragCallback(
|
||||
directions: Int,
|
||||
itemTouchCallback: ItemTouchCallback
|
||||
) : SimpleDragCallback(directions, itemTouchCallback) {
|
||||
|
||||
private var draggingView: TabIItem.ViewHolder? = null
|
||||
|
||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
super.onSelectedChanged(viewHolder, actionState)
|
||||
when (actionState) {
|
||||
ItemTouchHelper.ACTION_STATE_DRAG -> {
|
||||
(viewHolder as? TabIItem.ViewHolder)?.apply {
|
||||
draggingView = this
|
||||
itemView.animate().scaleXY(1.3f)
|
||||
text.animate().alpha(0f)
|
||||
}
|
||||
}
|
||||
ItemTouchHelper.ACTION_STATE_IDLE -> {
|
||||
draggingView?.apply {
|
||||
itemView.animate().scaleXY(1f)
|
||||
text.animate().alpha(1f)
|
||||
}
|
||||
draggingView = null
|
||||
}
|
||||
}
|
||||
ItemTouchHelper.ACTION_STATE_IDLE -> {
|
||||
draggingView?.apply {
|
||||
itemView.animate().scaleXY(1f)
|
||||
text.animate().alpha(1f)
|
||||
}
|
||||
draggingView = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,243 +83,236 @@ 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() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val requiresAction = !parseActionSend()
|
||||
super.onCreate(savedInstanceState)
|
||||
if (requiresAction) {
|
||||
/*
|
||||
* Signifies that we need to let the user know of a bad url
|
||||
* We will subscribe to the load cycle once,
|
||||
* and pop a dialog giving the user the option to copy the shared text
|
||||
*/
|
||||
content.scope.launch(Dispatchers.IO) {
|
||||
content.refreshFlow.take(1).collect()
|
||||
withMainContext {
|
||||
materialDialog {
|
||||
title(R.string.invalid_share_url)
|
||||
message(R.string.invalid_share_url_desc)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val requiresAction = !parseActionSend()
|
||||
super.onCreate(savedInstanceState)
|
||||
if (requiresAction) {
|
||||
/*
|
||||
* Signifies that we need to let the user know of a bad url
|
||||
* We will subscribe to the load cycle once,
|
||||
* and pop a dialog giving the user the option to copy the shared text
|
||||
*/
|
||||
content.scope.launch(Dispatchers.IO) {
|
||||
content.refreshFlow.take(1).collect()
|
||||
withMainContext {
|
||||
materialDialog {
|
||||
title(R.string.invalid_share_url)
|
||||
message(R.string.invalid_share_url_desc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return true
|
||||
val url = text.toHttpUrlOrNull()?.toString()
|
||||
return if (url == null) {
|
||||
L.i { "Attempted to share a non-url" }
|
||||
L._i { "Shared text: $text" }
|
||||
copyToClipboard(text, "Text to Share", showToast = false)
|
||||
intent.putExtra(ARG_URL, FbItem.FEED.url)
|
||||
false
|
||||
} else {
|
||||
L.i { "Sharing url through overlay" }
|
||||
L._i { "Url: $url" }
|
||||
intent.putExtra(ARG_URL, "${FB_URL_BASE}sharer/sharer.php?u=$url")
|
||||
true
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return true
|
||||
val url = text.toHttpUrlOrNull()?.toString()
|
||||
return if (url == null) {
|
||||
L.i { "Attempted to share a non-url" }
|
||||
L._i { "Shared text: $text" }
|
||||
copyToClipboard(text, "Text to Share", showToast = false)
|
||||
intent.putExtra(ARG_URL, FbItem.FEED.url)
|
||||
false
|
||||
} else {
|
||||
L.i { "Sharing url through overlay" }
|
||||
L._i { "Url: $url" }
|
||||
intent.putExtra(ARG_URL, "${FB_URL_BASE}sharer/sharer.php?u=$url")
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
val content: FrostContentWeb by bindView(R.id.frost_content_web)
|
||||
val web: FrostWebView
|
||||
get() = content.coreView
|
||||
private val coordinator: CoordinatorLayout by bindView(R.id.overlay_main_content)
|
||||
override val frameWrapper: FrameLayout by bindView(R.id.frame_wrapper)
|
||||
val toolbar: Toolbar by bindView(R.id.overlay_toolbar)
|
||||
val content: FrostContentWeb by bindView(R.id.frost_content_web)
|
||||
val web: FrostWebView
|
||||
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
|
||||
private inline val urlTest: String?
|
||||
get() = intent.getStringExtra(ARG_URL) ?: intent.dataString
|
||||
|
||||
lateinit var swipeBack: SwipeBackContract
|
||||
lateinit var swipeBack: SwipeBackContract
|
||||
|
||||
/**
|
||||
* Nonnull variant; verify by checking [urlTest]
|
||||
*/
|
||||
override val baseUrl: String
|
||||
get() = urlTest!!.formattedFbUrl
|
||||
/** Nonnull variant; verify by checking [urlTest] */
|
||||
override val baseUrl: String
|
||||
get() = urlTest!!.formattedFbUrl
|
||||
|
||||
override val baseEnum: FbItem? = null
|
||||
override val baseEnum: FbItem? = null
|
||||
|
||||
private inline val userId: Long
|
||||
get() = intent.getLongExtra(ARG_USER_ID, prefs.userId)
|
||||
private inline val userId: Long
|
||||
get() = intent.getLongExtra(ARG_USER_ID, prefs.userId)
|
||||
|
||||
private val overlayContext: OverlayContext?
|
||||
get() = OverlayContext[intent.extras]
|
||||
private val overlayContext: OverlayContext?
|
||||
get() = OverlayContext[intent.extras]
|
||||
|
||||
override fun setTitle(title: String) {
|
||||
toolbar.title = title
|
||||
override fun setTitle(title: String) {
|
||||
toolbar.title = title
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (urlTest == null) {
|
||||
L.e { "Empty link on web overlay" }
|
||||
toast(R.string.null_url_overlay)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
setFrameContentView(R.layout.activity_web_overlay)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
toolbar.navigationIcon =
|
||||
GoogleMaterial.Icon.gmd_close.toDrawable(this, 16, themeProvider.iconColor)
|
||||
toolbar.setNavigationOnClickListener { finishSlideOut() }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (urlTest == null) {
|
||||
L.e { "Empty link on web overlay" }
|
||||
toast(R.string.null_url_overlay)
|
||||
finish()
|
||||
return
|
||||
activityThemer.setFrostColors {
|
||||
toolbar(toolbar)
|
||||
themeWindow = false
|
||||
}
|
||||
coordinator.setBackgroundColor(themeProvider.bgColor.withAlpha(255))
|
||||
|
||||
content.bind(this)
|
||||
|
||||
content.titleFlow.onEach { toolbar.title = it }.launchIn(this)
|
||||
|
||||
with(web) {
|
||||
userAgentString = userAgent
|
||||
prefs.prevId = prefs.userId
|
||||
launch {
|
||||
val authDefer = BiometricUtils.authenticate(this@WebOverlayActivityBase, prefs)
|
||||
if (userId != prefs.userId) {
|
||||
fbCookie.switchUser(userId)
|
||||
}
|
||||
setFrameContentView(R.layout.activity_web_overlay)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
toolbar.navigationIcon =
|
||||
GoogleMaterial.Icon.gmd_close.toDrawable(this, 16, themeProvider.iconColor)
|
||||
toolbar.setNavigationOnClickListener { finishSlideOut() }
|
||||
|
||||
activityThemer.setFrostColors {
|
||||
toolbar(toolbar)
|
||||
themeWindow = false
|
||||
}
|
||||
coordinator.setBackgroundColor(themeProvider.bgColor.withAlpha(255))
|
||||
|
||||
content.bind(this)
|
||||
|
||||
content.titleFlow.onEach { toolbar.title = it }.launchIn(this)
|
||||
|
||||
with(web) {
|
||||
userAgentString = userAgent
|
||||
prefs.prevId = prefs.userId
|
||||
launch {
|
||||
val authDefer = BiometricUtils.authenticate(this@WebOverlayActivityBase, prefs)
|
||||
if (userId != prefs.userId) {
|
||||
fbCookie.switchUser(userId)
|
||||
}
|
||||
authDefer.await()
|
||||
reloadBase(true)
|
||||
if (prefs.firstWebOverlay) {
|
||||
coordinator.frostSnackbar(R.string.web_overlay_swipe_hint, themeProvider) {
|
||||
duration = BaseTransientBottomBar.LENGTH_INDEFINITE
|
||||
setAction(R.string.kau_got_it) { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
swipeBack = kauSwipeOnCreate {
|
||||
if (!prefs.overlayFullScreenSwipe) edgeSize = 20.dpToPx
|
||||
transitionSystemBars = false
|
||||
authDefer.await()
|
||||
reloadBase(true)
|
||||
if (prefs.firstWebOverlay) {
|
||||
coordinator.frostSnackbar(R.string.web_overlay_swipe_hint, themeProvider) {
|
||||
duration = BaseTransientBottomBar.LENGTH_INDEFINITE
|
||||
setAction(R.string.kau_got_it) { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
L.d { "New intent" }
|
||||
val newUrl = (intent.getStringExtra(ARG_URL) ?: intent.dataString)?.formattedFbUrl ?: return
|
||||
if (baseUrl != newUrl) {
|
||||
this.intent = intent
|
||||
content.baseUrl = newUrl
|
||||
web.reloadBase(true)
|
||||
}
|
||||
swipeBack = kauSwipeOnCreate {
|
||||
if (!prefs.overlayFullScreenSwipe) edgeSize = 20.dpToPx
|
||||
transitionSystemBars = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun backConsumer(): Boolean {
|
||||
if (!web.onBackPressed())
|
||||
finishSlideOut()
|
||||
return true
|
||||
/**
|
||||
* 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)
|
||||
L.d { "New intent" }
|
||||
val newUrl = (intent.getStringExtra(ARG_URL) ?: intent.dataString)?.formattedFbUrl ?: return
|
||||
if (baseUrl != newUrl) {
|
||||
this.intent = intent
|
||||
content.baseUrl = newUrl
|
||||
web.reloadBase(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Our theme for the overlay should be fully opaque
|
||||
*/
|
||||
fun theme() {
|
||||
val opaqueAccent = themeProvider.headerColor.withAlpha(255)
|
||||
statusBarColor = opaqueAccent.darken()
|
||||
navigationBarColor = opaqueAccent
|
||||
toolbar.setBackgroundColor(opaqueAccent)
|
||||
toolbar.setTitleTextColor(themeProvider.iconColor)
|
||||
coordinator.setBackgroundColor(themeProvider.bgColor.withAlpha(255))
|
||||
toolbar.overflowIcon?.setTint(themeProvider.iconColor)
|
||||
override fun backConsumer(): Boolean {
|
||||
if (!web.onBackPressed()) finishSlideOut()
|
||||
return true
|
||||
}
|
||||
|
||||
/** Our theme for the overlay should be fully opaque */
|
||||
fun theme() {
|
||||
val opaqueAccent = themeProvider.headerColor.withAlpha(255)
|
||||
statusBarColor = opaqueAccent.darken()
|
||||
navigationBarColor = opaqueAccent
|
||||
toolbar.setBackgroundColor(opaqueAccent)
|
||||
toolbar.setTitleTextColor(themeProvider.iconColor)
|
||||
coordinator.setBackgroundColor(themeProvider.bgColor.withAlpha(255))
|
||||
toolbar.overflowIcon?.setTint(themeProvider.iconColor)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
web.resumeTimers()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
web.pauseTimers()
|
||||
L.v { "Pause overlay web timers" }
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
web.destroy()
|
||||
super.onDestroy()
|
||||
kauSwipeOnDestroy()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (webFileChooser.onActivityResultWeb(requestCode, resultCode, data)) return
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_web, menu)
|
||||
overlayContext?.onMenuCreate(this, menu)
|
||||
toolbar.tint(themeProvider.iconColor)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val url = web.currentUrl.formattedFbUrl
|
||||
when (item.itemId) {
|
||||
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))
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
web.resumeTimers()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
web.pauseTimers()
|
||||
L.v { "Pause overlay web timers" }
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
web.destroy()
|
||||
super.onDestroy()
|
||||
kauSwipeOnDestroy()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (webFileChooser.onActivityResultWeb(requestCode, resultCode, data)) return
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_web, menu)
|
||||
overlayContext?.onMenuCreate(this, menu)
|
||||
toolbar.tint(themeProvider.iconColor)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val url = web.currentUrl.formattedFbUrl
|
||||
when (item.itemId) {
|
||||
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))
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------
|
||||
* Video Contract
|
||||
* ----------------------------------------------------
|
||||
*/
|
||||
override var videoViewer: FrostVideoViewer? = null
|
||||
override val lowerVideoPadding: PointF = PointF(0f, 0f)
|
||||
/*
|
||||
* ----------------------------------------------------
|
||||
* Video Contract
|
||||
* ----------------------------------------------------
|
||||
*/
|
||||
override var videoViewer: FrostVideoViewer? = null
|
||||
override val lowerVideoPadding: PointF = PointF(0f, 0f)
|
||||
}
|
||||
|
@ -22,24 +22,22 @@ import com.pitchedapps.frost.web.FrostEmitter
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
interface MainActivityContract : MainFabContract {
|
||||
val fragmentFlow: SharedFlow<Int>
|
||||
val fragmentEmit: FrostEmitter<Int>
|
||||
val fragmentFlow: SharedFlow<Int>
|
||||
val fragmentEmit: FrostEmitter<Int>
|
||||
|
||||
val headerFlow: SharedFlow<String>
|
||||
val headerEmit: FrostEmitter<String>
|
||||
val headerFlow: SharedFlow<String>
|
||||
val headerEmit: FrostEmitter<String>
|
||||
|
||||
fun setTitle(res: Int)
|
||||
fun setTitle(text: CharSequence)
|
||||
fun setTitle(res: Int)
|
||||
fun setTitle(text: CharSequence)
|
||||
|
||||
/**
|
||||
* Available on all threads
|
||||
*/
|
||||
fun collapseAppBar()
|
||||
/** Available on all threads */
|
||||
fun collapseAppBar()
|
||||
|
||||
fun reloadFragment(fragment: BaseFragment)
|
||||
fun reloadFragment(fragment: BaseFragment)
|
||||
}
|
||||
|
||||
interface MainFabContract {
|
||||
fun showFab(iicon: IIcon, clickEvent: () -> Unit)
|
||||
fun hideFab()
|
||||
fun showFab(iicon: IIcon, clickEvent: () -> Unit)
|
||||
fun hideFab()
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
fun reloadTheme()
|
||||
/** Change all necessary view components to the new theme Also propagate where applicable */
|
||||
fun reloadTheme()
|
||||
|
||||
/**
|
||||
* Change theme without propagation
|
||||
*/
|
||||
fun reloadThemeSelf()
|
||||
/** Change theme without propagation */
|
||||
fun reloadThemeSelf()
|
||||
|
||||
/**
|
||||
* Change text size & propagate
|
||||
*/
|
||||
fun reloadTextSize()
|
||||
/** Change text size & propagate */
|
||||
fun reloadTextSize()
|
||||
|
||||
/**
|
||||
* Change text size without propagation
|
||||
*/
|
||||
fun reloadTextSizeSelf()
|
||||
/** Change text size without propagation */
|
||||
fun reloadTextSizeSelf()
|
||||
}
|
||||
|
@ -35,70 +35,58 @@ 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 {
|
||||
fun openMediaPicker(
|
||||
filePathCallback: ValueCallback<Array<Uri>?>,
|
||||
fileChooserParams: WebChromeClient.FileChooserParams
|
||||
)
|
||||
fun openMediaPicker(
|
||||
filePathCallback: ValueCallback<Array<Uri>?>,
|
||||
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 {
|
||||
private var filePathCallback: ValueCallback<Array<Uri>?>? = null
|
||||
class WebFileChooserImpl
|
||||
@Inject
|
||||
internal constructor(private val activity: Activity, private val themeProvider: ThemeProvider) :
|
||||
WebFileChooser {
|
||||
private var filePathCallback: ValueCallback<Array<Uri>?>? = null
|
||||
|
||||
override fun openMediaPicker(
|
||||
filePathCallback: ValueCallback<Array<Uri>?>,
|
||||
fileChooserParams: WebChromeClient.FileChooserParams
|
||||
) {
|
||||
activity.kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ ->
|
||||
if (!granted) {
|
||||
L.d { "Failed to get write permissions" }
|
||||
activity.frostSnackbar(R.string.file_chooser_not_found, themeProvider)
|
||||
filePathCallback.onReceiveValue(null)
|
||||
return@kauRequestPermissions
|
||||
}
|
||||
this.filePathCallback = filePathCallback
|
||||
val intent = Intent()
|
||||
intent.type = fileChooserParams.acceptTypes.firstOrNull()
|
||||
intent.action = Intent.ACTION_GET_CONTENT
|
||||
activity.startActivityForResult(
|
||||
Intent.createChooser(intent, activity.string(R.string.pick_image)),
|
||||
MEDIA_CHOOSER_RESULT
|
||||
)
|
||||
}
|
||||
override fun openMediaPicker(
|
||||
filePathCallback: ValueCallback<Array<Uri>?>,
|
||||
fileChooserParams: WebChromeClient.FileChooserParams
|
||||
) {
|
||||
activity.kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ ->
|
||||
if (!granted) {
|
||||
L.d { "Failed to get write permissions" }
|
||||
activity.frostSnackbar(R.string.file_chooser_not_found, themeProvider)
|
||||
filePathCallback.onReceiveValue(null)
|
||||
return@kauRequestPermissions
|
||||
}
|
||||
this.filePathCallback = filePathCallback
|
||||
val intent = Intent()
|
||||
intent.type = fileChooserParams.acceptTypes.firstOrNull()
|
||||
intent.action = Intent.ACTION_GET_CONTENT
|
||||
activity.startActivityForResult(
|
||||
Intent.createChooser(intent, activity.string(R.string.pick_image)),
|
||||
MEDIA_CHOOSER_RESULT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
filePathCallback?.onReceiveValue(if (data != null) arrayOf(data) else null)
|
||||
filePathCallback = null
|
||||
return true
|
||||
}
|
||||
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
|
||||
filePathCallback?.onReceiveValue(if (data != null) arrayOf(data) else null)
|
||||
filePathCallback = null
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(ActivityComponent::class)
|
||||
interface WebFileChooserModule {
|
||||
@Binds
|
||||
@ActivityScoped
|
||||
fun webFileChooser(to: WebFileChooserImpl): WebFileChooser
|
||||
@Binds @ActivityScoped fun webFileChooser(to: WebFileChooserImpl): WebFileChooser
|
||||
}
|
||||
|
@ -22,159 +22,112 @@ 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 baseUrl: String
|
||||
|
||||
val baseEnum: FbItem?
|
||||
val baseEnum: FbItem?
|
||||
|
||||
/**
|
||||
* Update toolbar title
|
||||
*/
|
||||
fun setTitle(title: String)
|
||||
/** 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 scope: CoroutineScope
|
||||
|
||||
val core: FrostContentCore
|
||||
val core: FrostContentCore
|
||||
|
||||
/**
|
||||
* Observable to get data on whether view is refreshing or not
|
||||
*/
|
||||
val refreshFlow: SharedFlow<Boolean>
|
||||
/** Observable to get data on whether view is refreshing or not */
|
||||
val refreshFlow: SharedFlow<Boolean>
|
||||
|
||||
val refreshEmit: FrostEmitter<Boolean>
|
||||
val refreshEmit: FrostEmitter<Boolean>
|
||||
|
||||
/**
|
||||
* Observable to get data on refresh progress, with range [0, 100]
|
||||
*/
|
||||
val progressFlow: SharedFlow<Int>
|
||||
/** Observable to get data on refresh progress, with range [0, 100] */
|
||||
val progressFlow: SharedFlow<Int>
|
||||
|
||||
val progressEmit: FrostEmitter<Int>
|
||||
val progressEmit: FrostEmitter<Int>
|
||||
|
||||
/**
|
||||
* Observable to get new title data (unique values only)
|
||||
*/
|
||||
val titleFlow: SharedFlow<String>
|
||||
/** Observable to get new title data (unique values only) */
|
||||
val titleFlow: SharedFlow<String>
|
||||
|
||||
val titleEmit: FrostEmitter<String>
|
||||
val titleEmit: FrostEmitter<String>
|
||||
|
||||
var baseUrl: String
|
||||
var baseUrl: String
|
||||
|
||||
var baseEnum: FbItem?
|
||||
var baseEnum: FbItem?
|
||||
|
||||
val swipeEnabled: Boolean get() = swipeAllowedByPage && !swipeDisabledByAction
|
||||
val swipeEnabled: Boolean
|
||||
get() = swipeAllowedByPage && !swipeDisabledByAction
|
||||
|
||||
/**
|
||||
* Temporary disable swiping based on action
|
||||
*/
|
||||
var swipeDisabledByAction: Boolean
|
||||
/** Temporary disable swiping based on action */
|
||||
var swipeDisabledByAction: Boolean
|
||||
|
||||
/**
|
||||
* Decides if swipe should be allowed for the current page
|
||||
*/
|
||||
var swipeAllowedByPage: Boolean
|
||||
/** 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!
|
||||
*/
|
||||
fun bind(container: FrostContentContainer)
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
fun destroy()
|
||||
/** 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
|
||||
*
|
||||
* 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
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
val scope: CoroutineScope
|
||||
get() = parent.scope
|
||||
|
||||
/**
|
||||
* Reference to parent
|
||||
* Bound through calling [FrostContentParent.bind]
|
||||
*/
|
||||
val parent: FrostContentParent
|
||||
/** 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
|
||||
*/
|
||||
fun bind(parent: FrostContentParent, container: FrostContentContainer): View
|
||||
/**
|
||||
* Initializes view through given [container]
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
fun reload(animate: Boolean)
|
||||
/** Call to reload wrapped data */
|
||||
fun reload(animate: Boolean)
|
||||
|
||||
/**
|
||||
* Call to reload base data
|
||||
*/
|
||||
fun reloadBase(animate: Boolean)
|
||||
/** Call to reload base data */
|
||||
fun reloadBase(animate: Boolean)
|
||||
|
||||
/**
|
||||
* If possible, remove anything in the view stack
|
||||
* Applies namely to webviews
|
||||
*/
|
||||
fun clearHistory()
|
||||
/** 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
|
||||
*/
|
||||
fun onBackPressed(): Boolean
|
||||
/**
|
||||
* Should be called when a back press is triggered Return [true] if consumed, [false] otherwise
|
||||
*/
|
||||
fun onBackPressed(): Boolean
|
||||
|
||||
val currentUrl: String
|
||||
val currentUrl: String
|
||||
|
||||
/**
|
||||
* Condition to help pause certain background resources
|
||||
*/
|
||||
var active: Boolean
|
||||
/** Condition to help pause certain background resources */
|
||||
var active: Boolean
|
||||
|
||||
/**
|
||||
* Triggered when view is within viewpager
|
||||
* and tab is clicked
|
||||
*/
|
||||
fun onTabClicked()
|
||||
/** Triggered when view is within viewpager and tab is clicked */
|
||||
fun onTabClicked()
|
||||
|
||||
/**
|
||||
* Signal destruction to release some content manually
|
||||
*/
|
||||
fun destroy()
|
||||
/** Signal destruction to release some content manually */
|
||||
fun destroy()
|
||||
}
|
||||
|
@ -22,23 +22,23 @@ 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]
|
||||
*/
|
||||
fun reloadTheme()
|
||||
/**
|
||||
* Change all necessary view components to the new theme and call whatever other children that
|
||||
* also implement [FrostThemable]
|
||||
*/
|
||||
fun reloadTheme()
|
||||
|
||||
fun setTextColors(color: Int, vararg textViews: TextView?) =
|
||||
themeViews(color, *textViews) { setTextColor(it) }
|
||||
fun setTextColors(color: Int, vararg textViews: TextView?) =
|
||||
themeViews(color, *textViews) { setTextColor(it) }
|
||||
|
||||
fun setBackgrounds(color: Int, vararg views: View?) =
|
||||
themeViews(color, *views) { setBackgroundColor(it) }
|
||||
fun setBackgrounds(color: Int, vararg views: View?) =
|
||||
themeViews(color, *views) { setBackgroundColor(it) }
|
||||
|
||||
fun <T : View> themeViews(color: Int, vararg views: T?, action: T.(Int) -> Unit) =
|
||||
views.filterNotNull().forEach { it.action(color) }
|
||||
fun <T : View> themeViews(color: Int, vararg views: T?, action: T.(Int) -> Unit) =
|
||||
views.filterNotNull().forEach { it.action(color) }
|
||||
}
|
||||
|
@ -24,45 +24,38 @@ 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?
|
||||
var videoViewer: FrostVideoViewer?
|
||||
|
||||
fun showVideo(url: String) = showVideo(url, false)
|
||||
fun showVideo(url: String) = showVideo(url, false)
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
/** 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)
|
||||
}
|
||||
|
||||
fun videoOnStop() = videoViewer?.pause()
|
||||
fun videoOnStop() = videoViewer?.pause()
|
||||
|
||||
fun videoOnBackPress() = videoViewer?.onBackPressed() ?: false
|
||||
fun videoOnBackPress() = videoViewer?.onBackPressed() ?: false
|
||||
|
||||
override val videoContainer: FrameLayout
|
||||
get() = frameWrapper
|
||||
override val videoContainer: FrameLayout
|
||||
get() = frameWrapper
|
||||
|
||||
override fun onVideoFinished() {
|
||||
L.d { "Video view released" }
|
||||
videoViewer = null
|
||||
}
|
||||
override fun onVideoFinished() {
|
||||
L.d { "Video view released" }
|
||||
videoViewer = null
|
||||
}
|
||||
}
|
||||
|
||||
interface FrameWrapper {
|
||||
|
||||
val frameWrapper: FrameLayout
|
||||
val frameWrapper: FrameLayout
|
||||
|
||||
fun Activity.setFrameContentView(layoutRes: Int) {
|
||||
setContentView(R.layout.activity_frame_wrapper)
|
||||
frameWrapper.inflate(layoutRes, true)
|
||||
}
|
||||
fun Activity.setFrameContentView(layoutRes: Int) {
|
||||
setContentView(R.layout.activity_frame_wrapper)
|
||||
frameWrapper.inflate(layoutRes, true)
|
||||
}
|
||||
}
|
||||
|
@ -26,63 +26,52 @@ 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 = [
|
||||
ForeignKey(
|
||||
entity = CookieEntity::class,
|
||||
parentColumns = ["cookie_id"],
|
||||
childColumns = ["id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
tableName = "frost_cache",
|
||||
primaryKeys = ["id", "type"],
|
||||
foreignKeys =
|
||||
[
|
||||
ForeignKey(
|
||||
entity = CookieEntity::class,
|
||||
parentColumns = ["cookie_id"],
|
||||
childColumns = ["id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)]
|
||||
)
|
||||
@Parcelize
|
||||
data class CacheEntity(
|
||||
val id: Long,
|
||||
val type: String,
|
||||
val lastUpdated: Long,
|
||||
val contents: String
|
||||
val id: Long,
|
||||
val type: String,
|
||||
val lastUpdated: Long,
|
||||
val contents: String
|
||||
) : Parcelable
|
||||
|
||||
@Dao
|
||||
interface CacheDao {
|
||||
|
||||
@Query("SELECT * FROM frost_cache WHERE id = :id AND type = :type")
|
||||
fun _select(id: Long, type: String): CacheEntity?
|
||||
@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)
|
||||
@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))
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "Cache save failed for $type" }
|
||||
false
|
||||
}
|
||||
try {
|
||||
_insertCache(CacheEntity(id, type, System.currentTimeMillis(), contents))
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "Cache save failed for $type" }
|
||||
false
|
||||
}
|
||||
}
|
||||
|
@ -28,59 +28,57 @@ 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,
|
||||
val name: String?,
|
||||
val cookie: String?,
|
||||
val cookieMessenger: String? = null // Version 2
|
||||
@androidx.room.PrimaryKey @ColumnInfo(name = "cookie_id") val id: Long,
|
||||
val name: String?,
|
||||
val cookie: String?,
|
||||
val cookieMessenger: String? = null // Version 2
|
||||
) : Parcelable {
|
||||
override fun toString(): String = "CookieEntity(${hashCode()})"
|
||||
override fun toString(): String = "CookieEntity(${hashCode()})"
|
||||
|
||||
fun toSensitiveString(): String =
|
||||
"CookieEntity(id=$id, name=$name, cookie=$cookie cookieMessenger=$cookieMessenger)"
|
||||
fun toSensitiveString(): String =
|
||||
"CookieEntity(id=$id, name=$name, cookie=$cookie cookieMessenger=$cookieMessenger)"
|
||||
}
|
||||
|
||||
@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?)
|
||||
@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) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE cookies ADD COLUMN cookieMessenger TEXT")
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
@ -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() }
|
||||
|
@ -29,98 +29,100 @@ import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
interface FrostPrivateDao {
|
||||
fun cookieDao(): CookieDao
|
||||
fun notifDao(): NotificationDao
|
||||
fun cacheDao(): CacheDao
|
||||
fun cookieDao(): CookieDao
|
||||
fun notifDao(): NotificationDao
|
||||
fun cacheDao(): CacheDao
|
||||
}
|
||||
|
||||
@Database(
|
||||
entities = [CookieEntity::class, NotificationEntity::class, CacheEntity::class],
|
||||
version = 2,
|
||||
exportSchema = true
|
||||
entities = [CookieEntity::class, NotificationEntity::class, CacheEntity::class],
|
||||
version = 2,
|
||||
exportSchema = true
|
||||
)
|
||||
abstract class FrostPrivateDatabase : RoomDatabase(), FrostPrivateDao {
|
||||
companion object {
|
||||
const val DATABASE_NAME = "frost-priv-db"
|
||||
}
|
||||
companion object {
|
||||
const val DATABASE_NAME = "frost-priv-db"
|
||||
}
|
||||
}
|
||||
|
||||
interface FrostPublicDao {
|
||||
fun genericDao(): GenericDao
|
||||
fun genericDao(): GenericDao
|
||||
}
|
||||
|
||||
@Database(entities = [GenericEntity::class], version = 1, exportSchema = true)
|
||||
abstract class FrostPublicDatabase : RoomDatabase(), FrostPublicDao {
|
||||
companion object {
|
||||
const val DATABASE_NAME = "frost-db"
|
||||
}
|
||||
companion object {
|
||||
const val DATABASE_NAME = "frost-db"
|
||||
}
|
||||
}
|
||||
|
||||
interface FrostDao : FrostPrivateDao, FrostPublicDao {
|
||||
fun close()
|
||||
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 {
|
||||
private val privateDb: FrostPrivateDatabase,
|
||||
private val publicDb: FrostPublicDatabase
|
||||
) : FrostDao, FrostPrivateDao by privateDb, FrostPublicDao by publicDb {
|
||||
|
||||
override fun close() {
|
||||
privateDb.close()
|
||||
publicDb.close()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private fun <T : RoomDatabase> RoomDatabase.Builder<T>.frostBuild() =
|
||||
if (BuildConfig.DEBUG) {
|
||||
fallbackToDestructiveMigration().build()
|
||||
} else {
|
||||
build()
|
||||
}
|
||||
|
||||
fun create(context: Context): FrostDatabase {
|
||||
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,
|
||||
FrostPublicDatabase.DATABASE_NAME
|
||||
).frostBuild()
|
||||
return FrostDatabase(privateDb, publicDb)
|
||||
}
|
||||
override fun close() {
|
||||
privateDb.close()
|
||||
publicDb.close()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private fun <T : RoomDatabase> RoomDatabase.Builder<T>.frostBuild() =
|
||||
if (BuildConfig.DEBUG) {
|
||||
fallbackToDestructiveMigration().build()
|
||||
} else {
|
||||
build()
|
||||
}
|
||||
|
||||
fun create(context: Context): FrostDatabase {
|
||||
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,
|
||||
FrostPublicDatabase.DATABASE_NAME
|
||||
)
|
||||
.frostBuild()
|
||||
return FrostDatabase(privateDb, publicDb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun frostDatabase(@ApplicationContext context: Context): FrostDatabase =
|
||||
FrostDatabase.create(context)
|
||||
@Provides
|
||||
@Singleton
|
||||
fun frostDatabase(@ApplicationContext context: Context): FrostDatabase =
|
||||
FrostDatabase.create(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun cookieDao(frostDatabase: FrostDatabase): CookieDao = frostDatabase.cookieDao()
|
||||
@Provides
|
||||
@Singleton
|
||||
fun cookieDao(frostDatabase: FrostDatabase): CookieDao = frostDatabase.cookieDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun cacheDao(frostDatabase: FrostDatabase): CacheDao = frostDatabase.cacheDao()
|
||||
@Provides
|
||||
@Singleton
|
||||
fun cacheDao(frostDatabase: FrostDatabase): CacheDao = frostDatabase.cacheDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun notifDao(frostDatabase: FrostDatabase): NotificationDao = frostDatabase.notifDao()
|
||||
@Provides
|
||||
@Singleton
|
||||
fun notifDao(frostDatabase: FrostDatabase): NotificationDao = frostDatabase.notifDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun genericDao(frostDatabase: FrostDatabase): GenericDao = frostDatabase.genericDao()
|
||||
@Provides
|
||||
@Singleton
|
||||
fun genericDao(frostDatabase: FrostDatabase): GenericDao = frostDatabase.genericDao()
|
||||
}
|
||||
|
@ -25,49 +25,35 @@ 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"
|
||||
}
|
||||
companion object {
|
||||
const val TYPE_TABS = "generic_tabs"
|
||||
}
|
||||
}
|
||||
|
||||
const val TAB_COUNT = 4
|
||||
|
||||
suspend fun GenericDao.saveTabs(tabs: List<FbItem>) = dao {
|
||||
val content = tabs.joinToString(",") { it.name }
|
||||
_save(GenericEntity(GenericDao.TYPE_TABS, content))
|
||||
val content = tabs.joinToString(",") { it.name }
|
||||
_save(GenericEntity(GenericDao.TYPE_TABS, content))
|
||||
}
|
||||
|
||||
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() }
|
||||
?: defaultTabs()
|
||||
val allTabs = FbItem.values.map { it.name to it }.toMap()
|
||||
_select(GenericDao.TYPE_TABS)?.split(",")?.mapNotNull { allTabs[it] }?.takeIf { it.isNotEmpty() }
|
||||
?: defaultTabs()
|
||||
}
|
||||
|
@ -30,127 +30,120 @@ import com.pitchedapps.frost.services.NotificationContent
|
||||
import com.pitchedapps.frost.utils.L
|
||||
|
||||
@Entity(
|
||||
tableName = "notifications",
|
||||
primaryKeys = ["notif_id", "userId"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = CookieEntity::class,
|
||||
parentColumns = ["cookie_id"],
|
||||
childColumns = ["userId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [Index("notif_id"), Index("userId")]
|
||||
tableName = "notifications",
|
||||
primaryKeys = ["notif_id", "userId"],
|
||||
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,
|
||||
val userId: Long,
|
||||
val href: String,
|
||||
val title: String?,
|
||||
val text: String,
|
||||
val timestamp: Long,
|
||||
val profileUrl: String?,
|
||||
// Type essentially refers to channel
|
||||
val type: String,
|
||||
val unread: Boolean
|
||||
@ColumnInfo(name = "notif_id") val id: Long,
|
||||
val userId: Long,
|
||||
val href: String,
|
||||
val title: String?,
|
||||
val text: String,
|
||||
val timestamp: Long,
|
||||
val profileUrl: String?,
|
||||
// Type essentially refers to channel
|
||||
val type: String,
|
||||
val unread: Boolean
|
||||
) {
|
||||
constructor(
|
||||
type: String,
|
||||
content: NotificationContent
|
||||
) : this(
|
||||
content.id,
|
||||
content.data.id,
|
||||
content.href,
|
||||
content.title,
|
||||
content.text,
|
||||
content.timestamp,
|
||||
content.profileUrl,
|
||||
type,
|
||||
content.unread
|
||||
)
|
||||
constructor(
|
||||
type: String,
|
||||
content: NotificationContent
|
||||
) : this(
|
||||
content.id,
|
||||
content.data.id,
|
||||
content.href,
|
||||
content.title,
|
||||
content.text,
|
||||
content.timestamp,
|
||||
content.profileUrl,
|
||||
type,
|
||||
content.unread
|
||||
)
|
||||
}
|
||||
|
||||
data class NotificationContentEntity(
|
||||
@Embedded
|
||||
val cookie: CookieEntity,
|
||||
@Embedded
|
||||
val notif: NotificationEntity
|
||||
@Embedded val cookie: CookieEntity,
|
||||
@Embedded val notif: NotificationEntity
|
||||
) {
|
||||
fun toNotifContent() = NotificationContent(
|
||||
data = cookie,
|
||||
id = notif.id,
|
||||
href = notif.href,
|
||||
title = notif.title,
|
||||
text = notif.text,
|
||||
timestamp = notif.timestamp,
|
||||
profileUrl = notif.profileUrl,
|
||||
unread = notif.unread
|
||||
fun toNotifContent() =
|
||||
NotificationContent(
|
||||
data = cookie,
|
||||
id = notif.id,
|
||||
href = notif.href,
|
||||
title = notif.title,
|
||||
text = notif.text,
|
||||
timestamp = notif.timestamp,
|
||||
profileUrl = notif.profileUrl,
|
||||
unread = notif.unread
|
||||
)
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface NotificationDao {
|
||||
|
||||
/**
|
||||
* 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")
|
||||
fun _selectNotifications(userId: Long, type: String): List<NotificationContentEntity>
|
||||
/** 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"
|
||||
)
|
||||
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")
|
||||
fun _selectEpoch(userId: Long, type: String): Long?
|
||||
@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)
|
||||
fun _insertNotifications(notifs: List<NotificationEntity>)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun _insertNotifications(notifs: List<NotificationEntity>)
|
||||
|
||||
@Query("DELETE FROM notifications WHERE userId = :userId AND type = :type")
|
||||
fun _deleteNotifications(userId: Long, type: String)
|
||||
@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
|
||||
*/
|
||||
@Transaction
|
||||
fun _saveNotifications(type: String, notifs: List<NotificationContent>) {
|
||||
val userId = notifs.firstOrNull()?.data?.id ?: return
|
||||
val entities = notifs.map { NotificationEntity(type, it) }
|
||||
_deleteNotifications(userId, type)
|
||||
_insertNotifications(entities)
|
||||
}
|
||||
/** 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
|
||||
val entities = notifs.map { NotificationEntity(type, it) }
|
||||
_deleteNotifications(userId, type)
|
||||
_insertNotifications(entities)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun NotificationDao.deleteAll() = dao { _deleteAll() }
|
||||
|
||||
fun NotificationDao.selectNotificationsSync(userId: Long, type: String): List<NotificationContent> =
|
||||
_selectNotifications(userId, type).map { it.toNotifContent() }
|
||||
_selectNotifications(userId, type).map { it.toNotifContent() }
|
||||
|
||||
suspend fun NotificationDao.selectNotifications(
|
||||
userId: Long,
|
||||
type: String
|
||||
): List<NotificationContent> = dao {
|
||||
selectNotificationsSync(userId, type)
|
||||
}
|
||||
userId: Long,
|
||||
type: String
|
||||
): 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>
|
||||
type: String,
|
||||
notifs: List<NotificationContent>
|
||||
): Boolean = dao {
|
||||
try {
|
||||
_saveNotifications(type, notifs)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "Notif save failed for $type" }
|
||||
false
|
||||
}
|
||||
try {
|
||||
_saveNotifications(type, notifs)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "Notif save failed for $type" }
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun NotificationDao.latestEpoch(userId: Long, type: String): Long = dao {
|
||||
_selectEpoch(userId, type) ?: -1L
|
||||
_selectEpoch(userId, type) ?: -1L
|
||||
}
|
||||
|
@ -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.
|
||||
@ -52,281 +52,271 @@ import java.util.zip.ZipOutputStream
|
||||
* Inspired by <a href="https://github.com/JonasCz/save-for-offline">Save for Offline</a>
|
||||
*/
|
||||
class OfflineWebsite(
|
||||
private val url: String,
|
||||
private val cookie: String = "",
|
||||
baseUrl: String? = null,
|
||||
private val html: String? = null,
|
||||
/**
|
||||
* Directory that holds all the files
|
||||
*/
|
||||
val baseDir: File,
|
||||
private val userAgent: String = USER_AGENT
|
||||
private val url: String,
|
||||
private val cookie: String = "",
|
||||
baseUrl: String? = null,
|
||||
private val html: String? = null,
|
||||
/** 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}"
|
||||
}
|
||||
|
||||
private val mainFile = File(baseDir, "index.html")
|
||||
private val assetDir = File(baseDir, "assets")
|
||||
|
||||
private val urlMapper = ConcurrentHashMap<String, String>()
|
||||
private val atomicInt = AtomicInteger()
|
||||
|
||||
private val L = KauLoggerExtension("Offline", com.pitchedapps.frost.utils.L)
|
||||
|
||||
init {
|
||||
if (!this.baseUrl.startsWith("http"))
|
||||
throw IllegalArgumentException("Base Url must start with http")
|
||||
}
|
||||
|
||||
private val fileQueue = mutableSetOf<String>()
|
||||
|
||||
private val cssQueue = mutableSetOf<String>()
|
||||
|
||||
private fun request(url: String) =
|
||||
Request.Builder().header("Cookie", cookie).header("User-Agent", userAgent).url(url).get().call()
|
||||
|
||||
/**
|
||||
* Caller to bind callbacks and start the load Callback is guaranteed to be called unless the load
|
||||
* is cancelled
|
||||
*/
|
||||
suspend fun load(progress: (Int) -> Unit = {}): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
reset()
|
||||
|
||||
L.v { "Saving $url to ${baseDir.absolutePath}" }
|
||||
|
||||
if (!baseDir.isDirectory && !baseDir.mkdirs()) {
|
||||
L.e { "Could not make directory" }
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
if (!mainFile.createNewFile()) {
|
||||
L.e { "Could not create ${mainFile.absolutePath}" }
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
if (!assetDir.createFreshDir()) {
|
||||
L.e { "Could not create ${assetDir.absolutePath}" }
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
progress(10)
|
||||
|
||||
yield()
|
||||
|
||||
val doc: Document
|
||||
if (html == null || html.length < 100) {
|
||||
doc = frostJsoup(cookie, url)
|
||||
} else {
|
||||
doc = Jsoup.parse("<html>${html.unescapeHtml()}</html>")
|
||||
L.d { "Building data from supplied content of size ${html.length}" }
|
||||
}
|
||||
doc.setBaseUri(baseUrl)
|
||||
doc.outputSettings().escapeMode(Entities.EscapeMode.extended)
|
||||
if (doc.childNodeSize() == 0) {
|
||||
L.e { "No content found" }
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
yield()
|
||||
|
||||
progress(35)
|
||||
|
||||
doc.collect("link[href][rel=stylesheet]", "href", cssQueue)
|
||||
doc.collect("link[href]:not([rel=stylesheet])", "href", fileQueue)
|
||||
doc.collect("img[src]", "src", fileQueue)
|
||||
doc.collect("img[data-canonical-src]", "data-canonical-src", fileQueue)
|
||||
doc.collect("script[src]", "src", fileQueue)
|
||||
|
||||
// make links absolute
|
||||
doc.select("a[href]").forEach {
|
||||
val absLink = it.attr("abs:href")
|
||||
it.attr("href", absLink)
|
||||
}
|
||||
|
||||
yield()
|
||||
|
||||
mainFile.writeText(doc.html())
|
||||
|
||||
progress(50)
|
||||
|
||||
fun partialProgress(from: Int, to: Int, steps: Int): (Int) -> Unit {
|
||||
if (steps == 0) return { progress(to) }
|
||||
val section = (to - from) / steps
|
||||
return { progress(from + it * section) }
|
||||
}
|
||||
|
||||
val cssProgress = partialProgress(50, 70, cssQueue.size)
|
||||
|
||||
cssQueue.clean().forEachIndexed { index, url ->
|
||||
yield()
|
||||
cssProgress(index)
|
||||
val newUrls = downloadCss(url)
|
||||
fileQueue.addAll(newUrls)
|
||||
}
|
||||
|
||||
progress(70)
|
||||
|
||||
val fileProgress = partialProgress(70, 100, fileQueue.size)
|
||||
|
||||
fileQueue.clean().forEachIndexed { index, url ->
|
||||
yield()
|
||||
fileProgress(index)
|
||||
if (!downloadFile(url)) return@withContext false
|
||||
}
|
||||
|
||||
yield()
|
||||
progress(100)
|
||||
return@withContext true
|
||||
}
|
||||
|
||||
private val mainFile = File(baseDir, "index.html")
|
||||
private val assetDir = File(baseDir, "assets")
|
||||
fun zip(name: String): Boolean {
|
||||
try {
|
||||
val zip = File(baseDir, "$name.zip")
|
||||
if (!zip.createFreshFile()) {
|
||||
L.e { "Failed to create zip at ${zip.absolutePath}" }
|
||||
return false
|
||||
}
|
||||
|
||||
private val urlMapper = ConcurrentHashMap<String, String>()
|
||||
private val atomicInt = AtomicInteger()
|
||||
ZipOutputStream(FileOutputStream(zip)).use { out ->
|
||||
fun File.zip(name: String = this.name) {
|
||||
if (!isFile) return
|
||||
inputStream().use { file ->
|
||||
out.putNextEntry(ZipEntry(name))
|
||||
file.copyTo(out)
|
||||
}
|
||||
out.closeEntry()
|
||||
delete()
|
||||
}
|
||||
baseDir.listFiles { file -> file != zip }?.forEach { it.zip() }
|
||||
assetDir.listFiles()?.forEach { it.zip("assets/${it.name}") }
|
||||
|
||||
private val L = KauLoggerExtension("Offline", com.pitchedapps.frost.utils.L)
|
||||
|
||||
init {
|
||||
if (!this.baseUrl.startsWith("http"))
|
||||
throw IllegalArgumentException("Base Url must start with http")
|
||||
assetDir.delete()
|
||||
}
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
L.e { "Zip failed: ${e.message}" }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private val fileQueue = mutableSetOf<String>()
|
||||
|
||||
private val cssQueue = mutableSetOf<String>()
|
||||
|
||||
private fun request(url: String) = Request.Builder()
|
||||
.header("Cookie", cookie)
|
||||
.header("User-Agent", userAgent)
|
||||
.url(url)
|
||||
.get()
|
||||
.call()
|
||||
|
||||
/**
|
||||
* Caller to bind callbacks and start the load
|
||||
* Callback is guaranteed to be called unless the load is cancelled
|
||||
*/
|
||||
suspend fun load(progress: (Int) -> Unit = {}): Boolean = withContext(Dispatchers.IO) {
|
||||
reset()
|
||||
|
||||
L.v { "Saving $url to ${baseDir.absolutePath}" }
|
||||
|
||||
if (!baseDir.isDirectory && !baseDir.mkdirs()) {
|
||||
L.e { "Could not make directory" }
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
if (!mainFile.createNewFile()) {
|
||||
L.e { "Could not create ${mainFile.absolutePath}" }
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
if (!assetDir.createFreshDir()) {
|
||||
L.e { "Could not create ${assetDir.absolutePath}" }
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
progress(10)
|
||||
|
||||
yield()
|
||||
|
||||
val doc: Document
|
||||
if (html == null || html.length < 100) {
|
||||
doc = frostJsoup(cookie, url)
|
||||
} else {
|
||||
doc = Jsoup.parse("<html>${html.unescapeHtml()}</html>")
|
||||
L.d { "Building data from supplied content of size ${html.length}" }
|
||||
}
|
||||
doc.setBaseUri(baseUrl)
|
||||
doc.outputSettings().escapeMode(Entities.EscapeMode.extended)
|
||||
if (doc.childNodeSize() == 0) {
|
||||
L.e { "No content found" }
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
yield()
|
||||
|
||||
progress(35)
|
||||
|
||||
doc.collect("link[href][rel=stylesheet]", "href", cssQueue)
|
||||
doc.collect("link[href]:not([rel=stylesheet])", "href", fileQueue)
|
||||
doc.collect("img[src]", "src", fileQueue)
|
||||
doc.collect("img[data-canonical-src]", "data-canonical-src", fileQueue)
|
||||
doc.collect("script[src]", "src", fileQueue)
|
||||
|
||||
// make links absolute
|
||||
doc.select("a[href]").forEach {
|
||||
val absLink = it.attr("abs:href")
|
||||
it.attr("href", absLink)
|
||||
}
|
||||
|
||||
yield()
|
||||
|
||||
mainFile.writeText(doc.html())
|
||||
|
||||
progress(50)
|
||||
|
||||
fun partialProgress(from: Int, to: Int, steps: Int): (Int) -> Unit {
|
||||
if (steps == 0) return { progress(to) }
|
||||
val section = (to - from) / steps
|
||||
return { progress(from + it * section) }
|
||||
}
|
||||
|
||||
val cssProgress = partialProgress(50, 70, cssQueue.size)
|
||||
|
||||
cssQueue.clean().forEachIndexed { index, url ->
|
||||
yield()
|
||||
cssProgress(index)
|
||||
val newUrls = downloadCss(url)
|
||||
fileQueue.addAll(newUrls)
|
||||
}
|
||||
|
||||
progress(70)
|
||||
|
||||
val fileProgress = partialProgress(70, 100, fileQueue.size)
|
||||
|
||||
fileQueue.clean().forEachIndexed { index, url ->
|
||||
yield()
|
||||
fileProgress(index)
|
||||
if (!downloadFile(url))
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
yield()
|
||||
suspend fun loadAndZip(name: String, progress: (Int) -> Unit = {}): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
coroutineScope {
|
||||
val success = load { progress((it * 0.85f).toInt()) }
|
||||
if (!success) return@coroutineScope false
|
||||
val result = zip(name)
|
||||
progress(100)
|
||||
return@withContext true
|
||||
return@coroutineScope result
|
||||
}
|
||||
}
|
||||
|
||||
fun zip(name: String): Boolean {
|
||||
try {
|
||||
val zip = File(baseDir, "$name.zip")
|
||||
if (!zip.createFreshFile()) {
|
||||
L.e { "Failed to create zip at ${zip.absolutePath}" }
|
||||
return false
|
||||
}
|
||||
|
||||
ZipOutputStream(FileOutputStream(zip)).use { out ->
|
||||
|
||||
fun File.zip(name: String = this.name) {
|
||||
if (!isFile) return
|
||||
inputStream().use { file ->
|
||||
out.putNextEntry(ZipEntry(name))
|
||||
file.copyTo(out)
|
||||
}
|
||||
out.closeEntry()
|
||||
delete()
|
||||
}
|
||||
baseDir.listFiles { file -> file != zip }
|
||||
?.forEach { it.zip() }
|
||||
assetDir.listFiles()
|
||||
?.forEach { it.zip("assets/${it.name}") }
|
||||
|
||||
assetDir.delete()
|
||||
}
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
L.e { "Zip failed: ${e.message}" }
|
||||
return false
|
||||
}
|
||||
private fun downloadFile(url: String): Boolean {
|
||||
return try {
|
||||
val file = File(assetDir, fileName(url))
|
||||
file.createNewFile()
|
||||
val stream =
|
||||
request(url).execute().body?.byteStream()
|
||||
?: throw IllegalArgumentException("Response body not found for $url")
|
||||
file.copyFromInputStream(stream)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "Download file failed" }
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadAndZip(name: String, progress: (Int) -> Unit = {}): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
coroutineScope {
|
||||
val success = load { progress((it * 0.85f).toInt()) }
|
||||
if (!success) return@coroutineScope false
|
||||
val result = zip(name)
|
||||
progress(100)
|
||||
return@coroutineScope result
|
||||
}
|
||||
}
|
||||
private fun downloadCss(url: String): Set<String> {
|
||||
return try {
|
||||
val file = File(assetDir, fileName(url))
|
||||
file.createNewFile()
|
||||
|
||||
private fun downloadFile(url: String): Boolean {
|
||||
return try {
|
||||
val file = File(assetDir, fileName(url))
|
||||
file.createNewFile()
|
||||
val stream = request(url).execute().body?.byteStream()
|
||||
?: throw IllegalArgumentException("Response body not found for $url")
|
||||
file.copyFromInputStream(stream)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "Download file failed" }
|
||||
false
|
||||
}
|
||||
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 {
|
||||
it.startsWith("http") -> it
|
||||
it.startsWith("/") -> "$baseUrl$it"
|
||||
else -> return@mapNotNull null
|
||||
}
|
||||
// css files are already in the asset folder,
|
||||
// so the url does not point to another subfolder
|
||||
content = content.replace(it, fileName(newUrl))
|
||||
newUrl
|
||||
}
|
||||
.toSet()
|
||||
|
||||
file.writeText(content)
|
||||
absLinks
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "Download css failed" }
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadCss(url: String): Set<String> {
|
||||
return try {
|
||||
val file = File(assetDir, fileName(url))
|
||||
file.createNewFile()
|
||||
|
||||
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 {
|
||||
it.startsWith("http") -> it
|
||||
it.startsWith("/") -> "$baseUrl$it"
|
||||
else -> return@mapNotNull null
|
||||
}
|
||||
// css files are already in the asset folder,
|
||||
// so the url does not point to another subfolder
|
||||
content = content.replace(it, fileName(newUrl))
|
||||
newUrl
|
||||
}.toSet()
|
||||
|
||||
file.writeText(content)
|
||||
absLinks
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "Download css failed" }
|
||||
emptySet()
|
||||
}
|
||||
private fun Element.collect(query: String, key: String, collector: MutableSet<String>) {
|
||||
val data = select(query)
|
||||
L.v { "Found ${data.size} elements with $query" }
|
||||
data.forEach {
|
||||
val absLink = it.attr("abs:$key")
|
||||
if (!absLink.isValid) return@forEach
|
||||
collector.add(absLink)
|
||||
it.attr(key, "assets/${fileName(absLink)}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun Element.collect(query: String, key: String, collector: MutableSet<String>) {
|
||||
val data = select(query)
|
||||
L.v { "Found ${data.size} elements with $query" }
|
||||
data.forEach {
|
||||
val absLink = it.attr("abs:$key")
|
||||
if (!absLink.isValid) return@forEach
|
||||
collector.add(absLink)
|
||||
it.attr(key, "assets/${fileName(absLink)}")
|
||||
}
|
||||
}
|
||||
private inline val String.isValid
|
||||
get() = startsWith("http")
|
||||
|
||||
private inline val String.isValid
|
||||
get() = startsWith("http")
|
||||
/** 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 index = atomicInt.getAndIncrement()
|
||||
|
||||
var newUrl = "a${index}_$candidate"
|
||||
|
||||
/**
|
||||
* Fetch the previously discovered filename
|
||||
* or create a new one
|
||||
* This is thread-safe
|
||||
* This is primarily for zipping up and sending via emails As .js files typically aren't
|
||||
* allowed, we'll simply make everything txt files
|
||||
*/
|
||||
private fun fileName(url: String): String {
|
||||
val mapped = urlMapper[url]
|
||||
if (mapped != null) return mapped
|
||||
if (newUrl.endsWith(".js")) newUrl = "$newUrl.txt"
|
||||
|
||||
val candidate = url.substringBefore("?").trim('/')
|
||||
.substringAfterLast("/").shorten()
|
||||
urlMapper[url] = newUrl
|
||||
return newUrl
|
||||
}
|
||||
|
||||
val index = atomicInt.getAndIncrement()
|
||||
private fun String.shorten() = if (length <= 10) this else substring(length - 10)
|
||||
|
||||
var newUrl = "a${index}_$candidate"
|
||||
private fun Set<String>.clean(): List<String> =
|
||||
filter(String::isNotBlank).filter { it.startsWith("http") }
|
||||
|
||||
/**
|
||||
* 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"
|
||||
|
||||
urlMapper[url] = newUrl
|
||||
return newUrl
|
||||
}
|
||||
|
||||
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") }
|
||||
|
||||
private fun reset() {
|
||||
urlMapper.clear()
|
||||
atomicInt.set(0)
|
||||
fileQueue.clear()
|
||||
cssQueue.clear()
|
||||
}
|
||||
private fun reset() {
|
||||
urlMapper.clear()
|
||||
atomicInt.set(0)
|
||||
fileQueue.clear()
|
||||
cssQueue.clear()
|
||||
}
|
||||
}
|
||||
|
@ -20,16 +20,14 @@ 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),
|
||||
TOP(R.string.top_stories, FbItem.FEED_TOP_STORIES);
|
||||
DEFAULT(R.string.kau_default, FbItem.FEED),
|
||||
MOST_RECENT(R.string.most_recent, FbItem.FEED_MOST_RECENT),
|
||||
TOP(R.string.top_stories, FbItem.FEED_TOP_STORIES);
|
||||
|
||||
companion object {
|
||||
val values = values() // save one instance
|
||||
operator fun invoke(index: Int) = values[index]
|
||||
}
|
||||
companion object {
|
||||
val values = values() // save one instance
|
||||
operator fun invoke(index: Int) = values[index]
|
||||
}
|
||||
}
|
||||
|
@ -19,29 +19,17 @@ 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
|
||||
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
|
||||
operator fun invoke(index: Int) = values.getOrElse(index) { TOP_BAR }
|
||||
}
|
||||
companion object {
|
||||
val values = values() // save one instance
|
||||
operator fun invoke(index: Int) = values.getOrElse(index) { TOP_BAR }
|
||||
}
|
||||
}
|
||||
|
@ -30,51 +30,45 @@ 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));
|
||||
|
||||
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 */
|
||||
fun onMenuCreate(context: Context, menu: Menu) {
|
||||
menuItem?.addToMenu(context, menu, 0)
|
||||
}
|
||||
|
||||
override val bundleContract: EnumBundleCompanion<OverlayContext>
|
||||
get() = Companion
|
||||
|
||||
companion object : EnumCompanion<OverlayContext>("frost_arg_overlay_context", values()) {
|
||||
|
||||
/**
|
||||
* Inject the [menuItem] in the order that they are given at the front of the menu
|
||||
* Execute selection call for an item by id Returns [true] if selection was consumed, [false]
|
||||
* otherwise
|
||||
*/
|
||||
fun onMenuCreate(context: Context, menu: Menu) {
|
||||
menuItem?.addToMenu(context, menu, 0)
|
||||
}
|
||||
|
||||
override val bundleContract: EnumBundleCompanion<OverlayContext>
|
||||
get() = Companion
|
||||
|
||||
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
|
||||
*/
|
||||
fun onOptionsItemSelected(web: FrostWebView, id: Int): Boolean {
|
||||
val item = values.firstOrNull { id == it.menuItem?.id }?.menuItem ?: return false
|
||||
web.loadUrl(item.fbItem.url, true)
|
||||
return true
|
||||
}
|
||||
fun onOptionsItemSelected(web: FrostWebView, id: Int): Boolean {
|
||||
val item = values.firstOrNull { id == it.menuItem?.id }?.menuItem ?: return false
|
||||
web.loadUrl(item.fbItem.url, true)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Frame for an injectable menu item
|
||||
*/
|
||||
/** Frame for an injectable menu item */
|
||||
class FrostMenuItem(
|
||||
val id: Int,
|
||||
val fbItem: FbItem,
|
||||
val showAsAction: Int = MenuItem.SHOW_AS_ACTION_IF_ROOM
|
||||
val id: Int,
|
||||
val fbItem: FbItem,
|
||||
val showAsAction: Int = MenuItem.SHOW_AS_ACTION_IF_ROOM
|
||||
) {
|
||||
fun addToMenu(context: Context, menu: Menu, index: Int) {
|
||||
val item = menu.add(Menu.NONE, id, index, fbItem.titleId)
|
||||
item.icon = fbItem.icon.toDrawable(context, 18)
|
||||
item.setShowAsAction(showAsAction)
|
||||
}
|
||||
fun addToMenu(context: Context, menu: Menu, index: Int) {
|
||||
val item = menu.add(Menu.NONE, id, index, fbItem.titleId)
|
||||
item.icon = fbItem.icon.toDrawable(context, 18)
|
||||
item.setShowAsAction(showAsAction)
|
||||
}
|
||||
}
|
||||
|
@ -23,95 +23,85 @@ 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()
|
||||
|
||||
enum class Theme(
|
||||
@StringRes val textRes: Int,
|
||||
file: String?,
|
||||
val textColorGetter: (ThemePrefs) -> Int,
|
||||
val accentColorGetter: (ThemePrefs) -> Int,
|
||||
val backgroundColorGetter: (ThemePrefs) -> Int,
|
||||
val headerColorGetter: (ThemePrefs) -> Int,
|
||||
val iconColorGetter: (ThemePrefs) -> Int
|
||||
@StringRes val textRes: Int,
|
||||
file: String?,
|
||||
val textColorGetter: (ThemePrefs) -> Int,
|
||||
val accentColorGetter: (ThemePrefs) -> Int,
|
||||
val backgroundColorGetter: (ThemePrefs) -> Int,
|
||||
val headerColorGetter: (ThemePrefs) -> Int,
|
||||
val iconColorGetter: (ThemePrefs) -> Int
|
||||
) {
|
||||
DEFAULT(
|
||||
R.string.kau_default,
|
||||
"default",
|
||||
{ 0xde000000.toInt() },
|
||||
{ FACEBOOK_BLUE },
|
||||
{ 0xfffafafa.toInt() },
|
||||
{ FACEBOOK_BLUE },
|
||||
{ Color.WHITE }
|
||||
),
|
||||
LIGHT(
|
||||
R.string.kau_light,
|
||||
"material_light",
|
||||
{ 0xde000000.toInt() },
|
||||
{ FACEBOOK_BLUE },
|
||||
{ 0xfffafafa.toInt() },
|
||||
{ FACEBOOK_BLUE },
|
||||
{ Color.WHITE }
|
||||
),
|
||||
DARK(
|
||||
R.string.kau_dark,
|
||||
"material_dark",
|
||||
{ Color.WHITE },
|
||||
{ BLUE_LIGHT },
|
||||
{ 0xff303030.toInt() },
|
||||
{ 0xff2e4b86.toInt() },
|
||||
{ Color.WHITE }
|
||||
),
|
||||
AMOLED(
|
||||
R.string.kau_amoled,
|
||||
"material_amoled",
|
||||
{ Color.WHITE },
|
||||
{ BLUE_LIGHT },
|
||||
{ Color.BLACK },
|
||||
{ Color.BLACK },
|
||||
{ Color.WHITE }
|
||||
),
|
||||
GLASS(
|
||||
R.string.kau_glass,
|
||||
"material_glass",
|
||||
{ Color.WHITE },
|
||||
{ BLUE_LIGHT },
|
||||
{ 0x80000000.toInt() },
|
||||
{ 0xb3000000.toInt() },
|
||||
{ Color.WHITE }
|
||||
),
|
||||
CUSTOM(
|
||||
R.string.kau_custom,
|
||||
"custom",
|
||||
{ it.customTextColor },
|
||||
{ it.customAccentColor },
|
||||
{ it.customBackgroundColor },
|
||||
{ it.customHeaderColor },
|
||||
{ it.customIconColor }
|
||||
);
|
||||
|
||||
DEFAULT(
|
||||
R.string.kau_default,
|
||||
"default",
|
||||
{ 0xde000000.toInt() },
|
||||
{ FACEBOOK_BLUE },
|
||||
{ 0xfffafafa.toInt() },
|
||||
{ FACEBOOK_BLUE },
|
||||
{ Color.WHITE }
|
||||
),
|
||||
@VisibleForTesting internal val file = file?.let { "$it.css" }
|
||||
|
||||
LIGHT(
|
||||
R.string.kau_light,
|
||||
"material_light",
|
||||
{ 0xde000000.toInt() },
|
||||
{ FACEBOOK_BLUE },
|
||||
{ 0xfffafafa.toInt() },
|
||||
{ FACEBOOK_BLUE },
|
||||
{ Color.WHITE }
|
||||
),
|
||||
|
||||
DARK(
|
||||
R.string.kau_dark,
|
||||
"material_dark",
|
||||
{ Color.WHITE },
|
||||
{ BLUE_LIGHT },
|
||||
{ 0xff303030.toInt() },
|
||||
{ 0xff2e4b86.toInt() },
|
||||
{ Color.WHITE }
|
||||
),
|
||||
|
||||
AMOLED(
|
||||
R.string.kau_amoled,
|
||||
"material_amoled",
|
||||
{ Color.WHITE },
|
||||
{ BLUE_LIGHT },
|
||||
{ Color.BLACK },
|
||||
{ Color.BLACK },
|
||||
{ Color.WHITE }
|
||||
),
|
||||
|
||||
GLASS(
|
||||
R.string.kau_glass,
|
||||
"material_glass",
|
||||
{ Color.WHITE },
|
||||
{ BLUE_LIGHT },
|
||||
{ 0x80000000.toInt() },
|
||||
{ 0xb3000000.toInt() },
|
||||
{ Color.WHITE }
|
||||
),
|
||||
|
||||
CUSTOM(
|
||||
R.string.kau_custom,
|
||||
"custom",
|
||||
{ it.customTextColor },
|
||||
{ it.customAccentColor },
|
||||
{ it.customBackgroundColor },
|
||||
{ it.customHeaderColor },
|
||||
{ it.customIconColor }
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
internal val file = file?.let { "$it.css" }
|
||||
|
||||
companion object {
|
||||
val values = values() // save one instance
|
||||
operator fun invoke(index: Int) = values[index]
|
||||
}
|
||||
companion object {
|
||||
val values = values() // save one instance
|
||||
operator fun invoke(index: Int) = values[index]
|
||||
}
|
||||
}
|
||||
|
||||
enum class ThemeCategory {
|
||||
FACEBOOK, MESSENGER
|
||||
;
|
||||
FACEBOOK,
|
||||
MESSENGER;
|
||||
|
||||
@VisibleForTesting
|
||||
internal val folder = name.toLowerCase(Locale.CANADA)
|
||||
@VisibleForTesting internal val folder = name.toLowerCase(Locale.CANADA)
|
||||
}
|
||||
|
@ -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/"
|
||||
@ -45,23 +44,19 @@ const val MESSENGER_THREAD_PREFIX = "$HTTPS_MESSENGER_COM/t/"
|
||||
|
||||
// Default user agent
|
||||
const val USER_AGENT_MOBILE_CONST =
|
||||
"Mozilla/5.0 (Linux; Android 8.0.0; ONEPLUS A3000) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.90 Mobile Safari/537.36"
|
||||
"Mozilla/5.0 (Linux; Android 8.0.0; ONEPLUS A3000) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.90 Mobile Safari/537.36"
|
||||
|
||||
// Desktop agent, for pages like messages
|
||||
const val USER_AGENT_DESKTOP_CONST =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.90 Safari/537.36"
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.90 Safari/537.36"
|
||||
|
||||
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
|
||||
|
@ -28,149 +28,129 @@ 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.
|
||||
*/
|
||||
private const val FB_COOKIE_DOMAIN = ".$FACEBOOK_COM"
|
||||
private const val MESSENGER_COOKIE_DOMAIN = ".$MESSENGER_COM"
|
||||
companion object {
|
||||
/** 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 */
|
||||
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 {
|
||||
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 }
|
||||
L.d { "Cookies set" }
|
||||
L._d { "Set $cookie\n\tResult $webCookie" }
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun CookieManager.setSingleWebCookie(domain: String, cookie: String): Boolean =
|
||||
suspendCoroutine { cont ->
|
||||
setCookie(domain, cookie.trim()) { cont.resume(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the facebook cookie if it exists
|
||||
* Note that this is a synchronized call
|
||||
*/
|
||||
val webCookie: String?
|
||||
get() = CookieManager.getInstance().getCookie(HTTPS_FACEBOOK_COM)
|
||||
private suspend fun CookieManager.removeAllCookies(): Boolean = suspendCoroutine { cont ->
|
||||
removeAllCookies { cont.resume(it) }
|
||||
}
|
||||
|
||||
val messengerCookie: String?
|
||||
get() = CookieManager.getInstance().getCookie(HTTPS_MESSENGER_COM)
|
||||
suspend fun save(id: Long) {
|
||||
L.d { "New cookie found" }
|
||||
prefs.userId = id
|
||||
CookieManager.getInstance().flush()
|
||||
val cookie = CookieEntity(prefs.userId, null, webCookie)
|
||||
cookieDao.save(cookie)
|
||||
}
|
||||
|
||||
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 }
|
||||
L.d { "Cookies set" }
|
||||
L._d { "Set $cookie\n\tResult $webCookie" }
|
||||
result
|
||||
}
|
||||
suspend fun reset() {
|
||||
prefs.userId = -1L
|
||||
withContext(Dispatchers.Main + NonCancellable) {
|
||||
with(CookieManager.getInstance()) {
|
||||
removeAllCookies()
|
||||
flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun CookieManager.setSingleWebCookie(domain: String, cookie: String): Boolean =
|
||||
suspendCoroutine { cont ->
|
||||
setCookie(domain, cookie.trim()) {
|
||||
cont.resume(it)
|
||||
}
|
||||
}
|
||||
suspend fun switchUser(id: Long) {
|
||||
val cookie = cookieDao.selectById(id) ?: return L.e { "No cookie for id" }
|
||||
switchUser(cookie)
|
||||
}
|
||||
|
||||
private suspend fun CookieManager.removeAllCookies(): Boolean = suspendCoroutine { cont ->
|
||||
removeAllCookies {
|
||||
cont.resume(it)
|
||||
}
|
||||
suspend fun switchUser(cookie: CookieEntity?) {
|
||||
if (cookie?.cookie == null) {
|
||||
L.d { "Switching User; null cookie" }
|
||||
return
|
||||
}
|
||||
|
||||
suspend fun save(id: Long) {
|
||||
L.d { "New cookie found" }
|
||||
prefs.userId = id
|
||||
CookieManager.getInstance().flush()
|
||||
val cookie = CookieEntity(prefs.userId, null, webCookie)
|
||||
cookieDao.save(cookie)
|
||||
withContext(Dispatchers.Main + NonCancellable) {
|
||||
L.d { "Switching User" }
|
||||
prefs.userId = cookie.id
|
||||
CookieManager.getInstance().apply {
|
||||
removeAllCookies()
|
||||
suspendSetWebCookie(FB_COOKIE_DOMAIN, cookie.cookie)
|
||||
suspendSetWebCookie(MESSENGER_COOKIE_DOMAIN, cookie.cookieMessenger)
|
||||
flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reset() {
|
||||
prefs.userId = -1L
|
||||
withContext(Dispatchers.Main + NonCancellable) {
|
||||
with(CookieManager.getInstance()) {
|
||||
removeAllCookies()
|
||||
flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
/** 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 })
|
||||
logout(prefs.userId, deleteCookie)
|
||||
context.launchLogin(cookies, true)
|
||||
}
|
||||
|
||||
suspend fun switchUser(id: Long) {
|
||||
val cookie = cookieDao.selectById(id) ?: return L.e { "No cookie for id" }
|
||||
switchUser(cookie)
|
||||
/** Clear the cookies of the given id */
|
||||
suspend fun logout(id: Long, deleteCookie: Boolean = true) {
|
||||
L.d { "Logging out user" }
|
||||
if (deleteCookie) {
|
||||
cookieDao.deleteById(id)
|
||||
L.d { "Fb cookie deleted" }
|
||||
L._d { id }
|
||||
}
|
||||
reset()
|
||||
}
|
||||
|
||||
suspend fun switchUser(cookie: CookieEntity?) {
|
||||
if (cookie?.cookie == null) {
|
||||
L.d { "Switching User; null cookie" }
|
||||
return
|
||||
}
|
||||
withContext(Dispatchers.Main + NonCancellable) {
|
||||
L.d { "Switching User" }
|
||||
prefs.userId = cookie.id
|
||||
CookieManager.getInstance().apply {
|
||||
removeAllCookies()
|
||||
suspendSetWebCookie(FB_COOKIE_DOMAIN, cookie.cookie)
|
||||
suspendSetWebCookie(MESSENGER_COOKIE_DOMAIN, cookie.cookieMessenger)
|
||||
flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 })
|
||||
logout(prefs.userId, deleteCookie)
|
||||
context.launchLogin(cookies, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cookies of the given id
|
||||
*/
|
||||
suspend fun logout(id: Long, deleteCookie: Boolean = true) {
|
||||
L.d { "Logging out user" }
|
||||
if (deleteCookie) {
|
||||
cookieDao.deleteById(id)
|
||||
L.d { "Fb cookie deleted" }
|
||||
L._d { id }
|
||||
}
|
||||
reset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifications may come from different accounts, and we need to switch the cookies to load them
|
||||
* When coming back to the main app, switch back to our original account before continuing
|
||||
*/
|
||||
suspend fun switchBackUser() {
|
||||
if (prefs.prevId == -1L) return
|
||||
val prevId = prefs.prevId
|
||||
prefs.prevId = -1L
|
||||
if (prevId != prefs.userId) {
|
||||
switchUser(prevId)
|
||||
L.d { "Switch back user" }
|
||||
L._d { "${prefs.userId} to $prevId" }
|
||||
}
|
||||
/**
|
||||
* Notifications may come from different accounts, and we need to switch the cookies to load them
|
||||
* When coming back to the main app, switch back to our original account before continuing
|
||||
*/
|
||||
suspend fun switchBackUser() {
|
||||
if (prefs.prevId == -1L) return
|
||||
val prevId = prefs.prevId
|
||||
prefs.prevId = -1L
|
||||
if (prevId != prefs.userId) {
|
||||
switchUser(prevId)
|
||||
L.d { "Switch back user" }
|
||||
L._d { "${prefs.userId} to $prevId" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,79 +29,73 @@ import com.pitchedapps.frost.utils.EnumBundleCompanion
|
||||
import com.pitchedapps.frost.utils.EnumCompanion
|
||||
|
||||
enum class FbItem(
|
||||
@StringRes val titleId: Int,
|
||||
val icon: IIcon,
|
||||
relativeUrl: String,
|
||||
val fragmentCreator: () -> BaseFragment = ::WebFragment,
|
||||
prefix: String = FB_URL_BASE
|
||||
@StringRes val titleId: Int,
|
||||
val icon: IIcon,
|
||||
relativeUrl: String,
|
||||
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"),
|
||||
EVENTS(R.string.events, GoogleMaterial.Icon.gmd_event_note, "events/upcoming"),
|
||||
FEED(R.string.feed, CommunityMaterial.Icon3.cmd_newspaper, ""),
|
||||
FEED_MOST_RECENT(R.string.most_recent, GoogleMaterial.Icon.gmd_history, "home.php?sk=h_chr"),
|
||||
FEED_TOP_STORIES(R.string.top_stories, GoogleMaterial.Icon.gmd_star, "home.php?sk=h_nor"),
|
||||
FRIENDS(R.string.friends, GoogleMaterial.Icon.gmd_person_add, "friends/center/requests"),
|
||||
GROUPS(R.string.groups, GoogleMaterial.Icon.gmd_group, "groups"),
|
||||
MARKETPLACE(R.string.marketplace, GoogleMaterial.Icon.gmd_store, "marketplace"),
|
||||
|
||||
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"),
|
||||
EVENTS(R.string.events, GoogleMaterial.Icon.gmd_event_note, "events/upcoming"),
|
||||
FEED(R.string.feed, CommunityMaterial.Icon3.cmd_newspaper, ""),
|
||||
FEED_MOST_RECENT(R.string.most_recent, GoogleMaterial.Icon.gmd_history, "home.php?sk=h_chr"),
|
||||
FEED_TOP_STORIES(R.string.top_stories, GoogleMaterial.Icon.gmd_star, "home.php?sk=h_nor"),
|
||||
FRIENDS(R.string.friends, GoogleMaterial.Icon.gmd_person_add, "friends/center/requests"),
|
||||
GROUPS(R.string.groups, GoogleMaterial.Icon.gmd_group, "groups"),
|
||||
MARKETPLACE(R.string.marketplace, GoogleMaterial.Icon.gmd_store, "marketplace"),
|
||||
/*
|
||||
* Unlike other urls, menus cannot be linked directly as it is a soft reference. Instead, we can
|
||||
* pick any url with the blue bar and manually click to enter the menu.
|
||||
* We pick home.php as some back interactions default to home regardless of the base url.
|
||||
*/
|
||||
MENU(R.string.menu, GoogleMaterial.Icon.gmd_menu, "home.php"),
|
||||
MESSAGES(R.string.messages, MaterialDesignIconic.Icon.gmi_comments, "messages"),
|
||||
MESSENGER(
|
||||
R.string.messenger,
|
||||
CommunityMaterial.Icon2.cmd_facebook_messenger,
|
||||
"",
|
||||
prefix = HTTPS_MESSENGER_COM
|
||||
),
|
||||
NOTES(R.string.notes, CommunityMaterial.Icon3.cmd_note, "notes"),
|
||||
NOTIFICATIONS(R.string.notifications, MaterialDesignIconic.Icon.gmi_globe, "notifications"),
|
||||
ON_THIS_DAY(R.string.on_this_day, GoogleMaterial.Icon.gmd_today, "onthisday"),
|
||||
PAGES(R.string.pages, GoogleMaterial.Icon.gmd_flag, "pages"),
|
||||
PHOTOS(R.string.photos, GoogleMaterial.Icon.gmd_photo, "me/photos"),
|
||||
PROFILE(R.string.profile, CommunityMaterial.Icon.cmd_account, "me"),
|
||||
SAVED(R.string.saved, GoogleMaterial.Icon.gmd_bookmark, "saved"),
|
||||
|
||||
/*
|
||||
* Unlike other urls, menus cannot be linked directly as it is a soft reference. Instead, we can
|
||||
* pick any url with the blue bar and manually click to enter the menu.
|
||||
* We pick home.php as some back interactions default to home regardless of the base url.
|
||||
*/
|
||||
MENU(R.string.menu, GoogleMaterial.Icon.gmd_menu, "home.php"),
|
||||
MESSAGES(R.string.messages, MaterialDesignIconic.Icon.gmi_comments, "messages"),
|
||||
MESSENGER(
|
||||
R.string.messenger,
|
||||
CommunityMaterial.Icon2.cmd_facebook_messenger,
|
||||
"",
|
||||
prefix = HTTPS_MESSENGER_COM
|
||||
),
|
||||
NOTES(R.string.notes, CommunityMaterial.Icon3.cmd_note, "notes"),
|
||||
NOTIFICATIONS(R.string.notifications, MaterialDesignIconic.Icon.gmi_globe, "notifications"),
|
||||
ON_THIS_DAY(R.string.on_this_day, GoogleMaterial.Icon.gmd_today, "onthisday"),
|
||||
PAGES(R.string.pages, GoogleMaterial.Icon.gmd_flag, "pages"),
|
||||
PHOTOS(R.string.photos, GoogleMaterial.Icon.gmd_photo, "me/photos"),
|
||||
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. */
|
||||
_SEARCH_PARSE(
|
||||
R.string.kau_search,
|
||||
GoogleMaterial.Icon.gmd_search,
|
||||
"search/top",
|
||||
prefix = FB_URL_MBASIC_BASE
|
||||
),
|
||||
SETTINGS(R.string.settings, GoogleMaterial.Icon.gmd_settings, "settings"),
|
||||
;
|
||||
|
||||
/**
|
||||
* Non mbasic search cannot be parsed.
|
||||
*/
|
||||
_SEARCH_PARSE(
|
||||
R.string.kau_search,
|
||||
GoogleMaterial.Icon.gmd_search,
|
||||
"search/top",
|
||||
prefix = FB_URL_MBASIC_BASE
|
||||
),
|
||||
SETTINGS(R.string.settings, GoogleMaterial.Icon.gmd_settings, "settings"),
|
||||
;
|
||||
val url = "$prefix$relativeUrl"
|
||||
|
||||
val url = "$prefix$relativeUrl"
|
||||
val isFeed: Boolean
|
||||
get() =
|
||||
when (this) {
|
||||
FEED,
|
||||
FEED_MOST_RECENT,
|
||||
FEED_TOP_STORIES -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val isFeed: Boolean
|
||||
get() = when (this) {
|
||||
FEED, FEED_MOST_RECENT, FEED_TOP_STORIES -> true
|
||||
else -> false
|
||||
}
|
||||
override val bundleContract: EnumBundleCompanion<FbItem>
|
||||
get() = Companion
|
||||
|
||||
override val bundleContract: EnumBundleCompanion<FbItem>
|
||||
get() = Companion
|
||||
|
||||
companion object : EnumCompanion<FbItem>("frost_arg_fb_item", values())
|
||||
companion object : EnumCompanion<FbItem>("frost_arg_fb_item", values())
|
||||
}
|
||||
|
||||
fun defaultTabs(): List<FbItem> =
|
||||
listOf(FbItem.FEED, FbItem.MESSAGES, FbItem.NOTIFICATIONS, FbItem.MENU)
|
||||
listOf(FbItem.FEED, FbItem.MESSAGES, FbItem.NOTIFICATIONS, FbItem.MENU)
|
||||
|
@ -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]+)")
|
||||
|
@ -28,141 +28,164 @@ import java.nio.charset.StandardCharsets
|
||||
* Custom url builder so we can easily test it without the Android framework
|
||||
*/
|
||||
inline val String.formattedFbUrl: String
|
||||
get() = FbUrlFormatter(this).toString()
|
||||
get() = FbUrlFormatter(this).toString()
|
||||
|
||||
inline val Uri.formattedFbUri: Uri
|
||||
get() {
|
||||
val url = toString()
|
||||
return if (url.startsWith("http")) Uri.parse(url.formattedFbUrl) else this
|
||||
}
|
||||
get() {
|
||||
val url = toString()
|
||||
return if (url.startsWith("http")) Uri.parse(url.formattedFbUrl) else this
|
||||
}
|
||||
|
||||
class FbUrlFormatter(url: String) {
|
||||
private val queries = mutableMapOf<String, String>()
|
||||
private val cleaned: String
|
||||
private val queries = mutableMapOf<String, String>()
|
||||
private val cleaned: String
|
||||
|
||||
/**
|
||||
* Formats all facebook urls
|
||||
*
|
||||
* The order is very important:
|
||||
* 1. Wrapper links (discardables) are stripped away, resulting in the actual link
|
||||
* 2. CSS encoding is converted to normal encoding
|
||||
* 3. Url is completely decoded
|
||||
* 4. Url is split into sections
|
||||
*/
|
||||
init {
|
||||
cleaned = clean(url)
|
||||
/**
|
||||
* Formats all facebook urls
|
||||
*
|
||||
* The order is very important:
|
||||
* 1. Wrapper links (discardables) are stripped away, resulting in the actual link
|
||||
* 2. CSS encoding is converted to normal encoding
|
||||
* 3. Url is completely decoded
|
||||
* 4. Url is split into sections
|
||||
*/
|
||||
init {
|
||||
cleaned = clean(url)
|
||||
}
|
||||
|
||||
fun clean(url: String): String {
|
||||
if (url.isBlank()) return ""
|
||||
var cleanedUrl = url
|
||||
if (cleanedUrl.startsWith("#!")) cleanedUrl = cleanedUrl.substring(2)
|
||||
val urlRef = cleanedUrl
|
||||
discardable.forEach { cleanedUrl = cleanedUrl.replace(it, "", true) }
|
||||
val changed = cleanedUrl != urlRef
|
||||
converter.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v, true) }
|
||||
try {
|
||||
cleanedUrl = URLDecoder.decode(cleanedUrl, StandardCharsets.UTF_8.name())
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "Failed url formatting" }
|
||||
return url
|
||||
}
|
||||
|
||||
fun clean(url: String): String {
|
||||
if (url.isBlank()) return ""
|
||||
var cleanedUrl = url
|
||||
if (cleanedUrl.startsWith("#!")) cleanedUrl = cleanedUrl.substring(2)
|
||||
val urlRef = cleanedUrl
|
||||
discardable.forEach { cleanedUrl = cleanedUrl.replace(it, "", true) }
|
||||
val changed = cleanedUrl != urlRef
|
||||
converter.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v, true) }
|
||||
try {
|
||||
cleanedUrl = URLDecoder.decode(cleanedUrl, StandardCharsets.UTF_8.name())
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "Failed url formatting" }
|
||||
return url
|
||||
}
|
||||
cleanedUrl = cleanedUrl.replace("&", "&")
|
||||
if (changed && !cleanedUrl.contains("?")) // ensure we aren't missing '?'
|
||||
cleanedUrl = cleanedUrl.replaceFirst("&", "?")
|
||||
val qm = cleanedUrl.indexOf("?")
|
||||
if (qm > -1) {
|
||||
cleanedUrl.substring(qm + 1).split("&").forEach {
|
||||
val p = it.split("=")
|
||||
queries[p[0]] = p.elementAtOrNull(1) ?: ""
|
||||
}
|
||||
cleanedUrl = cleanedUrl.substring(0, qm)
|
||||
}
|
||||
discardableQueries.forEach { queries.remove(it) }
|
||||
// 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(
|
||||
".facebook.com//",
|
||||
".facebook.com/"
|
||||
) // sometimes we are given a bad url
|
||||
L.v { "Formatted url from $url to $cleanedUrl" }
|
||||
return cleanedUrl
|
||||
cleanedUrl = cleanedUrl.replace("&", "&")
|
||||
if (changed && !cleanedUrl.contains("?")) // ensure we aren't missing '?'
|
||||
cleanedUrl = cleanedUrl.replaceFirst("&", "?")
|
||||
val qm = cleanedUrl.indexOf("?")
|
||||
if (qm > -1) {
|
||||
cleanedUrl.substring(qm + 1).split("&").forEach {
|
||||
val p = it.split("=")
|
||||
queries[p[0]] = p.elementAtOrNull(1) ?: ""
|
||||
}
|
||||
cleanedUrl = cleanedUrl.substring(0, qm)
|
||||
}
|
||||
discardableQueries.forEach { queries.remove(it) }
|
||||
// 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(
|
||||
".facebook.com//",
|
||||
".facebook.com/"
|
||||
) // sometimes we are given a bad url
|
||||
L.v { "Formatted url from $url to $cleanedUrl" }
|
||||
return cleanedUrl
|
||||
}
|
||||
|
||||
override fun toString(): String = buildString {
|
||||
override fun toString(): String =
|
||||
buildString {
|
||||
append(cleaned)
|
||||
if (queries.isNotEmpty()) {
|
||||
append("?")
|
||||
queries.forEach { (k, v) ->
|
||||
if (v.isEmpty()) {
|
||||
append("${k.urlEncode()}&")
|
||||
} else {
|
||||
append("${k.urlEncode()}=${v.urlEncode()}&")
|
||||
}
|
||||
append("?")
|
||||
queries.forEach { (k, v) ->
|
||||
if (v.isEmpty()) {
|
||||
append("${k.urlEncode()}&")
|
||||
} else {
|
||||
append("${k.urlEncode()}=${v.urlEncode()}&")
|
||||
}
|
||||
}
|
||||
}
|
||||
}.removeSuffix("&")
|
||||
}
|
||||
.removeSuffix("&")
|
||||
|
||||
fun toLogList(): List<String> {
|
||||
val list = mutableListOf(cleaned)
|
||||
queries.forEach { (k, v) -> list.add("\n- $k\t=\t$v") }
|
||||
list.add("\n\n${toString()}")
|
||||
return list
|
||||
}
|
||||
fun toLogList(): List<String> {
|
||||
val list = mutableListOf(cleaned)
|
||||
queries.forEach { (k, v) -> list.add("\n- $k\t=\t$v") }
|
||||
list.add("\n\n${toString()}")
|
||||
return list
|
||||
}
|
||||
|
||||
companion object {
|
||||
companion object {
|
||||
|
||||
const val VIDEO_REDIRECT = "/video_redirect/?src="
|
||||
const val VIDEO_REDIRECT = "/video_redirect/?src="
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
val discardable = arrayOf(
|
||||
"http://lm.facebook.com/l.php?u=",
|
||||
"https://lm.facebook.com/l.php?u=",
|
||||
"http://m.facebook.com/l.php?u=",
|
||||
"https://m.facebook.com/l.php?u=",
|
||||
"http://touch.facebook.com/l.php?u=",
|
||||
"https://touch.facebook.com/l.php?u=",
|
||||
VIDEO_REDIRECT
|
||||
)
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
val discardable =
|
||||
arrayOf(
|
||||
"http://lm.facebook.com/l.php?u=",
|
||||
"https://lm.facebook.com/l.php?u=",
|
||||
"http://m.facebook.com/l.php?u=",
|
||||
"https://m.facebook.com/l.php?u=",
|
||||
"http://touch.facebook.com/l.php?u=",
|
||||
"https://touch.facebook.com/l.php?u=",
|
||||
VIDEO_REDIRECT
|
||||
)
|
||||
|
||||
/**
|
||||
* Queries that are not necessary for independent links
|
||||
*
|
||||
* acontext is not required for "friends interested in" notifications
|
||||
*/
|
||||
val discardableQueries = arrayOf(
|
||||
"ref",
|
||||
"refid",
|
||||
"SharedWith",
|
||||
"fbclid",
|
||||
"h",
|
||||
"_ft_",
|
||||
"_tn_",
|
||||
"_xt_",
|
||||
"bacr",
|
||||
"frefs",
|
||||
"hc_ref",
|
||||
"loc_ref",
|
||||
"pn_ref"
|
||||
)
|
||||
/**
|
||||
* Queries that are not necessary for independent links
|
||||
*
|
||||
* acontext is not required for "friends interested in" notifications
|
||||
*/
|
||||
val discardableQueries =
|
||||
arrayOf(
|
||||
"ref",
|
||||
"refid",
|
||||
"SharedWith",
|
||||
"fbclid",
|
||||
"h",
|
||||
"_ft_",
|
||||
"_tn_",
|
||||
"_xt_",
|
||||
"bacr",
|
||||
"frefs",
|
||||
"hc_ref",
|
||||
"loc_ref",
|
||||
"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",
|
||||
"\\20 " to "%20"
|
||||
)
|
||||
}
|
||||
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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -23,42 +23,38 @@ import org.jsoup.nodes.Document
|
||||
object BadgeParser : FrostParser<FrostBadges> by BadgeParserImpl()
|
||||
|
||||
data class FrostBadges(
|
||||
val feed: String?,
|
||||
val friends: String?,
|
||||
val messages: String?,
|
||||
val notifications: String?
|
||||
val feed: String?,
|
||||
val friends: String?,
|
||||
val messages: String?,
|
||||
val notifications: String?
|
||||
) : ParseData {
|
||||
override val isEmpty: Boolean
|
||||
get() = feed.isNullOrEmpty() &&
|
||||
friends.isNullOrEmpty() &&
|
||||
messages.isNullOrEmpty() &&
|
||||
notifications.isNullOrEmpty()
|
||||
override val isEmpty: Boolean
|
||||
get() =
|
||||
feed.isNullOrEmpty() &&
|
||||
friends.isNullOrEmpty() &&
|
||||
messages.isNullOrEmpty() &&
|
||||
notifications.isNullOrEmpty()
|
||||
}
|
||||
|
||||
private class BadgeParserImpl : FrostParserBase<FrostBadges>(false) {
|
||||
// Not actually displayed
|
||||
override var nameRes: Int = R.string.frost_name
|
||||
// Not actually displayed
|
||||
override var nameRes: Int = R.string.frost_name
|
||||
|
||||
override val url: String = FB_URL_BASE
|
||||
override val url: String = FB_URL_BASE
|
||||
|
||||
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"
|
||||
)
|
||||
.map { "[data-sigil*=$it] [data-sigil=count]" }
|
||||
.map { doc.select(it) }
|
||||
.map { e -> e?.getOrNull(0)?.ownText() }
|
||||
return FrostBadges(
|
||||
feed = feed,
|
||||
friends = requests,
|
||||
messages = messages,
|
||||
notifications = notifications
|
||||
)
|
||||
}
|
||||
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")
|
||||
.map { "[data-sigil*=$it] [data-sigil=count]" }
|
||||
.map { doc.select(it) }
|
||||
.map { e -> e?.getOrNull(0)?.ownText() }
|
||||
return FrostBadges(
|
||||
feed = feed,
|
||||
friends = requests,
|
||||
messages = messages,
|
||||
notifications = notifications
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -30,46 +30,33 @@ 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
|
||||
*/
|
||||
var nameRes: Int
|
||||
/** Name associated to parser Purely for display */
|
||||
var nameRes: Int
|
||||
|
||||
/**
|
||||
* Url to request from
|
||||
*/
|
||||
val url: String
|
||||
/** Url to request from */
|
||||
val url: String
|
||||
|
||||
/**
|
||||
* Call parsing with default implementation using cookie
|
||||
*/
|
||||
fun parse(cookie: String?): ParseResponse<T>?
|
||||
/** Call parsing with default implementation using cookie */
|
||||
fun parse(cookie: String?): ParseResponse<T>?
|
||||
|
||||
/**
|
||||
* Call parsing with given document
|
||||
*/
|
||||
fun parse(cookie: String?, document: Document): ParseResponse<T>?
|
||||
/** Call parsing with given document */
|
||||
fun parse(cookie: String?, document: Document): ParseResponse<T>?
|
||||
|
||||
/**
|
||||
* Call parsing using jsoup to fetch from given url
|
||||
*/
|
||||
fun parseFromUrl(cookie: String?, url: String): ParseResponse<T>?
|
||||
/** Call parsing using jsoup to fetch from given url */
|
||||
fun parseFromUrl(cookie: String?, url: String): ParseResponse<T>?
|
||||
|
||||
/**
|
||||
* Call parsing with given data
|
||||
*/
|
||||
fun parseFromData(cookie: String?, text: String): ParseResponse<T>?
|
||||
/** Call parsing with given data */
|
||||
fun parseFromData(cookie: String?, text: String): ParseResponse<T>?
|
||||
}
|
||||
|
||||
const val FALLBACK_TIME_MOD = 1000000
|
||||
@ -77,69 +64,73 @@ const val FALLBACK_TIME_MOD = 1000000
|
||||
data class FrostLink(val text: String, val href: String)
|
||||
|
||||
data class ParseResponse<out T : ParseData>(val cookie: String, val data: T) {
|
||||
override fun toString() = "ParseResponse\ncookie: $cookie\ndata:\n$data"
|
||||
override fun toString() = "ParseResponse\ncookie: $cookie\ndata:\n$data"
|
||||
}
|
||||
|
||||
interface ParseData {
|
||||
val isEmpty: Boolean
|
||||
val isEmpty: Boolean
|
||||
}
|
||||
|
||||
interface ParseNotification : ParseData {
|
||||
fun getUnreadNotifications(data: CookieEntity): List<NotificationContent>
|
||||
fun getUnreadNotifications(data: CookieEntity): List<NotificationContent>
|
||||
}
|
||||
|
||||
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()
|
||||
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()
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
FrostParser<T> {
|
||||
|
||||
final override fun parse(cookie: String?) = parseFromUrl(cookie, url)
|
||||
final override fun parse(cookie: String?) = parseFromUrl(cookie, url)
|
||||
|
||||
final override fun parseFromData(cookie: String?, text: String): ParseResponse<T>? {
|
||||
cookie ?: return null
|
||||
val doc = textToDoc(text) ?: return null
|
||||
val data = parseImpl(doc) ?: return null
|
||||
return ParseResponse(cookie, data)
|
||||
}
|
||||
final override fun parseFromData(cookie: String?, text: String): ParseResponse<T>? {
|
||||
cookie ?: return null
|
||||
val doc = textToDoc(text) ?: return null
|
||||
val data = parseImpl(doc) ?: return null
|
||||
return ParseResponse(cookie, data)
|
||||
}
|
||||
|
||||
final override fun parseFromUrl(cookie: String?, url: String): ParseResponse<T>? =
|
||||
parse(cookie, frostJsoup(cookie, url))
|
||||
final override fun parseFromUrl(cookie: String?, url: String): ParseResponse<T>? =
|
||||
parse(cookie, frostJsoup(cookie, url))
|
||||
|
||||
override fun parse(cookie: String?, document: Document): ParseResponse<T>? {
|
||||
cookie ?: return null
|
||||
if (redirectToText)
|
||||
return parseFromData(cookie, document.toString())
|
||||
val data = parseImpl(document) ?: return null
|
||||
return ParseResponse(cookie, data)
|
||||
}
|
||||
override fun parse(cookie: String?, document: Document): ParseResponse<T>? {
|
||||
cookie ?: return null
|
||||
if (redirectToText) return parseFromData(cookie, document.toString())
|
||||
val data = parseImpl(document) ?: return null
|
||||
return ParseResponse(cookie, data)
|
||||
}
|
||||
|
||||
protected abstract fun parseImpl(doc: Document): T?
|
||||
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
|
||||
*/
|
||||
protected fun Element.getInnerImgStyle(): String? =
|
||||
select("i.img[style*=url]").getStyleUrl()
|
||||
/**
|
||||
* 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 Elements.getStyleUrl(): String? =
|
||||
FB_CSS_URL_MATCHER.find(attr("style"))[1]?.formattedFbUrl
|
||||
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")
|
||||
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"
|
||||
)
|
||||
|
||||
protected fun parseLink(element: Element?): FrostLink? {
|
||||
val a = element?.getElementsByTag("a")?.first() ?: return null
|
||||
return FrostLink(a.text(), a.attr("href"))
|
||||
}
|
||||
protected fun parseLink(element: Element?): FrostLink? {
|
||||
val a = element?.getElementsByTag("a")?.first() ?: return null
|
||||
return FrostLink(a.text(), a.attr("href"))
|
||||
}
|
||||
}
|
||||
|
@ -33,126 +33,128 @@ 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() {
|
||||
|
||||
fun queryUser(cookie: String?, name: String) =
|
||||
parseFromUrl(cookie, "${FbItem.MESSAGES.url}/?q=${name.urlEncode()}")
|
||||
fun queryUser(cookie: String?, name: String) =
|
||||
parseFromUrl(cookie, "${FbItem.MESSAGES.url}/?q=${name.urlEncode()}")
|
||||
}
|
||||
|
||||
data class FrostMessages(
|
||||
val threads: List<FrostThread>,
|
||||
val seeMore: FrostLink?,
|
||||
val extraLinks: List<FrostLink>
|
||||
val threads: List<FrostThread>,
|
||||
val seeMore: FrostLink?,
|
||||
val extraLinks: List<FrostLink>
|
||||
) : ParseNotification {
|
||||
|
||||
override val isEmpty: Boolean
|
||||
get() = threads.isEmpty()
|
||||
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 {
|
||||
with(it) {
|
||||
NotificationContent(
|
||||
data = data,
|
||||
id = id,
|
||||
href = url,
|
||||
title = title,
|
||||
text = content ?: "",
|
||||
timestamp = time,
|
||||
profileUrl = img,
|
||||
unread = unread
|
||||
)
|
||||
}
|
||||
}.toList()
|
||||
override fun getUnreadNotifications(data: CookieEntity) =
|
||||
threads
|
||||
.asSequence()
|
||||
.filter(FrostThread::unread)
|
||||
.map {
|
||||
with(it) {
|
||||
NotificationContent(
|
||||
data = data,
|
||||
id = id,
|
||||
href = url,
|
||||
title = title,
|
||||
text = content ?: "",
|
||||
timestamp = time,
|
||||
profileUrl = img,
|
||||
unread = unread
|
||||
)
|
||||
}
|
||||
}
|
||||
.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,
|
||||
val img: String?,
|
||||
val title: String,
|
||||
val time: Long,
|
||||
val url: String,
|
||||
val unread: Boolean,
|
||||
val content: String?,
|
||||
val contentImgUrl: String?
|
||||
val id: Long,
|
||||
val img: String?,
|
||||
val title: String,
|
||||
val time: Long,
|
||||
val url: String,
|
||||
val unread: Boolean,
|
||||
val content: String?,
|
||||
val contentImgUrl: String?
|
||||
)
|
||||
|
||||
private class MessageParserImpl : FrostParserBase<FrostMessages>(true) {
|
||||
|
||||
override var nameRes = FbItem.MESSAGES.titleId
|
||||
override var nameRes = FbItem.MESSAGES.titleId
|
||||
|
||||
override val url = FbItem.MESSAGES.url
|
||||
override val url = FbItem.MESSAGES.url
|
||||
|
||||
override fun textToDoc(text: String): Document? {
|
||||
var content = StringEscapeUtils.unescapeEcmaScript(text)
|
||||
val begin = content.indexOf("id=\"threadlist_rows\"")
|
||||
if (begin <= 0) {
|
||||
L.d { "Threadlist not found" }
|
||||
return null
|
||||
}
|
||||
content = content.substring(begin)
|
||||
val end = content.indexOf("</script>")
|
||||
if (end <= 0) {
|
||||
L.d { "Script tail not found" }
|
||||
return null
|
||||
}
|
||||
content = content.substring(0, end).substringBeforeLast("</div>")
|
||||
return Jsoup.parseBodyFragment("<div $content")
|
||||
override fun textToDoc(text: String): Document? {
|
||||
var content = StringEscapeUtils.unescapeEcmaScript(text)
|
||||
val begin = content.indexOf("id=\"threadlist_rows\"")
|
||||
if (begin <= 0) {
|
||||
L.d { "Threadlist not found" }
|
||||
return null
|
||||
}
|
||||
|
||||
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}.*"
|
||||
)
|
||||
.mapNotNull(this::parseMessage)
|
||||
val seeMore = parseLink(doc.getElementById("see_older_threads"))
|
||||
val extraLinks = threadList.nextElementSibling()?.select("a")
|
||||
?.mapNotNull(this::parseLink) ?: emptyList()
|
||||
return FrostMessages(threads, seeMore, extraLinks)
|
||||
content = content.substring(begin)
|
||||
val end = content.indexOf("</script>")
|
||||
if (end <= 0) {
|
||||
L.d { "Script tail not found" }
|
||||
return null
|
||||
}
|
||||
content = content.substring(0, end).substringBeforeLast("</div>")
|
||||
return Jsoup.parseBodyFragment("<div $content")
|
||||
}
|
||||
|
||||
private fun parseMessage(element: Element): FrostThread? {
|
||||
val a = element.getElementsByTag("a").first() ?: return null
|
||||
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()
|
||||
?: System.currentTimeMillis() % FALLBACK_TIME_MOD
|
||||
val snippet = element.select("span.snippet").firstOrNull()
|
||||
val content = snippet?.text()?.trim()
|
||||
val contentImg = snippet?.select("i[style*=url]")?.getStyleUrl()
|
||||
val img = element.getInnerImgStyle()
|
||||
return FrostThread(
|
||||
id = id,
|
||||
img = img,
|
||||
title = a.text(),
|
||||
time = epoch,
|
||||
url = a.attr("href").formattedFbUrl,
|
||||
unread = !element.hasClass("acw"),
|
||||
content = content,
|
||||
contentImgUrl = contentImg
|
||||
)
|
||||
}
|
||||
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}.*")
|
||||
.mapNotNull(this::parseMessage)
|
||||
val seeMore = parseLink(doc.getElementById("see_older_threads"))
|
||||
val extraLinks =
|
||||
threadList.nextElementSibling()?.select("a")?.mapNotNull(this::parseLink) ?: emptyList()
|
||||
return FrostMessages(threads, seeMore, extraLinks)
|
||||
}
|
||||
|
||||
private fun parseMessage(element: Element): FrostThread? {
|
||||
val a = element.getElementsByTag("a").first() ?: return null
|
||||
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()
|
||||
?: System.currentTimeMillis() % FALLBACK_TIME_MOD
|
||||
val snippet = element.select("span.snippet").firstOrNull()
|
||||
val content = snippet?.text()?.trim()
|
||||
val contentImg = snippet?.select("i[style*=url]")?.getStyleUrl()
|
||||
val img = element.getInnerImgStyle()
|
||||
return FrostThread(
|
||||
id = id,
|
||||
img = img,
|
||||
title = a.text(),
|
||||
time = epoch,
|
||||
url = a.attr("href").formattedFbUrl,
|
||||
unread = !element.hasClass("acw"),
|
||||
content = content,
|
||||
contentImgUrl = contentImg
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -26,103 +26,101 @@ 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 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 {
|
||||
with(it) {
|
||||
NotificationContent(
|
||||
data = data,
|
||||
id = id,
|
||||
href = url,
|
||||
title = null,
|
||||
text = content,
|
||||
timestamp = time,
|
||||
profileUrl = img,
|
||||
unread = unread
|
||||
)
|
||||
}
|
||||
}.toList()
|
||||
override fun getUnreadNotifications(data: CookieEntity) =
|
||||
notifs
|
||||
.asSequence()
|
||||
.filter(FrostNotif::unread)
|
||||
.map {
|
||||
with(it) {
|
||||
NotificationContent(
|
||||
data = data,
|
||||
id = id,
|
||||
href = url,
|
||||
title = null,
|
||||
text = content,
|
||||
timestamp = time,
|
||||
profileUrl = img,
|
||||
unread = unread
|
||||
)
|
||||
}
|
||||
}
|
||||
.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,
|
||||
val img: String?,
|
||||
val time: Long,
|
||||
val url: String,
|
||||
val unread: Boolean,
|
||||
val content: String,
|
||||
val timeString: String,
|
||||
val thumbnailUrl: String?
|
||||
val id: Long,
|
||||
val img: String?,
|
||||
val time: Long,
|
||||
val url: String,
|
||||
val unread: Boolean,
|
||||
val content: String,
|
||||
val timeString: String,
|
||||
val thumbnailUrl: String?
|
||||
)
|
||||
|
||||
private class NotifParserImpl : FrostParserBase<FrostNotifs>(false) {
|
||||
|
||||
override var nameRes = FbItem.NOTIFICATIONS.titleId
|
||||
override var nameRes = FbItem.NOTIFICATIONS.titleId
|
||||
|
||||
override val url = FbItem.NOTIFICATIONS.url
|
||||
override val url = FbItem.NOTIFICATIONS.url
|
||||
|
||||
override fun parseImpl(doc: Document): FrostNotifs? {
|
||||
val notificationList = doc.getElementById("notifications_list") ?: return null
|
||||
val notifications = notificationList
|
||||
.getElementsByAttributeValueMatching("id", ".*${FB_NOTIF_ID_MATCHER.pattern}.*")
|
||||
.mapNotNull(this::parseNotif)
|
||||
val seeMore =
|
||||
parseLink(doc.getElementsByAttributeValue("href", "/notifications.php?more").first())
|
||||
return FrostNotifs(notifications, seeMore)
|
||||
}
|
||||
override fun parseImpl(doc: Document): FrostNotifs? {
|
||||
val notificationList = doc.getElementById("notifications_list") ?: return null
|
||||
val notifications =
|
||||
notificationList
|
||||
.getElementsByAttributeValueMatching("id", ".*${FB_NOTIF_ID_MATCHER.pattern}.*")
|
||||
.mapNotNull(this::parseNotif)
|
||||
val seeMore =
|
||||
parseLink(doc.getElementsByAttributeValue("href", "/notifications.php?more").first())
|
||||
return FrostNotifs(notifications, seeMore)
|
||||
}
|
||||
|
||||
private fun parseNotif(element: Element): FrostNotif? {
|
||||
val a = element.getElementsByTag("a").first() ?: return null
|
||||
a.selectFirst("span.accessible_elem")?.remove()
|
||||
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()
|
||||
?: System.currentTimeMillis() % FALLBACK_TIME_MOD
|
||||
val img = element.getInnerImgStyle()
|
||||
val timeString = abbr.text()
|
||||
val content =
|
||||
a.text().replace("\u00a0", " ").removeSuffix(timeString).trim() // remove
|
||||
val thumbnail = element.selectFirst("img.thumbnail")?.attr("src")
|
||||
return FrostNotif(
|
||||
id = id,
|
||||
img = img,
|
||||
time = epoch,
|
||||
url = a.attr("href").formattedFbUrl,
|
||||
unread = !element.hasClass("acw"),
|
||||
content = content,
|
||||
timeString = timeString,
|
||||
thumbnailUrl = if (thumbnail?.isNotEmpty() == true) thumbnail else null
|
||||
)
|
||||
}
|
||||
private fun parseNotif(element: Element): FrostNotif? {
|
||||
val a = element.getElementsByTag("a").first() ?: return null
|
||||
a.selectFirst("span.accessible_elem")?.remove()
|
||||
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()
|
||||
?: System.currentTimeMillis() % FALLBACK_TIME_MOD
|
||||
val img = element.getInnerImgStyle()
|
||||
val timeString = abbr.text()
|
||||
val content = a.text().replace("\u00a0", " ").removeSuffix(timeString).trim() // remove
|
||||
val thumbnail = element.selectFirst("img.thumbnail")?.attr("src")
|
||||
return FrostNotif(
|
||||
id = id,
|
||||
img = img,
|
||||
time = epoch,
|
||||
url = a.attr("href").formattedFbUrl,
|
||||
unread = !element.hasClass("acw"),
|
||||
content = content,
|
||||
timeString = timeString,
|
||||
thumbnailUrl = if (thumbnail?.isNotEmpty() == true) thumbnail else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -27,98 +27,91 @@ 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"}"
|
||||
L._i { "Search Query $url" }
|
||||
return parseFromUrl(cookie, url)
|
||||
}
|
||||
fun query(cookie: String?, input: String): ParseResponse<FrostSearches>? {
|
||||
val url = "${FbItem._SEARCH_PARSE.url}/?q=${if (input.isNotBlank()) input.urlEncode() else "a"}"
|
||||
L._i { "Search Query $url" }
|
||||
return parseFromUrl(cookie, url)
|
||||
}
|
||||
}
|
||||
|
||||
enum class SearchKeys(val key: String) {
|
||||
USERS("keywords_users"),
|
||||
EVENTS("keywords_events")
|
||||
USERS("keywords_users"),
|
||||
EVENTS("keywords_events")
|
||||
}
|
||||
|
||||
data class FrostSearches(val results: List<FrostSearch>) : ParseData {
|
||||
|
||||
override val isEmpty: Boolean
|
||||
get() = results.isEmpty()
|
||||
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]
|
||||
*/
|
||||
data class FrostSearch(val href: String, val title: String, val description: String?) {
|
||||
|
||||
fun toSearchItem() = SearchItem(href, title, description)
|
||||
fun toSearchItem() = SearchItem(href, title, description)
|
||||
|
||||
companion object {
|
||||
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()
|
||||
)
|
||||
}
|
||||
companion object {
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class SearchParserImpl : FrostParserBase<FrostSearches>(false) {
|
||||
|
||||
override var nameRes = FbItem._SEARCH_PARSE.titleId
|
||||
override var nameRes = FbItem._SEARCH_PARSE.titleId
|
||||
|
||||
override val url = "${FbItem._SEARCH_PARSE.url}?q=google"
|
||||
override val url = "${FbItem._SEARCH_PARSE.url}?q=google"
|
||||
|
||||
private val String.formattedSearchUrl: String
|
||||
get() = replace(FACEBOOK_MBASIC_COM, FACEBOOK_BASE_COM)
|
||||
private val String.formattedSearchUrl: String
|
||||
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
|
||||
override fun parseImpl(doc: Document): FrostSearches? {
|
||||
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 url =
|
||||
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
|
||||
// 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() }
|
||||
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 } }
|
||||
}
|
||||
)
|
||||
}
|
||||
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 url =
|
||||
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
|
||||
// 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()
|
||||
}
|
||||
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 } }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -24,22 +24,17 @@ import okhttp3.Request
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
|
||||
val httpClient: OkHttpClient by lazy {
|
||||
val builder = OkHttpClient.Builder()
|
||||
if (BuildConfig.DEBUG)
|
||||
builder.addInterceptor(
|
||||
HttpLoggingInterceptor()
|
||||
.setLevel(HttpLoggingInterceptor.Level.BASIC)
|
||||
)
|
||||
builder.build()
|
||||
val builder = OkHttpClient.Builder()
|
||||
if (BuildConfig.DEBUG)
|
||||
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)
|
||||
// .cacheControl(CacheControl.FORCE_NETWORK)
|
||||
return builder
|
||||
val builder = Request.Builder().header("User-Agent", USER_AGENT)
|
||||
if (this != null) builder.header("Cookie", this)
|
||||
// .cacheControl(CacheControl.FORCE_NETWORK)
|
||||
return builder
|
||||
}
|
||||
|
||||
fun Request.Builder.call(): Call = httpClient.newCall(build())
|
||||
|
@ -24,23 +24,19 @@ 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
|
||||
FB_REDIRECT_URL_MATCHER.find(redirect)[1]?.formattedFbUrl
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "Failed to load full size image url" }
|
||||
null
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
withTimeout(timeout) {
|
||||
val redirect =
|
||||
requestBuilder().url(url).get().call().execute().body?.string() ?: return@withTimeout null
|
||||
FB_REDIRECT_URL_MATCHER.find(redirect)[1]?.formattedFbUrl
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "Failed to load full size image url" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
@ -43,211 +43,199 @@ 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"
|
||||
private const val ARG_VALID = "arg_valid"
|
||||
companion object {
|
||||
private const val ARG_POSITION = "arg_position"
|
||||
private const val ARG_VALID = "arg_valid"
|
||||
|
||||
internal operator fun invoke(
|
||||
base: () -> BaseFragment,
|
||||
prefs: Prefs,
|
||||
useFallback: Boolean,
|
||||
data: FbItem,
|
||||
position: Int
|
||||
): 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
|
||||
)
|
||||
d.put(fragment.requireArguments())
|
||||
return fragment
|
||||
}
|
||||
internal operator fun invoke(
|
||||
base: () -> BaseFragment,
|
||||
prefs: Prefs,
|
||||
useFallback: Boolean,
|
||||
data: FbItem,
|
||||
position: Int
|
||||
): 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)
|
||||
d.put(fragment.requireArguments())
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
||||
@Inject protected lateinit var mainContract: MainActivityContract
|
||||
|
||||
@Inject protected lateinit var fbCookie: FbCookie
|
||||
|
||||
@Inject protected lateinit var prefs: Prefs
|
||||
|
||||
@Inject protected lateinit var themeProvider: ThemeProvider
|
||||
|
||||
open lateinit var job: Job
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = ContextHelper.dispatcher + job
|
||||
|
||||
override val baseUrl: String by lazy { requireArguments().getString(ARG_URL)!! }
|
||||
override val baseEnum: FbItem by lazy { FbItem[arguments]!! }
|
||||
override val position: Int by lazy { requireArguments().getInt(ARG_POSITION) }
|
||||
|
||||
override var valid: Boolean
|
||||
get() = requireArguments().getBoolean(ARG_VALID, true)
|
||||
set(value) {
|
||||
if (!isActive || value || this is WebFragment) return
|
||||
requireArguments().putBoolean(ARG_VALID, value)
|
||||
frostEvent("Native Fallback", "Item" to baseEnum.name)
|
||||
mainContract.reloadFragment(this)
|
||||
}
|
||||
|
||||
@Inject
|
||||
protected lateinit var mainContract: MainActivityContract
|
||||
override var firstLoad: Boolean = true
|
||||
private var onCreateRunnable: ((FragmentContract) -> Unit)? = null
|
||||
|
||||
@Inject
|
||||
protected lateinit var fbCookie: FbCookie
|
||||
override var content: FrostContentParent? = null
|
||||
|
||||
@Inject
|
||||
protected lateinit var prefs: Prefs
|
||||
protected abstract val layoutRes: Int
|
||||
|
||||
@Inject
|
||||
protected lateinit var themeProvider: ThemeProvider
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
job = SupervisorJob()
|
||||
firstLoad = true
|
||||
}
|
||||
|
||||
open lateinit var job: Job
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = ContextHelper.dispatcher + job
|
||||
final override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
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"
|
||||
)
|
||||
this.content = content
|
||||
content.bind(this)
|
||||
return view
|
||||
}
|
||||
|
||||
override val baseUrl: String by lazy { requireArguments().getString(ARG_URL)!! }
|
||||
override val baseEnum: FbItem by lazy { FbItem[arguments]!! }
|
||||
override val position: Int by lazy { requireArguments().getInt(ARG_POSITION) }
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
onCreateRunnable?.invoke(this)
|
||||
onCreateRunnable = null
|
||||
firstLoadRequest()
|
||||
attach(mainContract)
|
||||
}
|
||||
|
||||
override var valid: Boolean
|
||||
get() = requireArguments().getBoolean(ARG_VALID, true)
|
||||
set(value) {
|
||||
if (!isActive || value || this is WebFragment) return
|
||||
requireArguments().putBoolean(ARG_VALID, value)
|
||||
frostEvent(
|
||||
"Native Fallback",
|
||||
"Item" to baseEnum.name
|
||||
)
|
||||
mainContract.reloadFragment(this)
|
||||
}
|
||||
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
||||
super.setUserVisibleHint(isVisibleToUser)
|
||||
firstLoadRequest()
|
||||
}
|
||||
|
||||
override var firstLoad: Boolean = true
|
||||
private var onCreateRunnable: ((FragmentContract) -> Unit)? = null
|
||||
|
||||
override var content: FrostContentParent? = null
|
||||
|
||||
protected abstract val layoutRes: Int
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
job = SupervisorJob()
|
||||
firstLoad = true
|
||||
override fun firstLoadRequest() {
|
||||
val core = core ?: return
|
||||
if (userVisibleHint && isVisible && firstLoad) {
|
||||
core.reloadBase(true)
|
||||
firstLoad = false
|
||||
}
|
||||
}
|
||||
|
||||
final override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
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")
|
||||
this.content = content
|
||||
content.bind(this)
|
||||
return view
|
||||
}
|
||||
override fun post(action: (fragment: FragmentContract) -> Unit) {
|
||||
onCreateRunnable = action
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
onCreateRunnable?.invoke(this)
|
||||
onCreateRunnable = null
|
||||
firstLoadRequest()
|
||||
attach(mainContract)
|
||||
}
|
||||
override fun setTitle(title: String) {
|
||||
mainContract.setTitle(title)
|
||||
}
|
||||
|
||||
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
||||
super.setUserVisibleHint(isVisibleToUser)
|
||||
firstLoadRequest()
|
||||
}
|
||||
|
||||
override fun firstLoadRequest() {
|
||||
val core = core ?: return
|
||||
if (userVisibleHint && isVisible && firstLoad) {
|
||||
core.reloadBase(true)
|
||||
firstLoad = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun post(action: (fragment: FragmentContract) -> Unit) {
|
||||
onCreateRunnable = action
|
||||
}
|
||||
|
||||
override fun setTitle(title: String) {
|
||||
mainContract.setTitle(title)
|
||||
}
|
||||
|
||||
override fun attach(contract: MainActivityContract) {
|
||||
contract.fragmentFlow
|
||||
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
|
||||
.onEach { flag ->
|
||||
when (flag) {
|
||||
REQUEST_REFRESH -> {
|
||||
core?.apply {
|
||||
clearHistory()
|
||||
firstLoad = true
|
||||
firstLoadRequest()
|
||||
}
|
||||
}
|
||||
position -> {
|
||||
contract.setTitle(baseEnum.titleId)
|
||||
updateFab(contract)
|
||||
core?.active = true
|
||||
}
|
||||
-(position + 1) -> {
|
||||
core?.active = false
|
||||
}
|
||||
REQUEST_TEXT_ZOOM -> {
|
||||
reloadTextSize()
|
||||
}
|
||||
}
|
||||
}.launchIn(this)
|
||||
}
|
||||
|
||||
override fun updateFab(contract: MainFabContract) {
|
||||
contract.hideFab() // default
|
||||
}
|
||||
|
||||
protected fun FloatingActionButton.update(iicon: IIcon, click: () -> Unit) {
|
||||
if (isShown) {
|
||||
fadeScaleTransition {
|
||||
setIcon(iicon, themeProvider.iconColor)
|
||||
override fun attach(contract: MainActivityContract) {
|
||||
contract.fragmentFlow
|
||||
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
|
||||
.onEach { flag ->
|
||||
when (flag) {
|
||||
REQUEST_REFRESH -> {
|
||||
core?.apply {
|
||||
clearHistory()
|
||||
firstLoad = true
|
||||
firstLoadRequest()
|
||||
}
|
||||
} else {
|
||||
setIcon(iicon, themeProvider.iconColor)
|
||||
show()
|
||||
}
|
||||
position -> {
|
||||
contract.setTitle(baseEnum.titleId)
|
||||
updateFab(contract)
|
||||
core?.active = true
|
||||
}
|
||||
-(position + 1) -> {
|
||||
core?.active = false
|
||||
}
|
||||
REQUEST_TEXT_ZOOM -> {
|
||||
reloadTextSize()
|
||||
}
|
||||
}
|
||||
setOnClickListener { click() }
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
override fun updateFab(contract: MainFabContract) {
|
||||
contract.hideFab() // default
|
||||
}
|
||||
|
||||
protected fun FloatingActionButton.update(iicon: IIcon, click: () -> Unit) {
|
||||
if (isShown) {
|
||||
fadeScaleTransition { setIcon(iicon, themeProvider.iconColor) }
|
||||
} else {
|
||||
setIcon(iicon, themeProvider.iconColor)
|
||||
show()
|
||||
}
|
||||
setOnClickListener { click() }
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
L.i { "Fragment on destroy $position ${hashCode()}" }
|
||||
content?.destroy()
|
||||
content = null
|
||||
}
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
L.i { "Fragment on destroy $position ${hashCode()}" }
|
||||
content?.destroy()
|
||||
content = null
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
job.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
override fun onDestroy() {
|
||||
job.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun reloadTheme() {
|
||||
reloadThemeSelf()
|
||||
content?.reloadTextSize()
|
||||
}
|
||||
override fun reloadTheme() {
|
||||
reloadThemeSelf()
|
||||
content?.reloadTextSize()
|
||||
}
|
||||
|
||||
override fun reloadThemeSelf() {
|
||||
// intentionally blank
|
||||
}
|
||||
override fun reloadThemeSelf() {
|
||||
// intentionally blank
|
||||
}
|
||||
|
||||
override fun reloadTextSize() {
|
||||
reloadTextSizeSelf()
|
||||
content?.reloadTextSize()
|
||||
}
|
||||
override fun reloadTextSize() {
|
||||
reloadTextSizeSelf()
|
||||
content?.reloadTextSize()
|
||||
}
|
||||
|
||||
override fun reloadTextSizeSelf() {
|
||||
// intentionally blank
|
||||
}
|
||||
override fun reloadTextSizeSelf() {
|
||||
// intentionally blank
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean = content?.core?.onBackPressed() ?: false
|
||||
override fun onBackPressed(): Boolean = content?.core?.onBackPressed() ?: false
|
||||
|
||||
override fun onTabClick(): Unit = content?.core?.onTabClicked() ?: Unit
|
||||
override fun onTabClick(): Unit = content?.core?.onTabClicked() ?: Unit
|
||||
}
|
||||
|
@ -23,82 +23,65 @@ 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?
|
||||
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
|
||||
*/
|
||||
var valid: Boolean
|
||||
/**
|
||||
* 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]
|
||||
*/
|
||||
val core: FrostContentCore?
|
||||
get() = content?.core
|
||||
/** Helper to retrieve the core from [content] */
|
||||
val core: FrostContentCore?
|
||||
get() = content?.core
|
||||
|
||||
/**
|
||||
* Specifies position in Activity's viewpager
|
||||
*/
|
||||
val position: Int
|
||||
/** Specifies position in Activity's viewpager */
|
||||
val position: Int
|
||||
|
||||
/**
|
||||
* Specifies whether if current load
|
||||
* will be fragment's first load
|
||||
*
|
||||
* Defaults to true
|
||||
*/
|
||||
var firstLoad: Boolean
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
fun firstLoadRequest()
|
||||
/**
|
||||
* 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)
|
||||
fun updateFab(contract: MainFabContract)
|
||||
|
||||
/**
|
||||
* Single callable action to be executed upon creation
|
||||
* Note that this call is not guaranteed
|
||||
*/
|
||||
fun post(action: (fragment: FragmentContract) -> Unit)
|
||||
/** 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.
|
||||
*/
|
||||
fun attach(contract: MainActivityContract)
|
||||
/** Call whenever a fragment is attached so that it may listen to activity emissions. */
|
||||
fun attach(contract: MainActivityContract)
|
||||
|
||||
/*
|
||||
* -----------------------------------------
|
||||
* Delegates
|
||||
* -----------------------------------------
|
||||
*/
|
||||
/*
|
||||
* -----------------------------------------
|
||||
* Delegates
|
||||
* -----------------------------------------
|
||||
*/
|
||||
|
||||
fun onBackPressed(): Boolean
|
||||
fun onBackPressed(): Boolean
|
||||
|
||||
fun onTabClick()
|
||||
fun onTabClick()
|
||||
}
|
||||
|
||||
interface RecyclerContentContract {
|
||||
|
||||
fun bind(recyclerView: FrostRecyclerView)
|
||||
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
|
||||
*/
|
||||
suspend fun reload(progress: (Int) -> Unit): Boolean
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
@ -32,119 +32,113 @@ 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
|
||||
override val layoutRes: Int = R.layout.view_content_recycler
|
||||
|
||||
abstract val adapter: ModelAdapter<T, Item>
|
||||
abstract val adapter: ModelAdapter<T, Item>
|
||||
|
||||
override fun firstLoadRequest() {
|
||||
val core = core ?: return
|
||||
if (firstLoad) {
|
||||
core.reloadBase(true)
|
||||
firstLoad = false
|
||||
override fun firstLoadRequest() {
|
||||
val core = core ?: return
|
||||
if (firstLoad) {
|
||||
core.reloadBase(true)
|
||||
firstLoad = false
|
||||
}
|
||||
}
|
||||
|
||||
final override suspend fun reload(progress: (Int) -> Unit): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
val data =
|
||||
try {
|
||||
reloadImpl(progress)
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "Recycler reload fail $baseUrl" }
|
||||
null
|
||||
}
|
||||
withMainContext {
|
||||
if (data == null) {
|
||||
valid = false
|
||||
false
|
||||
} else {
|
||||
adapter.setNewList(data)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final override suspend fun reload(progress: (Int) -> Unit): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
val data = try {
|
||||
reloadImpl(progress)
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "Recycler reload fail $baseUrl" }
|
||||
null
|
||||
}
|
||||
withMainContext {
|
||||
if (data == null) {
|
||||
valid = false
|
||||
false
|
||||
} else {
|
||||
adapter.setNewList(data)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract suspend fun reloadImpl(progress: (Int) -> Unit): List<T>?
|
||||
protected abstract suspend fun reloadImpl(progress: (Int) -> Unit): List<T>?
|
||||
}
|
||||
|
||||
abstract class GenericRecyclerFragment<T, Item : GenericItem> : RecyclerFragment<T, Item>() {
|
||||
|
||||
abstract fun mapper(data: T): Item
|
||||
abstract fun mapper(data: T): Item
|
||||
|
||||
override val adapter: ModelAdapter<T, Item> = ModelAdapter { this.mapper(it) }
|
||||
override val adapter: ModelAdapter<T, Item> = ModelAdapter { this.mapper(it) }
|
||||
|
||||
final override fun bind(recyclerView: FrostRecyclerView) {
|
||||
recyclerView.adapter = getAdapter()
|
||||
recyclerView.onReloadClear = { adapter.clear() }
|
||||
bindImpl(recyclerView)
|
||||
}
|
||||
final override fun bind(recyclerView: FrostRecyclerView) {
|
||||
recyclerView.adapter = getAdapter()
|
||||
recyclerView.onReloadClear = { adapter.clear() }
|
||||
bindImpl(recyclerView)
|
||||
}
|
||||
|
||||
/**
|
||||
* Anything to call for one time bindings
|
||||
* At this stage, all adapters will have FastAdapter references
|
||||
*/
|
||||
open fun bindImpl(recyclerView: FrostRecyclerView) = Unit
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
open fun getAdapter(): GenericFastAdapter = fastAdapter(this.adapter)
|
||||
/** 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>() {
|
||||
RecyclerFragment<Item, Item>() {
|
||||
|
||||
/**
|
||||
* The parser to make this all happen
|
||||
*/
|
||||
abstract val parser: FrostParser<T>
|
||||
/** The parser to make this all happen */
|
||||
abstract val parser: FrostParser<T>
|
||||
|
||||
open fun getDoc(cookie: String?) = frostJsoup(cookie, parser.url)
|
||||
open fun getDoc(cookie: String?) = frostJsoup(cookie, parser.url)
|
||||
|
||||
abstract fun toItems(response: ParseResponse<T>): List<Item>
|
||||
abstract fun toItems(response: ParseResponse<T>): List<Item>
|
||||
|
||||
override val adapter: ItemAdapter<Item> = ItemAdapter()
|
||||
override val adapter: ItemAdapter<Item> = ItemAdapter()
|
||||
|
||||
final override fun bind(recyclerView: FrostRecyclerView) {
|
||||
recyclerView.adapter = getAdapter()
|
||||
recyclerView.onReloadClear = { adapter.clear() }
|
||||
bindImpl(recyclerView)
|
||||
}
|
||||
final override fun bind(recyclerView: FrostRecyclerView) {
|
||||
recyclerView.adapter = getAdapter()
|
||||
recyclerView.onReloadClear = { adapter.clear() }
|
||||
bindImpl(recyclerView)
|
||||
}
|
||||
|
||||
/**
|
||||
* Anything to call for one time bindings
|
||||
* At this stage, all adapters will have FastAdapter references
|
||||
*/
|
||||
open fun bindImpl(recyclerView: FrostRecyclerView) = Unit
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
open fun getAdapter(): GenericFastAdapter = fastAdapter(this.adapter)
|
||||
/** 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>? =
|
||||
withContext(Dispatchers.IO) {
|
||||
progress(10)
|
||||
val cookie = fbCookie.webCookie
|
||||
val doc = getDoc(cookie)
|
||||
progress(60)
|
||||
val response = try {
|
||||
parser.parse(cookie, doc)
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
}
|
||||
if (response == null) {
|
||||
L.i { "RecyclerFragment failed for ${baseEnum.name}" }
|
||||
L._d { "Cookie used: $cookie" }
|
||||
return@withContext null
|
||||
}
|
||||
progress(80)
|
||||
val items = toItems(response)
|
||||
progress(97)
|
||||
return@withContext items
|
||||
override suspend fun reloadImpl(progress: (Int) -> Unit): List<Item>? =
|
||||
withContext(Dispatchers.IO) {
|
||||
progress(10)
|
||||
val cookie = fbCookie.webCookie
|
||||
val doc = getDoc(cookie)
|
||||
progress(60)
|
||||
val response =
|
||||
try {
|
||||
parser.parse(cookie, doc)
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
}
|
||||
if (response == null) {
|
||||
L.i { "RecyclerFragment failed for ${baseEnum.name}" }
|
||||
L._d { "Cookie used: $cookie" }
|
||||
return@withContext null
|
||||
}
|
||||
progress(80)
|
||||
val items = toItems(response)
|
||||
progress(97)
|
||||
return@withContext items
|
||||
}
|
||||
}
|
||||
|
@ -27,19 +27,22 @@ 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
|
||||
override val parser = NotifParser
|
||||
|
||||
override fun getDoc(cookie: String?) = frostJsoup(cookie, "${FbItem.NOTIFICATIONS.url}?more")
|
||||
override fun getDoc(cookie: String?) = frostJsoup(cookie, "${FbItem.NOTIFICATIONS.url}?more")
|
||||
|
||||
override fun toItems(response: ParseResponse<FrostNotifs>): List<NotificationIItem> =
|
||||
response.data.notifs.map { NotificationIItem(it, response.cookie, themeProvider) }
|
||||
override fun toItems(response: ParseResponse<FrostNotifs>): List<NotificationIItem> =
|
||||
response.data.notifs.map { NotificationIItem(it, response.cookie, themeProvider) }
|
||||
|
||||
override fun bindImpl(recyclerView: FrostRecyclerView) {
|
||||
NotificationIItem.bindEvents(adapter, fbCookie, prefs, themeProvider)
|
||||
}
|
||||
override fun bindImpl(recyclerView: FrostRecyclerView) {
|
||||
NotificationIItem.bindEvents(adapter, fbCookie, prefs, themeProvider)
|
||||
}
|
||||
}
|
||||
|
@ -31,34 +31,30 @@ 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
|
||||
override val layoutRes: Int = R.layout.view_content_web
|
||||
|
||||
/**
|
||||
* Given a webview, output a client
|
||||
*/
|
||||
fun client(web: FrostWebView) = when (baseEnum) {
|
||||
FbItem.MESSENGER -> FrostWebViewClientMessenger(web)
|
||||
FbItem.MENU -> FrostWebViewClientMenu(web)
|
||||
else -> FrostWebViewClient(web)
|
||||
/** Given a webview, output a client */
|
||||
fun client(web: FrostWebView) =
|
||||
when (baseEnum) {
|
||||
FbItem.MESSENGER -> FrostWebViewClientMessenger(web)
|
||||
FbItem.MENU -> FrostWebViewClientMenu(web)
|
||||
else -> FrostWebViewClient(web)
|
||||
}
|
||||
|
||||
override fun updateFab(contract: MainFabContract) {
|
||||
val web = core as? WebView
|
||||
if (web == null) {
|
||||
L.e { "Webview not found in fragment $baseEnum" }
|
||||
return super.updateFab(contract)
|
||||
}
|
||||
if (baseEnum.isFeed && prefs.showCreateFab) {
|
||||
contract.showFab(GoogleMaterial.Icon.gmd_edit) {
|
||||
JsActions.CREATE_POST.inject(web, prefs)
|
||||
}
|
||||
return
|
||||
}
|
||||
super.updateFab(contract)
|
||||
override fun updateFab(contract: MainFabContract) {
|
||||
val web = core as? WebView
|
||||
if (web == null) {
|
||||
L.e { "Webview not found in fragment $baseEnum" }
|
||||
return super.updateFab(contract)
|
||||
}
|
||||
if (baseEnum.isFeed && prefs.showCreateFab) {
|
||||
contract.showFab(GoogleMaterial.Icon.gmd_edit) { JsActions.CREATE_POST.inject(web, prefs) }
|
||||
return
|
||||
}
|
||||
super.updateFab(contract)
|
||||
}
|
||||
}
|
||||
|
@ -27,50 +27,51 @@ 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()
|
||||
val circleCrop
|
||||
get() = CircleCrop()
|
||||
}
|
||||
|
||||
fun <T> RequestBuilder<T>.transform(vararg transformation: BitmapTransformation): RequestBuilder<T> =
|
||||
when (transformation.size) {
|
||||
0 -> this
|
||||
1 -> apply(RequestOptions.bitmapTransform(transformation[0]))
|
||||
else -> apply(RequestOptions.bitmapTransform(MultiTransformation(*transformation)))
|
||||
}
|
||||
fun <T> RequestBuilder<T>.transform(
|
||||
vararg transformation: BitmapTransformation
|
||||
): RequestBuilder<T> =
|
||||
when (transformation.size) {
|
||||
0 -> this
|
||||
1 -> apply(RequestOptions.bitmapTransform(transformation[0]))
|
||||
else -> apply(RequestOptions.bitmapTransform(MultiTransformation(*transformation)))
|
||||
}
|
||||
|
||||
@GlideModule
|
||||
class FrostGlideModule : AppGlideModule() {
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
// registry.replace(GlideUrl::class.java,
|
||||
// InputStream::class.java,
|
||||
// OkHttpUrlLoader.Factory(getFrostHttpClient()))
|
||||
// registry.prepend(HdImageMaybe::class.java, InputStream::class.java, HdImageLoadingFactory())
|
||||
}
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
// registry.replace(GlideUrl::class.java,
|
||||
// InputStream::class.java,
|
||||
// OkHttpUrlLoader.Factory(getFrostHttpClient()))
|
||||
// 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()
|
||||
val cookie = fbCookie.webCookie ?: return chain.proceed(origRequest)
|
||||
val request = origRequest.newBuilder().addHeader("Cookie", cookie).build()
|
||||
return chain.proceed(request)
|
||||
}
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val origRequest = chain.request()
|
||||
val cookie = fbCookie.webCookie ?: return chain.proceed(origRequest)
|
||||
val request = origRequest.newBuilder().addHeader("Cookie", cookie).build()
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
||||
|
@ -32,101 +32,86 @@ 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?
|
||||
val url: String?
|
||||
|
||||
fun click(context: Context, fbCookie: FbCookie, prefs: Prefs) {
|
||||
val url = url ?: return
|
||||
context.launchWebOverlay(url, fbCookie, prefs)
|
||||
}
|
||||
fun click(context: Context, fbCookie: FbCookie, prefs: Prefs) {
|
||||
val url = url ?: return
|
||||
context.launchWebOverlay(url, fbCookie, prefs)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun bindEvents(adapter: IAdapter<GenericItem>, fbCookie: FbCookie, prefs: Prefs) {
|
||||
adapter.fastAdapter?.apply {
|
||||
selectExtension {
|
||||
isSelectable = false
|
||||
}
|
||||
onClickListener = { v, _, item, _ ->
|
||||
if (item is ClickableIItemContract) {
|
||||
item.click(v!!.context, fbCookie, prefs)
|
||||
true
|
||||
} else
|
||||
false
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
fun bindEvents(adapter: IAdapter<GenericItem>, fbCookie: FbCookie, prefs: Prefs) {
|
||||
adapter.fastAdapter?.apply {
|
||||
selectExtension { isSelectable = false }
|
||||
onClickListener = { v, _, item, _ ->
|
||||
if (item is ClickableIItemContract) {
|
||||
item.click(v!!.context, fbCookie, prefs)
|
||||
true
|
||||
} 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>(
|
||||
val text: String?,
|
||||
itemId: Int = R.layout.iitem_header,
|
||||
private val themeProvider: ThemeProvider
|
||||
) :
|
||||
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)
|
||||
val text: TextView by bindView(R.id.item_header_text)
|
||||
|
||||
override fun bindView(item: HeaderIItem, payloads: List<Any>) {
|
||||
text.setTextColor(themeProvider.accentColor)
|
||||
text.text = item.text
|
||||
text.setBackgroundColor(themeProvider.nativeBgColor)
|
||||
}
|
||||
|
||||
override fun unbindView(item: HeaderIItem) {
|
||||
text.text = null
|
||||
}
|
||||
override fun bindView(item: HeaderIItem, payloads: List<Any>) {
|
||||
text.setTextColor(themeProvider.accentColor)
|
||||
text.text = item.text
|
||||
text.setBackgroundColor(themeProvider.nativeBgColor)
|
||||
}
|
||||
|
||||
override fun unbindView(item: HeaderIItem) {
|
||||
text.text = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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),
|
||||
ClickableIItemContract {
|
||||
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),
|
||||
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)
|
||||
val text: TextView by bindView(R.id.item_text_view)
|
||||
|
||||
override fun bindView(item: TextIItem, payloads: List<Any>) {
|
||||
text.setTextColor(themeProvider.textColor)
|
||||
text.text = item.text
|
||||
text.background =
|
||||
createSimpleRippleDrawable(themeProvider.bgColor, themeProvider.nativeBgColor)
|
||||
}
|
||||
|
||||
override fun unbindView(item: TextIItem) {
|
||||
text.text = null
|
||||
}
|
||||
override fun bindView(item: TextIItem, payloads: List<Any>) {
|
||||
text.setTextColor(themeProvider.textColor)
|
||||
text.text = item.text
|
||||
text.background =
|
||||
createSimpleRippleDrawable(themeProvider.bgColor, themeProvider.nativeBgColor)
|
||||
}
|
||||
|
||||
override fun unbindView(item: TextIItem) {
|
||||
text.text = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,118 +41,107 @@ 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) }
|
||||
) {
|
||||
val notification: FrostNotif,
|
||||
val cookie: String,
|
||||
private val themeProvider: ThemeProvider
|
||||
) :
|
||||
KauIItem<NotificationIItem.ViewHolder>(
|
||||
R.layout.iitem_notification,
|
||||
{ ViewHolder(it, themeProvider) }
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun bindEvents(
|
||||
adapter: ItemAdapter<NotificationIItem>,
|
||||
fbCookie: FbCookie,
|
||||
prefs: Prefs,
|
||||
themeProvider: ThemeProvider
|
||||
) {
|
||||
adapter.fastAdapter?.apply {
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
// TODO temp fix. If url is dependent, we cannot load it directly
|
||||
v!!.context.launchWebOverlay(
|
||||
if (notif.url.isIndependent) notif.url else FbItem.NOTIFICATIONS.url,
|
||||
fbCookie,
|
||||
prefs
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo see if necessary
|
||||
val DIFF: DiffCallback<NotificationIItem> by lazy(::Diff)
|
||||
}
|
||||
|
||||
private class Diff : DiffCallback<NotificationIItem> {
|
||||
|
||||
override fun areItemsTheSame(oldItem: NotificationIItem, newItem: NotificationIItem) =
|
||||
oldItem.notification.id == newItem.notification.id
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: NotificationIItem,
|
||||
newItem: NotificationIItem
|
||||
) =
|
||||
oldItem.notification == newItem.notification
|
||||
|
||||
override fun getChangePayload(
|
||||
oldItem: NotificationIItem,
|
||||
oldItemPosition: Int,
|
||||
newItem: NotificationIItem,
|
||||
newItemPosition: Int
|
||||
): Any? {
|
||||
return newItem
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
private val content: TextView by bindView(R.id.item_content)
|
||||
private val date: TextView by bindView(R.id.item_date)
|
||||
private val thumbnail: ImageView by bindView(R.id.item_thumbnail)
|
||||
|
||||
private val glide
|
||||
get() = GlideApp.with(itemView)
|
||||
|
||||
override fun bindView(item: NotificationIItem, payloads: List<Any>) {
|
||||
val notif = item.notification
|
||||
frame.background = createSimpleRippleDrawable(
|
||||
themeProvider.textColor,
|
||||
themeProvider.nativeBgColor(notif.unread)
|
||||
companion object {
|
||||
fun bindEvents(
|
||||
adapter: ItemAdapter<NotificationIItem>,
|
||||
fbCookie: FbCookie,
|
||||
prefs: Prefs,
|
||||
themeProvider: ThemeProvider
|
||||
) {
|
||||
adapter.fastAdapter?.apply {
|
||||
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)
|
||||
)
|
||||
content.setTextColor(themeProvider.textColor)
|
||||
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())
|
||||
|
||||
content.text = notif.content
|
||||
date.text = notif.timeString
|
||||
}
|
||||
|
||||
override fun unbindView(item: NotificationIItem) {
|
||||
frame.background = null
|
||||
val glide = glide
|
||||
glide.clear(avatar)
|
||||
glide.clear(thumbnail)
|
||||
thumbnail.gone()
|
||||
content.text = null
|
||||
date.text = null
|
||||
}
|
||||
// TODO temp fix. If url is dependent, we cannot load it directly
|
||||
v!!
|
||||
.context
|
||||
.launchWebOverlay(
|
||||
if (notif.url.isIndependent) notif.url else FbItem.NOTIFICATIONS.url,
|
||||
fbCookie,
|
||||
prefs
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo see if necessary
|
||||
val DIFF: DiffCallback<NotificationIItem> by lazy(::Diff)
|
||||
}
|
||||
|
||||
private class Diff : DiffCallback<NotificationIItem> {
|
||||
|
||||
override fun areItemsTheSame(oldItem: NotificationIItem, newItem: NotificationIItem) =
|
||||
oldItem.notification.id == newItem.notification.id
|
||||
|
||||
override fun areContentsTheSame(oldItem: NotificationIItem, newItem: NotificationIItem) =
|
||||
oldItem.notification == newItem.notification
|
||||
|
||||
override fun getChangePayload(
|
||||
oldItem: NotificationIItem,
|
||||
oldItemPosition: Int,
|
||||
newItem: NotificationIItem,
|
||||
newItemPosition: Int
|
||||
): Any? {
|
||||
return newItem
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
private val content: TextView by bindView(R.id.item_content)
|
||||
private val date: TextView by bindView(R.id.item_date)
|
||||
private val thumbnail: ImageView by bindView(R.id.item_thumbnail)
|
||||
|
||||
private val glide
|
||||
get() = GlideApp.with(itemView)
|
||||
|
||||
override fun bindView(item: NotificationIItem, payloads: List<Any>) {
|
||||
val notif = item.notification
|
||||
frame.background =
|
||||
createSimpleRippleDrawable(
|
||||
themeProvider.textColor,
|
||||
themeProvider.nativeBgColor(notif.unread)
|
||||
)
|
||||
content.setTextColor(themeProvider.textColor)
|
||||
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())
|
||||
|
||||
content.text = notif.content
|
||||
date.text = notif.timeString
|
||||
}
|
||||
|
||||
override fun unbindView(item: NotificationIItem) {
|
||||
frame.background = null
|
||||
val glide = glide
|
||||
glide.clear(avatar)
|
||||
glide.clear(thumbnail)
|
||||
thumbnail.gone()
|
||||
content.text = null
|
||||
date.text = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,41 +31,33 @@ 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) }
|
||||
),
|
||||
IDraggable {
|
||||
KauIItem<TabIItem.ViewHolder>(R.layout.iitem_tab_preview, { ViewHolder(it, themeProvider) }),
|
||||
IDraggable {
|
||||
|
||||
override val isDraggable: Boolean = true
|
||||
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)
|
||||
val image: ImageView by bindView(R.id.image)
|
||||
val text: TextView by bindView(R.id.text)
|
||||
|
||||
override fun bindView(item: TabIItem, payloads: List<Any>) {
|
||||
val isInToolbar = adapterPosition < 4
|
||||
val color = if (isInToolbar) themeProvider.iconColor else themeProvider.textColor
|
||||
image.setIcon(item.item.icon, 20, color)
|
||||
if (isInToolbar)
|
||||
text.invisible()
|
||||
else {
|
||||
text.visible().setText(item.item.titleId)
|
||||
text.setTextColor(color.withAlpha(200))
|
||||
}
|
||||
}
|
||||
|
||||
override fun unbindView(item: TabIItem) {
|
||||
image.setImageDrawable(null)
|
||||
text.visible().text = null
|
||||
}
|
||||
override fun bindView(item: TabIItem, payloads: List<Any>) {
|
||||
val isInToolbar = adapterPosition < 4
|
||||
val color = if (isInToolbar) themeProvider.iconColor else themeProvider.textColor
|
||||
image.setIcon(item.item.icon, 20, color)
|
||||
if (isInToolbar) text.invisible()
|
||||
else {
|
||||
text.visible().setText(item.item.titleId)
|
||||
text.setTextColor(color.withAlpha(200))
|
||||
}
|
||||
}
|
||||
|
||||
override fun unbindView(item: TabIItem) {
|
||||
image.setImageDrawable(null)
|
||||
text.visible().text = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,23 +19,24 @@ 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}")
|
||||
;
|
||||
/*
|
||||
* 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}"
|
||||
);
|
||||
|
||||
val injector: JsInjector by lazy {
|
||||
JsBuilder().css(content).single("css-small-assets-$name").build()
|
||||
}
|
||||
val injector: JsInjector by lazy {
|
||||
JsBuilder().css(content).single("css-small-assets-$name").build()
|
||||
}
|
||||
|
||||
override fun inject(webView: WebView, prefs: Prefs) {
|
||||
injector.inject(webView, prefs)
|
||||
}
|
||||
override fun inject(webView: WebView, prefs: Prefs) {
|
||||
injector.inject(webView, prefs)
|
||||
}
|
||||
}
|
||||
|
@ -25,41 +25,34 @@ import com.pitchedapps.frost.prefs.Prefs
|
||||
* List of elements to hide
|
||||
*/
|
||||
enum class CssHider(private vararg val items: String) : InjectorContract {
|
||||
CORE("[data-sigil=m_login_upsell]", "[role=progressbar]"),
|
||||
HEADER(
|
||||
"#header:not(.mFuturePageHeader):not(.titled)",
|
||||
"#mJewelNav",
|
||||
"[data-sigil=MTopBlueBarHeader]",
|
||||
"#header-notices",
|
||||
"[data-sigil*=m-promo-jewel-header]"
|
||||
),
|
||||
ADS(
|
||||
"article[data-xt*=sponsor]",
|
||||
"article[data-store*=sponsor]"
|
||||
),
|
||||
PEOPLE_YOU_MAY_KNOW("article._d2r"),
|
||||
SUGGESTED_GROUPS("article[data-ft*=\"ei\":]"),
|
||||
COMPOSER("#MComposer"),
|
||||
MESSENGER("._s15", "[data-testid=info_panel]", "js_i"),
|
||||
NON_RECENT("article:not([data-store*=actor_name])"),
|
||||
STORIES(
|
||||
"#MStoriesTray",
|
||||
// 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\"]"
|
||||
)
|
||||
;
|
||||
CORE("[data-sigil=m_login_upsell]", "[role=progressbar]"),
|
||||
HEADER(
|
||||
"#header:not(.mFuturePageHeader):not(.titled)",
|
||||
"#mJewelNav",
|
||||
"[data-sigil=MTopBlueBarHeader]",
|
||||
"#header-notices",
|
||||
"[data-sigil*=m-promo-jewel-header]"
|
||||
),
|
||||
ADS("article[data-xt*=sponsor]", "article[data-store*=sponsor]"),
|
||||
PEOPLE_YOU_MAY_KNOW("article._d2r"),
|
||||
SUGGESTED_GROUPS("article[data-ft*=\"ei\":]"),
|
||||
COMPOSER("#MComposer"),
|
||||
MESSENGER("._s15", "[data-testid=info_panel]", "js_i"),
|
||||
NON_RECENT("article:not([data-store*=actor_name])"),
|
||||
STORIES(
|
||||
"#MStoriesTray",
|
||||
// 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\"]");
|
||||
|
||||
val injector: JsInjector by lazy {
|
||||
JsBuilder().css("${items.joinToString(separator = ",")}{display:none !important}")
|
||||
.single("css-hider-$name").build()
|
||||
}
|
||||
val injector: JsInjector by lazy {
|
||||
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)
|
||||
}
|
||||
|
@ -26,27 +26,26 @@ import com.pitchedapps.frost.prefs.Prefs
|
||||
* Collection of short js functions that are embedded directly
|
||||
*/
|
||||
enum class JsActions(body: String) : InjectorContract {
|
||||
/**
|
||||
* 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);"""),
|
||||
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
|
||||
*/
|
||||
EMPTY("");
|
||||
/**
|
||||
* 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);"""
|
||||
),
|
||||
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 */
|
||||
EMPTY("");
|
||||
|
||||
val function = "(function(){$body})();"
|
||||
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")
|
||||
private inline fun clickBySelector(selector: String): String =
|
||||
"""document.querySelector("$selector").click()"""
|
||||
"""document.querySelector("$selector").click()"""
|
||||
|
@ -22,43 +22,49 @@ 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"
|
||||
private val injector = lazyContext {
|
||||
try {
|
||||
val content = it.assets.open("js/$file").bufferedReader().use(BufferedReader::readText)
|
||||
JsBuilder().js(content).run { if (singleLoad) single(name) else this }.build()
|
||||
} catch (e: FileNotFoundException) {
|
||||
L.e(e) { "JsAssets file not found" }
|
||||
JsInjector(JsActions.EMPTY.function)
|
||||
}
|
||||
@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)
|
||||
JsBuilder().js(content).run { if (singleLoad) single(name) else this }.build()
|
||||
} catch (e: FileNotFoundException) {
|
||||
L.e(e) { "JsAssets file not found" }
|
||||
JsInjector(JsActions.EMPTY.function)
|
||||
}
|
||||
}
|
||||
|
||||
override fun inject(webView: WebView, prefs: Prefs) =
|
||||
injector(webView.context).inject(webView, prefs)
|
||||
override fun inject(webView: WebView, prefs: Prefs) =
|
||||
injector(webView.context).inject(webView, prefs)
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
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) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,125 +21,115 @@ 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()
|
||||
private val js = StringBuilder()
|
||||
private val css = StringBuilder()
|
||||
private val js = StringBuilder()
|
||||
|
||||
private var tag: String? = null
|
||||
private var tag: String? = null
|
||||
|
||||
fun css(css: String): JsBuilder {
|
||||
this.css.append(StringEscapeUtils.escapeEcmaScript(css))
|
||||
return this
|
||||
}
|
||||
fun css(css: String): JsBuilder {
|
||||
this.css.append(StringEscapeUtils.escapeEcmaScript(css))
|
||||
return this
|
||||
}
|
||||
|
||||
fun js(content: String): JsBuilder {
|
||||
this.js.append(content)
|
||||
return this
|
||||
}
|
||||
fun js(content: String): JsBuilder {
|
||||
this.js.append(content)
|
||||
return this
|
||||
}
|
||||
|
||||
fun single(tag: String): JsBuilder {
|
||||
this.tag = TagObfuscator.obfuscateTag(tag)
|
||||
return this
|
||||
}
|
||||
fun single(tag: String): JsBuilder {
|
||||
this.tag = TagObfuscator.obfuscateTag(tag)
|
||||
return this
|
||||
}
|
||||
|
||||
fun build() = JsInjector(toString())
|
||||
fun build() = JsInjector(toString())
|
||||
|
||||
override fun toString(): String {
|
||||
val tag = this.tag
|
||||
val builder = StringBuilder().apply {
|
||||
append("!function(){")
|
||||
if (css.isNotBlank()) {
|
||||
val cssMin = css.replace(Regex("\\s*\n\\s*"), "")
|
||||
append("var a=document.createElement('style');")
|
||||
append("a.innerHTML='$cssMin';")
|
||||
if (tag != null) {
|
||||
append("a.id='$tag';")
|
||||
}
|
||||
append("document.head.appendChild(a);")
|
||||
}
|
||||
if (js.isNotBlank()) {
|
||||
append(js)
|
||||
}
|
||||
override fun toString(): String {
|
||||
val tag = this.tag
|
||||
val builder =
|
||||
StringBuilder().apply {
|
||||
append("!function(){")
|
||||
if (css.isNotBlank()) {
|
||||
val cssMin = css.replace(Regex("\\s*\n\\s*"), "")
|
||||
append("var a=document.createElement('style');")
|
||||
append("a.innerHTML='$cssMin';")
|
||||
if (tag != null) {
|
||||
append("a.id='$tag';")
|
||||
}
|
||||
append("document.head.appendChild(a);")
|
||||
}
|
||||
var content = builder.append("}()").toString()
|
||||
if (tag != null) {
|
||||
content = singleInjector(tag, content)
|
||||
if (js.isNotBlank()) {
|
||||
append(js)
|
||||
}
|
||||
return content
|
||||
}
|
||||
var content = builder.append("}()").toString()
|
||||
if (tag != null) {
|
||||
content = singleInjector(tag, content)
|
||||
}
|
||||
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)
|
||||
fun inject(webView: WebView, prefs: Prefs)
|
||||
|
||||
/**
|
||||
* 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
|
||||
/** 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)
|
||||
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 {
|
||||
|
||||
fun obfuscateTag(tag: String): String {
|
||||
val rnd = Random(tag.hashCode() + salt)
|
||||
val obfuscated = buildString {
|
||||
append(prefix)
|
||||
append('_')
|
||||
appendRandomChars(rnd, 16)
|
||||
}
|
||||
L.v { "TagObfuscator: Obfuscating tag '$tag' to '$obfuscated'" }
|
||||
return obfuscated
|
||||
fun obfuscateTag(tag: String): String {
|
||||
val rnd = Random(tag.hashCode() + salt)
|
||||
val obfuscated = buildString {
|
||||
append(prefix)
|
||||
append('_')
|
||||
appendRandomChars(rnd, 16)
|
||||
}
|
||||
L.v { "TagObfuscator: Obfuscating tag '$tag' to '$obfuscated'" }
|
||||
return obfuscated
|
||||
}
|
||||
|
||||
private val salt: Long = System.currentTimeMillis()
|
||||
private val salt: Long = System.currentTimeMillis()
|
||||
|
||||
private val prefix: String by lazy {
|
||||
val rnd = Random(System.currentTimeMillis())
|
||||
buildString { appendRandomChars(rnd, 8) }
|
||||
}
|
||||
|
||||
private fun Appendable.appendRandomChars(random: Random, count: Int) {
|
||||
for (i in 1..count) {
|
||||
append('a' + random.nextInt(26))
|
||||
}
|
||||
private val prefix: String by lazy {
|
||||
val rnd = Random(System.currentTimeMillis())
|
||||
buildString { appendRandomChars(rnd, 8) }
|
||||
}
|
||||
|
||||
private fun Appendable.appendRandomChars(random: Random, count: Int) {
|
||||
for (i in 1..count) {
|
||||
append('a' + random.nextInt(26))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,157 +35,151 @@ 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
|
||||
val textColor: Int
|
||||
|
||||
val accentColor: Int
|
||||
val accentColor: Int
|
||||
|
||||
val accentColorForWhite: Int
|
||||
val accentColorForWhite: Int
|
||||
|
||||
val nativeBgColor: Int
|
||||
val nativeBgColor: Int
|
||||
|
||||
fun nativeBgColor(unread: Boolean): Int
|
||||
fun nativeBgColor(unread: Boolean): Int
|
||||
|
||||
val bgColor: Int
|
||||
val bgColor: Int
|
||||
|
||||
val headerColor: Int
|
||||
val headerColor: Int
|
||||
|
||||
val iconColor: Int
|
||||
val iconColor: Int
|
||||
|
||||
val isCustomTheme: Boolean
|
||||
val isCustomTheme: Boolean
|
||||
|
||||
/**
|
||||
* Note that while this can be loaded from any thread, it is typically done through [preload]]
|
||||
*/
|
||||
fun injector(category: ThemeCategory): InjectorContract
|
||||
/** 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)
|
||||
fun setTheme(id: Int)
|
||||
|
||||
fun reset()
|
||||
fun reset()
|
||||
|
||||
suspend fun preload()
|
||||
suspend fun preload()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
field = value
|
||||
prefs.theme = value.ordinal
|
||||
}
|
||||
|
||||
private val injectors: MutableMap<ThemeCategory, InjectorContract> = mutableMapOf()
|
||||
|
||||
override val textColor: Int
|
||||
get() = theme.textColorGetter(prefs)
|
||||
|
||||
override val accentColor: Int
|
||||
get() = theme.accentColorGetter(prefs)
|
||||
|
||||
override val accentColorForWhite: Int
|
||||
get() = when {
|
||||
accentColor.isColorVisibleOn(Color.WHITE) -> accentColor
|
||||
textColor.isColorVisibleOn(Color.WHITE) -> textColor
|
||||
else -> FACEBOOK_BLUE
|
||||
}
|
||||
|
||||
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 val bgColor: Int
|
||||
get() = theme.backgroundColorGetter(prefs)
|
||||
|
||||
override val headerColor: Int
|
||||
get() = theme.headerColorGetter(prefs)
|
||||
|
||||
override val iconColor: Int
|
||||
get() = theme.iconColorGetter(prefs)
|
||||
|
||||
override val isCustomTheme: Boolean
|
||||
get() = theme == Theme.CUSTOM
|
||||
|
||||
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]
|
||||
*/
|
||||
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()
|
||||
.use(BufferedReader::readText)
|
||||
if (theme == Theme.CUSTOM) {
|
||||
val bt = if (Color.alpha(bgColor) == 255)
|
||||
bgColor.toRgbaString()
|
||||
else
|
||||
"transparent"
|
||||
|
||||
val bb = bgColor.colorToForeground(0.35f)
|
||||
|
||||
content = content
|
||||
.replace("\$T\$", textColor.toRgbaString())
|
||||
.replace("\$TT\$", textColor.colorToBackground(0.05f).toRgbaString())
|
||||
.replace("\$TD\$", textColor.adjustAlpha(0.6f).toRgbaString())
|
||||
.replace("\$A\$", accentColor.toRgbaString())
|
||||
.replace("\$AT\$", iconColor.toRgbaString())
|
||||
.replace("\$B\$", bgColor.toRgbaString())
|
||||
.replace("\$BT\$", bt)
|
||||
.replace("\$BBT\$", bb.withAlpha(51).toRgbaString())
|
||||
.replace("\$O\$", bgColor.withAlpha(255).toRgbaString())
|
||||
.replace("\$OO\$", bb.withAlpha(255).toRgbaString())
|
||||
.replace("\$D\$", textColor.adjustAlpha(0.3f).toRgbaString())
|
||||
.replace("\$TI\$", bb.withAlpha(60).toRgbaString())
|
||||
.replace("\$C\$", bt)
|
||||
}
|
||||
return JsBuilder().css(content).build()
|
||||
} catch (e: FileNotFoundException) {
|
||||
L.e(e) { "CssAssets file not found" }
|
||||
return JsActions.EMPTY
|
||||
}
|
||||
private var theme: Theme = Theme.values[prefs.theme]
|
||||
set(value) {
|
||||
field = value
|
||||
prefs.theme = value.ordinal
|
||||
}
|
||||
|
||||
override fun setTheme(id: Int) {
|
||||
if (theme.ordinal == id) return
|
||||
theme = Theme.values[id]
|
||||
reset()
|
||||
}
|
||||
private val injectors: MutableMap<ThemeCategory, InjectorContract> = mutableMapOf()
|
||||
|
||||
override fun reset() {
|
||||
injectors.clear()
|
||||
}
|
||||
override val textColor: Int
|
||||
get() = theme.textColorGetter(prefs)
|
||||
|
||||
override suspend fun preload() {
|
||||
withContext(Dispatchers.IO) {
|
||||
reset()
|
||||
ThemeCategory.values().forEach { injector(it) }
|
||||
}
|
||||
override val accentColor: Int
|
||||
get() = theme.accentColorGetter(prefs)
|
||||
|
||||
override val accentColorForWhite: Int
|
||||
get() =
|
||||
when {
|
||||
accentColor.isColorVisibleOn(Color.WHITE) -> accentColor
|
||||
textColor.isColorVisibleOn(Color.WHITE) -> textColor
|
||||
else -> FACEBOOK_BLUE
|
||||
}
|
||||
|
||||
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 val bgColor: Int
|
||||
get() = theme.backgroundColorGetter(prefs)
|
||||
|
||||
override val headerColor: Int
|
||||
get() = theme.headerColorGetter(prefs)
|
||||
|
||||
override val iconColor: Int
|
||||
get() = theme.iconColorGetter(prefs)
|
||||
|
||||
override val isCustomTheme: Boolean
|
||||
get() = theme == Theme.CUSTOM
|
||||
|
||||
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] */
|
||||
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()
|
||||
.use(BufferedReader::readText)
|
||||
if (theme == Theme.CUSTOM) {
|
||||
val bt = if (Color.alpha(bgColor) == 255) bgColor.toRgbaString() else "transparent"
|
||||
|
||||
val bb = bgColor.colorToForeground(0.35f)
|
||||
|
||||
content =
|
||||
content
|
||||
.replace("\$T\$", textColor.toRgbaString())
|
||||
.replace("\$TT\$", textColor.colorToBackground(0.05f).toRgbaString())
|
||||
.replace("\$TD\$", textColor.adjustAlpha(0.6f).toRgbaString())
|
||||
.replace("\$A\$", accentColor.toRgbaString())
|
||||
.replace("\$AT\$", iconColor.toRgbaString())
|
||||
.replace("\$B\$", bgColor.toRgbaString())
|
||||
.replace("\$BT\$", bt)
|
||||
.replace("\$BBT\$", bb.withAlpha(51).toRgbaString())
|
||||
.replace("\$O\$", bgColor.withAlpha(255).toRgbaString())
|
||||
.replace("\$OO\$", bb.withAlpha(255).toRgbaString())
|
||||
.replace("\$D\$", textColor.adjustAlpha(0.3f).toRgbaString())
|
||||
.replace("\$TI\$", bb.withAlpha(60).toRgbaString())
|
||||
.replace("\$C\$", bt)
|
||||
}
|
||||
return JsBuilder().css(content).build()
|
||||
} catch (e: FileNotFoundException) {
|
||||
L.e(e) { "CssAssets file not found" }
|
||||
return JsActions.EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
override fun setTheme(id: Int) {
|
||||
if (theme.ordinal == id) return
|
||||
theme = Theme.values[id]
|
||||
reset()
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
injectors.clear()
|
||||
}
|
||||
|
||||
override suspend fun preload() {
|
||||
withContext(Dispatchers.IO) {
|
||||
reset()
|
||||
ThemeCategory.values().forEach { injector(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface ThemeProviderModule {
|
||||
@Binds
|
||||
@Singleton
|
||||
fun themeProvider(to: ThemeProviderImpl): ThemeProvider
|
||||
@Binds @Singleton fun themeProvider(to: ThemeProviderImpl): ThemeProvider
|
||||
}
|
||||
|
@ -24,52 +24,48 @@ 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
|
||||
private lateinit var binding: IntroThemeBinding
|
||||
|
||||
val themeList
|
||||
get() = with(binding) {
|
||||
listOf(introThemeLight, introThemeDark, introThemeAmoled, introThemeGlass)
|
||||
}
|
||||
val themeList
|
||||
get() =
|
||||
with(binding) { listOf(introThemeLight, introThemeDark, introThemeAmoled, introThemeGlass) }
|
||||
|
||||
override fun viewArray(): Array<Array<out View>> = with(binding) {
|
||||
arrayOf(
|
||||
arrayOf(title),
|
||||
arrayOf(introThemeLight, introThemeDark),
|
||||
arrayOf(introThemeAmoled, introThemeGlass)
|
||||
)
|
||||
override fun viewArray(): Array<Array<out View>> =
|
||||
with(binding) {
|
||||
arrayOf(
|
||||
arrayOf(title),
|
||||
arrayOf(introThemeLight, introThemeDark),
|
||||
arrayOf(introThemeAmoled, introThemeGlass)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = IntroThemeBinding.bind(view)
|
||||
binding.init()
|
||||
}
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = IntroThemeBinding.bind(view)
|
||||
binding.init()
|
||||
}
|
||||
|
||||
private fun IntroThemeBinding.init() {
|
||||
introThemeLight.setThemeClick(Theme.LIGHT)
|
||||
introThemeDark.setThemeClick(Theme.DARK)
|
||||
introThemeAmoled.setThemeClick(Theme.AMOLED)
|
||||
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
|
||||
}
|
||||
}
|
||||
private fun IntroThemeBinding.init() {
|
||||
introThemeLight.setThemeClick(Theme.LIGHT)
|
||||
introThemeDark.setThemeClick(Theme.DARK)
|
||||
introThemeAmoled.setThemeClick(Theme.AMOLED)
|
||||
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 }
|
||||
}
|
||||
|
||||
private fun View.setThemeClick(theme: Theme) {
|
||||
setOnClickListener { v ->
|
||||
themeProvider.setTheme(theme.ordinal)
|
||||
(activity as IntroActivity).apply {
|
||||
binding.ripple.ripple(themeProvider.bgColor, v.x + v.pivotX, v.y + v.pivotY)
|
||||
theme()
|
||||
}
|
||||
themeList.forEach { it.animate().scaleXY(if (it == this) 1.6f else 0.8f).start() }
|
||||
}
|
||||
private fun View.setThemeClick(theme: Theme) {
|
||||
setOnClickListener { v ->
|
||||
themeProvider.setTheme(theme.ordinal)
|
||||
(activity as IntroActivity).apply {
|
||||
binding.ripple.ripple(themeProvider.bgColor, v.x + v.pivotX, v.y + v.pivotY)
|
||||
theme()
|
||||
}
|
||||
themeList.forEach { it.animate().scaleXY(if (it == this) 1.6f else 0.8f).start() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,138 +32,142 @@ 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 icon: ImageView by bindViewResettable(R.id.intro_button)
|
||||
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 icon: ImageView by bindViewResettable(R.id.intro_button)
|
||||
|
||||
override fun viewArray(): Array<Array<out View>> = arrayOf(arrayOf(title), arrayOf(desc))
|
||||
override fun viewArray(): Array<Array<out View>> = arrayOf(arrayOf(title), arrayOf(desc))
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
title.setText(titleRes)
|
||||
image.setImageResource(imageRes)
|
||||
desc.setText(descRes)
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
title.setText(titleRes)
|
||||
image.setImageResource(imageRes)
|
||||
desc.setText(descRes)
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun themeFragmentImpl() {
|
||||
super.themeFragmentImpl()
|
||||
title.setTextColor(themeProvider.textColor)
|
||||
desc.setTextColor(themeProvider.textColor)
|
||||
phone.tint(themeProvider.textColor)
|
||||
screen.tint(themeProvider.bgColor)
|
||||
}
|
||||
|
||||
fun themeImageComponent(color: Int, vararg id: Int) {
|
||||
id.forEach { imageDrawable.findDrawableByLayerId(it).tint(color) }
|
||||
}
|
||||
|
||||
override fun onPageScrolledImpl(positionOffset: Float) {
|
||||
super.onPageScrolledImpl(positionOffset)
|
||||
val alpha = ((1 - abs(positionOffset)) * 255).toInt()
|
||||
// apply alpha to all layers except the phone base
|
||||
(0 until imageDrawable.numberOfLayers).forEach {
|
||||
val d = imageDrawable.getDrawable(it)
|
||||
if (d != phone) d.alpha = alpha
|
||||
}
|
||||
}
|
||||
|
||||
override fun themeFragmentImpl() {
|
||||
super.themeFragmentImpl()
|
||||
title.setTextColor(themeProvider.textColor)
|
||||
desc.setTextColor(themeProvider.textColor)
|
||||
phone.tint(themeProvider.textColor)
|
||||
screen.tint(themeProvider.bgColor)
|
||||
}
|
||||
fun firstImageFragmentTransition(offset: Float) {
|
||||
if (offset < 0) image.alpha = 1 + offset
|
||||
}
|
||||
|
||||
fun themeImageComponent(color: Int, vararg id: Int) {
|
||||
id.forEach { imageDrawable.findDrawableByLayerId(it).tint(color) }
|
||||
}
|
||||
|
||||
override fun onPageScrolledImpl(positionOffset: Float) {
|
||||
super.onPageScrolledImpl(positionOffset)
|
||||
val alpha = ((1 - abs(positionOffset)) * 255).toInt()
|
||||
// apply alpha to all layers except the phone base
|
||||
(0 until imageDrawable.numberOfLayers).forEach {
|
||||
val d = imageDrawable.getDrawable(it)
|
||||
if (d != phone) d.alpha = alpha
|
||||
}
|
||||
}
|
||||
|
||||
fun firstImageFragmentTransition(offset: Float) {
|
||||
if (offset < 0)
|
||||
image.alpha = 1 + offset
|
||||
}
|
||||
|
||||
fun lastImageFragmentTransition(offset: Float) {
|
||||
if (offset > 0)
|
||||
image.alpha = 1 - offset
|
||||
}
|
||||
fun lastImageFragmentTransition(offset: Float) {
|
||||
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
|
||||
) {
|
||||
) {
|
||||
|
||||
override fun themeFragmentImpl() {
|
||||
super.themeFragmentImpl()
|
||||
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)
|
||||
}
|
||||
override fun themeFragmentImpl() {
|
||||
super.themeFragmentImpl()
|
||||
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)
|
||||
}
|
||||
|
||||
override fun onPageScrolledImpl(positionOffset: Float) {
|
||||
super.onPageScrolledImpl(positionOffset)
|
||||
firstImageFragmentTransition(positionOffset)
|
||||
}
|
||||
override fun onPageScrolledImpl(positionOffset: Float) {
|
||||
super.onPageScrolledImpl(positionOffset)
|
||||
firstImageFragmentTransition(positionOffset)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
icon.visible().setIcon(GoogleMaterial.Icon.gmd_edit, 24)
|
||||
icon.setOnClickListener { activity?.launchTabCustomizerActivity() }
|
||||
}
|
||||
|
||||
override fun themeFragmentImpl() {
|
||||
super.themeFragmentImpl()
|
||||
themeImageComponent(
|
||||
themeProvider.iconColor,
|
||||
R.id.intro_phone_icon_1,
|
||||
R.id.intro_phone_icon_2,
|
||||
R.id.intro_phone_icon_3,
|
||||
R.id.intro_phone_icon_4
|
||||
)
|
||||
themeImageComponent(themeProvider.headerColor, R.id.intro_phone_tab)
|
||||
themeImageComponent(themeProvider.textColor.withAlpha(80), R.id.intro_phone_icon_ripple)
|
||||
}
|
||||
override fun themeFragmentImpl() {
|
||||
super.themeFragmentImpl()
|
||||
themeImageComponent(
|
||||
themeProvider.iconColor,
|
||||
R.id.intro_phone_icon_1,
|
||||
R.id.intro_phone_icon_2,
|
||||
R.id.intro_phone_icon_3,
|
||||
R.id.intro_phone_icon_4
|
||||
)
|
||||
themeImageComponent(themeProvider.headerColor, R.id.intro_phone_tab)
|
||||
themeImageComponent(themeProvider.textColor.withAlpha(80), R.id.intro_phone_icon_ripple)
|
||||
}
|
||||
}
|
||||
|
||||
class IntroTabContextFragment : BaseImageIntroFragment(
|
||||
class IntroTabContextFragment :
|
||||
BaseImageIntroFragment(
|
||||
R.string.intro_context_aware,
|
||||
R.drawable.intro_phone_long_press,
|
||||
R.string.intro_context_aware_desc
|
||||
) {
|
||||
) {
|
||||
|
||||
override fun themeFragmentImpl() {
|
||||
super.themeFragmentImpl()
|
||||
themeImageComponent(themeProvider.headerColor, R.id.intro_phone_toolbar)
|
||||
themeImageComponent(themeProvider.bgColor.colorToForeground(0.1f), R.id.intro_phone_image)
|
||||
themeImageComponent(
|
||||
themeProvider.bgColor.colorToForeground(0.2f),
|
||||
R.id.intro_phone_like,
|
||||
R.id.intro_phone_share
|
||||
)
|
||||
themeImageComponent(themeProvider.bgColor.colorToForeground(0.3f), R.id.intro_phone_comment)
|
||||
themeImageComponent(
|
||||
themeProvider.bgColor.colorToForeground(0.1f),
|
||||
R.id.intro_phone_card_1,
|
||||
R.id.intro_phone_card_2
|
||||
)
|
||||
themeImageComponent(
|
||||
themeProvider.textColor,
|
||||
R.id.intro_phone_image_indicator,
|
||||
R.id.intro_phone_comment_indicator,
|
||||
R.id.intro_phone_card_indicator
|
||||
)
|
||||
}
|
||||
override fun themeFragmentImpl() {
|
||||
super.themeFragmentImpl()
|
||||
themeImageComponent(themeProvider.headerColor, R.id.intro_phone_toolbar)
|
||||
themeImageComponent(themeProvider.bgColor.colorToForeground(0.1f), R.id.intro_phone_image)
|
||||
themeImageComponent(
|
||||
themeProvider.bgColor.colorToForeground(0.2f),
|
||||
R.id.intro_phone_like,
|
||||
R.id.intro_phone_share
|
||||
)
|
||||
themeImageComponent(themeProvider.bgColor.colorToForeground(0.3f), R.id.intro_phone_comment)
|
||||
themeImageComponent(
|
||||
themeProvider.bgColor.colorToForeground(0.1f),
|
||||
R.id.intro_phone_card_1,
|
||||
R.id.intro_phone_card_2
|
||||
)
|
||||
themeImageComponent(
|
||||
themeProvider.textColor,
|
||||
R.id.intro_phone_image_indicator,
|
||||
R.id.intro_phone_comment_indicator,
|
||||
R.id.intro_phone_card_indicator
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPageScrolledImpl(positionOffset: Float) {
|
||||
super.onPageScrolledImpl(positionOffset)
|
||||
lastImageFragmentTransition(positionOffset)
|
||||
}
|
||||
override fun onPageScrolledImpl(positionOffset: Float) {
|
||||
super.onPageScrolledImpl(positionOffset)
|
||||
lastImageFragmentTransition(positionOffset)
|
||||
}
|
||||
}
|
||||
|
@ -45,122 +45,118 @@ 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
|
||||
val screenWidth
|
||||
get() = resources.displayMetrics.widthPixels
|
||||
|
||||
val lazyRegistry = LazyResettableRegistry()
|
||||
val lazyRegistry = LazyResettableRegistry()
|
||||
|
||||
protected fun translate(offset: Float, views: Array<Array<out View>>) {
|
||||
val maxTranslation = offset * screenWidth
|
||||
val increment = maxTranslation / views.size
|
||||
views.forEachIndexed { i, group ->
|
||||
group.forEach {
|
||||
it.translationX =
|
||||
if (offset > 0) -maxTranslation + i * increment else -(i + 1) * increment
|
||||
it.alpha = 1 - abs(offset)
|
||||
}
|
||||
}
|
||||
protected fun translate(offset: Float, views: Array<Array<out View>>) {
|
||||
val maxTranslation = offset * screenWidth
|
||||
val increment = maxTranslation / views.size
|
||||
views.forEachIndexed { i, group ->
|
||||
group.forEach {
|
||||
it.translationX = if (offset > 0) -maxTranslation + i * increment else -(i + 1) * increment
|
||||
it.alpha = 1 - abs(offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Any> lazyResettableRegistered(initializer: () -> T) = lazyRegistry.lazy(initializer)
|
||||
fun <T : Any> lazyResettableRegistered(initializer: () -> T) = lazyRegistry.lazy(initializer)
|
||||
|
||||
/*
|
||||
* Note that these ids aren't actually inside all layouts
|
||||
* However, they are in most of them, so they are added here
|
||||
* for convenience
|
||||
*/
|
||||
protected val title: TextView by bindViewResettable(R.id.intro_title)
|
||||
protected val image: ImageView by bindViewResettable(R.id.intro_image)
|
||||
protected val desc: TextView by bindViewResettable(R.id.intro_desc)
|
||||
/*
|
||||
* Note that these ids aren't actually inside all layouts
|
||||
* However, they are in most of them, so they are added here
|
||||
* for convenience
|
||||
*/
|
||||
protected val title: TextView by bindViewResettable(R.id.intro_title)
|
||||
protected val image: ImageView by bindViewResettable(R.id.intro_image)
|
||||
protected val desc: TextView by bindViewResettable(R.id.intro_desc)
|
||||
|
||||
protected fun defaultViewArray(): Array<Array<out View>> =
|
||||
arrayOf(arrayOf(title), arrayOf(image), arrayOf(desc))
|
||||
protected fun defaultViewArray(): Array<Array<out View>> =
|
||||
arrayOf(arrayOf(title), arrayOf(image), arrayOf(desc))
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(layoutRes, container, false)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(layoutRes, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
themeFragment()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
Kotterknife.reset(this)
|
||||
lazyRegistry.invalidateAll()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
fun themeFragment() {
|
||||
if (view != null) themeFragmentImpl()
|
||||
}
|
||||
|
||||
protected open fun themeFragmentImpl() {
|
||||
(view as? ViewGroup)?.children?.forEach {
|
||||
(it as? TextView)?.setTextColor(themeProvider.textColor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
themeFragment()
|
||||
}
|
||||
protected val viewArray: Array<Array<out View>> by lazyResettableRegistered { viewArray() }
|
||||
|
||||
override fun onDestroyView() {
|
||||
Kotterknife.reset(this)
|
||||
lazyRegistry.invalidateAll()
|
||||
super.onDestroyView()
|
||||
}
|
||||
protected abstract fun viewArray(): Array<Array<out View>>
|
||||
|
||||
fun themeFragment() {
|
||||
if (view != null) themeFragmentImpl()
|
||||
}
|
||||
fun onPageScrolled(positionOffset: Float) {
|
||||
if (view != null) onPageScrolledImpl(positionOffset)
|
||||
}
|
||||
|
||||
protected open fun themeFragmentImpl() {
|
||||
(view as? ViewGroup)?.children?.forEach { (it as? TextView)?.setTextColor(themeProvider.textColor) }
|
||||
}
|
||||
protected open fun onPageScrolledImpl(positionOffset: Float) {
|
||||
translate(positionOffset, viewArray)
|
||||
}
|
||||
|
||||
protected val viewArray: Array<Array<out View>> by lazyResettableRegistered { viewArray() }
|
||||
fun onPageSelected() {
|
||||
if (view != null) onPageSelectedImpl()
|
||||
}
|
||||
|
||||
protected abstract fun viewArray(): Array<Array<out View>>
|
||||
|
||||
fun onPageScrolled(positionOffset: Float) {
|
||||
if (view != null) onPageScrolledImpl(positionOffset)
|
||||
}
|
||||
|
||||
protected open fun onPageScrolledImpl(positionOffset: Float) {
|
||||
translate(positionOffset, viewArray)
|
||||
}
|
||||
|
||||
fun onPageSelected() {
|
||||
if (view != null) onPageSelectedImpl()
|
||||
}
|
||||
|
||||
protected open fun onPageSelectedImpl() {
|
||||
}
|
||||
protected open fun onPageSelectedImpl() {}
|
||||
}
|
||||
|
||||
class IntroFragmentWelcome : BaseIntroFragment(R.layout.intro_welcome) {
|
||||
|
||||
override fun viewArray(): Array<Array<out View>> = defaultViewArray()
|
||||
override fun viewArray(): Array<Array<out View>> = defaultViewArray()
|
||||
|
||||
override fun themeFragmentImpl() {
|
||||
super.themeFragmentImpl()
|
||||
image.imageTintList = ColorStateList.valueOf(themeProvider.textColor)
|
||||
}
|
||||
override fun themeFragmentImpl() {
|
||||
super.themeFragmentImpl()
|
||||
image.imageTintList = ColorStateList.valueOf(themeProvider.textColor)
|
||||
}
|
||||
}
|
||||
|
||||
class IntroFragmentEnd : BaseIntroFragment(R.layout.intro_end) {
|
||||
|
||||
val container: ConstraintLayout by bindViewResettable(R.id.intro_end_container)
|
||||
val container: ConstraintLayout by bindViewResettable(R.id.intro_end_container)
|
||||
|
||||
override fun viewArray(): Array<Array<out View>> = defaultViewArray()
|
||||
override fun viewArray(): Array<Array<out View>> = defaultViewArray()
|
||||
|
||||
override fun themeFragmentImpl() {
|
||||
super.themeFragmentImpl()
|
||||
image.imageTintList = ColorStateList.valueOf(themeProvider.textColor)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
container.setOnSingleTapListener { _, event ->
|
||||
(activity as IntroActivity).finish(event.x, event.y)
|
||||
}
|
||||
override fun themeFragmentImpl() {
|
||||
super.themeFragmentImpl()
|
||||
image.imageTintList = ColorStateList.valueOf(themeProvider.textColor)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
container.setOnSingleTapListener { _, event ->
|
||||
(activity as IntroActivity).finish(event.x, event.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,109 +31,109 @@ import javax.inject.Inject
|
||||
*/
|
||||
@Deprecated(level = DeprecationLevel.WARNING, message = "Use pref segments")
|
||||
class OldPrefs @Inject internal constructor(factory: KPrefFactory) :
|
||||
KPref("${BuildConfig.APPLICATION_ID}.prefs", factory) {
|
||||
KPref("${BuildConfig.APPLICATION_ID}.prefs", factory) {
|
||||
|
||||
var lastLaunch: Long by kpref("last_launch", -1L)
|
||||
var lastLaunch: Long by kpref("last_launch", -1L)
|
||||
|
||||
var userId: Long by kpref("user_id", -1L)
|
||||
var userId: Long by kpref("user_id", -1L)
|
||||
|
||||
var prevId: Long by kpref("prev_id", -1L)
|
||||
var prevId: Long by kpref("prev_id", -1L)
|
||||
|
||||
var theme: Int by kpref("theme", 0)
|
||||
var theme: Int by kpref("theme", 0)
|
||||
|
||||
var customTextColor: Int by kpref("color_text", 0xffeceff1.toInt())
|
||||
var customTextColor: Int by kpref("color_text", 0xffeceff1.toInt())
|
||||
|
||||
var customAccentColor: Int by kpref("color_accent", 0xff0288d1.toInt())
|
||||
var customAccentColor: Int by kpref("color_accent", 0xff0288d1.toInt())
|
||||
|
||||
var customBackgroundColor: Int by kpref("color_bg", 0xff212121.toInt())
|
||||
var customBackgroundColor: Int by kpref("color_bg", 0xff212121.toInt())
|
||||
|
||||
var customHeaderColor: Int by kpref("color_header", 0xff01579b.toInt())
|
||||
var customHeaderColor: Int by kpref("color_header", 0xff01579b.toInt())
|
||||
|
||||
var customIconColor: Int by kpref("color_icons", 0xffeceff1.toInt())
|
||||
var customIconColor: Int by kpref("color_icons", 0xffeceff1.toInt())
|
||||
|
||||
var exitConfirmation: Boolean by kpref("exit_confirmation", true)
|
||||
var exitConfirmation: Boolean by kpref("exit_confirmation", true)
|
||||
|
||||
var notificationFreq: Long by kpref("notification_freq", 15L)
|
||||
var notificationFreq: Long by kpref("notification_freq", 15L)
|
||||
|
||||
var versionCode: Int by kpref("version_code", -1)
|
||||
var versionCode: Int by kpref("version_code", -1)
|
||||
|
||||
var prevVersionCode: Int by kpref("prev_version_code", -1)
|
||||
var prevVersionCode: Int by kpref("prev_version_code", -1)
|
||||
|
||||
var installDate: Long by kpref("install_date", -1L)
|
||||
var installDate: Long by kpref("install_date", -1L)
|
||||
|
||||
var identifier: Int by kpref("identifier", -1)
|
||||
var identifier: Int by kpref("identifier", -1)
|
||||
|
||||
var tintNavBar: Boolean by kpref("tint_nav_bar", true)
|
||||
var tintNavBar: Boolean by kpref("tint_nav_bar", true)
|
||||
|
||||
var webTextScaling: Int by kpref("web_text_scaling", 100)
|
||||
var webTextScaling: Int by kpref("web_text_scaling", 100)
|
||||
|
||||
var feedSort: Int by kpref("feed_sort", FeedSort.DEFAULT.ordinal)
|
||||
var feedSort: Int by kpref("feed_sort", FeedSort.DEFAULT.ordinal)
|
||||
|
||||
var aggressiveRecents: Boolean by kpref("aggressive_recents", false)
|
||||
var aggressiveRecents: Boolean by kpref("aggressive_recents", false)
|
||||
|
||||
var showComposer: Boolean by kpref("status_composer_feed", true)
|
||||
var showComposer: Boolean by kpref("status_composer_feed", true)
|
||||
|
||||
var showSuggestedFriends: Boolean by kpref("suggested_friends_feed", true)
|
||||
var showSuggestedFriends: Boolean by kpref("suggested_friends_feed", true)
|
||||
|
||||
var showSuggestedGroups: Boolean by kpref("suggested_groups_feed", true)
|
||||
var showSuggestedGroups: Boolean by kpref("suggested_groups_feed", true)
|
||||
|
||||
var showFacebookAds: Boolean by kpref("facebook_ads", false)
|
||||
var showFacebookAds: Boolean by kpref("facebook_ads", false)
|
||||
|
||||
var showStories: Boolean by kpref("show_stories", true)
|
||||
var showStories: Boolean by kpref("show_stories", true)
|
||||
|
||||
var animate: Boolean by kpref("fancy_animations", true)
|
||||
var animate: Boolean by kpref("fancy_animations", true)
|
||||
|
||||
var notificationKeywords: Set<String> by kpref("notification_keywords", mutableSetOf())
|
||||
var notificationKeywords: Set<String> by kpref("notification_keywords", mutableSetOf())
|
||||
|
||||
var notificationsGeneral: Boolean by kpref("notification_general", true)
|
||||
var notificationsGeneral: Boolean by kpref("notification_general", true)
|
||||
|
||||
var notificationAllAccounts: Boolean by kpref("notification_all_accounts", true)
|
||||
var notificationAllAccounts: Boolean by kpref("notification_all_accounts", true)
|
||||
|
||||
var notificationsInstantMessages: Boolean by kpref("notification_im", true)
|
||||
var notificationsInstantMessages: Boolean by kpref("notification_im", true)
|
||||
|
||||
var notificationsImAllAccounts: Boolean by kpref("notification_im_all_accounts", false)
|
||||
var notificationsImAllAccounts: Boolean by kpref("notification_im_all_accounts", false)
|
||||
|
||||
var notificationVibrate: Boolean by kpref("notification_vibrate", true)
|
||||
var notificationVibrate: Boolean by kpref("notification_vibrate", true)
|
||||
|
||||
var notificationSound: Boolean by kpref("notification_sound", true)
|
||||
var notificationSound: Boolean by kpref("notification_sound", true)
|
||||
|
||||
var notificationRingtone: String by kpref("notification_ringtone", "")
|
||||
var notificationRingtone: String by kpref("notification_ringtone", "")
|
||||
|
||||
var messageRingtone: String by kpref("message_ringtone", "")
|
||||
var messageRingtone: String by kpref("message_ringtone", "")
|
||||
|
||||
var notificationLights: Boolean by kpref("notification_lights", true)
|
||||
var notificationLights: Boolean by kpref("notification_lights", true)
|
||||
|
||||
var messageScrollToBottom: Boolean by kpref("message_scroll_to_bottom", false)
|
||||
var messageScrollToBottom: Boolean by kpref("message_scroll_to_bottom", false)
|
||||
|
||||
var enablePip: Boolean by kpref("enable_pip", true)
|
||||
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.
|
||||
*/
|
||||
var verboseLogging: Boolean by kpref("verbose_logging", false)
|
||||
/**
|
||||
* 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)
|
||||
|
||||
var biometricsEnabled: Boolean by kpref("biometrics_enabled", false)
|
||||
var biometricsEnabled: Boolean by kpref("biometrics_enabled", false)
|
||||
|
||||
var overlayEnabled: Boolean by kpref("overlay_enabled", true)
|
||||
var overlayEnabled: Boolean by kpref("overlay_enabled", true)
|
||||
|
||||
var overlayFullScreenSwipe: Boolean by kpref("overlay_full_screen_swipe", true)
|
||||
var overlayFullScreenSwipe: Boolean by kpref("overlay_full_screen_swipe", true)
|
||||
|
||||
var viewpagerSwipe: Boolean by kpref("viewpager_swipe", true)
|
||||
var viewpagerSwipe: Boolean by kpref("viewpager_swipe", true)
|
||||
|
||||
var loadMediaOnMeteredNetwork: Boolean by kpref("media_on_metered_network", true)
|
||||
var loadMediaOnMeteredNetwork: Boolean by kpref("media_on_metered_network", true)
|
||||
|
||||
var debugSettings: Boolean by kpref("debug_settings", false)
|
||||
var debugSettings: Boolean by kpref("debug_settings", false)
|
||||
|
||||
var linksInDefaultApp: Boolean by kpref("link_in_default_app", false)
|
||||
var linksInDefaultApp: Boolean by kpref("link_in_default_app", false)
|
||||
|
||||
var mainActivityLayoutType: Int by kpref("main_activity_layout_type", 0)
|
||||
var mainActivityLayoutType: Int by kpref("main_activity_layout_type", 0)
|
||||
|
||||
var blackMediaBg: Boolean by kpref("black_media_bg", false)
|
||||
var blackMediaBg: Boolean by kpref("black_media_bg", false)
|
||||
|
||||
var autoRefreshFeed: Boolean by kpref("auto_refresh_feed", false)
|
||||
var autoRefreshFeed: Boolean by kpref("auto_refresh_feed", false)
|
||||
|
||||
var showCreateFab: Boolean by kpref("show_create_fab", true)
|
||||
var showCreateFab: Boolean by kpref("show_create_fab", true)
|
||||
|
||||
var fullSizeImage: Boolean by kpref("full_size_image", false)
|
||||
var fullSizeImage: Boolean by kpref("full_size_image", false)
|
||||
}
|
||||
|
@ -41,92 +41,76 @@ 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()
|
||||
fun deleteKeys(vararg keys: String)
|
||||
fun reset()
|
||||
fun deleteKeys(vararg keys: String)
|
||||
}
|
||||
|
||||
interface Prefs :
|
||||
BehaviourPrefs,
|
||||
CorePrefs,
|
||||
FeedPrefs,
|
||||
NotifPrefs,
|
||||
ThemePrefs,
|
||||
ShowcasePrefs,
|
||||
PrefsBase
|
||||
BehaviourPrefs, CorePrefs, FeedPrefs, NotifPrefs, ThemePrefs, ShowcasePrefs, PrefsBase
|
||||
|
||||
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,
|
||||
BehaviourPrefs by behaviourPrefs,
|
||||
CorePrefs by corePrefs,
|
||||
FeedPrefs by feedPrefs,
|
||||
NotifPrefs by notifPrefs,
|
||||
ThemePrefs by themePrefs,
|
||||
ShowcasePrefs by showcasePrefs {
|
||||
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,
|
||||
BehaviourPrefs by behaviourPrefs,
|
||||
CorePrefs by corePrefs,
|
||||
FeedPrefs by feedPrefs,
|
||||
NotifPrefs by notifPrefs,
|
||||
ThemePrefs by themePrefs,
|
||||
ShowcasePrefs by showcasePrefs {
|
||||
|
||||
override fun reset() {
|
||||
behaviourPrefs.reset()
|
||||
corePrefs.reset()
|
||||
feedPrefs.reset()
|
||||
notifPrefs.reset()
|
||||
themePrefs.reset()
|
||||
showcasePrefs.reset()
|
||||
}
|
||||
override fun reset() {
|
||||
behaviourPrefs.reset()
|
||||
corePrefs.reset()
|
||||
feedPrefs.reset()
|
||||
notifPrefs.reset()
|
||||
themePrefs.reset()
|
||||
showcasePrefs.reset()
|
||||
}
|
||||
|
||||
override fun deleteKeys(vararg keys: String) {
|
||||
behaviourPrefs.deleteKeys()
|
||||
corePrefs.deleteKeys()
|
||||
feedPrefs.deleteKeys()
|
||||
notifPrefs.deleteKeys()
|
||||
themePrefs.deleteKeys()
|
||||
showcasePrefs.deleteKeys()
|
||||
}
|
||||
override fun deleteKeys(vararg keys: String) {
|
||||
behaviourPrefs.deleteKeys()
|
||||
corePrefs.deleteKeys()
|
||||
feedPrefs.deleteKeys()
|
||||
notifPrefs.deleteKeys()
|
||||
themePrefs.deleteKeys()
|
||||
showcasePrefs.deleteKeys()
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object PrefFactoryModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun factory(@ApplicationContext context: Context): KPrefFactory = KPrefFactoryAndroid(context)
|
||||
@Provides
|
||||
@Singleton
|
||||
fun factory(@ApplicationContext context: Context): KPrefFactory = KPrefFactoryAndroid(context)
|
||||
}
|
||||
|
@ -24,87 +24,67 @@ import com.pitchedapps.frost.prefs.PrefsBase
|
||||
import javax.inject.Inject
|
||||
|
||||
interface BehaviourPrefs : PrefsBase {
|
||||
var biometricsEnabled: Boolean
|
||||
var biometricsEnabled: Boolean
|
||||
|
||||
var overlayEnabled: Boolean
|
||||
var overlayEnabled: Boolean
|
||||
|
||||
var overlayFullScreenSwipe: Boolean
|
||||
var overlayFullScreenSwipe: Boolean
|
||||
|
||||
var viewpagerSwipe: Boolean
|
||||
var viewpagerSwipe: Boolean
|
||||
|
||||
var loadMediaOnMeteredNetwork: Boolean
|
||||
var loadMediaOnMeteredNetwork: Boolean
|
||||
|
||||
var debugSettings: Boolean
|
||||
var debugSettings: Boolean
|
||||
|
||||
var linksInDefaultApp: Boolean
|
||||
var linksInDefaultApp: Boolean
|
||||
|
||||
var blackMediaBg: Boolean
|
||||
var blackMediaBg: Boolean
|
||||
|
||||
var autoRefreshFeed: Boolean
|
||||
var autoRefreshFeed: Boolean
|
||||
|
||||
var showCreateFab: Boolean
|
||||
var showCreateFab: Boolean
|
||||
|
||||
var fullSizeImage: Boolean
|
||||
var fullSizeImage: Boolean
|
||||
|
||||
var autoExpandTextBox: Boolean
|
||||
var autoExpandTextBox: Boolean
|
||||
}
|
||||
|
||||
class BehaviourPrefsImpl @Inject internal constructor(
|
||||
factory: KPrefFactory,
|
||||
oldPrefs: OldPrefs,
|
||||
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 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)
|
||||
override var autoExpandTextBox: Boolean by kpref("auto_expand_text_box", true)
|
||||
}
|
||||
|
@ -24,78 +24,71 @@ import com.pitchedapps.frost.prefs.PrefsBase
|
||||
import javax.inject.Inject
|
||||
|
||||
interface CorePrefs : PrefsBase {
|
||||
var lastLaunch: Long
|
||||
var lastLaunch: Long
|
||||
|
||||
var userId: Long
|
||||
var userId: Long
|
||||
|
||||
var prevId: Long
|
||||
var prevId: Long
|
||||
|
||||
val frostId: String
|
||||
val frostId: String
|
||||
|
||||
var versionCode: Int
|
||||
var versionCode: Int
|
||||
|
||||
var prevVersionCode: Int
|
||||
var prevVersionCode: Int
|
||||
|
||||
var installDate: Long
|
||||
var installDate: Long
|
||||
|
||||
var identifier: Int
|
||||
var identifier: Int
|
||||
|
||||
/**
|
||||
* Despite the naming, this toggle currently only enables debug logging.
|
||||
* Verbose is never logged in release builds.
|
||||
*/
|
||||
var verboseLogging: Boolean
|
||||
/**
|
||||
* Despite the naming, this toggle currently only enables debug logging. Verbose is never logged
|
||||
* in release builds.
|
||||
*/
|
||||
var verboseLogging: Boolean
|
||||
|
||||
var enablePip: Boolean
|
||||
var enablePip: Boolean
|
||||
|
||||
var exitConfirmation: Boolean
|
||||
var exitConfirmation: Boolean
|
||||
|
||||
var animate: Boolean
|
||||
var animate: Boolean
|
||||
|
||||
var messageScrollToBottom: Boolean
|
||||
var messageScrollToBottom: Boolean
|
||||
}
|
||||
|
||||
class CorePrefsImpl @Inject internal constructor(
|
||||
factory: KPrefFactory,
|
||||
oldPrefs: OldPrefs,
|
||||
class CorePrefsImpl
|
||||
@Inject
|
||||
internal constructor(
|
||||
factory: KPrefFactory,
|
||||
oldPrefs: OldPrefs,
|
||||
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.core", factory), CorePrefs {
|
||||
|
||||
override var lastLaunch: Long by kpref("last_launch", oldPrefs.lastLaunch /* -1L */)
|
||||
override var lastLaunch: Long by kpref("last_launch", oldPrefs.lastLaunch /* -1L */)
|
||||
|
||||
override var userId: Long by kpref("user_id", oldPrefs.userId /* -1L */)
|
||||
override var userId: Long by kpref("user_id", oldPrefs.userId /* -1L */)
|
||||
|
||||
override var prevId: Long by kpref("prev_id", oldPrefs.prevId /* -1L */)
|
||||
override var prevId: Long by kpref("prev_id", oldPrefs.prevId /* -1L */)
|
||||
|
||||
override val frostId: String
|
||||
get() = "$installDate-$identifier"
|
||||
override val frostId: String
|
||||
get() = "$installDate-$identifier"
|
||||
|
||||
override var versionCode: Int by kpref("version_code", oldPrefs.versionCode /* -1 */)
|
||||
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 installDate: Long by kpref("install_date", oldPrefs.installDate /* -1L */)
|
||||
|
||||
override var identifier: Int by kpref("identifier", oldPrefs.identifier /* -1 */)
|
||||
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 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 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 */)
|
||||
}
|
||||
|
@ -25,79 +25,62 @@ import com.pitchedapps.frost.prefs.PrefsBase
|
||||
import javax.inject.Inject
|
||||
|
||||
interface FeedPrefs : PrefsBase {
|
||||
var webTextScaling: Int
|
||||
var webTextScaling: Int
|
||||
|
||||
var feedSort: Int
|
||||
var feedSort: Int
|
||||
|
||||
var aggressiveRecents: Boolean
|
||||
var aggressiveRecents: Boolean
|
||||
|
||||
var showComposer: Boolean
|
||||
var showComposer: Boolean
|
||||
|
||||
var showSuggestedFriends: Boolean
|
||||
var showSuggestedFriends: Boolean
|
||||
|
||||
var showSuggestedGroups: Boolean
|
||||
var showSuggestedGroups: Boolean
|
||||
|
||||
var showFacebookAds: Boolean
|
||||
var showFacebookAds: Boolean
|
||||
|
||||
var showStories: Boolean
|
||||
var showStories: Boolean
|
||||
|
||||
var mainActivityLayoutType: Int
|
||||
var mainActivityLayoutType: Int
|
||||
|
||||
val mainActivityLayout: MainActivityLayout
|
||||
val mainActivityLayout: MainActivityLayout
|
||||
|
||||
var showPostActions: Boolean
|
||||
var showPostActions: Boolean
|
||||
|
||||
var showPostReactions: Boolean
|
||||
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 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 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)
|
||||
override val mainActivityLayout: MainActivityLayout
|
||||
get() = MainActivityLayout(mainActivityLayoutType)
|
||||
|
||||
override var showPostActions: Boolean by kpref("show_post_actions", true)
|
||||
override var showPostActions: Boolean by kpref("show_post_actions", true)
|
||||
|
||||
override var showPostReactions: Boolean by kpref("show_post_reactions", true)
|
||||
override var showPostReactions: Boolean by kpref("show_post_reactions", true)
|
||||
}
|
||||
|
@ -24,86 +24,66 @@ import com.pitchedapps.frost.prefs.PrefsBase
|
||||
import javax.inject.Inject
|
||||
|
||||
interface NotifPrefs : PrefsBase {
|
||||
var notificationKeywords: Set<String>
|
||||
var notificationKeywords: Set<String>
|
||||
|
||||
var notificationsGeneral: Boolean
|
||||
var notificationsGeneral: Boolean
|
||||
|
||||
var notificationAllAccounts: Boolean
|
||||
var notificationAllAccounts: Boolean
|
||||
|
||||
var notificationsInstantMessages: Boolean
|
||||
var notificationsInstantMessages: Boolean
|
||||
|
||||
var notificationsImAllAccounts: Boolean
|
||||
var notificationsImAllAccounts: Boolean
|
||||
|
||||
var notificationVibrate: Boolean
|
||||
var notificationVibrate: Boolean
|
||||
|
||||
var notificationSound: Boolean
|
||||
var notificationSound: Boolean
|
||||
|
||||
var notificationRingtone: String
|
||||
var notificationRingtone: String
|
||||
|
||||
var messageRingtone: String
|
||||
var messageRingtone: String
|
||||
|
||||
var notificationLights: Boolean
|
||||
var notificationLights: Boolean
|
||||
|
||||
var notificationFreq: Long
|
||||
var notificationFreq: Long
|
||||
}
|
||||
|
||||
class NotifPrefsImpl @Inject internal constructor(
|
||||
factory: KPrefFactory,
|
||||
oldPrefs: OldPrefs,
|
||||
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 */)
|
||||
}
|
||||
|
@ -23,12 +23,10 @@ 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
|
||||
*/
|
||||
val firstWebOverlay: Boolean
|
||||
/** Check if this is the first time launching the web overlay; show snackbar if true */
|
||||
val firstWebOverlay: Boolean
|
||||
|
||||
val intro: Boolean
|
||||
val intro: Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@ -36,11 +34,10 @@ 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")
|
||||
override val firstWebOverlay: Boolean by kprefSingle("first_web_overlay")
|
||||
|
||||
override val intro: Boolean by kprefSingle("intro_pages")
|
||||
override val intro: Boolean by kprefSingle("intro_pages")
|
||||
}
|
||||
|
@ -24,56 +24,45 @@ import com.pitchedapps.frost.prefs.PrefsBase
|
||||
import javax.inject.Inject
|
||||
|
||||
interface ThemePrefs : PrefsBase {
|
||||
var theme: Int
|
||||
var theme: Int
|
||||
|
||||
var customTextColor: Int
|
||||
var customTextColor: Int
|
||||
|
||||
var customAccentColor: Int
|
||||
var customAccentColor: Int
|
||||
|
||||
var customBackgroundColor: Int
|
||||
var customBackgroundColor: Int
|
||||
|
||||
var customHeaderColor: Int
|
||||
var customHeaderColor: Int
|
||||
|
||||
var customIconColor: Int
|
||||
var customIconColor: Int
|
||||
|
||||
var tintNavBar: Boolean
|
||||
var tintNavBar: Boolean
|
||||
}
|
||||
|
||||
class ThemePrefsImpl @Inject internal constructor(
|
||||
factory: KPrefFactory,
|
||||
oldPrefs: OldPrefs,
|
||||
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
|
||||
*/
|
||||
override var theme: Int by kpref("theme", oldPrefs.theme /* 0 */)
|
||||
/** 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 */)
|
||||
override var tintNavBar: Boolean by kpref("tint_nav_bar", oldPrefs.tintNavBar /* true */)
|
||||
}
|
||||
|
@ -20,32 +20,30 @@ 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 {
|
||||
|
||||
private lateinit var job: Job
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = ContextHelper.dispatcher + job
|
||||
private lateinit var job: Job
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = ContextHelper.dispatcher + job
|
||||
|
||||
protected val startTime = System.currentTimeMillis()
|
||||
protected val startTime = System.currentTimeMillis()
|
||||
|
||||
/**
|
||||
* Note that if a job plans on running asynchronously, it should return true
|
||||
*/
|
||||
@CallSuper
|
||||
override fun onStartJob(params: JobParameters?): Boolean {
|
||||
job = Job()
|
||||
return false
|
||||
}
|
||||
/** Note that if a job plans on running asynchronously, it should return true */
|
||||
@CallSuper
|
||||
override fun onStartJob(params: JobParameters?): Boolean {
|
||||
job = Job()
|
||||
return false
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onStopJob(params: JobParameters?): Boolean {
|
||||
job.cancel()
|
||||
return false
|
||||
}
|
||||
@CallSuper
|
||||
override fun onStopJob(params: JobParameters?): Boolean {
|
||||
job.cancel()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -60,283 +60,271 @@ import kotlin.math.abs
|
||||
private val _40_DP = 40.dpToPx
|
||||
|
||||
private val pendingIntentFlagUpdateCurrent: Int
|
||||
get() = PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
||||
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,
|
||||
private val fbItem: FbItem,
|
||||
private val parser: FrostParser<ParseNotification>,
|
||||
private val ringtoneProvider: (Prefs) -> String
|
||||
val channelId: String,
|
||||
private val overlayContext: OverlayContext,
|
||||
private val fbItem: FbItem,
|
||||
private val parser: FrostParser<ParseNotification>,
|
||||
private val ringtoneProvider: (Prefs) -> String
|
||||
) {
|
||||
GENERAL(
|
||||
NOTIF_CHANNEL_GENERAL,
|
||||
OverlayContext.NOTIFICATION,
|
||||
FbItem.NOTIFICATIONS,
|
||||
NotifParser,
|
||||
{ it.notificationRingtone }
|
||||
),
|
||||
MESSAGE(
|
||||
NOTIF_CHANNEL_MESSAGES,
|
||||
OverlayContext.MESSAGE,
|
||||
FbItem.MESSAGES,
|
||||
MessageParser,
|
||||
{ it.messageRingtone }
|
||||
);
|
||||
|
||||
GENERAL(
|
||||
NOTIF_CHANNEL_GENERAL,
|
||||
OverlayContext.NOTIFICATION,
|
||||
FbItem.NOTIFICATIONS,
|
||||
NotifParser,
|
||||
{ it.notificationRingtone }
|
||||
),
|
||||
private val groupPrefix = "frost_${name.toLowerCase(Locale.CANADA)}"
|
||||
|
||||
MESSAGE(
|
||||
NOTIF_CHANNEL_MESSAGES,
|
||||
OverlayContext.MESSAGE,
|
||||
FbItem.MESSAGES,
|
||||
MessageParser,
|
||||
{ it.messageRingtone }
|
||||
);
|
||||
/** Optional binder to return the request bundle builder */
|
||||
internal open fun bindRequest(
|
||||
content: NotificationContent,
|
||||
cookie: String
|
||||
): (BaseBundle.() -> Unit)? = null
|
||||
|
||||
private val groupPrefix = "frost_${name.toLowerCase(Locale.CANADA)}"
|
||||
private fun bindRequest(intent: Intent, content: NotificationContent) {
|
||||
val cookie = content.data.cookie ?: return
|
||||
val binder = bindRequest(content, cookie) ?: return
|
||||
val bundle = Bundle()
|
||||
bundle.binder()
|
||||
intent.putExtras(bundle)
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional binder to return the request bundle builder
|
||||
*/
|
||||
internal open fun bindRequest(
|
||||
content: NotificationContent,
|
||||
cookie: String
|
||||
): (BaseBundle.() -> Unit)? = null
|
||||
|
||||
private fun bindRequest(intent: Intent, content: NotificationContent) {
|
||||
val cookie = content.data.cookie ?: return
|
||||
val binder = bindRequest(content, cookie) ?: return
|
||||
val bundle = Bundle()
|
||||
bundle.binder()
|
||||
intent.putExtras(bundle)
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
suspend fun fetch(
|
||||
context: Context,
|
||||
data: CookieEntity,
|
||||
prefs: Prefs,
|
||||
notifDao: NotificationDao
|
||||
): Int {
|
||||
val response =
|
||||
try {
|
||||
parser.parse(data.cookie)
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
}
|
||||
if (response == null) {
|
||||
L.v { "$name notification data not found" }
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
suspend fun fetch(
|
||||
context: Context,
|
||||
data: CookieEntity,
|
||||
prefs: Prefs,
|
||||
notifDao: NotificationDao
|
||||
): Int {
|
||||
val response = try {
|
||||
parser.parse(data.cookie)
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
}
|
||||
if (response == null) {
|
||||
L.v { "$name notification data not found" }
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
|
||||
val notifContents = response.data.getUnreadNotifications(data).filter { notif ->
|
||||
validText(notif.title) && validText(notif.text)
|
||||
}
|
||||
if (notifContents.isEmpty()) return 0
|
||||
|
||||
val userId = data.id
|
||||
val prevLatestEpoch = notifDao.latestEpoch(userId, channelId)
|
||||
L.v { "Notif $name prev epoch $prevLatestEpoch" }
|
||||
|
||||
if (!notifDao.saveNotifications(channelId, notifContents)) {
|
||||
L.d { "Skip notifs for $name as saving failed" }
|
||||
return -1
|
||||
}
|
||||
|
||||
if (prevLatestEpoch == -1L && !BuildConfig.DEBUG) {
|
||||
L.d { "Skipping first notification fetch" }
|
||||
return 0 // do not notify the first time
|
||||
}
|
||||
|
||||
val newNotifContents = notifContents.filter { it.timestamp > prevLatestEpoch }
|
||||
|
||||
if (newNotifContents.isEmpty()) {
|
||||
L.d { "No new notifs found for $name" }
|
||||
return 0
|
||||
}
|
||||
|
||||
L.d { "${newNotifContents.size} new notifs found for $name" }
|
||||
|
||||
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)
|
||||
val ringtone = ringtoneProvider(prefs)
|
||||
notifs.forEachIndexed { i, notif ->
|
||||
// Ring at most twice
|
||||
notif.withAlert(context, i < 2, ringtone, prefs).notify(context)
|
||||
}
|
||||
return notifs.size
|
||||
/** 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) }
|
||||
}
|
||||
|
||||
fun debugNotification(context: Context, data: CookieEntity) {
|
||||
val content = NotificationContent(
|
||||
data,
|
||||
System.currentTimeMillis(),
|
||||
"https://github.com/AllanWang/Frost-for-Facebook",
|
||||
"Debug Notif",
|
||||
"Test 123",
|
||||
System.currentTimeMillis() / 1000,
|
||||
"https://www.iconexperience.com/_img/v_collection_png/256x256/shadow/dog.png",
|
||||
false
|
||||
)
|
||||
createNotification(context, content).notify(context)
|
||||
val notifContents =
|
||||
response.data.getUnreadNotifications(data).filter { notif ->
|
||||
validText(notif.title) && validText(notif.text)
|
||||
}
|
||||
if (notifContents.isEmpty()) return 0
|
||||
|
||||
val userId = data.id
|
||||
val prevLatestEpoch = notifDao.latestEpoch(userId, channelId)
|
||||
L.v { "Notif $name prev epoch $prevLatestEpoch" }
|
||||
|
||||
if (!notifDao.saveNotifications(channelId, notifContents)) {
|
||||
L.d { "Skip notifs for $name as saving failed" }
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 =
|
||||
Uri.parse(if (content.href.isIndependent) content.href else FbItem.NOTIFICATIONS.url)
|
||||
bindRequest(intent, content)
|
||||
return intent
|
||||
if (prevLatestEpoch == -1L && !BuildConfig.DEBUG) {
|
||||
L.d { "Skipping first notification fetch" }
|
||||
return 0 // do not notify the first time
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
intent.putExtra(ARG_USER_ID, userId)
|
||||
overlayContext.put(intent)
|
||||
return intent
|
||||
val newNotifContents = notifContents.filter { it.timestamp > prevLatestEpoch }
|
||||
|
||||
if (newNotifContents.isEmpty()) {
|
||||
L.d { "No new notifs found for $name" }
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and submit a new notification with the given [content]
|
||||
*/
|
||||
private fun createNotification(
|
||||
context: Context,
|
||||
content: NotificationContent
|
||||
): FrostNotification =
|
||||
with(content) {
|
||||
val intent = createCommonIntent(context, content.data.id)
|
||||
putContentExtra(intent, content)
|
||||
val group = "${groupPrefix}_${data.id}"
|
||||
val pendingIntent =
|
||||
PendingIntent.getActivity(context, 0, intent, pendingIntentFlagUpdateCurrent)
|
||||
val notifBuilder = context.frostNotification(channelId)
|
||||
.setContentTitle(title ?: context.string(R.string.frost_name))
|
||||
.setContentText(text)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setCategory(Notification.CATEGORY_SOCIAL)
|
||||
.setSubText(data.name)
|
||||
.setGroup(group)
|
||||
L.d { "${newNotifContents.size} new notifs found for $name" }
|
||||
|
||||
if (timestamp != -1L) notifBuilder.setWhen(timestamp * 1000)
|
||||
L.v { "Notif load $content" }
|
||||
val notifs = newNotifContents.map { createNotification(context, it) }
|
||||
|
||||
if (profileUrl != null) {
|
||||
try {
|
||||
val profileImg = GlideApp.with(context)
|
||||
.asBitmap()
|
||||
.load(profileUrl)
|
||||
.transform(FrostGlide.circleCrop)
|
||||
.submit(_40_DP, _40_DP)
|
||||
.get()
|
||||
notifBuilder.setLargeIcon(profileImg)
|
||||
} catch (e: Exception) {
|
||||
L.e { "Failed to get image $profileUrl" }
|
||||
}
|
||||
}
|
||||
frostEvent("Notifications", "Type" to name, "Count" to notifs.size)
|
||||
if (notifs.size > 1) summaryNotification(context, userId, notifs.size).notify(context)
|
||||
val ringtone = ringtoneProvider(prefs)
|
||||
notifs.forEachIndexed { i, notif ->
|
||||
// Ring at most twice
|
||||
notif.withAlert(context, i < 2, ringtone, prefs).notify(context)
|
||||
}
|
||||
return notifs.size
|
||||
}
|
||||
|
||||
FrostNotification(group, notifId, notifBuilder)
|
||||
fun debugNotification(context: Context, data: CookieEntity) {
|
||||
val content =
|
||||
NotificationContent(
|
||||
data,
|
||||
System.currentTimeMillis(),
|
||||
"https://github.com/AllanWang/Frost-for-Facebook",
|
||||
"Debug Notif",
|
||||
"Test 123",
|
||||
System.currentTimeMillis() / 1000,
|
||||
"https://www.iconexperience.com/_img/v_collection_png/256x256/shadow/dog.png",
|
||||
false
|
||||
)
|
||||
createNotification(context, content).notify(context)
|
||||
}
|
||||
|
||||
/** 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 =
|
||||
Uri.parse(if (content.href.isIndependent) content.href else FbItem.NOTIFICATIONS.url)
|
||||
bindRequest(intent, content)
|
||||
return intent
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
intent.putExtra(ARG_USER_ID, userId)
|
||||
overlayContext.put(intent)
|
||||
return intent
|
||||
}
|
||||
|
||||
/** Create and submit a new notification with the given [content] */
|
||||
private fun createNotification(
|
||||
context: Context,
|
||||
content: NotificationContent
|
||||
): FrostNotification =
|
||||
with(content) {
|
||||
val intent = createCommonIntent(context, content.data.id)
|
||||
putContentExtra(intent, content)
|
||||
val group = "${groupPrefix}_${data.id}"
|
||||
val pendingIntent =
|
||||
PendingIntent.getActivity(context, 0, intent, pendingIntentFlagUpdateCurrent)
|
||||
val notifBuilder =
|
||||
context
|
||||
.frostNotification(channelId)
|
||||
.setContentTitle(title ?: context.string(R.string.frost_name))
|
||||
.setContentText(text)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setCategory(Notification.CATEGORY_SOCIAL)
|
||||
.setSubText(data.name)
|
||||
.setGroup(group)
|
||||
|
||||
if (timestamp != -1L) notifBuilder.setWhen(timestamp * 1000)
|
||||
L.v { "Notif load $content" }
|
||||
|
||||
if (profileUrl != null) {
|
||||
try {
|
||||
val profileImg =
|
||||
GlideApp.with(context)
|
||||
.asBitmap()
|
||||
.load(profileUrl)
|
||||
.transform(FrostGlide.circleCrop)
|
||||
.submit(_40_DP, _40_DP)
|
||||
.get()
|
||||
notifBuilder.setLargeIcon(profileImg)
|
||||
} catch (e: Exception) {
|
||||
L.e { "Failed to get image $profileUrl" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
intent.data = Uri.parse(fbItem.url)
|
||||
val group = "${groupPrefix}_$userId"
|
||||
val pendingIntent =
|
||||
PendingIntent.getActivity(context, 0, intent, pendingIntentFlagUpdateCurrent)
|
||||
val notifBuilder = context.frostNotification(channelId)
|
||||
.setContentTitle(context.string(R.string.frost_name))
|
||||
.setContentText("$count ${context.string(fbItem.titleId)}")
|
||||
.setGroup(group)
|
||||
.setGroupSummary(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setCategory(Notification.CATEGORY_SOCIAL)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notifBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
|
||||
}
|
||||
|
||||
return FrostNotification(group, 1, notifBuilder)
|
||||
FrostNotification(group, notifId, notifBuilder)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
intent.data = Uri.parse(fbItem.url)
|
||||
val group = "${groupPrefix}_$userId"
|
||||
val pendingIntent =
|
||||
PendingIntent.getActivity(context, 0, intent, pendingIntentFlagUpdateCurrent)
|
||||
val notifBuilder =
|
||||
context
|
||||
.frostNotification(channelId)
|
||||
.setContentTitle(context.string(R.string.frost_name))
|
||||
.setContentText("$count ${context.string(fbItem.titleId)}")
|
||||
.setGroup(group)
|
||||
.setGroupSummary(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setCategory(Notification.CATEGORY_SOCIAL)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notifBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
|
||||
}
|
||||
|
||||
return FrostNotification(group, 1, notifBuilder)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification data holder
|
||||
*/
|
||||
/** Notification data holder */
|
||||
data class NotificationContent(
|
||||
// TODO replace data with userId?
|
||||
val data: CookieEntity,
|
||||
val id: Long,
|
||||
val href: String,
|
||||
val title: String? = null, // defaults to frost title
|
||||
val text: String,
|
||||
val timestamp: Long,
|
||||
val profileUrl: String?,
|
||||
val unread: Boolean
|
||||
// TODO replace data with userId?
|
||||
val data: CookieEntity,
|
||||
val id: Long,
|
||||
val href: String,
|
||||
val title: String? = null, // defaults to frost title
|
||||
val text: String,
|
||||
val timestamp: Long,
|
||||
val profileUrl: String?,
|
||||
val unread: Boolean
|
||||
) {
|
||||
|
||||
val notifId = abs(id.toInt())
|
||||
val notifId = abs(id.toInt())
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
private val id: Int,
|
||||
val notif: NotificationCompat.Builder
|
||||
private val tag: String,
|
||||
private val id: Int,
|
||||
val notif: NotificationCompat.Builder
|
||||
) {
|
||||
|
||||
fun withAlert(
|
||||
context: Context,
|
||||
enable: Boolean,
|
||||
ringtone: String,
|
||||
prefs: Prefs
|
||||
): FrostNotification {
|
||||
notif.setFrostAlert(context, enable, ringtone, prefs)
|
||||
return this
|
||||
}
|
||||
fun withAlert(
|
||||
context: Context,
|
||||
enable: Boolean,
|
||||
ringtone: String,
|
||||
prefs: Prefs
|
||||
): FrostNotification {
|
||||
notif.setFrostAlert(context, enable, ringtone, prefs)
|
||||
return this
|
||||
}
|
||||
|
||||
fun notify(context: Context) =
|
||||
NotificationManagerCompat.from(context).notify(tag, id, notif.build())
|
||||
fun notify(context: Context) =
|
||||
NotificationManagerCompat.from(context).notify(tag, id, notif.build())
|
||||
}
|
||||
|
||||
fun Context.scheduleNotificationsFromPrefs(prefs: Prefs): Boolean {
|
||||
val shouldSchedule = prefs.hasNotifications
|
||||
return if (shouldSchedule) scheduleNotifications(prefs.notificationFreq)
|
||||
else scheduleNotifications(-1)
|
||||
val shouldSchedule = prefs.hasNotifications
|
||||
return if (shouldSchedule) scheduleNotifications(prefs.notificationFreq)
|
||||
else scheduleNotifications(-1)
|
||||
}
|
||||
|
||||
fun Context.scheduleNotifications(minutes: Long): Boolean =
|
||||
scheduleJob<NotificationService>(NOTIFICATION_PERIODIC_JOB, minutes)
|
||||
scheduleJob<NotificationService>(NOTIFICATION_PERIODIC_JOB, minutes)
|
||||
|
||||
fun Context.fetchNotifications(): Boolean =
|
||||
fetchJob<NotificationService>(NOTIFICATION_JOB_NOW)
|
||||
fun Context.fetchNotifications(): Boolean = fetchJob<NotificationService>(NOTIFICATION_JOB_NOW)
|
||||
|
@ -30,122 +30,116 @@ 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)
|
||||
prepareFinish(true)
|
||||
return false
|
||||
override fun onStopJob(params: JobParameters?): Boolean {
|
||||
super.onStopJob(params)
|
||||
prepareFinish(true)
|
||||
return false
|
||||
}
|
||||
|
||||
private var preparedFinish = false
|
||||
|
||||
private fun prepareFinish(abrupt: Boolean) {
|
||||
if (preparedFinish) return
|
||||
preparedFinish = true
|
||||
val time = System.currentTimeMillis() - startTime
|
||||
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"),
|
||||
"IM Included" to prefs.notificationsInstantMessages,
|
||||
"Duration" to time
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStartJob(params: JobParameters?): Boolean {
|
||||
super.onStartJob(params)
|
||||
L.i { "Fetching notifications" }
|
||||
launch {
|
||||
try {
|
||||
sendNotifications(params)
|
||||
} finally {
|
||||
if (!isActive) prepareFinish(false)
|
||||
jobFinished(params, false)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun sendNotifications(params: JobParameters?): Unit =
|
||||
withContext(Dispatchers.Default) {
|
||||
val currentId = prefs.userId
|
||||
val cookies = cookieDao.selectAll()
|
||||
yield()
|
||||
val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1
|
||||
var notifCount = 0
|
||||
for (cookie in cookies) {
|
||||
yield()
|
||||
val current = cookie.id == currentId
|
||||
if (prefs.notificationsGeneral && (current || prefs.notificationAllAccounts))
|
||||
notifCount += fetch(jobId, NotificationType.GENERAL, cookie)
|
||||
if (prefs.notificationsInstantMessages && (current || prefs.notificationsImAllAccounts))
|
||||
notifCount += fetch(jobId, NotificationType.MESSAGE, cookie)
|
||||
}
|
||||
|
||||
L.i { "Sent $notifCount notifications" }
|
||||
if (notifCount == 0 && jobId == NOTIFICATION_JOB_NOW)
|
||||
generalNotification(665, R.string.no_new_notifications, BuildConfig.DEBUG)
|
||||
if (notifCount > 0) {
|
||||
NotificationWidget.forceUpdate(this@NotificationService)
|
||||
}
|
||||
}
|
||||
|
||||
private var preparedFinish = false
|
||||
|
||||
private fun prepareFinish(abrupt: Boolean) {
|
||||
if (preparedFinish)
|
||||
return
|
||||
preparedFinish = true
|
||||
val time = System.currentTimeMillis() - startTime
|
||||
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"),
|
||||
"IM Included" to prefs.notificationsInstantMessages,
|
||||
"Duration" to time
|
||||
)
|
||||
/**
|
||||
* 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)
|
||||
if (count < 0) {
|
||||
if (jobId == NOTIFICATION_JOB_NOW)
|
||||
generalNotification(666, R.string.error_notification, BuildConfig.DEBUG)
|
||||
return 0
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
override fun onStartJob(params: JobParameters?): Boolean {
|
||||
super.onStartJob(params)
|
||||
L.i { "Fetching notifications" }
|
||||
launch {
|
||||
try {
|
||||
sendNotifications(params)
|
||||
} finally {
|
||||
if (!isActive)
|
||||
prepareFinish(false)
|
||||
jobFinished(params, false)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
private fun logNotif(text: String): NotificationContent? {
|
||||
L.eThrow("NotificationService: $text")
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun sendNotifications(params: JobParameters?): Unit =
|
||||
withContext(Dispatchers.Default) {
|
||||
val currentId = prefs.userId
|
||||
val cookies = cookieDao.selectAll()
|
||||
yield()
|
||||
val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1
|
||||
var notifCount = 0
|
||||
for (cookie in cookies) {
|
||||
yield()
|
||||
val current = cookie.id == currentId
|
||||
if (prefs.notificationsGeneral &&
|
||||
(current || prefs.notificationAllAccounts)
|
||||
)
|
||||
notifCount += fetch(jobId, NotificationType.GENERAL, cookie)
|
||||
if (prefs.notificationsInstantMessages &&
|
||||
(current || prefs.notificationsImAllAccounts)
|
||||
)
|
||||
notifCount += fetch(jobId, NotificationType.MESSAGE, cookie)
|
||||
}
|
||||
|
||||
L.i { "Sent $notifCount notifications" }
|
||||
if (notifCount == 0 && jobId == NOTIFICATION_JOB_NOW)
|
||||
generalNotification(665, R.string.no_new_notifications, BuildConfig.DEBUG)
|
||||
if (notifCount > 0) {
|
||||
NotificationWidget.forceUpdate(this@NotificationService)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
if (count < 0) {
|
||||
if (jobId == NOTIFICATION_JOB_NOW)
|
||||
generalNotification(666, R.string.error_notification, BuildConfig.DEBUG)
|
||||
return 0
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
private fun logNotif(text: String): NotificationContent? {
|
||||
L.eThrow("NotificationService: $text")
|
||||
return null
|
||||
}
|
||||
|
||||
private fun generalNotification(id: Int, textRes: Int, withDefaults: Boolean) {
|
||||
val notifBuilder = frostNotification(NOTIF_CHANNEL_GENERAL)
|
||||
.setFrostAlert(this, withDefaults, prefs.notificationRingtone, prefs)
|
||||
.setContentTitle(string(R.string.frost_name))
|
||||
.setContentText(string(textRes))
|
||||
NotificationManagerCompat.from(this).notify(id, notifBuilder.build())
|
||||
}
|
||||
private fun generalNotification(id: Int, textRes: Int, withDefaults: Boolean) {
|
||||
val notifBuilder =
|
||||
frostNotification(NOTIF_CHANNEL_GENERAL)
|
||||
.setFrostAlert(this, withDefaults, prefs.notificationRingtone, prefs)
|
||||
.setContentTitle(string(R.string.frost_name))
|
||||
.setContentText(string(textRes))
|
||||
NotificationManagerCompat.from(this).notify(id, notifBuilder.build())
|
||||
}
|
||||
}
|
||||
|
@ -36,84 +36,76 @@ 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"
|
||||
|
||||
fun setupNotificationChannels(c: Context, themeProvider: ThemeProvider) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
val manager = c.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val appName = c.string(R.string.frost_name)
|
||||
val msg = c.string(R.string.messages)
|
||||
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
|
||||
}
|
||||
.forEach { manager.deleteNotificationChannel(it.id) }
|
||||
L.d { "Created notification channels: ${manager.notificationChannels.size} channels, ${manager.notificationChannelGroups.size} groups" }
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
val manager = c.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val appName = c.string(R.string.frost_name)
|
||||
val msg = c.string(R.string.messages)
|
||||
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 }
|
||||
.forEach { manager.deleteNotificationChannel(it.id) }
|
||||
L.d {
|
||||
"Created notification channels: ${manager.notificationChannels.size} channels, ${manager.notificationChannelGroups.size} groups"
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun NotificationManager.createNotificationChannel(
|
||||
id: String,
|
||||
name: String,
|
||||
themeProvider: ThemeProvider
|
||||
id: String,
|
||||
name: String,
|
||||
themeProvider: ThemeProvider
|
||||
): NotificationChannel {
|
||||
val channel = NotificationChannel(
|
||||
id,
|
||||
name, NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
channel.enableLights(true)
|
||||
channel.lightColor = themeProvider.accentColor
|
||||
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||
createNotificationChannel(channel)
|
||||
return channel
|
||||
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT)
|
||||
channel.enableLights(true)
|
||||
channel.lightColor = themeProvider.accentColor
|
||||
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||
createNotificationChannel(channel)
|
||||
return channel
|
||||
}
|
||||
|
||||
fun Context.frostNotification(id: String) =
|
||||
NotificationCompat.Builder(this, id)
|
||||
.apply {
|
||||
setSmallIcon(R.drawable.frost_f_24)
|
||||
setAutoCancel(true)
|
||||
setOnlyAlertOnce(true)
|
||||
setStyle(NotificationCompat.BigTextStyle())
|
||||
color = color(R.color.frost_notification_accent)
|
||||
}
|
||||
NotificationCompat.Builder(this, id).apply {
|
||||
setSmallIcon(R.drawable.frost_f_24)
|
||||
setAutoCancel(true)
|
||||
setOnlyAlertOnce(true)
|
||||
setStyle(NotificationCompat.BigTextStyle())
|
||||
color = color(R.color.frost_notification_accent)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
enable: Boolean,
|
||||
ringtone: String,
|
||||
prefs: Prefs
|
||||
context: Context,
|
||||
enable: Boolean,
|
||||
ringtone: String,
|
||||
prefs: Prefs
|
||||
): NotificationCompat.Builder {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setGroupAlertBehavior(
|
||||
if (enable) NotificationCompat.GROUP_ALERT_CHILDREN
|
||||
else NotificationCompat.GROUP_ALERT_SUMMARY
|
||||
)
|
||||
} else if (!enable) {
|
||||
setDefaults(0)
|
||||
} else {
|
||||
var defaults = 0
|
||||
if (prefs.notificationVibrate) defaults = defaults or Notification.DEFAULT_VIBRATE
|
||||
if (prefs.notificationSound) {
|
||||
if (ringtone.isNotBlank()) setSound(context.frostUri(ringtone))
|
||||
else defaults = defaults or Notification.DEFAULT_SOUND
|
||||
}
|
||||
if (prefs.notificationLights) defaults = defaults or Notification.DEFAULT_LIGHTS
|
||||
setDefaults(defaults)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setGroupAlertBehavior(
|
||||
if (enable) NotificationCompat.GROUP_ALERT_CHILDREN
|
||||
else NotificationCompat.GROUP_ALERT_SUMMARY
|
||||
)
|
||||
} else if (!enable) {
|
||||
setDefaults(0)
|
||||
} else {
|
||||
var defaults = 0
|
||||
if (prefs.notificationVibrate) defaults = defaults or Notification.DEFAULT_VIBRATE
|
||||
if (prefs.notificationSound) {
|
||||
if (ringtone.isNotBlank()) setSound(context.frostUri(ringtone))
|
||||
else defaults = defaults or Notification.DEFAULT_SOUND
|
||||
}
|
||||
return this
|
||||
if (prefs.notificationLights) defaults = defaults or Notification.DEFAULT_LIGHTS
|
||||
setDefaults(defaults)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/*
|
||||
@ -125,48 +117,47 @@ fun NotificationCompat.Builder.setFrostAlert(
|
||||
const val NOTIFICATION_PARAM_ID = "notif_param_id"
|
||||
|
||||
fun JobInfo.Builder.setExtras(id: Int): JobInfo.Builder {
|
||||
val bundle = PersistableBundle()
|
||||
bundle.putInt(NOTIFICATION_PARAM_ID, id)
|
||||
return setExtras(bundle)
|
||||
val bundle = PersistableBundle()
|
||||
bundle.putInt(NOTIFICATION_PARAM_ID, id)
|
||||
return setExtras(bundle)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
.setPeriodic(minutes * 60000)
|
||||
.setExtras(id)
|
||||
.setPersisted(true)
|
||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) // TODO add options
|
||||
val result = scheduler.schedule(builder.build())
|
||||
if (result <= 0) {
|
||||
L.eThrow("${T::class.java.simpleName} scheduler failed")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
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)
|
||||
.setPeriodic(minutes * 60000)
|
||||
.setExtras(id)
|
||||
.setPersisted(true)
|
||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) // TODO add options
|
||||
val result = scheduler.schedule(builder.build())
|
||||
if (result <= 0) {
|
||||
L.eThrow("${T::class.java.simpleName} scheduler failed")
|
||||
return false
|
||||
}
|
||||
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)
|
||||
.setMinimumLatency(0L)
|
||||
.setExtras(id)
|
||||
.setOverrideDeadline(2000L)
|
||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||
val result = scheduler.schedule(builder.build())
|
||||
if (result <= 0) {
|
||||
L.eThrow("${T::class.java.simpleName} instant scheduler failed")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
||||
val serviceComponent = ComponentName(this, T::class.java)
|
||||
val builder =
|
||||
JobInfo.Builder(id, serviceComponent)
|
||||
.setMinimumLatency(0L)
|
||||
.setExtras(id)
|
||||
.setOverrideDeadline(2000L)
|
||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||
val result = scheduler.schedule(builder.build())
|
||||
if (result <= 0) {
|
||||
L.eThrow("${T::class.java.simpleName} instant scheduler failed")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -32,12 +32,11 @@ 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
|
||||
L.d { "Frost has updated" }
|
||||
context.scheduleNotifications(prefs.notificationFreq) // Update notifications
|
||||
}
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
|
||||
L.d { "Frost has updated" }
|
||||
context.scheduleNotifications(prefs.notificationFreq) // Update notifications
|
||||
}
|
||||
}
|
||||
|
@ -35,173 +35,170 @@ 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)
|
||||
|
||||
header(R.string.theme_customization)
|
||||
|
||||
text(R.string.theme, prefs::theme, { themeProvider.setTheme(it) }) {
|
||||
onClick = {
|
||||
materialDialog {
|
||||
title(R.string.theme)
|
||||
listItemsSingleChoice(
|
||||
items = Theme.values().map { string(it.textRes) },
|
||||
initialSelection = item.pref
|
||||
) { _, index, _ ->
|
||||
if (item.pref != index) {
|
||||
item.pref = index
|
||||
shouldRestartMain()
|
||||
reload()
|
||||
activityThemer.setFrostTheme(forceTransparent = true)
|
||||
themeExterior()
|
||||
invalidateOptionsMenu()
|
||||
frostEvent("Theme", "Count" to Theme(index).name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
textGetter = {
|
||||
string(Theme(it).textRes)
|
||||
}
|
||||
}
|
||||
|
||||
fun KPrefColorPicker.KPrefColorContract.dependsOnCustom() {
|
||||
enabler = themeProvider::isCustomTheme
|
||||
onDisabledClick = { frostSnackbar(R.string.requires_custom_theme, themeProvider) }
|
||||
allowCustom = true
|
||||
}
|
||||
|
||||
fun invalidateCustomTheme() {
|
||||
themeProvider.reset()
|
||||
}
|
||||
|
||||
colorPicker(
|
||||
R.string.text_color, prefs::customTextColor,
|
||||
{
|
||||
prefs.customTextColor = it
|
||||
reload()
|
||||
invalidateCustomTheme()
|
||||
text(R.string.theme, prefs::theme, { themeProvider.setTheme(it) }) {
|
||||
onClick = {
|
||||
materialDialog {
|
||||
title(R.string.theme)
|
||||
listItemsSingleChoice(
|
||||
items = Theme.values().map { string(it.textRes) },
|
||||
initialSelection = item.pref
|
||||
) { _, index, _ ->
|
||||
if (item.pref != index) {
|
||||
item.pref = index
|
||||
shouldRestartMain()
|
||||
}
|
||||
) {
|
||||
dependsOnCustom()
|
||||
allowCustomAlpha = false
|
||||
}
|
||||
|
||||
colorPicker(
|
||||
R.string.accent_color, prefs::customAccentColor,
|
||||
{
|
||||
prefs.customAccentColor = it
|
||||
reload()
|
||||
invalidateCustomTheme()
|
||||
shouldRestartMain()
|
||||
}
|
||||
) {
|
||||
dependsOnCustom()
|
||||
allowCustomAlpha = false
|
||||
}
|
||||
|
||||
colorPicker(
|
||||
R.string.background_color, prefs::customBackgroundColor,
|
||||
{
|
||||
prefs.customBackgroundColor = it
|
||||
bgCanvas.ripple(it, duration = 500L)
|
||||
invalidateCustomTheme()
|
||||
activityThemer.setFrostTheme(forceTransparent = true)
|
||||
shouldRestartMain()
|
||||
}
|
||||
) {
|
||||
dependsOnCustom()
|
||||
allowCustomAlpha = true
|
||||
}
|
||||
|
||||
colorPicker(
|
||||
R.string.header_color, prefs::customHeaderColor,
|
||||
{
|
||||
prefs.customHeaderColor = it
|
||||
frostNavigationBar(prefs, themeProvider)
|
||||
toolbarCanvas.ripple(it, RippleCanvas.MIDDLE, RippleCanvas.END, duration = 500L)
|
||||
reload()
|
||||
shouldRestartMain()
|
||||
}
|
||||
) {
|
||||
dependsOnCustom()
|
||||
allowCustomAlpha = true
|
||||
}
|
||||
|
||||
colorPicker(
|
||||
R.string.icon_color, prefs::customIconColor,
|
||||
{
|
||||
prefs.customIconColor = it
|
||||
themeExterior()
|
||||
invalidateOptionsMenu()
|
||||
frostEvent("Theme", "Count" to Theme(index).name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
textGetter = { string(Theme(it).textRes) }
|
||||
}
|
||||
|
||||
fun KPrefColorPicker.KPrefColorContract.dependsOnCustom() {
|
||||
enabler = themeProvider::isCustomTheme
|
||||
onDisabledClick = { frostSnackbar(R.string.requires_custom_theme, themeProvider) }
|
||||
allowCustom = true
|
||||
}
|
||||
|
||||
fun invalidateCustomTheme() {
|
||||
themeProvider.reset()
|
||||
}
|
||||
|
||||
colorPicker(
|
||||
R.string.text_color,
|
||||
prefs::customTextColor,
|
||||
{
|
||||
prefs.customTextColor = it
|
||||
reload()
|
||||
invalidateCustomTheme()
|
||||
shouldRestartMain()
|
||||
}
|
||||
) {
|
||||
dependsOnCustom()
|
||||
allowCustomAlpha = false
|
||||
}
|
||||
|
||||
colorPicker(
|
||||
R.string.accent_color,
|
||||
prefs::customAccentColor,
|
||||
{
|
||||
prefs.customAccentColor = it
|
||||
reload()
|
||||
invalidateCustomTheme()
|
||||
shouldRestartMain()
|
||||
}
|
||||
) {
|
||||
dependsOnCustom()
|
||||
allowCustomAlpha = false
|
||||
}
|
||||
|
||||
colorPicker(
|
||||
R.string.background_color,
|
||||
prefs::customBackgroundColor,
|
||||
{
|
||||
prefs.customBackgroundColor = it
|
||||
bgCanvas.ripple(it, duration = 500L)
|
||||
invalidateCustomTheme()
|
||||
activityThemer.setFrostTheme(forceTransparent = true)
|
||||
shouldRestartMain()
|
||||
}
|
||||
) {
|
||||
dependsOnCustom()
|
||||
allowCustomAlpha = true
|
||||
}
|
||||
|
||||
colorPicker(
|
||||
R.string.header_color,
|
||||
prefs::customHeaderColor,
|
||||
{
|
||||
prefs.customHeaderColor = it
|
||||
frostNavigationBar(prefs, themeProvider)
|
||||
toolbarCanvas.ripple(it, RippleCanvas.MIDDLE, RippleCanvas.END, duration = 500L)
|
||||
reload()
|
||||
shouldRestartMain()
|
||||
}
|
||||
) {
|
||||
dependsOnCustom()
|
||||
allowCustomAlpha = true
|
||||
}
|
||||
|
||||
colorPicker(
|
||||
R.string.icon_color,
|
||||
prefs::customIconColor,
|
||||
{
|
||||
prefs.customIconColor = it
|
||||
invalidateOptionsMenu()
|
||||
shouldRestartMain()
|
||||
}
|
||||
) {
|
||||
dependsOnCustom()
|
||||
allowCustomAlpha = false
|
||||
}
|
||||
|
||||
header(R.string.global_customization)
|
||||
|
||||
text(
|
||||
R.string.main_activity_layout,
|
||||
prefs::mainActivityLayoutType,
|
||||
{ prefs.mainActivityLayoutType = it }
|
||||
) {
|
||||
textGetter = { string(prefs.mainActivityLayout.titleRes) }
|
||||
onClick = {
|
||||
materialDialog {
|
||||
title(R.string.main_activity_layout_desc)
|
||||
listItemsSingleChoice(
|
||||
items = MainActivityLayout.values.map { string(it.titleRes) },
|
||||
initialSelection = item.pref
|
||||
) { _, index, _ ->
|
||||
if (item.pref != index) {
|
||||
item.pref = index
|
||||
shouldRestartMain()
|
||||
frostEvent("Main Layout", "Type" to MainActivityLayout(index).name)
|
||||
}
|
||||
}
|
||||
) {
|
||||
dependsOnCustom()
|
||||
allowCustomAlpha = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header(R.string.global_customization)
|
||||
plainText(R.string.main_tabs) {
|
||||
descRes = R.string.main_tabs_desc
|
||||
onClick = { launchTabCustomizerActivity() }
|
||||
}
|
||||
|
||||
text(
|
||||
R.string.main_activity_layout,
|
||||
prefs::mainActivityLayoutType,
|
||||
{ prefs.mainActivityLayoutType = it }
|
||||
) {
|
||||
textGetter = { string(prefs.mainActivityLayout.titleRes) }
|
||||
onClick = {
|
||||
materialDialog {
|
||||
title(R.string.main_activity_layout_desc)
|
||||
listItemsSingleChoice(
|
||||
items = MainActivityLayout.values.map { string(it.titleRes) },
|
||||
initialSelection = item.pref
|
||||
) { _, index, _ ->
|
||||
if (item.pref != index) {
|
||||
item.pref = index
|
||||
shouldRestartMain()
|
||||
frostEvent("Main Layout", "Type" to MainActivityLayout(index).name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
checkbox(
|
||||
R.string.tint_nav,
|
||||
prefs::tintNavBar,
|
||||
{
|
||||
prefs.tintNavBar = it
|
||||
frostNavigationBar(prefs, themeProvider)
|
||||
setFrostResult(REQUEST_NAV)
|
||||
}
|
||||
) {
|
||||
descRes = R.string.tint_nav_desc
|
||||
}
|
||||
|
||||
plainText(R.string.main_tabs) {
|
||||
descRes = R.string.main_tabs_desc
|
||||
onClick = { launchTabCustomizerActivity() }
|
||||
}
|
||||
|
||||
checkbox(
|
||||
R.string.tint_nav, prefs::tintNavBar,
|
||||
{
|
||||
prefs.tintNavBar = it
|
||||
frostNavigationBar(prefs, themeProvider)
|
||||
setFrostResult(REQUEST_NAV)
|
||||
}
|
||||
) {
|
||||
descRes = R.string.tint_nav_desc
|
||||
}
|
||||
|
||||
list.add(
|
||||
KPrefTextSeekbar(
|
||||
KPrefSeekbar.KPrefSeekbarBuilder(
|
||||
globalOptions,
|
||||
R.string.web_text_scaling, prefs::webTextScaling
|
||||
) {
|
||||
prefs.webTextScaling = it
|
||||
setFrostResult(REQUEST_TEXT_ZOOM)
|
||||
}
|
||||
)
|
||||
list.add(
|
||||
KPrefTextSeekbar(
|
||||
KPrefSeekbar.KPrefSeekbarBuilder(
|
||||
globalOptions,
|
||||
R.string.web_text_scaling,
|
||||
prefs::webTextScaling
|
||||
) {
|
||||
prefs.webTextScaling = it
|
||||
setFrostResult(REQUEST_TEXT_ZOOM)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
checkbox(
|
||||
R.string.enforce_black_media_bg, prefs::blackMediaBg,
|
||||
{
|
||||
prefs.blackMediaBg = it
|
||||
}
|
||||
) {
|
||||
descRes = R.string.enforce_black_media_bg_desc
|
||||
}
|
||||
checkbox(R.string.enforce_black_media_bg, prefs::blackMediaBg, { prefs.blackMediaBg = it }) {
|
||||
descRes = R.string.enforce_black_media_bg_desc
|
||||
}
|
||||
}
|
||||
|
@ -20,76 +20,86 @@ 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.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
|
||||
}
|
||||
) {
|
||||
descRes = R.string.fancy_animations_desc
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
) {
|
||||
descRes = R.string.overlay_swipe_desc
|
||||
}
|
||||
|
||||
checkbox(
|
||||
R.string.overlay_swipe,
|
||||
prefs::overlayEnabled,
|
||||
{ prefs.overlayEnabled = it; shouldRefreshMain() }
|
||||
) {
|
||||
descRes = R.string.overlay_swipe_desc
|
||||
checkbox(
|
||||
R.string.overlay_full_screen_swipe,
|
||||
prefs::overlayFullScreenSwipe,
|
||||
{ prefs.overlayFullScreenSwipe = it }
|
||||
) {
|
||||
descRes = R.string.overlay_full_screen_swipe_desc
|
||||
}
|
||||
|
||||
checkbox(
|
||||
R.string.open_links_in_default,
|
||||
prefs::linksInDefaultApp,
|
||||
{ prefs.linksInDefaultApp = it }
|
||||
) {
|
||||
descRes = R.string.open_links_in_default_desc
|
||||
}
|
||||
|
||||
checkbox(R.string.viewpager_swipe, prefs::viewpagerSwipe, { prefs.viewpagerSwipe = it }) {
|
||||
descRes = R.string.viewpager_swipe_desc
|
||||
}
|
||||
|
||||
checkbox(
|
||||
R.string.force_message_bottom,
|
||||
prefs::messageScrollToBottom,
|
||||
{ prefs.messageScrollToBottom = it }
|
||||
) {
|
||||
descRes = R.string.force_message_bottom_desc
|
||||
}
|
||||
|
||||
checkbox(
|
||||
R.string.auto_expand_text_box,
|
||||
prefs::autoExpandTextBox,
|
||||
{
|
||||
prefs.autoExpandTextBox = it
|
||||
shouldRefreshMain()
|
||||
}
|
||||
) {
|
||||
descRes = R.string.auto_expand_text_box_desc
|
||||
}
|
||||
|
||||
checkbox(
|
||||
R.string.overlay_full_screen_swipe,
|
||||
prefs::overlayFullScreenSwipe,
|
||||
{ prefs.overlayFullScreenSwipe = it }
|
||||
) {
|
||||
descRes = R.string.overlay_full_screen_swipe_desc
|
||||
}
|
||||
checkbox(R.string.enable_pip, prefs::enablePip, { prefs.enablePip = it }) {
|
||||
descRes = R.string.enable_pip_desc
|
||||
}
|
||||
|
||||
checkbox(
|
||||
R.string.open_links_in_default,
|
||||
prefs::linksInDefaultApp,
|
||||
{ prefs.linksInDefaultApp = it }
|
||||
) {
|
||||
descRes = R.string.open_links_in_default_desc
|
||||
}
|
||||
// Not available for desktop user agent for now
|
||||
// plainText(R.string.autoplay_settings) {
|
||||
// descRes = R.string.autoplay_settings_desc
|
||||
// onClick = {
|
||||
// launchWebOverlay("${FB_URL_BASE}settings/videos/")
|
||||
// }
|
||||
// }
|
||||
|
||||
checkbox(R.string.viewpager_swipe, prefs::viewpagerSwipe, { prefs.viewpagerSwipe = it }) {
|
||||
descRes = R.string.viewpager_swipe_desc
|
||||
}
|
||||
|
||||
checkbox(
|
||||
R.string.force_message_bottom,
|
||||
prefs::messageScrollToBottom,
|
||||
{ prefs.messageScrollToBottom = it }
|
||||
) {
|
||||
descRes = R.string.force_message_bottom_desc
|
||||
}
|
||||
|
||||
checkbox(
|
||||
R.string.auto_expand_text_box,
|
||||
prefs::autoExpandTextBox,
|
||||
{ prefs.autoExpandTextBox = it; shouldRefreshMain() }
|
||||
) {
|
||||
descRes = R.string.auto_expand_text_box_desc
|
||||
}
|
||||
|
||||
checkbox(R.string.enable_pip, prefs::enablePip, { prefs.enablePip = it }) {
|
||||
descRes = R.string.enable_pip_desc
|
||||
}
|
||||
|
||||
// Not available for desktop user agent for now
|
||||
// plainText(R.string.autoplay_settings) {
|
||||
// descRes = R.string.autoplay_settings_desc
|
||||
// onClick = {
|
||||
// launchWebOverlay("${FB_URL_BASE}settings/videos/")
|
||||
// }
|
||||
// }
|
||||
|
||||
checkbox(R.string.exit_confirmation, prefs::exitConfirmation, { prefs.exitConfirmation = it }) {
|
||||
descRes = R.string.exit_confirmation_desc
|
||||
}
|
||||
checkbox(R.string.exit_confirmation, prefs::exitConfirmation, { prefs.exitConfirmation = it }) {
|
||||
descRes = R.string.exit_confirmation_desc
|
||||
}
|
||||
}
|
||||
|
@ -40,123 +40,114 @@ 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) }
|
||||
}
|
||||
|
||||
plainText(R.string.debug_web) {
|
||||
descRes = R.string.debug_web_desc
|
||||
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)
|
||||
|
||||
plainText(R.string.debug_parsers) {
|
||||
descRes = R.string.debug_parsers_desc
|
||||
onClick = {
|
||||
materialDialog {
|
||||
// noinspection CheckResult
|
||||
listItems(items = parsers.map { string(it.nameRes) }) { dialog, position, _ ->
|
||||
dialog.dismiss()
|
||||
val parser = parsers[position]
|
||||
var attempt: Job? = null
|
||||
val loading = materialDialog {
|
||||
message(parser.nameRes)
|
||||
// TODO change dialog? No more progress view
|
||||
negativeButton(R.string.kau_cancel) {
|
||||
attempt?.cancel()
|
||||
it.dismiss()
|
||||
}
|
||||
cancelOnTouchOutside(false)
|
||||
}
|
||||
|
||||
val parsers = arrayOf(NotifParser, MessageParser, SearchParser)
|
||||
|
||||
materialDialog {
|
||||
// noinspection CheckResult
|
||||
listItems(items = parsers.map { string(it.nameRes) }) { dialog, position, _ ->
|
||||
dialog.dismiss()
|
||||
val parser = parsers[position]
|
||||
var attempt: Job? = null
|
||||
val loading = materialDialog {
|
||||
message(parser.nameRes)
|
||||
// TODO change dialog? No more progress view
|
||||
negativeButton(R.string.kau_cancel) {
|
||||
attempt?.cancel()
|
||||
it.dismiss()
|
||||
}
|
||||
cancelOnTouchOutside(false)
|
||||
}
|
||||
|
||||
attempt = launch(Dispatchers.IO) {
|
||||
try {
|
||||
val data = parser.parse(fbCookie.webCookie)
|
||||
withMainContext {
|
||||
loading.dismiss()
|
||||
createEmail(parser, data?.data, prefs)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
createEmail(parser, "Error: ${e.message}", prefs)
|
||||
}
|
||||
}
|
||||
attempt =
|
||||
launch(Dispatchers.IO) {
|
||||
try {
|
||||
val data = parser.parse(fbCookie.webCookie)
|
||||
withMainContext {
|
||||
loading.dismiss()
|
||||
createEmail(parser, data?.data, prefs)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
createEmail(parser, "Error: ${e.message}", prefs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.createEmail(parser: FrostParser<*>, content: Any?, prefs: Prefs) =
|
||||
sendFrostEmail(
|
||||
"${string(R.string.debug_report)}: ${parser::class.java.simpleName}",
|
||||
prefs = prefs
|
||||
) {
|
||||
addItem("Url", parser.url)
|
||||
addItem("Contents", "$content")
|
||||
}
|
||||
sendFrostEmail(
|
||||
"${string(R.string.debug_report)}: ${parser::class.java.simpleName}",
|
||||
prefs = prefs
|
||||
) {
|
||||
addItem("Url", parser.url)
|
||||
addItem("Contents", "$content")
|
||||
}
|
||||
|
||||
private const val ZIP_NAME = "debug"
|
||||
|
||||
fun SettingsActivity.sendDebug(url: String, html: String?) {
|
||||
|
||||
val downloader = OfflineWebsite(
|
||||
url,
|
||||
cookie = fbCookie.webCookie ?: "",
|
||||
baseUrl = FB_URL_BASE,
|
||||
html = html,
|
||||
baseDir = DebugActivity.baseDir(this)
|
||||
val downloader =
|
||||
OfflineWebsite(
|
||||
url,
|
||||
cookie = fbCookie.webCookie ?: "",
|
||||
baseUrl = FB_URL_BASE,
|
||||
html = html,
|
||||
baseDir = DebugActivity.baseDir(this)
|
||||
)
|
||||
|
||||
val job = Job()
|
||||
val job = Job()
|
||||
|
||||
val md = materialDialog {
|
||||
title(R.string.parsing_data)
|
||||
// TODO remove dialog? No progress ui
|
||||
negativeButton(R.string.kau_cancel) { it.dismiss() }
|
||||
cancelOnTouchOutside(false)
|
||||
onDismiss { job.cancel() }
|
||||
}
|
||||
|
||||
val progressFlow = MutableStateFlow(0)
|
||||
|
||||
// progressFlow.onEach { md.setProgress(it) }.launchIn(this)
|
||||
|
||||
launchMain {
|
||||
val success = downloader.loadAndZip(ZIP_NAME) {
|
||||
progressFlow.tryEmit(it)
|
||||
}
|
||||
md.dismiss()
|
||||
if (success) {
|
||||
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"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toast(R.string.error_generic)
|
||||
}
|
||||
val md = materialDialog {
|
||||
title(R.string.parsing_data)
|
||||
// TODO remove dialog? No progress ui
|
||||
negativeButton(R.string.kau_cancel) { it.dismiss() }
|
||||
cancelOnTouchOutside(false)
|
||||
onDismiss { job.cancel() }
|
||||
}
|
||||
|
||||
val progressFlow = MutableStateFlow(0)
|
||||
|
||||
// progressFlow.onEach { md.setProgress(it) }.launchIn(this)
|
||||
|
||||
launchMain {
|
||||
val success = downloader.loadAndZip(ZIP_NAME) { progressFlow.tryEmit(it) }
|
||||
md.dismiss()
|
||||
if (success) {
|
||||
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" }
|
||||
}
|
||||
} else {
|
||||
toast(R.string.error_generic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user