mirror of
https://github.com/AllanWang/Frost-for-Facebook.git
synced 2024-09-18 21:12:24 +02: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
|
import dagger.hilt.android.testing.HiltTestApplication
|
||||||
|
|
||||||
class FrostTestRunner : AndroidJUnitRunner() {
|
class FrostTestRunner : AndroidJUnitRunner() {
|
||||||
override fun newApplication(
|
override fun newApplication(
|
||||||
cl: ClassLoader?,
|
cl: ClassLoader?,
|
||||||
className: String?,
|
className: String?,
|
||||||
context: Context?
|
context: Context?
|
||||||
): Application {
|
): Application {
|
||||||
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
|
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,22 +27,18 @@ import org.junit.Test
|
|||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class StartActivityTest {
|
class StartActivityTest {
|
||||||
|
|
||||||
@get:Rule(order = 0)
|
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||||
val hildAndroidRule = HiltAndroidRule(this)
|
|
||||||
|
|
||||||
@get:Rule(order = 1)
|
@get:Rule(order = 1)
|
||||||
val activityRule = activityRule<StartActivity>(
|
val activityRule =
|
||||||
intentAction = {
|
activityRule<StartActivity>(intentAction = { putExtra(ARG_URL, TEST_FORMATTED_URL) })
|
||||||
putExtra(ARG_URL, TEST_FORMATTED_URL)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun initializesSuccessfully() {
|
fun initializesSuccessfully() {
|
||||||
activityRule.scenario.use {
|
activityRule.scenario.use {
|
||||||
it.onActivity {
|
it.onActivity {
|
||||||
// Verify no crash
|
// Verify no crash
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,11 +25,7 @@ import dagger.hilt.components.SingletonComponent
|
|||||||
import dagger.hilt.testing.TestInstallIn
|
import dagger.hilt.testing.TestInstallIn
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@TestInstallIn(
|
@TestInstallIn(components = [SingletonComponent::class], replaces = [PrefFactoryModule::class])
|
||||||
components = [SingletonComponent::class],
|
|
||||||
replaces = [PrefFactoryModule::class]
|
|
||||||
)
|
|
||||||
object PrefFactoryTestModule {
|
object PrefFactoryTestModule {
|
||||||
@Provides
|
@Provides fun factory(): KPrefFactory = KPrefFactoryInMemory
|
||||||
fun factory(): KPrefFactory = KPrefFactoryInMemory
|
|
||||||
}
|
}
|
||||||
|
@ -25,18 +25,16 @@ import org.junit.Test
|
|||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class AboutActivityTest {
|
class AboutActivityTest {
|
||||||
|
|
||||||
@get:Rule(order = 0)
|
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||||
val hildAndroidRule = HiltAndroidRule(this)
|
|
||||||
|
|
||||||
@get:Rule(order = 1)
|
@get:Rule(order = 1) val activityRule = activityRule<AboutActivity>()
|
||||||
val activityRule = activityRule<AboutActivity>()
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun initializesSuccessfully() {
|
fun initializesSuccessfully() {
|
||||||
activityRule.scenario.use {
|
activityRule.scenario.use {
|
||||||
it.onActivity {
|
it.onActivity {
|
||||||
// Verify no crash
|
// Verify no crash
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,18 +25,16 @@ import org.junit.Test
|
|||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class DebugActivityTest {
|
class DebugActivityTest {
|
||||||
|
|
||||||
@get:Rule(order = 0)
|
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||||
val hildAndroidRule = HiltAndroidRule(this)
|
|
||||||
|
|
||||||
@get:Rule(order = 1)
|
@get:Rule(order = 1) val activityRule = activityRule<DebugActivity>()
|
||||||
val activityRule = activityRule<DebugActivity>()
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun initializesSuccessfully() {
|
fun initializesSuccessfully() {
|
||||||
activityRule.scenario.use {
|
activityRule.scenario.use {
|
||||||
it.onActivity {
|
it.onActivity {
|
||||||
// Verify no crash
|
// Verify no crash
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,22 +27,18 @@ import org.junit.Test
|
|||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class FrostWebActivityTest {
|
class FrostWebActivityTest {
|
||||||
|
|
||||||
@get:Rule(order = 0)
|
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||||
val hildAndroidRule = HiltAndroidRule(this)
|
|
||||||
|
|
||||||
@get:Rule(order = 1)
|
@get:Rule(order = 1)
|
||||||
val activityRule = activityRule<FrostWebActivity>(
|
val activityRule =
|
||||||
intentAction = {
|
activityRule<FrostWebActivity>(intentAction = { putExtra(ARG_URL, TEST_FORMATTED_URL) })
|
||||||
putExtra(ARG_URL, TEST_FORMATTED_URL)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun initializesSuccessfully() {
|
fun initializesSuccessfully() {
|
||||||
activityRule.scenario.use {
|
activityRule.scenario.use {
|
||||||
it.onActivity {
|
it.onActivity {
|
||||||
// Verify no crash
|
// Verify no crash
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,11 @@ import com.pitchedapps.frost.utils.ARG_TEXT
|
|||||||
import com.pitchedapps.frost.utils.isIndirectImageUrl
|
import com.pitchedapps.frost.utils.isIndirectImageUrl
|
||||||
import dagger.hilt.android.testing.HiltAndroidRule
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
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.internal.closeQuietly
|
||||||
import okhttp3.mockwebserver.Dispatcher
|
import okhttp3.mockwebserver.Dispatcher
|
||||||
import okhttp3.mockwebserver.MockResponse
|
import okhttp3.mockwebserver.MockResponse
|
||||||
@ -42,140 +47,125 @@ import org.junit.Ignore
|
|||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.rules.Timeout
|
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
|
@HiltAndroidTest
|
||||||
class ImageActivityTest {
|
class ImageActivityTest {
|
||||||
|
|
||||||
@get:Rule(order = 0)
|
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||||
val hildAndroidRule = HiltAndroidRule(this)
|
|
||||||
|
|
||||||
@get:Rule(order = 1)
|
@get:Rule(order = 1)
|
||||||
val activityRule = activityRule<ImageActivity>(
|
val activityRule =
|
||||||
intentAction = {
|
activityRule<ImageActivity>(intentAction = { putExtra(ARG_IMAGE_URL, TEST_FORMATTED_URL) })
|
||||||
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)
|
private fun mockServer(): MockWebServer {
|
||||||
val globalTimeout: Timeout = Timeout.seconds(15)
|
val img = Buffer()
|
||||||
|
img.writeAll(getResource("bayer-pattern.jpg").source())
|
||||||
lateinit var mockServer: MockWebServer
|
return MockWebServer().apply {
|
||||||
|
dispatcher =
|
||||||
@Before
|
object : Dispatcher() {
|
||||||
fun before() {
|
override fun dispatch(request: RecordedRequest): MockResponse =
|
||||||
mockServer = mockServer()
|
when {
|
||||||
}
|
request.path?.contains("text") == true ->
|
||||||
|
MockResponse().setResponseCode(200).setBody("Valid mock text response")
|
||||||
@After
|
request.path?.contains("image") == true ->
|
||||||
fun after() {
|
MockResponse().setResponseCode(200).setBody(img)
|
||||||
mockServer.closeQuietly()
|
else -> MockResponse().setResponseCode(404).setBody("Error mock response")
|
||||||
}
|
|
||||||
|
|
||||||
@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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
@HiltAndroidTest
|
||||||
class IntroActivityTest {
|
class IntroActivityTest {
|
||||||
|
|
||||||
@get:Rule(order = 0)
|
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||||
val hildAndroidRule = HiltAndroidRule(this)
|
|
||||||
|
|
||||||
@get:Rule(order = 1)
|
@get:Rule(order = 1) val activityRule = activityRule<IntroActivity>()
|
||||||
val activityRule = activityRule<IntroActivity>()
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun initializesSuccessfully() {
|
fun initializesSuccessfully() {
|
||||||
activityRule.scenario.use {
|
activityRule.scenario.use {
|
||||||
it.onActivity {
|
it.onActivity {
|
||||||
// Verify no crash
|
// Verify no crash
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,18 +25,16 @@ import org.junit.Test
|
|||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class LoginActivityTest {
|
class LoginActivityTest {
|
||||||
|
|
||||||
@get:Rule(order = 0)
|
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||||
val hildAndroidRule = HiltAndroidRule(this)
|
|
||||||
|
|
||||||
@get:Rule(order = 1)
|
@get:Rule(order = 1) val activityRule = activityRule<LoginActivity>()
|
||||||
val activityRule = activityRule<LoginActivity>()
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun initializesSuccessfully() {
|
fun initializesSuccessfully() {
|
||||||
activityRule.scenario.use {
|
activityRule.scenario.use {
|
||||||
it.onActivity {
|
it.onActivity {
|
||||||
// Verify no crash
|
// Verify no crash
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,18 +25,16 @@ import org.junit.Test
|
|||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class MainActivityTest {
|
class MainActivityTest {
|
||||||
|
|
||||||
@get:Rule(order = 0)
|
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||||
val hildAndroidRule = HiltAndroidRule(this)
|
|
||||||
|
|
||||||
@get:Rule(order = 1)
|
@get:Rule(order = 1) val activityRule = activityRule<MainActivity>()
|
||||||
val activityRule = activityRule<MainActivity>()
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun initializesSuccessfully() {
|
fun initializesSuccessfully() {
|
||||||
activityRule.scenario.use {
|
activityRule.scenario.use {
|
||||||
it.onActivity {
|
it.onActivity {
|
||||||
// Verify no crash
|
// Verify no crash
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,18 +25,16 @@ import org.junit.Test
|
|||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class SelectorActivityTest {
|
class SelectorActivityTest {
|
||||||
|
|
||||||
@get:Rule(order = 0)
|
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||||
val hildAndroidRule = HiltAndroidRule(this)
|
|
||||||
|
|
||||||
@get:Rule(order = 1)
|
@get:Rule(order = 1) val activityRule = activityRule<SelectorActivity>()
|
||||||
val activityRule = activityRule<SelectorActivity>()
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun initializesSuccessfully() {
|
fun initializesSuccessfully() {
|
||||||
activityRule.scenario.use {
|
activityRule.scenario.use {
|
||||||
it.onActivity {
|
it.onActivity {
|
||||||
// Verify no crash
|
// Verify no crash
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,18 +25,16 @@ import org.junit.Test
|
|||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class SettingActivityTest {
|
class SettingActivityTest {
|
||||||
|
|
||||||
@get:Rule(order = 0)
|
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||||
val hildAndroidRule = HiltAndroidRule(this)
|
|
||||||
|
|
||||||
@get:Rule(order = 1)
|
@get:Rule(order = 1) val activityRule = activityRule<SettingsActivity>()
|
||||||
val activityRule = activityRule<SettingsActivity>()
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun initializesSuccessfully() {
|
fun initializesSuccessfully() {
|
||||||
activityRule.scenario.use {
|
activityRule.scenario.use {
|
||||||
it.onActivity {
|
it.onActivity {
|
||||||
// Verify no crash
|
// Verify no crash
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,18 +25,16 @@ import org.junit.Test
|
|||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class TabCustomizerActivityTest {
|
class TabCustomizerActivityTest {
|
||||||
|
|
||||||
@get:Rule(order = 0)
|
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||||
val hildAndroidRule = HiltAndroidRule(this)
|
|
||||||
|
|
||||||
@get:Rule(order = 1)
|
@get:Rule(order = 1) val activityRule = activityRule<TabCustomizerActivity>()
|
||||||
val activityRule = activityRule<TabCustomizerActivity>()
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun initializesSuccessfully() {
|
fun initializesSuccessfully() {
|
||||||
activityRule.scenario.use {
|
activityRule.scenario.use {
|
||||||
it.onActivity {
|
it.onActivity {
|
||||||
// Verify no crash
|
// Verify no crash
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,18 +25,16 @@ import org.junit.Test
|
|||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class WebOverlayActivityTest {
|
class WebOverlayActivityTest {
|
||||||
|
|
||||||
@get:Rule(order = 0)
|
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||||
val hildAndroidRule = HiltAndroidRule(this)
|
|
||||||
|
|
||||||
@get:Rule(order = 1)
|
@get:Rule(order = 1) val activityRule = activityRule<AboutActivity>()
|
||||||
val activityRule = activityRule<AboutActivity>()
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun initializesSuccessfully() {
|
fun initializesSuccessfully() {
|
||||||
activityRule.scenario.use {
|
activityRule.scenario.use {
|
||||||
it.onActivity {
|
it.onActivity {
|
||||||
// Verify no crash
|
// Verify no crash
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,29 +20,25 @@ import android.content.Context
|
|||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import kotlin.test.AfterTest
|
import kotlin.test.AfterTest
|
||||||
import kotlin.test.BeforeTest
|
import kotlin.test.BeforeTest
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
abstract class BaseDbTest {
|
abstract class BaseDbTest {
|
||||||
|
|
||||||
protected lateinit var db: FrostDatabase
|
protected lateinit var db: FrostDatabase
|
||||||
|
|
||||||
@BeforeTest
|
@BeforeTest
|
||||||
fun before() {
|
fun before() {
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
val privateDb = Room.inMemoryDatabaseBuilder(
|
val privateDb = Room.inMemoryDatabaseBuilder(context, FrostPrivateDatabase::class.java).build()
|
||||||
context, FrostPrivateDatabase::class.java
|
val publicDb = Room.inMemoryDatabaseBuilder(context, FrostPublicDatabase::class.java).build()
|
||||||
).build()
|
db = FrostDatabase(privateDb, publicDb)
|
||||||
val publicDb = Room.inMemoryDatabaseBuilder(
|
}
|
||||||
context, FrostPublicDatabase::class.java
|
|
||||||
).build()
|
|
||||||
db = FrostDatabase(privateDb, publicDb)
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterTest
|
@AfterTest
|
||||||
fun after() {
|
fun after() {
|
||||||
db.close()
|
db.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,33 +16,35 @@
|
|||||||
*/
|
*/
|
||||||
package com.pitchedapps.frost.db
|
package com.pitchedapps.frost.db
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
import kotlin.test.fail
|
import kotlin.test.fail
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class CacheDbTest : BaseDbTest() {
|
class CacheDbTest : BaseDbTest() {
|
||||||
|
|
||||||
private val dao get() = db.cacheDao()
|
private val dao
|
||||||
private val cookieDao get() = db.cookieDao()
|
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
|
@Test
|
||||||
fun save() {
|
fun save() {
|
||||||
val cookie = cookie(1L)
|
val cookie = cookie(1L)
|
||||||
val type = "test"
|
val type = "test"
|
||||||
val content = "long test".repeat(10000)
|
val content = "long test".repeat(10000)
|
||||||
runBlocking {
|
runBlocking {
|
||||||
cookieDao.save(cookie)
|
cookieDao.save(cookie)
|
||||||
dao.save(cookie.id, type, content)
|
dao.save(cookie.id, type, content)
|
||||||
val cache = dao.select(cookie.id, type) ?: fail("Cache not found")
|
val cache = dao.select(cookie.id, type) ?: fail("Cache not found")
|
||||||
assertEquals(content, cache.contents, "Content mismatch")
|
assertEquals(content, cache.contents, "Content mismatch")
|
||||||
assertTrue(
|
assertTrue(
|
||||||
System.currentTimeMillis() - cache.lastUpdated < 500,
|
System.currentTimeMillis() - cache.lastUpdated < 500,
|
||||||
"Cache retrieval took over 500ms (${System.currentTimeMillis() - cache.lastUpdated})"
|
"Cache retrieval took over 500ms (${System.currentTimeMillis() - cache.lastUpdated})"
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,70 +16,71 @@
|
|||||||
*/
|
*/
|
||||||
package com.pitchedapps.frost.db
|
package com.pitchedapps.frost.db
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertNull
|
import kotlin.test.assertNull
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class CookieDbTest : BaseDbTest() {
|
class CookieDbTest : BaseDbTest() {
|
||||||
|
|
||||||
private val dao get() = db.cookieDao()
|
private val dao
|
||||||
|
get() = db.cookieDao()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun basicCookie() {
|
fun basicCookie() {
|
||||||
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||||
runBlocking {
|
runBlocking {
|
||||||
dao.save(cookie)
|
dao.save(cookie)
|
||||||
val cookies = dao.selectAll()
|
val cookies = dao.selectAll()
|
||||||
assertEquals(listOf(cookie), cookies, "Cookie mismatch")
|
assertEquals(listOf(cookie), cookies, "Cookie mismatch")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun deleteCookie() {
|
fun deleteCookie() {
|
||||||
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
dao.save(cookie)
|
dao.save(cookie)
|
||||||
dao.deleteById(cookie.id + 1)
|
dao.deleteById(cookie.id + 1)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf(cookie),
|
listOf(cookie),
|
||||||
dao.selectAll(),
|
dao.selectAll(),
|
||||||
"Cookie list should be the same after inexistent deletion"
|
"Cookie list should be the same after inexistent deletion"
|
||||||
)
|
)
|
||||||
dao.deleteById(cookie.id)
|
dao.deleteById(cookie.id)
|
||||||
assertEquals(emptyList(), dao.selectAll(), "Cookie list should be empty after deletion")
|
assertEquals(emptyList(), dao.selectAll(), "Cookie list should be empty after deletion")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun insertReplaceCookie() {
|
fun insertReplaceCookie() {
|
||||||
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||||
runBlocking {
|
runBlocking {
|
||||||
dao.save(cookie)
|
dao.save(cookie)
|
||||||
assertEquals(listOf(cookie), dao.selectAll(), "Cookie insertion failed")
|
assertEquals(listOf(cookie), dao.selectAll(), "Cookie insertion failed")
|
||||||
dao.save(cookie.copy(name = "testName2"))
|
dao.save(cookie.copy(name = "testName2"))
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf(cookie.copy(name = "testName2")),
|
listOf(cookie.copy(name = "testName2")),
|
||||||
dao.selectAll(),
|
dao.selectAll(),
|
||||||
"Cookie replacement failed"
|
"Cookie replacement failed"
|
||||||
)
|
)
|
||||||
dao.save(cookie.copy(id = 123L))
|
dao.save(cookie.copy(id = 123L))
|
||||||
assertEquals(
|
assertEquals(
|
||||||
setOf(cookie.copy(id = 123L), cookie.copy(name = "testName2")),
|
setOf(cookie.copy(id = 123L), cookie.copy(name = "testName2")),
|
||||||
dao.selectAll().toSet(),
|
dao.selectAll().toSet(),
|
||||||
"New cookie insertion failed"
|
"New cookie insertion failed"
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun selectCookie() {
|
fun selectCookie() {
|
||||||
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||||
runBlocking {
|
runBlocking {
|
||||||
dao.save(cookie)
|
dao.save(cookie)
|
||||||
assertEquals(cookie, dao.selectById(cookie.id), "Cookie selection failed")
|
assertEquals(cookie, dao.selectById(cookie.id), "Cookie selection failed")
|
||||||
assertNull(dao.selectById(cookie.id + 1), "Inexistent 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.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class CookieMigrationTest {
|
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(
|
val helper: MigrationTestHelper =
|
||||||
InstrumentationRegistry.getInstrumentation(),
|
MigrationTestHelper(
|
||||||
FrostPrivateDatabase::class.java.canonicalName,
|
InstrumentationRegistry.getInstrumentation(),
|
||||||
FrameworkSQLiteOpenHelperFactory()
|
FrostPrivateDatabase::class.java.canonicalName,
|
||||||
|
FrameworkSQLiteOpenHelperFactory()
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun migrateAll() {
|
fun migrateAll() {
|
||||||
// Create earliest version of the database.
|
// Create earliest version of the database.
|
||||||
helper.createDatabase(TEST_DB, 1).apply {
|
helper.createDatabase(TEST_DB, 1).apply { close() }
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open latest version of the database. Room will validate the schema
|
// Open latest version of the database. Room will validate the schema
|
||||||
// once all migrations execute.
|
// once all migrations execute.
|
||||||
Room.databaseBuilder(
|
Room.databaseBuilder(
|
||||||
InstrumentationRegistry.getInstrumentation().targetContext,
|
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
FrostPrivateDatabase::class.java,
|
FrostPrivateDatabase::class.java,
|
||||||
TEST_DB
|
TEST_DB
|
||||||
).addMigrations(*ALL_MIGRATIONS).build().apply {
|
)
|
||||||
openHelper.writableDatabase
|
.addMigrations(*ALL_MIGRATIONS)
|
||||||
close()
|
.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.FbItem
|
||||||
import com.pitchedapps.frost.facebook.defaultTabs
|
import com.pitchedapps.frost.facebook.defaultTabs
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class GenericDbTest : BaseDbTest() {
|
class GenericDbTest : BaseDbTest() {
|
||||||
|
|
||||||
private val dao get() = db.genericDao()
|
private val dao
|
||||||
|
get() = db.genericDao()
|
||||||
|
|
||||||
/**
|
/** Note that order is also preserved here */
|
||||||
* Note that order is also preserved here
|
@Test
|
||||||
*/
|
fun save() {
|
||||||
@Test
|
val tabs =
|
||||||
fun save() {
|
listOf(
|
||||||
val tabs = listOf(
|
FbItem.ACTIVITY_LOG,
|
||||||
FbItem.ACTIVITY_LOG,
|
FbItem.BIRTHDAYS,
|
||||||
FbItem.BIRTHDAYS,
|
FbItem.EVENTS,
|
||||||
FbItem.EVENTS,
|
FbItem.MARKETPLACE,
|
||||||
FbItem.MARKETPLACE,
|
FbItem.ACTIVITY_LOG
|
||||||
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(
|
||||||
assertEquals(tabs, dao.getTabs(), "Tab saving failed")
|
listOf(FbItem.ACTIVITY_LOG, FbItem.EVENTS),
|
||||||
val newTabs = listOf(FbItem.PAGES, FbItem.MENU)
|
dao.getTabs(),
|
||||||
dao.saveTabs(newTabs)
|
"Tab fetching does not ignore unknown names"
|
||||||
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"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,139 +19,134 @@ package com.pitchedapps.frost.db
|
|||||||
import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL
|
import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL
|
||||||
import com.pitchedapps.frost.services.NOTIF_CHANNEL_MESSAGES
|
import com.pitchedapps.frost.services.NOTIF_CHANNEL_MESSAGES
|
||||||
import com.pitchedapps.frost.services.NotificationContent
|
import com.pitchedapps.frost.services.NotificationContent
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class NotificationDbTest : BaseDbTest() {
|
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(
|
private fun notifContent(id: Long, cookie: CookieEntity, time: Long = id) =
|
||||||
data = cookie,
|
NotificationContent(
|
||||||
id = id,
|
data = cookie,
|
||||||
href = "",
|
id = id,
|
||||||
title = null,
|
href = "",
|
||||||
text = "",
|
title = null,
|
||||||
timestamp = time,
|
text = "",
|
||||||
profileUrl = null,
|
timestamp = time,
|
||||||
unread = true
|
profileUrl = null,
|
||||||
|
unread = true
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun saveAndRetrieve() {
|
fun saveAndRetrieve() {
|
||||||
val cookie = cookie(12345L)
|
val cookie = cookie(12345L)
|
||||||
// Unique unsorted ids
|
// Unique unsorted ids
|
||||||
val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
|
val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
|
||||||
runBlocking {
|
runBlocking {
|
||||||
db.cookieDao().save(cookie)
|
db.cookieDao().save(cookie)
|
||||||
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
|
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
|
||||||
val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL)
|
val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
notifs.sortedByDescending { it.timestamp },
|
notifs.sortedByDescending { it.timestamp },
|
||||||
dbNotifs,
|
dbNotifs,
|
||||||
"Incorrect notification list received"
|
"Incorrect notification list received"
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun selectConditions() {
|
fun selectConditions() {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val cookie1 = cookie(12345L)
|
val cookie1 = cookie(12345L)
|
||||||
val cookie2 = cookie(12L)
|
val cookie2 = cookie(12L)
|
||||||
val notifs1 = (0L..2L).map { notifContent(it, cookie1) }
|
val notifs1 = (0L..2L).map { notifContent(it, cookie1) }
|
||||||
val notifs2 = (5L..10L).map { notifContent(it, cookie2) }
|
val notifs2 = (5L..10L).map { notifContent(it, cookie2) }
|
||||||
db.cookieDao().save(cookie1)
|
db.cookieDao().save(cookie1)
|
||||||
db.cookieDao().save(cookie2)
|
db.cookieDao().save(cookie2)
|
||||||
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1)
|
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1)
|
||||||
dao.saveNotifications(NOTIF_CHANNEL_MESSAGES, notifs2)
|
dao.saveNotifications(NOTIF_CHANNEL_MESSAGES, notifs2)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
emptyList(),
|
emptyList(),
|
||||||
dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_MESSAGES),
|
dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_MESSAGES),
|
||||||
"Filtering by type did not work for cookie1"
|
"Filtering by type did not work for cookie1"
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
notifs1.sortedByDescending { it.timestamp },
|
notifs1.sortedByDescending { it.timestamp },
|
||||||
dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_GENERAL),
|
dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_GENERAL),
|
||||||
"Selection for cookie1 failed"
|
"Selection for cookie1 failed"
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
emptyList(),
|
emptyList(),
|
||||||
dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_GENERAL),
|
dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_GENERAL),
|
||||||
"Filtering by type did not work for cookie2"
|
"Filtering by type did not work for cookie2"
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
notifs2.sortedByDescending { it.timestamp },
|
notifs2.sortedByDescending { it.timestamp },
|
||||||
dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_MESSAGES),
|
dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_MESSAGES),
|
||||||
"Selection for cookie2 failed"
|
"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
|
* Primary key is both id and userId, in the event that the same notification to multiple users
|
||||||
*/
|
* has the same id
|
||||||
@Test
|
*/
|
||||||
fun primaryKeyCheck() {
|
@Test
|
||||||
runBlocking {
|
fun primaryKeyCheck() {
|
||||||
val cookie1 = cookie(12345L)
|
runBlocking {
|
||||||
val cookie2 = cookie(12L)
|
val cookie1 = cookie(12345L)
|
||||||
val notifs1 = (0L..2L).map { notifContent(it, cookie1) }
|
val cookie2 = cookie(12L)
|
||||||
val notifs2 = notifs1.map { it.copy(data = cookie2) }
|
val notifs1 = (0L..2L).map { notifContent(it, cookie1) }
|
||||||
db.cookieDao().save(cookie1)
|
val notifs2 = notifs1.map { it.copy(data = cookie2) }
|
||||||
db.cookieDao().save(cookie2)
|
db.cookieDao().save(cookie1)
|
||||||
assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1), "Notif1 save failed")
|
db.cookieDao().save(cookie2)
|
||||||
assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs2), "Notif2 save failed")
|
assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1), "Notif1 save failed")
|
||||||
}
|
assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs2), "Notif2 save failed")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun cascadeDeletion() {
|
fun cascadeDeletion() {
|
||||||
val cookie = cookie(12345L)
|
val cookie = cookie(12345L)
|
||||||
// Unique unsorted ids
|
// Unique unsorted ids
|
||||||
val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
|
val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
|
||||||
runBlocking {
|
runBlocking {
|
||||||
db.cookieDao().save(cookie)
|
db.cookieDao().save(cookie)
|
||||||
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
|
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
|
||||||
db.cookieDao().deleteById(cookie.id)
|
db.cookieDao().deleteById(cookie.id)
|
||||||
val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL)
|
val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL)
|
||||||
assertTrue(dbNotifs.isEmpty(), "Cascade deletion failed")
|
assertTrue(dbNotifs.isEmpty(), "Cascade deletion failed")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun latestEpoch() {
|
fun latestEpoch() {
|
||||||
val cookie = cookie(12345L)
|
val cookie = cookie(12345L)
|
||||||
// Unique unsorted ids
|
// Unique unsorted ids
|
||||||
val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
|
val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
|
||||||
runBlocking {
|
runBlocking {
|
||||||
assertEquals(
|
assertEquals(-1L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Default epoch failed")
|
||||||
-1L,
|
db.cookieDao().save(cookie)
|
||||||
dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL),
|
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
|
||||||
"Default epoch failed"
|
assertEquals(99L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Latest 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
|
@Test
|
||||||
fun insertionWithInvalidCookies() {
|
fun insertionWithInvalidCookies() {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
assertFalse(
|
assertFalse(
|
||||||
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, listOf(notifContent(1L, cookie(2L)))),
|
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, listOf(notifContent(1L, cookie(2L)))),
|
||||||
"Notif save should not have passed without relevant cookie entries"
|
"Notif save should not have passed without relevant cookie entries"
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,16 +17,16 @@
|
|||||||
package com.pitchedapps.frost.facebook
|
package com.pitchedapps.frost.facebook
|
||||||
|
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import org.junit.Test
|
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
class FbCookieTest {
|
class FbCookieTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun managerAcceptsCookie() {
|
fun managerAcceptsCookie() {
|
||||||
assertTrue(
|
assertTrue(
|
||||||
CookieManager.getInstance().acceptCookie(),
|
CookieManager.getInstance().acceptCookie(),
|
||||||
"Cookie manager should accept cookie by default"
|
"Cookie manager should accept cookie by default"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,23 +26,21 @@ import androidx.test.platform.app.InstrumentationRegistry
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
val context: Context
|
val context: Context
|
||||||
get() = InstrumentationRegistry.getInstrumentation().targetContext
|
get() = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
|
||||||
fun getAsset(asset: String): InputStream =
|
fun getAsset(asset: String): InputStream = context.assets.open(asset)
|
||||||
context.assets.open(asset)
|
|
||||||
|
|
||||||
private class Helper
|
private class Helper
|
||||||
|
|
||||||
fun getResource(resource: String): InputStream =
|
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(
|
inline fun <reified A : Activity> activityRule(
|
||||||
intentAction: Intent.() -> Unit = {},
|
intentAction: Intent.() -> Unit = {},
|
||||||
activityOptions: Bundle? = null
|
activityOptions: Bundle? = null
|
||||||
): ActivityScenarioRule<A> {
|
): ActivityScenarioRule<A> {
|
||||||
val intent =
|
val intent = Intent(ApplicationProvider.getApplicationContext(), A::class.java).also(intentAction)
|
||||||
Intent(ApplicationProvider.getApplicationContext(), A::class.java).also(intentAction)
|
return ActivityScenarioRule(intent, activityOptions)
|
||||||
return ActivityScenarioRule(intent, activityOptions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const val TEST_FORMATTED_URL = "https://www.google.com"
|
const val TEST_FORMATTED_URL = "https://www.google.com"
|
||||||
|
@ -37,78 +37,75 @@ import dagger.hilt.android.HiltAndroidApp
|
|||||||
import java.util.Random
|
import java.util.Random
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/** Created by Allan Wang on 2017-05-28. */
|
||||||
* Created by Allan Wang on 2017-05-28.
|
|
||||||
*/
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class FrostApp : Application() {
|
class FrostApp : Application() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var prefs: Prefs
|
||||||
lateinit var prefs: Prefs
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var themeProvider: ThemeProvider
|
||||||
lateinit var themeProvider: ThemeProvider
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var cookieDao: CookieDao
|
||||||
lateinit var cookieDao: CookieDao
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var notifDao: NotificationDao
|
||||||
lateinit var notifDao: NotificationDao
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
if (!buildIsLollipopAndUp) return // not supported
|
if (!buildIsLollipopAndUp) return // not supported
|
||||||
|
|
||||||
initPrefs()
|
initPrefs()
|
||||||
|
|
||||||
L.i { "Begin Frost for Facebook" }
|
L.i { "Begin Frost for Facebook" }
|
||||||
FrostPglAdBlock.init(this)
|
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) {
|
if (BuildConfig.DEBUG) {
|
||||||
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
|
registerActivityLifecycleCallbacks(
|
||||||
override fun onActivityPaused(activity: Activity) {}
|
object : ActivityLifecycleCallbacks {
|
||||||
override fun onActivityResumed(activity: Activity) {}
|
override fun onActivityPaused(activity: Activity) {}
|
||||||
override fun onActivityStarted(activity: Activity) {}
|
override fun onActivityResumed(activity: Activity) {}
|
||||||
|
override fun onActivityStarted(activity: Activity) {}
|
||||||
|
|
||||||
override fun onActivityDestroyed(activity: Activity) {
|
override fun onActivityDestroyed(activity: Activity) {
|
||||||
L.d { "Activity ${activity.localClassName} destroyed" }
|
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?) {
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||||
L.d { "Activity ${activity.localClassName} created" }
|
L.d { "Activity ${activity.localClassName} created" }
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun initPrefs() {
|
private fun initPrefs() {
|
||||||
prefs.deleteKeys("search_bar", "shown_release", "experimental_by_default")
|
prefs.deleteKeys("search_bar", "shown_release", "experimental_by_default")
|
||||||
KL.shouldLog = { BuildConfig.DEBUG }
|
KL.shouldLog = { BuildConfig.DEBUG }
|
||||||
L.shouldLog = {
|
L.shouldLog = {
|
||||||
when (it) {
|
when (it) {
|
||||||
Log.VERBOSE -> BuildConfig.DEBUG
|
Log.VERBOSE -> BuildConfig.DEBUG
|
||||||
Log.INFO, Log.ERROR -> true
|
Log.INFO,
|
||||||
else -> BuildConfig.DEBUG || prefs.verboseLogging
|
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()
|
|
||||||
}
|
}
|
||||||
|
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.launchNewTask
|
||||||
import com.pitchedapps.frost.utils.loadAssets
|
import com.pitchedapps.frost.utils.loadAssets
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
import javax.inject.Inject
|
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
|
@AndroidEntryPoint
|
||||||
class StartActivity : KauBaseActivity() {
|
class StartActivity : KauBaseActivity() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var fbCookie: FbCookie
|
||||||
lateinit var fbCookie: FbCookie
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var prefs: Prefs
|
||||||
lateinit var prefs: Prefs
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var themeProvider: ThemeProvider
|
||||||
lateinit var themeProvider: ThemeProvider
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var cookieDao: CookieDao
|
||||||
lateinit var cookieDao: CookieDao
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var genericDao: GenericDao
|
||||||
lateinit var genericDao: GenericDao
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (!buildIsLollipopAndUp) { // not supported
|
if (!buildIsLollipopAndUp) { // not supported
|
||||||
showInvalidSdkView()
|
showInvalidSdkView()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO add better descriptions
|
// TODO add better descriptions
|
||||||
CookieManager.getInstance()
|
CookieManager.getInstance()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
L.e(e) { "No cookiemanager instance" }
|
L.e(e) { "No cookiemanager instance" }
|
||||||
showInvalidWebView()
|
showInvalidWebView()
|
||||||
}
|
}
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
try {
|
try {
|
||||||
val authDefer = BiometricUtils.authenticate(this@StartActivity, prefs)
|
val authDefer = BiometricUtils.authenticate(this@StartActivity, prefs)
|
||||||
fbCookie.switchBackUser()
|
fbCookie.switchBackUser()
|
||||||
val cookies = ArrayList(cookieDao.selectAll())
|
val cookies = ArrayList(cookieDao.selectAll())
|
||||||
L.i { "Cookies loaded at time ${System.currentTimeMillis()}" }
|
L.i { "Cookies loaded at time ${System.currentTimeMillis()}" }
|
||||||
L._d {
|
L._d {
|
||||||
"Cookies: ${
|
"Cookies: ${
|
||||||
cookies.joinToString(
|
cookies.joinToString(
|
||||||
"\t",
|
"\t",
|
||||||
transform = CookieEntity::toSensitiveString
|
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() =
|
private fun showInvalidWebView() = showInvalidView(R.string.error_webview)
|
||||||
showInvalidView(R.string.error_webview)
|
|
||||||
|
|
||||||
private fun showInvalidSdkView() {
|
private fun showInvalidSdkView() {
|
||||||
val text = String.format(string(R.string.error_sdk), Build.VERSION.SDK_INT)
|
val text = String.format(string(R.string.error_sdk), Build.VERSION.SDK_INT)
|
||||||
showInvalidView(text)
|
showInvalidView(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showInvalidView(textRes: Int) =
|
private fun showInvalidView(textRes: Int) = showInvalidView(string(textRes))
|
||||||
showInvalidView(string(textRes))
|
|
||||||
|
|
||||||
private fun showInvalidView(text: String) {
|
private fun showInvalidView(text: String) {
|
||||||
setContentView(R.layout.activity_invalid)
|
setContentView(R.layout.activity_invalid)
|
||||||
findViewById<ImageView>(R.id.invalid_icon)
|
findViewById<ImageView>(R.id.invalid_icon).setIcon(GoogleMaterial.Icon.gmd_adb, -1, Color.WHITE)
|
||||||
.setIcon(GoogleMaterial.Icon.gmd_adb, -1, Color.WHITE)
|
findViewById<TextView>(R.id.invalid_text).text = text
|
||||||
findViewById<TextView>(R.id.invalid_text).text = text
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -52,154 +52,140 @@ import com.pitchedapps.frost.utils.L
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/** Created by Allan Wang on 2017-06-26. */
|
||||||
* Created by Allan Wang on 2017-06-26.
|
|
||||||
*/
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AboutActivity : AboutActivityBase(null) {
|
class AboutActivity : AboutActivityBase(null) {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var prefs: Prefs
|
||||||
lateinit var prefs: Prefs
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var themeProvider: ThemeProvider
|
||||||
lateinit var themeProvider: ThemeProvider
|
|
||||||
|
|
||||||
override fun Configs.buildConfigs() {
|
override fun Configs.buildConfigs() {
|
||||||
textColor = themeProvider.textColor
|
textColor = themeProvider.textColor
|
||||||
accentColor = themeProvider.accentColor
|
accentColor = themeProvider.accentColor
|
||||||
backgroundColor = themeProvider.bgColor.withMinAlpha(200)
|
backgroundColor = themeProvider.bgColor.withMinAlpha(200)
|
||||||
cutoutForeground = themeProvider.accentColor
|
cutoutForeground = themeProvider.accentColor
|
||||||
cutoutDrawableRes = R.drawable.frost_f_200
|
cutoutDrawableRes = R.drawable.frost_f_200
|
||||||
faqPageTitleRes = R.string.faq_title
|
faqPageTitleRes = R.string.faq_title
|
||||||
faqXmlRes = R.xml.frost_faq
|
faqXmlRes = R.xml.frost_faq
|
||||||
faqParseNewLine = false
|
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
|
class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
|
||||||
var clickCount = 0
|
|
||||||
|
|
||||||
override fun postInflateMainPage(adapter: FastItemThemedAdapter<GenericItem>) {
|
val container: ConstraintLayout by bindView(R.id.about_icons_container)
|
||||||
/**
|
val images: List<ImageView>
|
||||||
* Frost may not be a library but we're conveying the same info
|
|
||||||
*/
|
/**
|
||||||
val frost = Library(
|
* There are a lot of constraints to be added to each item just to have them chained properly
|
||||||
uniqueId = "com.pitchedapps.frost",
|
* My as well do it programmatically Initializing the viewholder will setup the icons, scale
|
||||||
name = string(R.string.frost_name),
|
* type and background of all icons, link their click listeners and chain them together via a
|
||||||
developers = listOf(
|
* horizontal spread
|
||||||
Developer(name = string(R.string.dev_name), organisationUrl = null)
|
*/
|
||||||
),
|
init {
|
||||||
website = string(R.string.github_url),
|
val c = itemView.context
|
||||||
description = string(R.string.frost_description),
|
val size = c.dimenPixelSize(R.dimen.kau_avatar_bounds)
|
||||||
artifactVersion = BuildConfig.VERSION_NAME,
|
|
||||||
licenses = setOf(
|
val icons: Array<Pair<Int, () -> Unit>> =
|
||||||
License(
|
arrayOf(R.drawable.ic_fdroid_24 to { c.startLink(R.string.fdroid_url) })
|
||||||
spdxId = "gplv3",
|
val iicons: Array<Pair<IIcon, () -> Unit>> =
|
||||||
name = "GNU GPL v3",
|
arrayOf(
|
||||||
url = "https://www.gnu.org/licenses/gpl-3.0.en.html",
|
GoogleMaterial.Icon.gmd_file_download to { c.startLink(R.string.github_downloads_url) },
|
||||||
hash = "gplv3"
|
CommunityMaterial.Icon3.cmd_reddit to { c.startLink(R.string.reddit_url) },
|
||||||
)
|
CommunityMaterial.Icon2.cmd_github to { c.startLink(R.string.github_url) }
|
||||||
),
|
)
|
||||||
scm = null,
|
|
||||||
organization = null,
|
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())
|
set.applyTo(container)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,48 +28,40 @@ import com.pitchedapps.frost.utils.ActivityThemer
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/** Created by Allan Wang on 2017-06-12. */
|
||||||
* Created by Allan Wang on 2017-06-12.
|
|
||||||
*/
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
abstract class BaseActivity : KauBaseActivity() {
|
abstract class BaseActivity : KauBaseActivity() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var fbCookie: FbCookie
|
||||||
lateinit var fbCookie: FbCookie
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var prefs: Prefs
|
||||||
lateinit var prefs: Prefs
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var themeProvider: ThemeProvider
|
||||||
lateinit var themeProvider: ThemeProvider
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var activityThemer: ActivityThemer
|
||||||
lateinit var activityThemer: ActivityThemer
|
|
||||||
|
|
||||||
/**
|
/** Inherited consumer to customize back press */
|
||||||
* Inherited consumer to customize back press
|
protected open fun backConsumer(): Boolean = false
|
||||||
*/
|
|
||||||
protected open fun backConsumer(): Boolean = false
|
|
||||||
|
|
||||||
final override fun onBackPressed() {
|
final override fun onBackPressed() {
|
||||||
if (this is SearchViewHolder && searchViewOnBackPress()) return
|
if (this is SearchViewHolder && searchViewOnBackPress()) return
|
||||||
if (this is VideoViewHolder && videoOnBackPress()) return
|
if (this is VideoViewHolder && videoOnBackPress()) return
|
||||||
if (backConsumer()) return
|
if (backConsumer()) return
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
if (this !is WebOverlayActivityBase) activityThemer.setFrostTheme()
|
if (this !is WebOverlayActivityBase) activityThemer.setFrostTheme()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
if (this is VideoViewHolder) videoOnStop()
|
if (this is VideoViewHolder) videoOnStop()
|
||||||
super.onStop()
|
super.onStop()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
if (this is VideoViewHolder) videoViewer?.updateLocation()
|
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.L
|
||||||
import com.pitchedapps.frost.utils.createFreshDir
|
import com.pitchedapps.frost.utils.createFreshDir
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
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
|
@AndroidEntryPoint
|
||||||
class DebugActivity : KauBaseActivity() {
|
class DebugActivity : KauBaseActivity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val RESULT_URL = "extra_result_url"
|
const val RESULT_URL = "extra_result_url"
|
||||||
const val RESULT_SCREENSHOT = "extra_result_screenshot"
|
const val RESULT_SCREENSHOT = "extra_result_screenshot"
|
||||||
const val RESULT_BODY = "extra_result_body"
|
const val RESULT_BODY = "extra_result_body"
|
||||||
fun baseDir(context: Context) = File(context.externalCacheDir, "offline_debug")
|
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
|
activityThemer.setFrostColors { toolbar(toolbar) }
|
||||||
lateinit var activityThemer: ActivityThemer
|
debugWebview.loadUrl(FbItem.FEED.url)
|
||||||
|
debugWebview.onPageFinished = { swipeRefresh.isRefreshing = false }
|
||||||
|
|
||||||
@Inject
|
swipeRefresh.setOnRefreshListener(debugWebview::reload)
|
||||||
lateinit var themeProvider: ThemeProvider
|
|
||||||
|
|
||||||
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?) {
|
val errorHandler = CoroutineExceptionHandler { _, throwable ->
|
||||||
super.onCreate(savedInstanceState)
|
L.e { "DebugActivity error ${throwable.message}" }
|
||||||
binding = ActivityDebugBinding.inflate(layoutInflater)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
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 {
|
|
||||||
finish()
|
finish()
|
||||||
return true
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
launchMain(errorHandler) {
|
||||||
super.onResume()
|
val parent = baseDir(this@DebugActivity)
|
||||||
binding.debugWebview.resumeTimers()
|
parent.createFreshDir()
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
val body: String? = suspendCoroutine { cont ->
|
||||||
binding.debugWebview.pauseTimers()
|
debugWebview.evaluateJavascript(JsActions.RETURN_BODY.function) { cont.resume(it) }
|
||||||
super.onPause()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
val hasScreenshot: Boolean = debugWebview.getScreenshot(File(parent, "screenshot.png"))
|
||||||
if (binding.debugWebview.canGoBack())
|
|
||||||
binding.debugWebview.goBack()
|
val intent = Intent()
|
||||||
else
|
intent.putExtra(RESULT_URL, debugWebview.url)
|
||||||
super.onBackPressed()
|
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.isIndirectImageUrl
|
||||||
import com.pitchedapps.frost.utils.logFrostEvent
|
import com.pitchedapps.frost.utils.logFrostEvent
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
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
|
@AndroidEntryPoint
|
||||||
class ImageActivity : KauBaseActivity() {
|
class ImageActivity : KauBaseActivity() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var activityThemer: ActivityThemer
|
||||||
lateinit var activityThemer: ActivityThemer
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var prefs: Prefs
|
||||||
lateinit var prefs: Prefs
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var themeProvider: ThemeProvider
|
||||||
lateinit var themeProvider: ThemeProvider
|
|
||||||
|
|
||||||
@Volatile
|
@Volatile internal var errorRef: Throwable? = null
|
||||||
internal var errorRef: Throwable? = null
|
|
||||||
|
|
||||||
/**
|
/** Reference to the temporary file path */
|
||||||
* Reference to the temporary file path
|
internal val tempFile: File?
|
||||||
*/
|
get() = binding.imagePhoto.currentImageFile
|
||||||
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
|
lateinit var binding: ActivityImageBinding
|
||||||
private var bottomBehavior: BottomSheetBehavior<View>? = null
|
private var bottomBehavior: BottomSheetBehavior<View>? = null
|
||||||
|
|
||||||
private val baseBackgroundColor: Int
|
private val baseBackgroundColor: Int
|
||||||
get() = if (prefs.blackMediaBg) Color.BLACK
|
get() = if (prefs.blackMediaBg) Color.BLACK else themeProvider.bgColor.withMinAlpha(235)
|
||||||
else themeProvider.bgColor.withMinAlpha(235)
|
|
||||||
|
|
||||||
private fun loadError(e: Throwable) {
|
private fun loadError(e: Throwable) {
|
||||||
if (e.message?.contains("<!DOCTYPE html>") == true) {
|
if (e.message?.contains("<!DOCTYPE html>") == true) {
|
||||||
applicationContext.toast(R.string.image_not_found)
|
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()
|
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?) {
|
override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
|
||||||
super.onCreate(savedInstanceState)
|
val overScrolled = scrollPercent > scrollThreshold
|
||||||
if (imageUrl.isEmpty()) {
|
val maxOffset = releasedChild.height + 10
|
||||||
return finish()
|
val finalTop =
|
||||||
}
|
when {
|
||||||
L.i { "Displaying image" }
|
scrollToTop && (overScrolled || yvel < -dragHelper.minVelocity) -> -maxOffset
|
||||||
trueImageUrl = async(Dispatchers.IO) {
|
!scrollToTop && (overScrolled || yvel > dragHelper.minVelocity) -> maxOffset
|
||||||
val result = if (!imageUrl.isIndirectImageUrl) imageUrl
|
else -> 0
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
dragHelper.settleCapturedViewAt(0, finalTop)
|
||||||
|
binding.imageDrag.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ActivityImageBinding.showImage(url: String) {
|
override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int = 0
|
||||||
imagePhoto.showImage(Uri.parse(url))
|
|
||||||
imagePhoto.setImageShownCallback(object : ImageShownCallback {
|
|
||||||
override fun onThumbnailShown() {}
|
|
||||||
|
|
||||||
override fun onMainImageShown() {
|
override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int = top
|
||||||
imageProgress.fadeOut()
|
}
|
||||||
imagePhoto.animate().alpha(1f).scaleXY(1f).start()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ActivityImageBinding.init() {
|
internal suspend fun saveImage() {
|
||||||
imageContainer.setBackgroundColor(baseBackgroundColor)
|
frostDownload(cookie = cookie, url = trueImageUrl.await())
|
||||||
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) {
|
companion object {
|
||||||
// No op
|
private val L = KauLoggerExtension("Image", com.pitchedapps.frost.utils.L)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal enum class FabStates(
|
internal enum class FabStates(
|
||||||
val iicon: IIcon,
|
val iicon: IIcon,
|
||||||
val iconColorProvider: (ThemeProvider) -> Int = { it.iconColor },
|
val iconColorProvider: (ThemeProvider) -> Int = { it.iconColor },
|
||||||
val backgroundTint: Int = Int.MAX_VALUE
|
val backgroundTint: Int = Int.MAX_VALUE
|
||||||
) {
|
) {
|
||||||
ERROR(GoogleMaterial.Icon.gmd_error, { Color.WHITE }, Color.RED) {
|
ERROR(GoogleMaterial.Icon.gmd_error, { Color.WHITE }, Color.RED) {
|
||||||
override fun onClick(activity: ImageActivity) {
|
override fun onClick(activity: ImageActivity) {
|
||||||
val err =
|
val err =
|
||||||
activity.errorRef?.takeIf { it !is FileNotFoundException && it.message != "Image failed to decode using JPEG decoder" }
|
activity.errorRef?.takeIf {
|
||||||
?: return
|
it !is FileNotFoundException && it.message != "Image failed to decode using JPEG decoder"
|
||||||
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()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
?: 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.utils.loadAssets
|
||||||
import com.pitchedapps.frost.widgets.NotificationWidget
|
import com.pitchedapps.frost.widgets.NotificationWidget
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 2017-07-25.
|
* Created by Allan Wang on 2017-07-25.
|
||||||
*
|
*
|
||||||
* A beautiful intro activity
|
* A beautiful intro activity Phone showcases are drawn via layers
|
||||||
* Phone showcases are drawn via layers
|
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class IntroActivity :
|
class IntroActivity : KauBaseActivity(), ViewPager.PageTransformer, ViewPager.OnPageChangeListener {
|
||||||
KauBaseActivity(),
|
|
||||||
ViewPager.PageTransformer,
|
|
||||||
ViewPager.OnPageChangeListener {
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var prefs: Prefs
|
||||||
lateinit var prefs: Prefs
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var themeProvider: ThemeProvider
|
||||||
lateinit var themeProvider: ThemeProvider
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var activityThemer: ActivityThemer
|
||||||
lateinit var activityThemer: ActivityThemer
|
|
||||||
|
|
||||||
lateinit var binding: ActivityIntroBinding
|
lateinit var binding: ActivityIntroBinding
|
||||||
private var barHasNext = true
|
private var barHasNext = true
|
||||||
|
|
||||||
private val fragments by lazyUi {
|
private val fragments by lazyUi {
|
||||||
listOf(
|
listOf(
|
||||||
IntroFragmentWelcome(),
|
IntroFragmentWelcome(),
|
||||||
IntroFragmentTheme(),
|
IntroFragmentTheme(),
|
||||||
IntroAccountFragment(),
|
IntroAccountFragment(),
|
||||||
IntroTabTouchFragment(),
|
IntroTabTouchFragment(),
|
||||||
IntroTabContextFragment(),
|
IntroTabContextFragment(),
|
||||||
IntroFragmentEnd()
|
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)
|
||||||
}
|
}
|
||||||
|
indicator.setViewPager(viewpager)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
next.setIcon(GoogleMaterial.Icon.gmd_navigate_next)
|
||||||
super.onCreate(savedInstanceState)
|
next.setOnClickListener {
|
||||||
binding = ActivityIntroBinding.inflate(layoutInflater)
|
if (barHasNext) viewpager.setCurrentItem(viewpager.currentItem + 1, true)
|
||||||
setContentView(binding.root)
|
else finish(next.x + next.pivotX, next.y + next.pivotY)
|
||||||
binding.init()
|
|
||||||
}
|
}
|
||||||
|
skip.setOnClickListener { finish() }
|
||||||
|
ripple.set(themeProvider.bgColor)
|
||||||
|
theme()
|
||||||
|
}
|
||||||
|
|
||||||
private fun ActivityIntroBinding.init() {
|
fun theme() {
|
||||||
viewpager.apply {
|
statusBarColor = themeProvider.headerColor
|
||||||
setPageTransformer(true, this@IntroActivity)
|
navigationBarColor = themeProvider.headerColor
|
||||||
addOnPageChangeListener(this@IntroActivity)
|
with(binding) {
|
||||||
adapter = IntroPageAdapter(supportFragmentManager, fragments)
|
skip.setTextColor(themeProvider.textColor)
|
||||||
}
|
next.imageTintList = ColorStateList.valueOf(themeProvider.textColor)
|
||||||
indicator.setViewPager(viewpager)
|
indicator.setColour(themeProvider.textColor)
|
||||||
next.setIcon(GoogleMaterial.Icon.gmd_navigate_next)
|
indicator.invalidate()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
fragments.forEach { it.themeFragment() }
|
||||||
|
activityThemer.setFrostTheme(forceTransparent = true)
|
||||||
|
}
|
||||||
|
|
||||||
fun theme() {
|
/**
|
||||||
statusBarColor = themeProvider.headerColor
|
* Transformations are mainly handled on a per view basis This makes the first fragment fade out
|
||||||
navigationBarColor = themeProvider.headerColor
|
* as the second fragment comes in All fragments are locked in position
|
||||||
with(binding) {
|
*/
|
||||||
skip.setTextColor(themeProvider.textColor)
|
override fun transformPage(page: View, position: Float) {
|
||||||
next.imageTintList = ColorStateList.valueOf(themeProvider.textColor)
|
// only apply to adjacent pages
|
||||||
indicator.setColour(themeProvider.textColor)
|
if ((position < 0 && position > -1) || (position > 0 && position < 1)) {
|
||||||
indicator.invalidate()
|
val pageWidth = page.width
|
||||||
}
|
val translateValue = position * -pageWidth
|
||||||
fragments.forEach { it.themeFragment() }
|
page.translationX = (if (translateValue > -pageWidth) translateValue else 0f)
|
||||||
activityThemer.setFrostTheme(forceTransparent = true)
|
page.alpha = if (position < 0) 1 + position else 1f
|
||||||
|
} else {
|
||||||
|
page.alpha = 1f
|
||||||
|
page.translationX = 0f
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
fun finish(x: Float, y: Float) {
|
||||||
* Transformations are mainly handled on a per view basis
|
val blue = color(R.color.facebook_blue)
|
||||||
* This makes the first fragment fade out as the second fragment comes in
|
window.setFlags(
|
||||||
* All fragments are locked in position
|
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
||||||
*/
|
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||||
override fun transformPage(page: View, position: Float) {
|
)
|
||||||
// only apply to adjacent pages
|
binding.ripple.ripple(blue, x, y, 600) { postDelayed(1000) { finish() } }
|
||||||
if ((position < 0 && position > -1) || (position > 0 && position < 1)) {
|
val lastView: View? = fragments.last().view
|
||||||
val pageWidth = page.width
|
arrayOf<View?>(
|
||||||
val translateValue = position * -pageWidth
|
binding.skip,
|
||||||
page.translationX = (if (translateValue > -pageWidth) translateValue else 0f)
|
binding.indicator,
|
||||||
page.alpha = if (position < 0) 1 + position else 1f
|
binding.next,
|
||||||
} else {
|
lastView?.findViewById(R.id.intro_title),
|
||||||
page.alpha = 1f
|
lastView?.findViewById(R.id.intro_desc)
|
||||||
page.translationX = 0f
|
)
|
||||||
|
.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) {
|
||||||
fun finish(x: Float, y: Float) {
|
ValueAnimator.ofFloat(0f, 1f).apply {
|
||||||
val blue = color(R.color.facebook_blue)
|
addUpdateListener {
|
||||||
window.setFlags(
|
val c = themeProvider.headerColor.blendWith(blue, it.animatedValue as Float)
|
||||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
statusBarColor = c
|
||||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
navigationBarColor = c
|
||||||
)
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
duration = 600
|
||||||
|
start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
launch(NonCancellable) {
|
launch(NonCancellable) {
|
||||||
loadAssets(themeProvider)
|
loadAssets(themeProvider)
|
||||||
NotificationWidget.forceUpdate(this@IntroActivity)
|
NotificationWidget.forceUpdate(this@IntroActivity)
|
||||||
launchNewTask<MainActivity>(cookies(), false)
|
launchNewTask<MainActivity>(cookies(), false)
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
with(binding) {
|
with(binding) {
|
||||||
if (viewpager.currentItem > 0) viewpager.setCurrentItem(viewpager.currentItem - 1, true)
|
if (viewpager.currentItem > 0) viewpager.setCurrentItem(viewpager.currentItem - 1, true)
|
||||||
else finish()
|
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) {
|
class IntroPageAdapter(fm: FragmentManager, private val fragments: List<BaseIntroFragment>) :
|
||||||
fragments[position].onPageScrolled(positionOffset)
|
FragmentPagerAdapter(fm) {
|
||||||
if (position + 1 < fragments.size)
|
|
||||||
fragments[position + 1].onPageScrolled(positionOffset - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageSelected(position: Int) {
|
override fun getItem(position: Int): Fragment = fragments[position]
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
class IntroPageAdapter(fm: FragmentManager, private val fragments: List<BaseIntroFragment>) :
|
override fun getCount(): Int = fragments.size
|
||||||
FragmentPagerAdapter(fm) {
|
}
|
||||||
|
|
||||||
override fun getItem(position: Int): Fragment = fragments[position]
|
|
||||||
|
|
||||||
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.LoginWebView
|
||||||
import com.pitchedapps.frost.web.asFrostEmitter
|
import com.pitchedapps.frost.web.asFrostEmitter
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.resume
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
@ -63,165 +66,157 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeout
|
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
|
@AndroidEntryPoint
|
||||||
class LoginActivity : BaseActivity() {
|
class LoginActivity : BaseActivity() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var cookieDao: CookieDao
|
||||||
lateinit var cookieDao: CookieDao
|
|
||||||
|
|
||||||
private val toolbar: Toolbar by bindView(R.id.toolbar)
|
private val toolbar: Toolbar by bindView(R.id.toolbar)
|
||||||
private val web: LoginWebView by bindView(R.id.login_webview)
|
private val web: LoginWebView by bindView(R.id.login_webview)
|
||||||
private val swipeRefresh: SwipeRefreshLayout by bindView(R.id.swipe_refresh)
|
private val swipeRefresh: SwipeRefreshLayout by bindView(R.id.swipe_refresh)
|
||||||
private val textview: AppCompatTextView by bindView(R.id.textview)
|
private val textview: AppCompatTextView by bindView(R.id.textview)
|
||||||
private val profile: ImageView by bindView(R.id.profile)
|
private val profile: ImageView by bindView(R.id.profile)
|
||||||
|
|
||||||
private lateinit var profileLoader: RequestManager
|
private lateinit var profileLoader: RequestManager
|
||||||
|
|
||||||
private val refreshMutableFlow = MutableSharedFlow<Boolean>(
|
private val refreshMutableFlow =
|
||||||
extraBufferCapacity = 10,
|
MutableSharedFlow<Boolean>(
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_login)
|
setContentView(R.layout.activity_login)
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
setTitle(R.string.kau_login)
|
setTitle(R.string.kau_login)
|
||||||
activityThemer.setFrostColors {
|
activityThemer.setFrostColors { toolbar(toolbar) }
|
||||||
toolbar(toolbar)
|
profileLoader = GlideApp.with(profile)
|
||||||
}
|
|
||||||
profileLoader = GlideApp.with(profile)
|
|
||||||
|
|
||||||
refreshFlow
|
refreshFlow.distinctUntilChanged().onEach { swipeRefresh.isRefreshing = it }.launchIn(this)
|
||||||
.distinctUntilChanged()
|
|
||||||
.onEach { swipeRefresh.isRefreshing = it }
|
|
||||||
.launchIn(this)
|
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
val cookie = web.loadLogin { refresh(it != 100) }.await()
|
val cookie = web.loadLogin { refresh(it != 100) }.await()
|
||||||
L.d { "Login found" }
|
L.d { "Login found" }
|
||||||
fbCookie.save(cookie.id)
|
fbCookie.save(cookie.id)
|
||||||
webFadeOut()
|
webFadeOut()
|
||||||
profile.fadeIn()
|
profile.fadeIn()
|
||||||
loadInfo(cookie)
|
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 ->
|
textview.text = String.format(getString(R.string.welcome), name ?: "")
|
||||||
web.fadeOut { cont.resume(Unit) }
|
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 {
|
private suspend fun loadProfile(id: Long): Boolean = withMainContext {
|
||||||
refresh(true)
|
suspendCancellableCoroutine<Boolean> { cont ->
|
||||||
|
profileLoader
|
||||||
val imageDeferred = async { loadProfile(cookie.id) }
|
.load(profilePictureUrl(id))
|
||||||
val nameDeferred = async { loadUsername(cookie) }
|
.transform(FrostGlide.circleCrop)
|
||||||
|
.listener(
|
||||||
val name: String? = nameDeferred.await()
|
object : RequestListener<Drawable> {
|
||||||
val foundImage: Boolean = imageDeferred.await()
|
override fun onResourceReady(
|
||||||
|
resource: Drawable?,
|
||||||
L._d { "Logged in and received data" }
|
model: Any?,
|
||||||
refresh(false)
|
target: Target<Drawable>?,
|
||||||
|
dataSource: DataSource?,
|
||||||
if (!foundImage) {
|
isFirstResource: Boolean
|
||||||
L.e { "Could not get profile photo; Invalid userId?" }
|
): Boolean {
|
||||||
L._i { cookie }
|
cont.resume(true)
|
||||||
}
|
return false
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
} catch (e: Exception) {
|
||||||
if (e !is UnknownHostException)
|
if (e !is UnknownHostException) e.logFrostEvent("Fetch username failed")
|
||||||
e.logFrostEvent("Fetch username failed")
|
null
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
cookieDao.save(cookie.copy(name = result))
|
cookieDao.save(cookie.copy(name = result))
|
||||||
return@withContext result
|
return@withContext result
|
||||||
}
|
}
|
||||||
|
|
||||||
return@withContext cookie.name
|
return@withContext cookie.name
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun backConsumer(): Boolean {
|
override fun backConsumer(): Boolean {
|
||||||
if (web.canGoBack()) {
|
if (web.canGoBack()) {
|
||||||
web.goBack()
|
web.goBack()
|
||||||
return true
|
return true
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
web.resumeTimers()
|
web.resumeTimers()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
web.pauseTimers()
|
web.pauseTimers()
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,93 +40,92 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
|
|
||||||
class MainActivity : BaseMainActivity() {
|
class MainActivity : BaseMainActivity() {
|
||||||
|
|
||||||
private val fragmentMutableFlow = MutableSharedFlow<Int>(
|
private val fragmentMutableFlow =
|
||||||
extraBufferCapacity = 10,
|
MutableSharedFlow<Int>(extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
override val fragmentFlow: SharedFlow<Int> = fragmentMutableFlow.asSharedFlow()
|
||||||
)
|
override val fragmentEmit: FrostEmitter<Int> = fragmentMutableFlow.asFrostEmitter()
|
||||||
override val fragmentFlow: SharedFlow<Int> = fragmentMutableFlow.asSharedFlow()
|
|
||||||
override val fragmentEmit: FrostEmitter<Int> = fragmentMutableFlow.asFrostEmitter()
|
|
||||||
|
|
||||||
private val headerMutableFlow = MutableStateFlow("")
|
private val headerMutableFlow = MutableStateFlow("")
|
||||||
override val headerFlow: SharedFlow<String> = headerMutableFlow.asSharedFlow()
|
override val headerFlow: SharedFlow<String> = headerMutableFlow.asSharedFlow()
|
||||||
override val headerEmit: FrostEmitter<String> = headerMutableFlow.asFrostEmitter()
|
override val headerEmit: FrostEmitter<String> = headerMutableFlow.asFrostEmitter()
|
||||||
|
|
||||||
override fun onNestedCreate(savedInstanceState: Bundle?) {
|
override fun onNestedCreate(savedInstanceState: Bundle?) {
|
||||||
with(contentBinding) {
|
with(contentBinding) {
|
||||||
setupTabs()
|
setupTabs()
|
||||||
setupViewPager()
|
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() {
|
override fun onPageScrolled(
|
||||||
viewpager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
|
position: Int,
|
||||||
override fun onPageSelected(position: Int) {
|
positionOffset: Float,
|
||||||
super.onPageSelected(position)
|
positionOffsetPixels: Int
|
||||||
if (lastPosition == position) {
|
) {
|
||||||
return
|
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
|
||||||
}
|
val delta = positionOffset * (SELECTED_TAB_ALPHA - UNSELECTED_TAB_ALPHA)
|
||||||
if (lastPosition != -1) {
|
tabsForEachView { tabPosition, view ->
|
||||||
fragmentEmit(-(lastPosition + 1))
|
view.setAllAlpha(
|
||||||
}
|
when (tabPosition) {
|
||||||
fragmentEmit(position)
|
position -> SELECTED_TAB_ALPHA - delta
|
||||||
lastPosition = position
|
position + 1 -> UNSELECTED_TAB_ALPHA + delta
|
||||||
}
|
else -> UNSELECTED_TAB_ALPHA
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPageScrolled(
|
private fun ActivityMainContentBinding.setupTabs() {
|
||||||
position: Int,
|
viewpager.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabs))
|
||||||
positionOffset: Float,
|
tabs.addOnTabSelectedListener(
|
||||||
positionOffsetPixels: Int
|
object : TabLayout.ViewPagerOnTabSelectedListener(viewpager) {
|
||||||
) {
|
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||||
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
|
super.onTabReselected(tab)
|
||||||
val delta = positionOffset * (SELECTED_TAB_ALPHA - UNSELECTED_TAB_ALPHA)
|
currentFragment?.onTabClick()
|
||||||
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() {
|
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||||
viewpager.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabs))
|
super.onTabSelected(tab)
|
||||||
tabs.addOnTabSelectedListener(object : TabLayout.ViewPagerOnTabSelectedListener(viewpager) {
|
(tab.customView as BadgedIcon).badgeText = null
|
||||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
}
|
||||||
super.onTabReselected(tab)
|
}
|
||||||
currentFragment?.onTabClick()
|
)
|
||||||
}
|
headerFlow
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
.mapNotNull { html ->
|
||||||
super.onTabSelected(tab)
|
BadgeParser.parseFromData(cookie = fbCookie.webCookie, text = html)?.data
|
||||||
(tab.customView as BadgedIcon).badgeText = null
|
}
|
||||||
}
|
.distinctUntilChanged()
|
||||||
})
|
.flowOn(Dispatchers.IO)
|
||||||
headerFlow
|
.onEach { data ->
|
||||||
.filter { it.isNotBlank() }
|
L.v { "Badges $data" }
|
||||||
.mapNotNull { html ->
|
tabsForEachView { _, view ->
|
||||||
BadgeParser.parseFromData(
|
when (view.iicon) {
|
||||||
cookie = fbCookie.webCookie,
|
FbItem.FEED.icon -> view.badgeText = data.feed
|
||||||
text = html
|
FbItem.FRIENDS.icon -> view.badgeText = data.friends
|
||||||
)?.data
|
FbItem.MESSAGES.icon -> view.badgeText = data.messages
|
||||||
}
|
FbItem.NOTIFICATIONS.icon -> view.badgeText = data.notifications
|
||||||
.distinctUntilChanged()
|
}
|
||||||
.flowOn(Dispatchers.IO)
|
}
|
||||||
.onEach { data ->
|
}
|
||||||
L.v { "Badges $data" }
|
.flowOn(Dispatchers.Main)
|
||||||
tabsForEachView { _, view ->
|
.launchIn(this@MainActivity)
|
||||||
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 com.pitchedapps.frost.views.AccountItem
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/** Created by Allan Wang on 2017-06-04. */
|
||||||
* Created by Allan Wang on 2017-06-04.
|
|
||||||
*/
|
|
||||||
class SelectorActivity : BaseActivity() {
|
class SelectorActivity : BaseActivity() {
|
||||||
|
|
||||||
val recycler: RecyclerView by bindView(R.id.selector_recycler)
|
val recycler: RecyclerView by bindView(R.id.selector_recycler)
|
||||||
val adapter = FastItemAdapter<AccountItem>()
|
val adapter = FastItemAdapter<AccountItem>()
|
||||||
val text: AppCompatTextView by bindView(R.id.text_select_account)
|
val text: AppCompatTextView by bindView(R.id.text_select_account)
|
||||||
val container: ConstraintLayout by bindView(R.id.container)
|
val container: ConstraintLayout by bindView(R.id.container)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_selector)
|
setContentView(R.layout.activity_selector)
|
||||||
recycler.layoutManager = GridLayoutManager(this, 2)
|
recycler.layoutManager = GridLayoutManager(this, 2)
|
||||||
recycler.adapter = adapter
|
recycler.adapter = adapter
|
||||||
adapter.add(cookies().map { AccountItem(it, themeProvider) })
|
adapter.add(cookies().map { AccountItem(it, themeProvider) })
|
||||||
adapter.add(AccountItem(null, themeProvider)) // add account
|
adapter.add(AccountItem(null, themeProvider)) // add account
|
||||||
adapter.addEventHook(object : ClickEventHook<AccountItem>() {
|
adapter.addEventHook(
|
||||||
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? =
|
object : ClickEventHook<AccountItem>() {
|
||||||
(viewHolder as? AccountItem.ViewHolder)?.itemView
|
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? =
|
||||||
|
(viewHolder as? AccountItem.ViewHolder)?.itemView
|
||||||
|
|
||||||
override fun onClick(
|
override fun onClick(
|
||||||
v: View,
|
v: View,
|
||||||
position: Int,
|
position: Int,
|
||||||
fastAdapter: FastAdapter<AccountItem>,
|
fastAdapter: FastAdapter<AccountItem>,
|
||||||
item: AccountItem
|
item: AccountItem
|
||||||
) {
|
) {
|
||||||
if (item.cookie == null) this@SelectorActivity.launchNewTask<LoginActivity>()
|
if (item.cookie == null) this@SelectorActivity.launchNewTask<LoginActivity>()
|
||||||
else launch {
|
else
|
||||||
fbCookie.switchUser(item.cookie)
|
launch {
|
||||||
launchNewTask<MainActivity>(cookies())
|
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.launchNewTask
|
||||||
import com.pitchedapps.frost.utils.loadAssets
|
import com.pitchedapps.frost.utils.loadAssets
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.launch
|
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
|
@AndroidEntryPoint
|
||||||
class SettingsActivity : KPrefActivity() {
|
class SettingsActivity : KPrefActivity() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var fbCookie: FbCookie
|
||||||
lateinit var fbCookie: FbCookie
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var prefs: Prefs
|
||||||
lateinit var prefs: Prefs
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var themeProvider: ThemeProvider
|
||||||
lateinit var themeProvider: ThemeProvider
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var notifDao: NotificationDao
|
||||||
lateinit var notifDao: NotificationDao
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var activityThemer: ActivityThemer
|
||||||
lateinit var activityThemer: ActivityThemer
|
|
||||||
|
|
||||||
private var resultFlag = Activity.RESULT_CANCELED
|
private var resultFlag = Activity.RESULT_CANCELED
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val REQUEST_RINGTONE = 0b10111 shl 5
|
private const val REQUEST_RINGTONE = 0b10111 shl 5
|
||||||
const val REQUEST_NOTIFICATION_RINGTONE = REQUEST_RINGTONE or 1
|
const val REQUEST_NOTIFICATION_RINGTONE = REQUEST_RINGTONE or 1
|
||||||
const val REQUEST_MESSAGE_RINGTONE = REQUEST_RINGTONE or 2
|
const val REQUEST_MESSAGE_RINGTONE = REQUEST_RINGTONE or 2
|
||||||
const val ACTIVITY_REQUEST_TABS = 29
|
const val ACTIVITY_REQUEST_TABS = 29
|
||||||
const val ACTIVITY_REQUEST_DEBUG = 53
|
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?) {
|
subItems(R.string.behaviour, getBehaviourPrefs()) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
descRes = R.string.behaviour_desc
|
||||||
if (fetchRingtone(requestCode, resultCode, data)) return
|
iicon = GoogleMaterial.Icon.gmd_trending_up
|
||||||
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.newsfeed, getFeedPrefs()) {
|
||||||
* Fetch ringtone and save uri
|
descRes = R.string.newsfeed_desc
|
||||||
* Returns [true] if consumed, [false] otherwise
|
iicon = CommunityMaterial.Icon3.cmd_newspaper
|
||||||
*/
|
|
||||||
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 = {
|
subItems(R.string.notifications, getNotificationPrefs()) {
|
||||||
textColor = { themeProvider.textColor }
|
descRes = R.string.notifications_desc
|
||||||
accentColor = { themeProvider.accentColor }
|
iicon = GoogleMaterial.Icon.gmd_notifications
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateKPrefs(savedInstanceState: Bundle?): KPrefAdapterBuilder.() -> Unit = {
|
subItems(R.string.security, getSecurityPrefs()) {
|
||||||
subItems(R.string.appearance, getAppearancePrefs()) {
|
descRes = R.string.security_desc
|
||||||
descRes = R.string.appearance_desc
|
iicon = GoogleMaterial.Icon.gmd_lock
|
||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setFrostResult(flag: Int) {
|
// subItems(R.string.network, getNetworkPrefs()) {
|
||||||
resultFlag = resultFlag or flag
|
// descRes = R.string.network_desc
|
||||||
}
|
// iicon = GoogleMaterial.Icon.gmd_network_cell
|
||||||
|
// }
|
||||||
|
|
||||||
fun shouldRestartMain() {
|
// todo add donation?
|
||||||
setFrostResult(REQUEST_RESTART)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun shouldRefreshMain() {
|
plainText(R.string.about_frost) {
|
||||||
setFrostResult(REQUEST_REFRESH)
|
descRes = R.string.about_frost_desc
|
||||||
}
|
iicon = GoogleMaterial.Icon.gmd_info
|
||||||
|
onClick = {
|
||||||
@SuppressLint("MissingSuperCall")
|
startActivityForResult<AboutActivity>(
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
9,
|
||||||
super.onCreate(savedInstanceState)
|
bundleBuilder = { withSceneTransitionAnimation(this@SettingsActivity) }
|
||||||
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() {
|
plainText(R.string.help_translate) {
|
||||||
if (!super.backPress()) {
|
descRes = R.string.help_translate_desc
|
||||||
setResult(resultFlag)
|
iicon = GoogleMaterial.Icon.gmd_translate
|
||||||
launch(NonCancellable) {
|
onClick = { startLink(R.string.translation_url) }
|
||||||
loadAssets(themeProvider)
|
|
||||||
finishSlideOut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
plainText(R.string.replay_intro) {
|
||||||
menuInflater.inflate(R.menu.menu_settings, menu)
|
iicon = GoogleMaterial.Icon.gmd_replay
|
||||||
toolbar.tint(themeProvider.iconColor)
|
onClick = { launchNewTask<IntroActivity>(cookies(), true) }
|
||||||
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 {
|
subItems(R.string.experimental, getExperimentalPrefs()) {
|
||||||
when (item.itemId) {
|
descRes = R.string.experimental_desc
|
||||||
R.id.action_github -> startLink(R.string.github_url)
|
iicon = CommunityMaterial.Icon2.cmd_flask_outline
|
||||||
R.id.action_changelog -> frostChangelog()
|
|
||||||
else -> return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.iitems.TabIItem
|
||||||
import com.pitchedapps.frost.utils.L
|
import com.pitchedapps.frost.utils.L
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.NonCancellable
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import javax.inject.Inject
|
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
|
@AndroidEntryPoint
|
||||||
class TabCustomizerActivity : BaseActivity() {
|
class TabCustomizerActivity : BaseActivity() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var genericDao: GenericDao
|
||||||
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityTabCustomizerBinding.inflate(layoutInflater)
|
binding = ActivityTabCustomizerBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
binding.init()
|
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() {
|
setResult(Activity.RESULT_CANCELED)
|
||||||
pseudoToolbar.setBackgroundColor(themeProvider.headerColor)
|
|
||||||
|
|
||||||
tabRecycler.layoutManager =
|
fabSave.setIcon(GoogleMaterial.Icon.gmd_check, themeProvider.iconColor)
|
||||||
GridLayoutManager(this@TabCustomizerActivity, TAB_COUNT, RecyclerView.VERTICAL, false)
|
fabSave.backgroundTintList = ColorStateList.valueOf(themeProvider.accentColor)
|
||||||
tabRecycler.adapter = adapter
|
fabSave.setOnClickListener {
|
||||||
tabRecycler.setHasFixedSize(true)
|
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))
|
private fun View.wobble() = startAnimation(wobble(context))
|
||||||
instructions.setTextColor(themeProvider.textColor)
|
|
||||||
|
|
||||||
launch {
|
private fun bindSwapper(adapter: FastItemAdapter<*>, recycler: RecyclerView) {
|
||||||
val tabs = genericDao.getTabs().toMutableList()
|
val dragCallback = TabDragCallback(SimpleDragCallback.ALL, swapper(adapter))
|
||||||
L.d { "Tabs $tabs" }
|
ItemTouchHelper(dragCallback).attachToRecyclerView(recycler)
|
||||||
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)
|
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 }
|
override fun itemTouchDropped(oldPosition: Int, newPosition: Int) = Unit
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun View.wobble() = startAnimation(wobble(context))
|
private class TabDragCallback(directions: Int, itemTouchCallback: ItemTouchCallback) :
|
||||||
|
SimpleDragCallback(directions, itemTouchCallback) {
|
||||||
|
|
||||||
private fun bindSwapper(adapter: FastItemAdapter<*>, recycler: RecyclerView) {
|
private var draggingView: TabIItem.ViewHolder? = null
|
||||||
val dragCallback = TabDragCallback(SimpleDragCallback.ALL, swapper(adapter))
|
|
||||||
ItemTouchHelper(dragCallback).attachToRecyclerView(recycler)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun swapper(adapter: FastItemAdapter<*>) = object : ItemTouchCallback {
|
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||||
override fun itemTouchOnMove(oldPosition: Int, newPosition: Int): Boolean {
|
super.onSelectedChanged(viewHolder, actionState)
|
||||||
Collections.swap(adapter.adapterItems, oldPosition, newPosition)
|
when (actionState) {
|
||||||
adapter.notifyAdapterDataSetChanged()
|
ItemTouchHelper.ACTION_STATE_DRAG -> {
|
||||||
return true
|
(viewHolder as? TabIItem.ViewHolder)?.apply {
|
||||||
|
draggingView = this
|
||||||
|
itemView.animate().scaleXY(1.3f)
|
||||||
|
text.animate().alpha(0f)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
ItemTouchHelper.ACTION_STATE_IDLE -> {
|
||||||
override fun itemTouchDropped(oldPosition: Int, newPosition: Int) = Unit
|
draggingView?.apply {
|
||||||
}
|
itemView.animate().scaleXY(1f)
|
||||||
|
text.animate().alpha(1f)
|
||||||
private class TabDragCallback(
|
}
|
||||||
directions: Int,
|
draggingView = null
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,7 @@ import com.pitchedapps.frost.views.FrostContentWeb
|
|||||||
import com.pitchedapps.frost.views.FrostVideoViewer
|
import com.pitchedapps.frost.views.FrostVideoViewer
|
||||||
import com.pitchedapps.frost.views.FrostWebView
|
import com.pitchedapps.frost.views.FrostWebView
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@ -71,7 +72,6 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import kotlinx.coroutines.flow.take
|
import kotlinx.coroutines.flow.take
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 2017-06-01.
|
* 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
|
* Used by notifications. Unlike the other overlays, this runs as a singleInstance Going back will
|
||||||
* Going back will bring you back to the previous app
|
* bring you back to the previous app
|
||||||
*/
|
*/
|
||||||
class FrostWebActivity : WebOverlayActivityBase() {
|
class FrostWebActivity : WebOverlayActivityBase() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val requiresAction = !parseActionSend()
|
val requiresAction = !parseActionSend()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
if (requiresAction) {
|
if (requiresAction) {
|
||||||
/*
|
/*
|
||||||
* Signifies that we need to let the user know of a bad url
|
* Signifies that we need to let the user know of a bad url
|
||||||
* We will subscribe to the load cycle once,
|
* We will subscribe to the load cycle once,
|
||||||
* and pop a dialog giving the user the option to copy the shared text
|
* and pop a dialog giving the user the option to copy the shared text
|
||||||
*/
|
*/
|
||||||
content.scope.launch(Dispatchers.IO) {
|
content.scope.launch(Dispatchers.IO) {
|
||||||
content.refreshFlow.take(1).collect()
|
content.refreshFlow.take(1).collect()
|
||||||
withMainContext {
|
withMainContext {
|
||||||
materialDialog {
|
materialDialog {
|
||||||
title(R.string.invalid_share_url)
|
title(R.string.invalid_share_url)
|
||||||
message(R.string.invalid_share_url_desc)
|
message(R.string.invalid_share_url_desc)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to parse the action url
|
* Attempts to parse the action url Returns [true] if no action exists or if the action has been
|
||||||
* 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
|
* consumed, [false] if we need to notify the user of a bad action
|
||||||
*/
|
*/
|
||||||
private fun parseActionSend(): Boolean {
|
private fun parseActionSend(): Boolean {
|
||||||
if (intent.action != Intent.ACTION_SEND || intent.type != "text/plain") return true
|
if (intent.action != Intent.ACTION_SEND || intent.type != "text/plain") return true
|
||||||
val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return true
|
val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return true
|
||||||
val url = text.toHttpUrlOrNull()?.toString()
|
val url = text.toHttpUrlOrNull()?.toString()
|
||||||
return if (url == null) {
|
return if (url == null) {
|
||||||
L.i { "Attempted to share a non-url" }
|
L.i { "Attempted to share a non-url" }
|
||||||
L._i { "Shared text: $text" }
|
L._i { "Shared text: $text" }
|
||||||
copyToClipboard(text, "Text to Share", showToast = false)
|
copyToClipboard(text, "Text to Share", showToast = false)
|
||||||
intent.putExtra(ARG_URL, FbItem.FEED.url)
|
intent.putExtra(ARG_URL, FbItem.FEED.url)
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
L.i { "Sharing url through overlay" }
|
L.i { "Sharing url through overlay" }
|
||||||
L._i { "Url: $url" }
|
L._i { "Url: $url" }
|
||||||
intent.putExtra(ARG_URL, "${FB_URL_BASE}sharer/sharer.php?u=$url")
|
intent.putExtra(ARG_URL, "${FB_URL_BASE}sharer/sharer.php?u=$url")
|
||||||
true
|
true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Variant that forces a mobile user agent. This is largely internal,
|
* Variant that forces a mobile user agent. This is largely internal, and is only necessary when we
|
||||||
* and is only necessary when we are launching from an existing [WebOverlayActivityBase]
|
* are launching from an existing [WebOverlayActivityBase]
|
||||||
*/
|
*/
|
||||||
class WebOverlayMobileActivity : WebOverlayActivityBase(USER_AGENT_MOBILE_CONST)
|
class WebOverlayMobileActivity : WebOverlayActivityBase(USER_AGENT_MOBILE_CONST)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Variant that forces a desktop user agent. This is largely internal,
|
* Variant that forces a desktop user agent. This is largely internal, and is only necessary when we
|
||||||
* and is only necessary when we are launching from an existing [WebOverlayActivityBase]
|
* are launching from an existing [WebOverlayActivityBase]
|
||||||
*/
|
*/
|
||||||
class WebOverlayDesktopActivity : WebOverlayActivityBase(USER_AGENT_DESKTOP_CONST)
|
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()
|
class WebOverlayActivity : WebOverlayActivityBase()
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
abstract class WebOverlayActivityBase(private val userAgent: String = USER_AGENT) :
|
abstract class WebOverlayActivityBase(private val userAgent: String = USER_AGENT) :
|
||||||
BaseActivity(),
|
BaseActivity(), FrostContentContainer, VideoViewHolder {
|
||||||
FrostContentContainer,
|
|
||||||
VideoViewHolder {
|
|
||||||
|
|
||||||
override val frameWrapper: FrameLayout by bindView(R.id.frame_wrapper)
|
override val frameWrapper: FrameLayout by bindView(R.id.frame_wrapper)
|
||||||
val toolbar: Toolbar by bindView(R.id.overlay_toolbar)
|
val toolbar: Toolbar by bindView(R.id.overlay_toolbar)
|
||||||
val content: FrostContentWeb by bindView(R.id.frost_content_web)
|
val content: FrostContentWeb by bindView(R.id.frost_content_web)
|
||||||
val web: FrostWebView
|
val web: FrostWebView
|
||||||
get() = content.coreView
|
get() = content.coreView
|
||||||
private val coordinator: CoordinatorLayout by bindView(R.id.overlay_main_content)
|
private val coordinator: CoordinatorLayout by bindView(R.id.overlay_main_content)
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var webFileChooser: WebFileChooser
|
||||||
lateinit var webFileChooser: WebFileChooser
|
|
||||||
|
|
||||||
private inline val urlTest: String?
|
private inline val urlTest: String?
|
||||||
get() = intent.getStringExtra(ARG_URL) ?: intent.dataString
|
get() = intent.getStringExtra(ARG_URL) ?: intent.dataString
|
||||||
|
|
||||||
lateinit var swipeBack: SwipeBackContract
|
lateinit var swipeBack: SwipeBackContract
|
||||||
|
|
||||||
/**
|
/** Nonnull variant; verify by checking [urlTest] */
|
||||||
* Nonnull variant; verify by checking [urlTest]
|
override val baseUrl: String
|
||||||
*/
|
get() = urlTest!!.formattedFbUrl
|
||||||
override val baseUrl: String
|
|
||||||
get() = urlTest!!.formattedFbUrl
|
|
||||||
|
|
||||||
override val baseEnum: FbItem? = null
|
override val baseEnum: FbItem? = null
|
||||||
|
|
||||||
private inline val userId: Long
|
private inline val userId: Long
|
||||||
get() = intent.getLongExtra(ARG_USER_ID, prefs.userId)
|
get() = intent.getLongExtra(ARG_USER_ID, prefs.userId)
|
||||||
|
|
||||||
private val overlayContext: OverlayContext?
|
private val overlayContext: OverlayContext?
|
||||||
get() = OverlayContext[intent.extras]
|
get() = OverlayContext[intent.extras]
|
||||||
|
|
||||||
override fun setTitle(title: String) {
|
override fun setTitle(title: String) {
|
||||||
toolbar.title = title
|
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?) {
|
activityThemer.setFrostColors {
|
||||||
super.onCreate(savedInstanceState)
|
toolbar(toolbar)
|
||||||
if (urlTest == null) {
|
themeWindow = false
|
||||||
L.e { "Empty link on web overlay" }
|
}
|
||||||
toast(R.string.null_url_overlay)
|
coordinator.setBackgroundColor(themeProvider.bgColor.withAlpha(255))
|
||||||
finish()
|
|
||||||
return
|
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)
|
authDefer.await()
|
||||||
setSupportActionBar(toolbar)
|
reloadBase(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
if (prefs.firstWebOverlay) {
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
coordinator.frostSnackbar(R.string.web_overlay_swipe_hint, themeProvider) {
|
||||||
toolbar.navigationIcon =
|
duration = BaseTransientBottomBar.LENGTH_INDEFINITE
|
||||||
GoogleMaterial.Icon.gmd_close.toDrawable(this, 16, themeProvider.iconColor)
|
setAction(R.string.kau_got_it) { dismiss() }
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
swipeBack = kauSwipeOnCreate {
|
||||||
* Manage url loadings
|
if (!prefs.overlayFullScreenSwipe) edgeSize = 20.dpToPx
|
||||||
* This is usually only called when multiple listeners are added and inject the same url
|
transitionSystemBars = false
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun backConsumer(): Boolean {
|
/**
|
||||||
if (!web.onBackPressed())
|
* Manage url loadings This is usually only called when multiple listeners are added and inject
|
||||||
finishSlideOut()
|
* the same url We will avoid reloading if the url is the same
|
||||||
return true
|
*/
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
override fun backConsumer(): Boolean {
|
||||||
* Our theme for the overlay should be fully opaque
|
if (!web.onBackPressed()) finishSlideOut()
|
||||||
*/
|
return true
|
||||||
fun theme() {
|
}
|
||||||
val opaqueAccent = themeProvider.headerColor.withAlpha(255)
|
|
||||||
statusBarColor = opaqueAccent.darken()
|
/** Our theme for the overlay should be fully opaque */
|
||||||
navigationBarColor = opaqueAccent
|
fun theme() {
|
||||||
toolbar.setBackgroundColor(opaqueAccent)
|
val opaqueAccent = themeProvider.headerColor.withAlpha(255)
|
||||||
toolbar.setTitleTextColor(themeProvider.iconColor)
|
statusBarColor = opaqueAccent.darken()
|
||||||
coordinator.setBackgroundColor(themeProvider.bgColor.withAlpha(255))
|
navigationBarColor = opaqueAccent
|
||||||
toolbar.overflowIcon?.setTint(themeProvider.iconColor)
|
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()
|
* Video Contract
|
||||||
}
|
* ----------------------------------------------------
|
||||||
|
*/
|
||||||
override fun onPause() {
|
override var videoViewer: FrostVideoViewer? = null
|
||||||
web.pauseTimers()
|
override val lowerVideoPadding: PointF = PointF(0f, 0f)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
@ -22,24 +22,22 @@ import com.pitchedapps.frost.web.FrostEmitter
|
|||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
|
||||||
interface MainActivityContract : MainFabContract {
|
interface MainActivityContract : MainFabContract {
|
||||||
val fragmentFlow: SharedFlow<Int>
|
val fragmentFlow: SharedFlow<Int>
|
||||||
val fragmentEmit: FrostEmitter<Int>
|
val fragmentEmit: FrostEmitter<Int>
|
||||||
|
|
||||||
val headerFlow: SharedFlow<String>
|
val headerFlow: SharedFlow<String>
|
||||||
val headerEmit: FrostEmitter<String>
|
val headerEmit: FrostEmitter<String>
|
||||||
|
|
||||||
fun setTitle(res: Int)
|
fun setTitle(res: Int)
|
||||||
fun setTitle(text: CharSequence)
|
fun setTitle(text: CharSequence)
|
||||||
|
|
||||||
/**
|
/** Available on all threads */
|
||||||
* Available on all threads
|
fun collapseAppBar()
|
||||||
*/
|
|
||||||
fun collapseAppBar()
|
|
||||||
|
|
||||||
fun reloadFragment(fragment: BaseFragment)
|
fun reloadFragment(fragment: BaseFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MainFabContract {
|
interface MainFabContract {
|
||||||
fun showFab(iicon: IIcon, clickEvent: () -> Unit)
|
fun showFab(iicon: IIcon, clickEvent: () -> Unit)
|
||||||
fun hideFab()
|
fun hideFab()
|
||||||
}
|
}
|
||||||
|
@ -16,29 +16,18 @@
|
|||||||
*/
|
*/
|
||||||
package com.pitchedapps.frost.contracts
|
package com.pitchedapps.frost.contracts
|
||||||
|
|
||||||
/**
|
/** Functions that will modify the current ui */
|
||||||
* Functions that will modify the current ui
|
|
||||||
*/
|
|
||||||
interface DynamicUiContract {
|
interface DynamicUiContract {
|
||||||
|
|
||||||
/**
|
/** Change all necessary view components to the new theme Also propagate where applicable */
|
||||||
* Change all necessary view components to the new theme
|
fun reloadTheme()
|
||||||
* Also propagate where applicable
|
|
||||||
*/
|
|
||||||
fun reloadTheme()
|
|
||||||
|
|
||||||
/**
|
/** Change theme without propagation */
|
||||||
* Change theme without propagation
|
fun reloadThemeSelf()
|
||||||
*/
|
|
||||||
fun reloadThemeSelf()
|
|
||||||
|
|
||||||
/**
|
/** Change text size & propagate */
|
||||||
* Change text size & propagate
|
fun reloadTextSize()
|
||||||
*/
|
|
||||||
fun reloadTextSize()
|
|
||||||
|
|
||||||
/**
|
/** Change text size without propagation */
|
||||||
* Change text size without propagation
|
fun reloadTextSizeSelf()
|
||||||
*/
|
|
||||||
fun reloadTextSizeSelf()
|
|
||||||
}
|
}
|
||||||
|
@ -35,70 +35,58 @@ import dagger.hilt.android.components.ActivityComponent
|
|||||||
import dagger.hilt.android.scopes.ActivityScoped
|
import dagger.hilt.android.scopes.ActivityScoped
|
||||||
import javax.inject.Inject
|
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
|
private const val MEDIA_CHOOSER_RESULT = 67
|
||||||
|
|
||||||
interface WebFileChooser {
|
interface WebFileChooser {
|
||||||
fun openMediaPicker(
|
fun openMediaPicker(
|
||||||
filePathCallback: ValueCallback<Array<Uri>?>,
|
filePathCallback: ValueCallback<Array<Uri>?>,
|
||||||
fileChooserParams: WebChromeClient.FileChooserParams
|
fileChooserParams: WebChromeClient.FileChooserParams
|
||||||
)
|
)
|
||||||
|
|
||||||
fun onActivityResultWeb(
|
fun onActivityResultWeb(requestCode: Int, resultCode: Int, intent: Intent?): Boolean
|
||||||
requestCode: Int,
|
|
||||||
resultCode: Int,
|
|
||||||
intent: Intent?
|
|
||||||
): Boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebFileChooserImpl @Inject internal constructor(
|
class WebFileChooserImpl
|
||||||
private val activity: Activity,
|
@Inject
|
||||||
private val themeProvider: ThemeProvider
|
internal constructor(private val activity: Activity, private val themeProvider: ThemeProvider) :
|
||||||
) : WebFileChooser {
|
WebFileChooser {
|
||||||
private var filePathCallback: ValueCallback<Array<Uri>?>? = null
|
private var filePathCallback: ValueCallback<Array<Uri>?>? = null
|
||||||
|
|
||||||
override fun openMediaPicker(
|
override fun openMediaPicker(
|
||||||
filePathCallback: ValueCallback<Array<Uri>?>,
|
filePathCallback: ValueCallback<Array<Uri>?>,
|
||||||
fileChooserParams: WebChromeClient.FileChooserParams
|
fileChooserParams: WebChromeClient.FileChooserParams
|
||||||
) {
|
) {
|
||||||
activity.kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ ->
|
activity.kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ ->
|
||||||
if (!granted) {
|
if (!granted) {
|
||||||
L.d { "Failed to get write permissions" }
|
L.d { "Failed to get write permissions" }
|
||||||
activity.frostSnackbar(R.string.file_chooser_not_found, themeProvider)
|
activity.frostSnackbar(R.string.file_chooser_not_found, themeProvider)
|
||||||
filePathCallback.onReceiveValue(null)
|
filePathCallback.onReceiveValue(null)
|
||||||
return@kauRequestPermissions
|
return@kauRequestPermissions
|
||||||
}
|
}
|
||||||
this.filePathCallback = filePathCallback
|
this.filePathCallback = filePathCallback
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
intent.type = fileChooserParams.acceptTypes.firstOrNull()
|
intent.type = fileChooserParams.acceptTypes.firstOrNull()
|
||||||
intent.action = Intent.ACTION_GET_CONTENT
|
intent.action = Intent.ACTION_GET_CONTENT
|
||||||
activity.startActivityForResult(
|
activity.startActivityForResult(
|
||||||
Intent.createChooser(intent, activity.string(R.string.pick_image)),
|
Intent.createChooser(intent, activity.string(R.string.pick_image)),
|
||||||
MEDIA_CHOOSER_RESULT
|
MEDIA_CHOOSER_RESULT
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onActivityResultWeb(
|
override fun onActivityResultWeb(requestCode: Int, resultCode: Int, intent: Intent?): Boolean {
|
||||||
requestCode: Int,
|
L.d { "FileChooser On activity results web $requestCode" }
|
||||||
resultCode: Int,
|
if (requestCode != MEDIA_CHOOSER_RESULT) return false
|
||||||
intent: Intent?
|
val data = intent?.data
|
||||||
): Boolean {
|
filePathCallback?.onReceiveValue(if (data != null) arrayOf(data) else null)
|
||||||
L.d { "FileChooser On activity results web $requestCode" }
|
filePathCallback = null
|
||||||
if (requestCode != MEDIA_CHOOSER_RESULT) return false
|
return true
|
||||||
val data = intent?.data
|
}
|
||||||
filePathCallback?.onReceiveValue(if (data != null) arrayOf(data) else null)
|
|
||||||
filePathCallback = null
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(ActivityComponent::class)
|
@InstallIn(ActivityComponent::class)
|
||||||
interface WebFileChooserModule {
|
interface WebFileChooserModule {
|
||||||
@Binds
|
@Binds @ActivityScoped fun webFileChooser(to: WebFileChooserImpl): WebFileChooser
|
||||||
@ActivityScoped
|
|
||||||
fun webFileChooser(to: WebFileChooserImpl): WebFileChooser
|
|
||||||
}
|
}
|
||||||
|
@ -22,159 +22,112 @@ import com.pitchedapps.frost.web.FrostEmitter
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
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 {
|
interface FrostContentContainer : CoroutineScope {
|
||||||
|
|
||||||
val baseUrl: String
|
val baseUrl: String
|
||||||
|
|
||||||
val baseEnum: FbItem?
|
val baseEnum: FbItem?
|
||||||
|
|
||||||
/**
|
/** Update toolbar title */
|
||||||
* Update toolbar title
|
fun setTitle(title: String)
|
||||||
*/
|
|
||||||
fun setTitle(title: String)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Contract for components shared among all content providers */
|
||||||
* Contract for components shared among
|
|
||||||
* all content providers
|
|
||||||
*/
|
|
||||||
interface FrostContentParent : DynamicUiContract {
|
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 */
|
||||||
* Observable to get data on whether view is refreshing or not
|
val refreshFlow: SharedFlow<Boolean>
|
||||||
*/
|
|
||||||
val refreshFlow: SharedFlow<Boolean>
|
|
||||||
|
|
||||||
val refreshEmit: FrostEmitter<Boolean>
|
val refreshEmit: FrostEmitter<Boolean>
|
||||||
|
|
||||||
/**
|
/** Observable to get data on refresh progress, with range [0, 100] */
|
||||||
* Observable to get data on refresh progress, with range [0, 100]
|
val progressFlow: SharedFlow<Int>
|
||||||
*/
|
|
||||||
val progressFlow: SharedFlow<Int>
|
|
||||||
|
|
||||||
val progressEmit: FrostEmitter<Int>
|
val progressEmit: FrostEmitter<Int>
|
||||||
|
|
||||||
/**
|
/** Observable to get new title data (unique values only) */
|
||||||
* Observable to get new title data (unique values only)
|
val titleFlow: SharedFlow<String>
|
||||||
*/
|
|
||||||
val 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 */
|
||||||
* Temporary disable swiping based on action
|
var swipeDisabledByAction: Boolean
|
||||||
*/
|
|
||||||
var swipeDisabledByAction: Boolean
|
|
||||||
|
|
||||||
/**
|
/** Decides if swipe should be allowed for the current page */
|
||||||
* Decides if swipe should be allowed for the current page
|
var swipeAllowedByPage: Boolean
|
||||||
*/
|
|
||||||
var swipeAllowedByPage: Boolean
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds the container to self
|
* Binds the container to self this will also handle all future bindings Must be called by
|
||||||
* this will also handle all future bindings
|
* container!
|
||||||
* Must be called by container!
|
*/
|
||||||
*/
|
fun bind(container: FrostContentContainer)
|
||||||
fun bind(container: FrostContentContainer)
|
|
||||||
|
|
||||||
/**
|
/** Signal that the contract will not be used again Clean up resources where applicable */
|
||||||
* Signal that the contract will not be used again
|
fun destroy()
|
||||||
* Clean up resources where applicable
|
|
||||||
*/
|
|
||||||
fun destroy()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook onto the refresh observable for one cycle
|
* Hook onto the refresh observable for one cycle Animate toggles between the fancy ripple and the
|
||||||
* Animate toggles between the fancy ripple and the basic fade
|
* basic fade The cycle only starts on the first load since there may have been another process
|
||||||
* The cycle only starts on the first load since
|
* when this is registered
|
||||||
* 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
|
||||||
* Returns true to proceed with load
|
* advisable to proceed with the load For those cases, we will return false to stop it
|
||||||
* In some cases when the url has not changed,
|
*/
|
||||||
* it may not be advisable to proceed with the load
|
fun registerTransition(urlChanged: Boolean, animate: Boolean): Boolean
|
||||||
* 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 {
|
interface FrostContentCore : DynamicUiContract {
|
||||||
|
|
||||||
val scope: CoroutineScope
|
val scope: CoroutineScope
|
||||||
get() = parent.scope
|
get() = parent.scope
|
||||||
|
|
||||||
/**
|
/** Reference to parent Bound through calling [FrostContentParent.bind] */
|
||||||
* Reference to parent
|
val parent: FrostContentParent
|
||||||
* Bound through calling [FrostContentParent.bind]
|
|
||||||
*/
|
|
||||||
val parent: FrostContentParent
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes view through given [container]
|
* Initializes view through given [container]
|
||||||
*
|
*
|
||||||
* The content may be free to extract other data from
|
* The content may be free to extract other data from the container if necessary
|
||||||
* the container if necessary
|
*/
|
||||||
*/
|
fun bind(parent: FrostContentParent, container: FrostContentContainer): View
|
||||||
fun bind(parent: FrostContentParent, container: FrostContentContainer): View
|
|
||||||
|
|
||||||
/**
|
/** Call to reload wrapped data */
|
||||||
* Call to reload wrapped data
|
fun reload(animate: Boolean)
|
||||||
*/
|
|
||||||
fun reload(animate: Boolean)
|
|
||||||
|
|
||||||
/**
|
/** Call to reload base data */
|
||||||
* Call to reload base data
|
fun reloadBase(animate: Boolean)
|
||||||
*/
|
|
||||||
fun reloadBase(animate: Boolean)
|
|
||||||
|
|
||||||
/**
|
/** If possible, remove anything in the view stack Applies namely to webviews */
|
||||||
* If possible, remove anything in the view stack
|
fun clearHistory()
|
||||||
* Applies namely to webviews
|
|
||||||
*/
|
|
||||||
fun clearHistory()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should be called when a back press is triggered
|
* Should be called when a back press is triggered Return [true] if consumed, [false] otherwise
|
||||||
* Return [true] if consumed, [false] otherwise
|
*/
|
||||||
*/
|
fun onBackPressed(): Boolean
|
||||||
fun onBackPressed(): Boolean
|
|
||||||
|
|
||||||
val currentUrl: String
|
val currentUrl: String
|
||||||
|
|
||||||
/**
|
/** Condition to help pause certain background resources */
|
||||||
* Condition to help pause certain background resources
|
var active: Boolean
|
||||||
*/
|
|
||||||
var active: Boolean
|
|
||||||
|
|
||||||
/**
|
/** Triggered when view is within viewpager and tab is clicked */
|
||||||
* Triggered when view is within viewpager
|
fun onTabClicked()
|
||||||
* and tab is clicked
|
|
||||||
*/
|
|
||||||
fun onTabClicked()
|
|
||||||
|
|
||||||
/**
|
/** Signal destruction to release some content manually */
|
||||||
* Signal destruction to release some content manually
|
fun destroy()
|
||||||
*/
|
|
||||||
fun destroy()
|
|
||||||
}
|
}
|
||||||
|
@ -22,23 +22,23 @@ import android.widget.TextView
|
|||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 2017-11-07.
|
* Created by Allan Wang on 2017-11-07.
|
||||||
*
|
*
|
||||||
* Should be implemented by all views in [com.pitchedapps.frost.activities.MainActivity]
|
* Should be implemented by all views in [com.pitchedapps.frost.activities.MainActivity] to allow
|
||||||
* to allow for instant view reloading
|
* for instant view reloading
|
||||||
*/
|
*/
|
||||||
interface FrostThemable {
|
interface FrostThemable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change all necessary view components to the new theme
|
* Change all necessary view components to the new theme and call whatever other children that
|
||||||
* and call whatever other children that also implement [FrostThemable]
|
* also implement [FrostThemable]
|
||||||
*/
|
*/
|
||||||
fun reloadTheme()
|
fun reloadTheme()
|
||||||
|
|
||||||
fun setTextColors(color: Int, vararg textViews: TextView?) =
|
fun setTextColors(color: Int, vararg textViews: TextView?) =
|
||||||
themeViews(color, *textViews) { setTextColor(it) }
|
themeViews(color, *textViews) { setTextColor(it) }
|
||||||
|
|
||||||
fun setBackgrounds(color: Int, vararg views: View?) =
|
fun setBackgrounds(color: Int, vararg views: View?) =
|
||||||
themeViews(color, *views) { setBackgroundColor(it) }
|
themeViews(color, *views) { setBackgroundColor(it) }
|
||||||
|
|
||||||
fun <T : View> themeViews(color: Int, vararg views: T?, action: T.(Int) -> Unit) =
|
fun <T : View> themeViews(color: Int, vararg views: T?, action: T.(Int) -> Unit) =
|
||||||
views.filterNotNull().forEach { it.action(color) }
|
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.FrostVideoContainerContract
|
||||||
import com.pitchedapps.frost.views.FrostVideoViewer
|
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 {
|
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 */
|
||||||
* Create new viewer and reuse existing one
|
fun showVideo(url: String, repeat: Boolean) {
|
||||||
* The url will be formatted upon loading
|
if (videoViewer != null) videoViewer?.setVideo(url, repeat)
|
||||||
*/
|
else videoViewer = FrostVideoViewer.showVideo(url, repeat, this)
|
||||||
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
|
override val videoContainer: FrameLayout
|
||||||
get() = frameWrapper
|
get() = frameWrapper
|
||||||
|
|
||||||
override fun onVideoFinished() {
|
override fun onVideoFinished() {
|
||||||
L.d { "Video view released" }
|
L.d { "Video view released" }
|
||||||
videoViewer = null
|
videoViewer = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FrameWrapper {
|
interface FrameWrapper {
|
||||||
|
|
||||||
val frameWrapper: FrameLayout
|
val frameWrapper: FrameLayout
|
||||||
|
|
||||||
fun Activity.setFrameContentView(layoutRes: Int) {
|
fun Activity.setFrameContentView(layoutRes: Int) {
|
||||||
setContentView(R.layout.activity_frame_wrapper)
|
setContentView(R.layout.activity_frame_wrapper)
|
||||||
frameWrapper.inflate(layoutRes, true)
|
frameWrapper.inflate(layoutRes, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,63 +26,52 @@ import androidx.room.Query
|
|||||||
import com.pitchedapps.frost.utils.L
|
import com.pitchedapps.frost.utils.L
|
||||||
import kotlinx.android.parcel.Parcelize
|
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(
|
@Entity(
|
||||||
tableName = "frost_cache",
|
tableName = "frost_cache",
|
||||||
primaryKeys = ["id", "type"],
|
primaryKeys = ["id", "type"],
|
||||||
foreignKeys = [
|
foreignKeys =
|
||||||
ForeignKey(
|
[
|
||||||
entity = CookieEntity::class,
|
ForeignKey(
|
||||||
parentColumns = ["cookie_id"],
|
entity = CookieEntity::class,
|
||||||
childColumns = ["id"],
|
parentColumns = ["cookie_id"],
|
||||||
onDelete = ForeignKey.CASCADE
|
childColumns = ["id"],
|
||||||
)
|
onDelete = ForeignKey.CASCADE
|
||||||
]
|
)]
|
||||||
)
|
)
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class CacheEntity(
|
data class CacheEntity(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val type: String,
|
val type: String,
|
||||||
val lastUpdated: Long,
|
val lastUpdated: Long,
|
||||||
val contents: String
|
val contents: String
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface CacheDao {
|
interface CacheDao {
|
||||||
|
|
||||||
@Query("SELECT * FROM frost_cache WHERE id = :id AND type = :type")
|
@Query("SELECT * FROM frost_cache WHERE id = :id AND type = :type")
|
||||||
fun _select(id: Long, type: String): CacheEntity?
|
fun _select(id: Long, type: String): CacheEntity?
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) fun _insertCache(cache: CacheEntity)
|
||||||
fun _insertCache(cache: CacheEntity)
|
|
||||||
|
|
||||||
@Query("DELETE FROM frost_cache WHERE id = :id AND type = :type")
|
@Query("DELETE FROM frost_cache WHERE id = :id AND type = :type")
|
||||||
fun _delete(id: Long, type: String)
|
fun _delete(id: Long, type: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun CacheDao.select(id: Long, type: String) = dao {
|
suspend fun CacheDao.select(id: Long, type: String) = dao { _select(id, type) }
|
||||||
_select(id, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun CacheDao.delete(id: Long, type: String) = dao {
|
suspend fun CacheDao.delete(id: Long, type: String) = dao { _delete(id, type) }
|
||||||
_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 {
|
suspend fun CacheDao.save(id: Long, type: String, contents: String): Boolean = dao {
|
||||||
try {
|
try {
|
||||||
_insertCache(CacheEntity(id, type, System.currentTimeMillis(), contents))
|
_insertCache(CacheEntity(id, type, System.currentTimeMillis(), contents))
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
L.e(e) { "Cache save failed for $type" }
|
L.e(e) { "Cache save failed for $type" }
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,59 +28,57 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
import com.pitchedapps.frost.prefs.Prefs
|
import com.pitchedapps.frost.prefs.Prefs
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
/**
|
/** Created by Allan Wang on 2017-05-30. */
|
||||||
* Created by Allan Wang on 2017-05-30.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Entity(tableName = "cookies")
|
@Entity(tableName = "cookies")
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class CookieEntity(
|
data class CookieEntity(
|
||||||
@androidx.room.PrimaryKey
|
@androidx.room.PrimaryKey @ColumnInfo(name = "cookie_id") val id: Long,
|
||||||
@ColumnInfo(name = "cookie_id")
|
val name: String?,
|
||||||
val id: Long,
|
val cookie: String?,
|
||||||
val name: String?,
|
val cookieMessenger: String? = null // Version 2
|
||||||
val cookie: String?,
|
|
||||||
val cookieMessenger: String? = null // Version 2
|
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
override fun toString(): String = "CookieEntity(${hashCode()})"
|
override fun toString(): String = "CookieEntity(${hashCode()})"
|
||||||
|
|
||||||
fun toSensitiveString(): String =
|
fun toSensitiveString(): String =
|
||||||
"CookieEntity(id=$id, name=$name, cookie=$cookie cookieMessenger=$cookieMessenger)"
|
"CookieEntity(id=$id, name=$name, cookie=$cookie cookieMessenger=$cookieMessenger)"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface CookieDao {
|
interface CookieDao {
|
||||||
|
|
||||||
@Query("SELECT * FROM cookies")
|
@Query("SELECT * FROM cookies") fun _selectAll(): List<CookieEntity>
|
||||||
fun _selectAll(): List<CookieEntity>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM cookies WHERE cookie_id = :id")
|
@Query("SELECT * FROM cookies WHERE cookie_id = :id") fun _selectById(id: Long): CookieEntity?
|
||||||
fun _selectById(id: Long): CookieEntity?
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) fun _save(cookie: CookieEntity)
|
||||||
fun _save(cookie: CookieEntity)
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) fun _save(cookies: List<CookieEntity>)
|
||||||
fun _save(cookies: List<CookieEntity>)
|
|
||||||
|
|
||||||
@Query("DELETE FROM cookies WHERE cookie_id = :id")
|
@Query("DELETE FROM cookies WHERE cookie_id = :id") fun _deleteById(id: Long)
|
||||||
fun _deleteById(id: Long)
|
|
||||||
|
|
||||||
@Query("UPDATE cookies SET cookieMessenger = :cookie WHERE cookie_id = :id")
|
@Query("UPDATE cookies SET cookieMessenger = :cookie WHERE cookie_id = :id")
|
||||||
fun _updateMessengerCookie(id: Long, cookie: String?)
|
fun _updateMessengerCookie(id: Long, cookie: String?)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun CookieDao.selectAll() = dao { _selectAll() }
|
suspend fun CookieDao.selectAll() = dao { _selectAll() }
|
||||||
suspend fun CookieDao.selectById(id: Long) = dao { _selectById(id) }
|
|
||||||
suspend fun CookieDao.save(cookie: CookieEntity) = dao { _save(cookie) }
|
|
||||||
suspend fun CookieDao.save(cookies: List<CookieEntity>) = dao { _save(cookies) }
|
|
||||||
suspend fun CookieDao.deleteById(id: Long) = dao { _deleteById(id) }
|
|
||||||
suspend fun CookieDao.currentCookie(prefs: Prefs) = selectById(prefs.userId)
|
|
||||||
suspend fun CookieDao.updateMessengerCookie(id: Long, cookie: String?) =
|
|
||||||
dao { _updateMessengerCookie(id, cookie) }
|
|
||||||
|
|
||||||
val COOKIES_MIGRATION_1_2 = object : Migration(1, 2) {
|
suspend fun CookieDao.selectById(id: Long) = dao { _selectById(id) }
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
|
||||||
database.execSQL("ALTER TABLE cookies ADD COLUMN cookieMessenger TEXT")
|
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
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps dao calls to work with coroutines
|
* Wraps dao calls to work with coroutines Non transactional queries were supposed to be fixed in
|
||||||
* Non transactional queries were supposed to be fixed in https://issuetracker.google.com/issues/69474692,
|
* https://issuetracker.google.com/issues/69474692, but it still requires dispatch from a non ui
|
||||||
* but it still requires dispatch from a non ui thread.
|
* thread. This avoids that constraint
|
||||||
* This avoids that constraint
|
|
||||||
*/
|
*/
|
||||||
suspend inline fun <T> dao(crossinline block: () -> T) = withContext(Dispatchers.IO) { block() }
|
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
|
import javax.inject.Singleton
|
||||||
|
|
||||||
interface FrostPrivateDao {
|
interface FrostPrivateDao {
|
||||||
fun cookieDao(): CookieDao
|
fun cookieDao(): CookieDao
|
||||||
fun notifDao(): NotificationDao
|
fun notifDao(): NotificationDao
|
||||||
fun cacheDao(): CacheDao
|
fun cacheDao(): CacheDao
|
||||||
}
|
}
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [CookieEntity::class, NotificationEntity::class, CacheEntity::class],
|
entities = [CookieEntity::class, NotificationEntity::class, CacheEntity::class],
|
||||||
version = 2,
|
version = 2,
|
||||||
exportSchema = true
|
exportSchema = true
|
||||||
)
|
)
|
||||||
abstract class FrostPrivateDatabase : RoomDatabase(), FrostPrivateDao {
|
abstract class FrostPrivateDatabase : RoomDatabase(), FrostPrivateDao {
|
||||||
companion object {
|
companion object {
|
||||||
const val DATABASE_NAME = "frost-priv-db"
|
const val DATABASE_NAME = "frost-priv-db"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FrostPublicDao {
|
interface FrostPublicDao {
|
||||||
fun genericDao(): GenericDao
|
fun genericDao(): GenericDao
|
||||||
}
|
}
|
||||||
|
|
||||||
@Database(entities = [GenericEntity::class], version = 1, exportSchema = true)
|
@Database(entities = [GenericEntity::class], version = 1, exportSchema = true)
|
||||||
abstract class FrostPublicDatabase : RoomDatabase(), FrostPublicDao {
|
abstract class FrostPublicDatabase : RoomDatabase(), FrostPublicDao {
|
||||||
companion object {
|
companion object {
|
||||||
const val DATABASE_NAME = "frost-db"
|
const val DATABASE_NAME = "frost-db"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FrostDao : FrostPrivateDao, FrostPublicDao {
|
interface FrostDao : FrostPrivateDao, FrostPublicDao {
|
||||||
fun close()
|
fun close()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Composition of all database interfaces */
|
||||||
* Composition of all database interfaces
|
|
||||||
*/
|
|
||||||
class FrostDatabase(
|
class FrostDatabase(
|
||||||
private val privateDb: FrostPrivateDatabase,
|
private val privateDb: FrostPrivateDatabase,
|
||||||
private val publicDb: FrostPublicDatabase
|
private val publicDb: FrostPublicDatabase
|
||||||
) :
|
) : FrostDao, FrostPrivateDao by privateDb, FrostPublicDao by publicDb {
|
||||||
FrostDao,
|
|
||||||
FrostPrivateDao by privateDb,
|
|
||||||
FrostPublicDao by publicDb {
|
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
privateDb.close()
|
privateDb.close()
|
||||||
publicDb.close()
|
publicDb.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private fun <T : RoomDatabase> RoomDatabase.Builder<T>.frostBuild() =
|
private fun <T : RoomDatabase> RoomDatabase.Builder<T>.frostBuild() =
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
fallbackToDestructiveMigration().build()
|
fallbackToDestructiveMigration().build()
|
||||||
} else {
|
} else {
|
||||||
build()
|
build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun create(context: Context): FrostDatabase {
|
fun create(context: Context): FrostDatabase {
|
||||||
val privateDb = Room.databaseBuilder(
|
val privateDb =
|
||||||
context, FrostPrivateDatabase::class.java,
|
Room.databaseBuilder(
|
||||||
FrostPrivateDatabase.DATABASE_NAME
|
context,
|
||||||
).addMigrations(COOKIES_MIGRATION_1_2).frostBuild()
|
FrostPrivateDatabase::class.java,
|
||||||
val publicDb = Room.databaseBuilder(
|
FrostPrivateDatabase.DATABASE_NAME
|
||||||
context, FrostPublicDatabase::class.java,
|
)
|
||||||
FrostPublicDatabase.DATABASE_NAME
|
.addMigrations(COOKIES_MIGRATION_1_2)
|
||||||
).frostBuild()
|
.frostBuild()
|
||||||
return FrostDatabase(privateDb, publicDb)
|
val publicDb =
|
||||||
}
|
Room.databaseBuilder(
|
||||||
|
context,
|
||||||
|
FrostPublicDatabase::class.java,
|
||||||
|
FrostPublicDatabase.DATABASE_NAME
|
||||||
|
)
|
||||||
|
.frostBuild()
|
||||||
|
return FrostDatabase(privateDb, publicDb)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object DatabaseModule {
|
object DatabaseModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun frostDatabase(@ApplicationContext context: Context): FrostDatabase =
|
fun frostDatabase(@ApplicationContext context: Context): FrostDatabase =
|
||||||
FrostDatabase.create(context)
|
FrostDatabase.create(context)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun cookieDao(frostDatabase: FrostDatabase): CookieDao = frostDatabase.cookieDao()
|
fun cookieDao(frostDatabase: FrostDatabase): CookieDao = frostDatabase.cookieDao()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun cacheDao(frostDatabase: FrostDatabase): CacheDao = frostDatabase.cacheDao()
|
fun cacheDao(frostDatabase: FrostDatabase): CacheDao = frostDatabase.cacheDao()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun notifDao(frostDatabase: FrostDatabase): NotificationDao = frostDatabase.notifDao()
|
fun notifDao(frostDatabase: FrostDatabase): NotificationDao = frostDatabase.notifDao()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun genericDao(frostDatabase: FrostDatabase): GenericDao = frostDatabase.genericDao()
|
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.FbItem
|
||||||
import com.pitchedapps.frost.facebook.defaultTabs
|
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")
|
@Entity(tableName = "frost_generic")
|
||||||
data class GenericEntity(
|
data class GenericEntity(@PrimaryKey val type: String, val contents: String)
|
||||||
@PrimaryKey
|
|
||||||
val type: String,
|
|
||||||
val contents: String
|
|
||||||
)
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface GenericDao {
|
interface GenericDao {
|
||||||
|
|
||||||
@Query("SELECT contents FROM frost_generic WHERE type = :type")
|
@Query("SELECT contents FROM frost_generic WHERE type = :type") fun _select(type: String): String?
|
||||||
fun _select(type: String): String?
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) fun _save(entity: GenericEntity)
|
||||||
fun _save(entity: GenericEntity)
|
|
||||||
|
|
||||||
@Query("DELETE FROM frost_generic WHERE type = :type")
|
@Query("DELETE FROM frost_generic WHERE type = :type") fun _delete(type: String)
|
||||||
fun _delete(type: String)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TYPE_TABS = "generic_tabs"
|
const val TYPE_TABS = "generic_tabs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const val TAB_COUNT = 4
|
const val TAB_COUNT = 4
|
||||||
|
|
||||||
suspend fun GenericDao.saveTabs(tabs: List<FbItem>) = dao {
|
suspend fun GenericDao.saveTabs(tabs: List<FbItem>) = dao {
|
||||||
val content = tabs.joinToString(",") { it.name }
|
val content = tabs.joinToString(",") { it.name }
|
||||||
_save(GenericEntity(GenericDao.TYPE_TABS, content))
|
_save(GenericEntity(GenericDao.TYPE_TABS, content))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun GenericDao.getTabs(): List<FbItem> = dao {
|
suspend fun GenericDao.getTabs(): List<FbItem> = dao {
|
||||||
val allTabs = FbItem.values.map { it.name to it }.toMap()
|
val allTabs = FbItem.values.map { it.name to it }.toMap()
|
||||||
_select(GenericDao.TYPE_TABS)
|
_select(GenericDao.TYPE_TABS)?.split(",")?.mapNotNull { allTabs[it] }?.takeIf { it.isNotEmpty() }
|
||||||
?.split(",")
|
?: defaultTabs()
|
||||||
?.mapNotNull { allTabs[it] }
|
|
||||||
?.takeIf { it.isNotEmpty() }
|
|
||||||
?: defaultTabs()
|
|
||||||
}
|
}
|
||||||
|
@ -30,127 +30,120 @@ import com.pitchedapps.frost.services.NotificationContent
|
|||||||
import com.pitchedapps.frost.utils.L
|
import com.pitchedapps.frost.utils.L
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "notifications",
|
tableName = "notifications",
|
||||||
primaryKeys = ["notif_id", "userId"],
|
primaryKeys = ["notif_id", "userId"],
|
||||||
foreignKeys = [
|
foreignKeys =
|
||||||
ForeignKey(
|
[
|
||||||
entity = CookieEntity::class,
|
ForeignKey(
|
||||||
parentColumns = ["cookie_id"],
|
entity = CookieEntity::class,
|
||||||
childColumns = ["userId"],
|
parentColumns = ["cookie_id"],
|
||||||
onDelete = ForeignKey.CASCADE
|
childColumns = ["userId"],
|
||||||
)
|
onDelete = ForeignKey.CASCADE
|
||||||
],
|
)],
|
||||||
indices = [Index("notif_id"), Index("userId")]
|
indices = [Index("notif_id"), Index("userId")]
|
||||||
)
|
)
|
||||||
data class NotificationEntity(
|
data class NotificationEntity(
|
||||||
@ColumnInfo(name = "notif_id")
|
@ColumnInfo(name = "notif_id") val id: Long,
|
||||||
val id: Long,
|
val userId: Long,
|
||||||
val userId: Long,
|
val href: String,
|
||||||
val href: String,
|
val title: String?,
|
||||||
val title: String?,
|
val text: String,
|
||||||
val text: String,
|
val timestamp: Long,
|
||||||
val timestamp: Long,
|
val profileUrl: String?,
|
||||||
val profileUrl: String?,
|
// Type essentially refers to channel
|
||||||
// Type essentially refers to channel
|
val type: String,
|
||||||
val type: String,
|
val unread: Boolean
|
||||||
val unread: Boolean
|
|
||||||
) {
|
) {
|
||||||
constructor(
|
constructor(
|
||||||
type: String,
|
type: String,
|
||||||
content: NotificationContent
|
content: NotificationContent
|
||||||
) : this(
|
) : this(
|
||||||
content.id,
|
content.id,
|
||||||
content.data.id,
|
content.data.id,
|
||||||
content.href,
|
content.href,
|
||||||
content.title,
|
content.title,
|
||||||
content.text,
|
content.text,
|
||||||
content.timestamp,
|
content.timestamp,
|
||||||
content.profileUrl,
|
content.profileUrl,
|
||||||
type,
|
type,
|
||||||
content.unread
|
content.unread
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class NotificationContentEntity(
|
data class NotificationContentEntity(
|
||||||
@Embedded
|
@Embedded val cookie: CookieEntity,
|
||||||
val cookie: CookieEntity,
|
@Embedded val notif: NotificationEntity
|
||||||
@Embedded
|
|
||||||
val notif: NotificationEntity
|
|
||||||
) {
|
) {
|
||||||
fun toNotifContent() = NotificationContent(
|
fun toNotifContent() =
|
||||||
data = cookie,
|
NotificationContent(
|
||||||
id = notif.id,
|
data = cookie,
|
||||||
href = notif.href,
|
id = notif.id,
|
||||||
title = notif.title,
|
href = notif.href,
|
||||||
text = notif.text,
|
title = notif.title,
|
||||||
timestamp = notif.timestamp,
|
text = notif.text,
|
||||||
profileUrl = notif.profileUrl,
|
timestamp = notif.timestamp,
|
||||||
unread = notif.unread
|
profileUrl = notif.profileUrl,
|
||||||
|
unread = notif.unread
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface NotificationDao {
|
interface NotificationDao {
|
||||||
|
|
||||||
/**
|
/** Note that notifications are guaranteed to be ordered by descending timestamp */
|
||||||
* Note that notifications are guaranteed to be ordered by descending timestamp
|
@Transaction
|
||||||
*/
|
@Query(
|
||||||
@Transaction
|
"SELECT * FROM cookies INNER JOIN notifications ON cookie_id = userId WHERE userId = :userId AND type = :type ORDER BY timestamp DESC"
|
||||||
@Query("SELECT * FROM cookies INNER JOIN notifications ON cookie_id = userId WHERE userId = :userId AND type = :type ORDER BY timestamp DESC")
|
)
|
||||||
fun _selectNotifications(userId: Long, type: String): List<NotificationContentEntity>
|
fun _selectNotifications(userId: Long, type: String): List<NotificationContentEntity>
|
||||||
|
|
||||||
@Query("SELECT timestamp FROM notifications WHERE userId = :userId AND type = :type ORDER BY timestamp DESC LIMIT 1")
|
@Query(
|
||||||
fun _selectEpoch(userId: Long, type: String): Long?
|
"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)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun _insertNotifications(notifs: List<NotificationEntity>)
|
fun _insertNotifications(notifs: List<NotificationEntity>)
|
||||||
|
|
||||||
@Query("DELETE FROM notifications WHERE userId = :userId AND type = :type")
|
@Query("DELETE FROM notifications WHERE userId = :userId AND type = :type")
|
||||||
fun _deleteNotifications(userId: Long, type: String)
|
fun _deleteNotifications(userId: Long, type: String)
|
||||||
|
|
||||||
@Query("DELETE FROM notifications")
|
@Query("DELETE FROM notifications") fun _deleteAll()
|
||||||
fun _deleteAll()
|
|
||||||
|
|
||||||
/**
|
/** It is assumed that the notification batch comes from the same user */
|
||||||
* It is assumed that the notification batch comes from the same user
|
@Transaction
|
||||||
*/
|
fun _saveNotifications(type: String, notifs: List<NotificationContent>) {
|
||||||
@Transaction
|
val userId = notifs.firstOrNull()?.data?.id ?: return
|
||||||
fun _saveNotifications(type: String, notifs: List<NotificationContent>) {
|
val entities = notifs.map { NotificationEntity(type, it) }
|
||||||
val userId = notifs.firstOrNull()?.data?.id ?: return
|
_deleteNotifications(userId, type)
|
||||||
val entities = notifs.map { NotificationEntity(type, it) }
|
_insertNotifications(entities)
|
||||||
_deleteNotifications(userId, type)
|
}
|
||||||
_insertNotifications(entities)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun NotificationDao.deleteAll() = dao { _deleteAll() }
|
suspend fun NotificationDao.deleteAll() = dao { _deleteAll() }
|
||||||
|
|
||||||
fun NotificationDao.selectNotificationsSync(userId: Long, type: String): List<NotificationContent> =
|
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(
|
suspend fun NotificationDao.selectNotifications(
|
||||||
userId: Long,
|
userId: Long,
|
||||||
type: String
|
type: String
|
||||||
): List<NotificationContent> = dao {
|
): List<NotificationContent> = dao { selectNotificationsSync(userId, type) }
|
||||||
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(
|
suspend fun NotificationDao.saveNotifications(
|
||||||
type: String,
|
type: String,
|
||||||
notifs: List<NotificationContent>
|
notifs: List<NotificationContent>
|
||||||
): Boolean = dao {
|
): Boolean = dao {
|
||||||
try {
|
try {
|
||||||
_saveNotifications(type, notifs)
|
_saveNotifications(type, notifs)
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
L.e(e) { "Notif save failed for $type" }
|
L.e(e) { "Notif save failed for $type" }
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun NotificationDao.latestEpoch(userId: Long, type: String): Long = dao {
|
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.createFreshFile
|
||||||
import com.pitchedapps.frost.utils.frostJsoup
|
import com.pitchedapps.frost.utils.frostJsoup
|
||||||
import com.pitchedapps.frost.utils.unescapeHtml
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -37,12 +43,6 @@ import org.jsoup.Jsoup
|
|||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.nodes.Entities
|
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.
|
* 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>
|
* Inspired by <a href="https://github.com/JonasCz/save-for-offline">Save for Offline</a>
|
||||||
*/
|
*/
|
||||||
class OfflineWebsite(
|
class OfflineWebsite(
|
||||||
private val url: String,
|
private val url: String,
|
||||||
private val cookie: String = "",
|
private val cookie: String = "",
|
||||||
baseUrl: String? = null,
|
baseUrl: String? = null,
|
||||||
private val html: String? = null,
|
private val html: String? = null,
|
||||||
/**
|
/** Directory that holds all the files */
|
||||||
* Directory that holds all the files
|
val baseDir: File,
|
||||||
*/
|
private val userAgent: String = USER_AGENT
|
||||||
val baseDir: File,
|
|
||||||
private val userAgent: String = USER_AGENT
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/** Supplied url without the queries */
|
||||||
* Supplied url without the queries
|
private val baseUrl: String =
|
||||||
*/
|
baseUrl
|
||||||
private val baseUrl: String = baseUrl ?: run {
|
?: run {
|
||||||
val url: HttpUrl = url.toHttpUrlOrNull() ?: throw IllegalArgumentException("Malformed url")
|
val url: HttpUrl = url.toHttpUrlOrNull() ?: throw IllegalArgumentException("Malformed url")
|
||||||
return@run "${url.scheme}://${url.host}"
|
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")
|
fun zip(name: String): Boolean {
|
||||||
private val assetDir = File(baseDir, "assets")
|
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>()
|
ZipOutputStream(FileOutputStream(zip)).use { out ->
|
||||||
private val atomicInt = AtomicInteger()
|
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)
|
assetDir.delete()
|
||||||
|
}
|
||||||
init {
|
return true
|
||||||
if (!this.baseUrl.startsWith("http"))
|
} catch (e: Exception) {
|
||||||
throw IllegalArgumentException("Base Url must start with http")
|
L.e { "Zip failed: ${e.message}" }
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val fileQueue = mutableSetOf<String>()
|
suspend fun loadAndZip(name: String, progress: (Int) -> Unit = {}): Boolean =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
private val cssQueue = mutableSetOf<String>()
|
coroutineScope {
|
||||||
|
val success = load { progress((it * 0.85f).toInt()) }
|
||||||
private fun request(url: String) = Request.Builder()
|
if (!success) return@coroutineScope false
|
||||||
.header("Cookie", cookie)
|
val result = zip(name)
|
||||||
.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)
|
progress(100)
|
||||||
return@withContext true
|
return@coroutineScope result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun zip(name: String): Boolean {
|
private fun downloadFile(url: String): Boolean {
|
||||||
try {
|
return try {
|
||||||
val zip = File(baseDir, "$name.zip")
|
val file = File(assetDir, fileName(url))
|
||||||
if (!zip.createFreshFile()) {
|
file.createNewFile()
|
||||||
L.e { "Failed to create zip at ${zip.absolutePath}" }
|
val stream =
|
||||||
return false
|
request(url).execute().body?.byteStream()
|
||||||
}
|
?: throw IllegalArgumentException("Response body not found for $url")
|
||||||
|
file.copyFromInputStream(stream)
|
||||||
ZipOutputStream(FileOutputStream(zip)).use { out ->
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
fun File.zip(name: String = this.name) {
|
L.e(e) { "Download file failed" }
|
||||||
if (!isFile) return
|
false
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun loadAndZip(name: String, progress: (Int) -> Unit = {}): Boolean =
|
private fun downloadCss(url: String): Set<String> {
|
||||||
withContext(Dispatchers.IO) {
|
return try {
|
||||||
coroutineScope {
|
val file = File(assetDir, fileName(url))
|
||||||
val success = load { progress((it * 0.85f).toInt()) }
|
file.createNewFile()
|
||||||
if (!success) return@coroutineScope false
|
|
||||||
val result = zip(name)
|
|
||||||
progress(100)
|
|
||||||
return@coroutineScope result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun downloadFile(url: String): Boolean {
|
var content =
|
||||||
return try {
|
request(url).execute().body?.string()
|
||||||
val file = File(assetDir, fileName(url))
|
?: throw IllegalArgumentException("Response body not found for $url")
|
||||||
file.createNewFile()
|
val links = FB_CSS_URL_MATCHER.findAll(content).mapNotNull { it[1] }
|
||||||
val stream = request(url).execute().body?.byteStream()
|
val absLinks =
|
||||||
?: throw IllegalArgumentException("Response body not found for $url")
|
links
|
||||||
file.copyFromInputStream(stream)
|
.mapNotNull {
|
||||||
true
|
val newUrl =
|
||||||
} catch (e: Exception) {
|
when {
|
||||||
L.e(e) { "Download file failed" }
|
it.startsWith("http") -> it
|
||||||
false
|
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> {
|
private fun Element.collect(query: String, key: String, collector: MutableSet<String>) {
|
||||||
return try {
|
val data = select(query)
|
||||||
val file = File(assetDir, fileName(url))
|
L.v { "Found ${data.size} elements with $query" }
|
||||||
file.createNewFile()
|
data.forEach {
|
||||||
|
val absLink = it.attr("abs:$key")
|
||||||
var content = request(url).execute().body?.string()
|
if (!absLink.isValid) return@forEach
|
||||||
?: throw IllegalArgumentException("Response body not found for $url")
|
collector.add(absLink)
|
||||||
val links = FB_CSS_URL_MATCHER.findAll(content).mapNotNull { it[1] }
|
it.attr(key, "assets/${fileName(absLink)}")
|
||||||
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>) {
|
private inline val String.isValid
|
||||||
val data = select(query)
|
get() = startsWith("http")
|
||||||
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
|
/** Fetch the previously discovered filename or create a new one This is thread-safe */
|
||||||
get() = startsWith("http")
|
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
|
* This is primarily for zipping up and sending via emails As .js files typically aren't
|
||||||
* or create a new one
|
* allowed, we'll simply make everything txt files
|
||||||
* This is thread-safe
|
|
||||||
*/
|
*/
|
||||||
private fun fileName(url: String): String {
|
if (newUrl.endsWith(".js")) newUrl = "$newUrl.txt"
|
||||||
val mapped = urlMapper[url]
|
|
||||||
if (mapped != null) return mapped
|
|
||||||
|
|
||||||
val candidate = url.substringBefore("?").trim('/')
|
urlMapper[url] = newUrl
|
||||||
.substringAfterLast("/").shorten()
|
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") }
|
||||||
|
|
||||||
/**
|
private fun reset() {
|
||||||
* This is primarily for zipping up and sending via emails
|
urlMapper.clear()
|
||||||
* As .js files typically aren't allowed, we'll simply make everything txt files
|
atomicInt.set(0)
|
||||||
*/
|
fileQueue.clear()
|
||||||
if (newUrl.endsWith(".js"))
|
cssQueue.clear()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -20,16 +20,14 @@ import androidx.annotation.StringRes
|
|||||||
import com.pitchedapps.frost.R
|
import com.pitchedapps.frost.R
|
||||||
import com.pitchedapps.frost.facebook.FbItem
|
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) {
|
enum class FeedSort(@StringRes val textRes: Int, val item: FbItem) {
|
||||||
DEFAULT(R.string.kau_default, FbItem.FEED),
|
DEFAULT(R.string.kau_default, FbItem.FEED),
|
||||||
MOST_RECENT(R.string.most_recent, FbItem.FEED_MOST_RECENT),
|
MOST_RECENT(R.string.most_recent, FbItem.FEED_MOST_RECENT),
|
||||||
TOP(R.string.top_stories, FbItem.FEED_TOP_STORIES);
|
TOP(R.string.top_stories, FbItem.FEED_TOP_STORIES);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val values = values() // save one instance
|
val values = values() // save one instance
|
||||||
operator fun invoke(index: Int) = values[index]
|
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.R
|
||||||
import com.pitchedapps.frost.injectors.ThemeProvider
|
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(
|
enum class MainActivityLayout(
|
||||||
val titleRes: Int,
|
val titleRes: Int,
|
||||||
val backgroundColor: (ThemeProvider) -> Int,
|
val backgroundColor: (ThemeProvider) -> Int,
|
||||||
val iconColor: (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(
|
companion object {
|
||||||
R.string.top_bar,
|
val values = values() // save one instance
|
||||||
{ it.headerColor },
|
operator fun invoke(index: Int) = values.getOrElse(index) { TOP_BAR }
|
||||||
{ 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 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -30,51 +30,45 @@ import com.pitchedapps.frost.views.FrostWebView
|
|||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 2017-09-16.
|
* Created by Allan Wang on 2017-09-16.
|
||||||
*
|
*
|
||||||
* Options for [WebOverlayActivityBase] to give more info as to what kind of
|
* Options for [WebOverlayActivityBase] to give more info as to what kind of overlay is present.
|
||||||
* overlay is present.
|
|
||||||
*
|
*
|
||||||
* For now, this is able to add new menu options upon first load
|
* For now, this is able to add new menu options upon first load
|
||||||
*/
|
*/
|
||||||
enum class OverlayContext(private val menuItem: FrostMenuItem?) : EnumBundle<OverlayContext> {
|
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)),
|
/** Inject the [menuItem] in the order that they are given at the front of the menu */
|
||||||
MESSAGE(FrostMenuItem(R.id.action_messages, FbItem.MESSAGES));
|
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) {
|
fun onOptionsItemSelected(web: FrostWebView, id: Int): Boolean {
|
||||||
menuItem?.addToMenu(context, menu, 0)
|
val item = values.firstOrNull { id == it.menuItem?.id }?.menuItem ?: return false
|
||||||
}
|
web.loadUrl(item.fbItem.url, true)
|
||||||
|
return true
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Frame for an injectable menu item */
|
||||||
* Frame for an injectable menu item
|
|
||||||
*/
|
|
||||||
class FrostMenuItem(
|
class FrostMenuItem(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val fbItem: FbItem,
|
val fbItem: FbItem,
|
||||||
val showAsAction: Int = MenuItem.SHOW_AS_ACTION_IF_ROOM
|
val showAsAction: Int = MenuItem.SHOW_AS_ACTION_IF_ROOM
|
||||||
) {
|
) {
|
||||||
fun addToMenu(context: Context, menu: Menu, index: Int) {
|
fun addToMenu(context: Context, menu: Menu, index: Int) {
|
||||||
val item = menu.add(Menu.NONE, id, index, fbItem.titleId)
|
val item = menu.add(Menu.NONE, id, index, fbItem.titleId)
|
||||||
item.icon = fbItem.icon.toDrawable(context, 18)
|
item.icon = fbItem.icon.toDrawable(context, 18)
|
||||||
item.setShowAsAction(showAsAction)
|
item.setShowAsAction(showAsAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,95 +23,85 @@ import com.pitchedapps.frost.R
|
|||||||
import com.pitchedapps.frost.prefs.sections.ThemePrefs
|
import com.pitchedapps.frost.prefs.sections.ThemePrefs
|
||||||
import java.util.Locale
|
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 FACEBOOK_BLUE = 0xff3b5998.toInt()
|
||||||
const val BLUE_LIGHT = 0xff5d86dd.toInt()
|
const val BLUE_LIGHT = 0xff5d86dd.toInt()
|
||||||
|
|
||||||
enum class Theme(
|
enum class Theme(
|
||||||
@StringRes val textRes: Int,
|
@StringRes val textRes: Int,
|
||||||
file: String?,
|
file: String?,
|
||||||
val textColorGetter: (ThemePrefs) -> Int,
|
val textColorGetter: (ThemePrefs) -> Int,
|
||||||
val accentColorGetter: (ThemePrefs) -> Int,
|
val accentColorGetter: (ThemePrefs) -> Int,
|
||||||
val backgroundColorGetter: (ThemePrefs) -> Int,
|
val backgroundColorGetter: (ThemePrefs) -> Int,
|
||||||
val headerColorGetter: (ThemePrefs) -> Int,
|
val headerColorGetter: (ThemePrefs) -> Int,
|
||||||
val iconColorGetter: (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(
|
@VisibleForTesting internal val file = file?.let { "$it.css" }
|
||||||
R.string.kau_default,
|
|
||||||
"default",
|
|
||||||
{ 0xde000000.toInt() },
|
|
||||||
{ FACEBOOK_BLUE },
|
|
||||||
{ 0xfffafafa.toInt() },
|
|
||||||
{ FACEBOOK_BLUE },
|
|
||||||
{ Color.WHITE }
|
|
||||||
),
|
|
||||||
|
|
||||||
LIGHT(
|
companion object {
|
||||||
R.string.kau_light,
|
val values = values() // save one instance
|
||||||
"material_light",
|
operator fun invoke(index: Int) = values[index]
|
||||||
{ 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]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ThemeCategory {
|
enum class ThemeCategory {
|
||||||
FACEBOOK, MESSENGER
|
FACEBOOK,
|
||||||
;
|
MESSENGER;
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting internal val folder = name.toLowerCase(Locale.CANADA)
|
||||||
internal val folder = name.toLowerCase(Locale.CANADA)
|
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.pitchedapps.frost.facebook
|
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 FACEBOOK_COM = "facebook.com"
|
||||||
const val MESSENGER_COM = "messenger.com"
|
const val MESSENGER_COM = "messenger.com"
|
||||||
const val FBCDN_NET = "fbcdn.net"
|
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 FB_URL_BASE = "https://$FACEBOOK_BASE_COM/"
|
||||||
const val FACEBOOK_MBASIC_COM = "mbasic.$FACEBOOK_COM"
|
const val FACEBOOK_MBASIC_COM = "mbasic.$FACEBOOK_COM"
|
||||||
const val FB_URL_MBASIC_BASE = "https://$FACEBOOK_MBASIC_COM/"
|
const val FB_URL_MBASIC_BASE = "https://$FACEBOOK_MBASIC_COM/"
|
||||||
|
|
||||||
fun profilePictureUrl(id: Long) = "https://graph.facebook.com/$id/picture?type=large"
|
fun profilePictureUrl(id: Long) = "https://graph.facebook.com/$id/picture?type=large"
|
||||||
|
|
||||||
const val FB_LOGIN_URL = "${FB_URL_BASE}login"
|
const val FB_LOGIN_URL = "${FB_URL_BASE}login"
|
||||||
const val FB_HOME_URL = "${FB_URL_BASE}home.php"
|
const val FB_HOME_URL = "${FB_URL_BASE}home.php"
|
||||||
const val MESSENGER_THREAD_PREFIX = "$HTTPS_MESSENGER_COM/t/"
|
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
|
// Default user agent
|
||||||
const val USER_AGENT_MOBILE_CONST =
|
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
|
// Desktop agent, for pages like messages
|
||||||
const val USER_AGENT_DESKTOP_CONST =
|
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
|
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
|
const val WEB_LOAD_DELAY = 50L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Additional delay for transition when called from commit.
|
* Additional delay for transition when called from commit. Note that transitions are also called
|
||||||
* Note that transitions are also called from onFinish, so this value
|
* from onFinish, so this value will never make a load slower than it is
|
||||||
* will never make a load slower than it is
|
|
||||||
*/
|
*/
|
||||||
const val WEB_COMMIT_LOAD_DELAY = 200L
|
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.L
|
||||||
import com.pitchedapps.frost.utils.cookies
|
import com.pitchedapps.frost.utils.cookies
|
||||||
import com.pitchedapps.frost.utils.launchLogin
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 2017-05-30.
|
* Created by Allan Wang on 2017-05-30.
|
||||||
*
|
*
|
||||||
* The following component manages all cookie transfers.
|
* The following component manages all cookie transfers.
|
||||||
*/
|
*/
|
||||||
class FbCookie @Inject internal constructor(
|
class FbCookie
|
||||||
private val prefs: Prefs,
|
@Inject
|
||||||
private val cookieDao: CookieDao
|
internal constructor(private val prefs: Prefs, private val cookieDao: CookieDao) {
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/** Domain information. Dot prefix still matters for Android browsers. */
|
||||||
* Domain information. Dot prefix still matters for Android browsers.
|
private const val FB_COOKIE_DOMAIN = ".$FACEBOOK_COM"
|
||||||
*/
|
private const val MESSENGER_COOKIE_DOMAIN = ".$MESSENGER_COM"
|
||||||
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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private suspend fun CookieManager.removeAllCookies(): Boolean = suspendCoroutine { cont ->
|
||||||
* Retrieves the facebook cookie if it exists
|
removeAllCookies { cont.resume(it) }
|
||||||
* Note that this is a synchronized call
|
}
|
||||||
*/
|
|
||||||
val webCookie: String?
|
|
||||||
get() = CookieManager.getInstance().getCookie(HTTPS_FACEBOOK_COM)
|
|
||||||
|
|
||||||
val messengerCookie: String?
|
suspend fun save(id: Long) {
|
||||||
get() = CookieManager.getInstance().getCookie(HTTPS_MESSENGER_COM)
|
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(
|
suspend fun reset() {
|
||||||
domain: String,
|
prefs.userId = -1L
|
||||||
cookie: String?
|
withContext(Dispatchers.Main + NonCancellable) {
|
||||||
): Boolean {
|
with(CookieManager.getInstance()) {
|
||||||
cookie ?: return true
|
removeAllCookies()
|
||||||
return withContext(NonCancellable) {
|
flush()
|
||||||
// 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 =
|
suspend fun switchUser(id: Long) {
|
||||||
suspendCoroutine { cont ->
|
val cookie = cookieDao.selectById(id) ?: return L.e { "No cookie for id" }
|
||||||
setCookie(domain, cookie.trim()) {
|
switchUser(cookie)
|
||||||
cont.resume(it)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun CookieManager.removeAllCookies(): Boolean = suspendCoroutine { cont ->
|
suspend fun switchUser(cookie: CookieEntity?) {
|
||||||
removeAllCookies {
|
if (cookie?.cookie == null) {
|
||||||
cont.resume(it)
|
L.d { "Switching User; null cookie" }
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
|
withContext(Dispatchers.Main + NonCancellable) {
|
||||||
suspend fun save(id: Long) {
|
L.d { "Switching User" }
|
||||||
L.d { "New cookie found" }
|
prefs.userId = cookie.id
|
||||||
prefs.userId = id
|
CookieManager.getInstance().apply {
|
||||||
CookieManager.getInstance().flush()
|
removeAllCookies()
|
||||||
val cookie = CookieEntity(prefs.userId, null, webCookie)
|
suspendSetWebCookie(FB_COOKIE_DOMAIN, cookie.cookie)
|
||||||
cookieDao.save(cookie)
|
suspendSetWebCookie(MESSENGER_COOKIE_DOMAIN, cookie.cookieMessenger)
|
||||||
|
flush()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun reset() {
|
/** Helper function to remove the current cookies and launch the proper login page */
|
||||||
prefs.userId = -1L
|
suspend fun logout(context: Context, deleteCookie: Boolean = true) {
|
||||||
withContext(Dispatchers.Main + NonCancellable) {
|
val cookies = arrayListOf<CookieEntity>()
|
||||||
with(CookieManager.getInstance()) {
|
if (context is Activity) cookies.addAll(context.cookies().filter { it.id != prefs.userId })
|
||||||
removeAllCookies()
|
logout(prefs.userId, deleteCookie)
|
||||||
flush()
|
context.launchLogin(cookies, true)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun switchUser(id: Long) {
|
/** Clear the cookies of the given id */
|
||||||
val cookie = cookieDao.selectById(id) ?: return L.e { "No cookie for id" }
|
suspend fun logout(id: Long, deleteCookie: Boolean = true) {
|
||||||
switchUser(cookie)
|
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) {
|
* Notifications may come from different accounts, and we need to switch the cookies to load them
|
||||||
L.d { "Switching User; null cookie" }
|
* When coming back to the main app, switch back to our original account before continuing
|
||||||
return
|
*/
|
||||||
}
|
suspend fun switchBackUser() {
|
||||||
withContext(Dispatchers.Main + NonCancellable) {
|
if (prefs.prevId == -1L) return
|
||||||
L.d { "Switching User" }
|
val prevId = prefs.prevId
|
||||||
prefs.userId = cookie.id
|
prefs.prevId = -1L
|
||||||
CookieManager.getInstance().apply {
|
if (prevId != prefs.userId) {
|
||||||
removeAllCookies()
|
switchUser(prevId)
|
||||||
suspendSetWebCookie(FB_COOKIE_DOMAIN, cookie.cookie)
|
L.d { "Switch back user" }
|
||||||
suspendSetWebCookie(MESSENGER_COOKIE_DOMAIN, cookie.cookieMessenger)
|
L._d { "${prefs.userId} to $prevId" }
|
||||||
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" }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,79 +29,73 @@ import com.pitchedapps.frost.utils.EnumBundleCompanion
|
|||||||
import com.pitchedapps.frost.utils.EnumCompanion
|
import com.pitchedapps.frost.utils.EnumCompanion
|
||||||
|
|
||||||
enum class FbItem(
|
enum class FbItem(
|
||||||
@StringRes val titleId: Int,
|
@StringRes val titleId: Int,
|
||||||
val icon: IIcon,
|
val icon: IIcon,
|
||||||
relativeUrl: String,
|
relativeUrl: String,
|
||||||
val fragmentCreator: () -> BaseFragment = ::WebFragment,
|
val fragmentCreator: () -> BaseFragment = ::WebFragment,
|
||||||
prefix: String = FB_URL_BASE
|
prefix: String = FB_URL_BASE
|
||||||
) : EnumBundle<FbItem> {
|
) : 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"),
|
* Unlike other urls, menus cannot be linked directly as it is a soft reference. Instead, we can
|
||||||
CHAT(R.string.chat, GoogleMaterial.Icon.gmd_chat, "buddylist"),
|
* pick any url with the blue bar and manually click to enter the menu.
|
||||||
EVENTS(R.string.events, GoogleMaterial.Icon.gmd_event_note, "events/upcoming"),
|
* We pick home.php as some back interactions default to home regardless of the base url.
|
||||||
FEED(R.string.feed, CommunityMaterial.Icon3.cmd_newspaper, ""),
|
*/
|
||||||
FEED_MOST_RECENT(R.string.most_recent, GoogleMaterial.Icon.gmd_history, "home.php?sk=h_chr"),
|
MENU(R.string.menu, GoogleMaterial.Icon.gmd_menu, "home.php"),
|
||||||
FEED_TOP_STORIES(R.string.top_stories, GoogleMaterial.Icon.gmd_star, "home.php?sk=h_nor"),
|
MESSAGES(R.string.messages, MaterialDesignIconic.Icon.gmi_comments, "messages"),
|
||||||
FRIENDS(R.string.friends, GoogleMaterial.Icon.gmd_person_add, "friends/center/requests"),
|
MESSENGER(
|
||||||
GROUPS(R.string.groups, GoogleMaterial.Icon.gmd_group, "groups"),
|
R.string.messenger,
|
||||||
MARKETPLACE(R.string.marketplace, GoogleMaterial.Icon.gmd_store, "marketplace"),
|
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 */
|
||||||
* Unlike other urls, menus cannot be linked directly as it is a soft reference. Instead, we can
|
_SEARCH(R.string.kau_search, GoogleMaterial.Icon.gmd_search, "search/top"),
|
||||||
* 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"),
|
|
||||||
|
|
||||||
/**
|
/** Non mbasic search cannot be parsed. */
|
||||||
* Note that this url only works if a query (?q=) is provided
|
_SEARCH_PARSE(
|
||||||
*/
|
R.string.kau_search,
|
||||||
_SEARCH(
|
GoogleMaterial.Icon.gmd_search,
|
||||||
R.string.kau_search,
|
"search/top",
|
||||||
GoogleMaterial.Icon.gmd_search,
|
prefix = FB_URL_MBASIC_BASE
|
||||||
"search/top"
|
),
|
||||||
),
|
SETTINGS(R.string.settings, GoogleMaterial.Icon.gmd_settings, "settings"),
|
||||||
|
;
|
||||||
|
|
||||||
/**
|
val url = "$prefix$relativeUrl"
|
||||||
* 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 isFeed: Boolean
|
||||||
|
get() =
|
||||||
|
when (this) {
|
||||||
|
FEED,
|
||||||
|
FEED_MOST_RECENT,
|
||||||
|
FEED_TOP_STORIES -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
val isFeed: Boolean
|
override val bundleContract: EnumBundleCompanion<FbItem>
|
||||||
get() = when (this) {
|
get() = Companion
|
||||||
FEED, FEED_MOST_RECENT, FEED_TOP_STORIES -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
override val bundleContract: EnumBundleCompanion<FbItem>
|
companion object : EnumCompanion<FbItem>("frost_arg_fb_item", values())
|
||||||
get() = Companion
|
|
||||||
|
|
||||||
companion object : EnumCompanion<FbItem>("frost_arg_fb_item", values())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun defaultTabs(): List<FbItem> =
|
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.
|
* Created by Allan Wang on 21/12/17.
|
||||||
*
|
*
|
||||||
* Collection of regex matchers
|
* Collection of regex matchers Input text must be properly unescaped
|
||||||
* Input text must be properly unescaped
|
|
||||||
*
|
*
|
||||||
* See [StringEscapeUtils]
|
* 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_DTSG_MATCHER: Regex by lazy { Regex("name=\"fb_dtsg\" value=\"(.*?)\"") }
|
||||||
val FB_REV_MATCHER: Regex by lazy { Regex("\"app_version\":\"(.*?)\"") }
|
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_USER_MATCHER: Regex = Regex("c_user=([0-9]*);")
|
||||||
|
|
||||||
val FB_EPOCH_MATCHER: Regex = Regex(":([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
|
* Custom url builder so we can easily test it without the Android framework
|
||||||
*/
|
*/
|
||||||
inline val String.formattedFbUrl: String
|
inline val String.formattedFbUrl: String
|
||||||
get() = FbUrlFormatter(this).toString()
|
get() = FbUrlFormatter(this).toString()
|
||||||
|
|
||||||
inline val Uri.formattedFbUri: Uri
|
inline val Uri.formattedFbUri: Uri
|
||||||
get() {
|
get() {
|
||||||
val url = toString()
|
val url = toString()
|
||||||
return if (url.startsWith("http")) Uri.parse(url.formattedFbUrl) else this
|
return if (url.startsWith("http")) Uri.parse(url.formattedFbUrl) else this
|
||||||
}
|
}
|
||||||
|
|
||||||
class FbUrlFormatter(url: String) {
|
class FbUrlFormatter(url: String) {
|
||||||
private val queries = mutableMapOf<String, String>()
|
private val queries = mutableMapOf<String, String>()
|
||||||
private val cleaned: String
|
private val cleaned: String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats all facebook urls
|
* Formats all facebook urls
|
||||||
*
|
*
|
||||||
* The order is very important:
|
* The order is very important:
|
||||||
* 1. Wrapper links (discardables) are stripped away, resulting in the actual link
|
* 1. Wrapper links (discardables) are stripped away, resulting in the actual link
|
||||||
* 2. CSS encoding is converted to normal encoding
|
* 2. CSS encoding is converted to normal encoding
|
||||||
* 3. Url is completely decoded
|
* 3. Url is completely decoded
|
||||||
* 4. Url is split into sections
|
* 4. Url is split into sections
|
||||||
*/
|
*/
|
||||||
init {
|
init {
|
||||||
cleaned = clean(url)
|
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
|
||||||
}
|
}
|
||||||
|
cleanedUrl = cleanedUrl.replace("&", "&")
|
||||||
fun clean(url: String): String {
|
if (changed && !cleanedUrl.contains("?")) // ensure we aren't missing '?'
|
||||||
if (url.isBlank()) return ""
|
cleanedUrl = cleanedUrl.replaceFirst("&", "?")
|
||||||
var cleanedUrl = url
|
val qm = cleanedUrl.indexOf("?")
|
||||||
if (cleanedUrl.startsWith("#!")) cleanedUrl = cleanedUrl.substring(2)
|
if (qm > -1) {
|
||||||
val urlRef = cleanedUrl
|
cleanedUrl.substring(qm + 1).split("&").forEach {
|
||||||
discardable.forEach { cleanedUrl = cleanedUrl.replace(it, "", true) }
|
val p = it.split("=")
|
||||||
val changed = cleanedUrl != urlRef
|
queries[p[0]] = p.elementAtOrNull(1) ?: ""
|
||||||
converter.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v, true) }
|
}
|
||||||
try {
|
cleanedUrl = cleanedUrl.substring(0, qm)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
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)
|
append(cleaned)
|
||||||
if (queries.isNotEmpty()) {
|
if (queries.isNotEmpty()) {
|
||||||
append("?")
|
append("?")
|
||||||
queries.forEach { (k, v) ->
|
queries.forEach { (k, v) ->
|
||||||
if (v.isEmpty()) {
|
if (v.isEmpty()) {
|
||||||
append("${k.urlEncode()}&")
|
append("${k.urlEncode()}&")
|
||||||
} else {
|
} else {
|
||||||
append("${k.urlEncode()}=${v.urlEncode()}&")
|
append("${k.urlEncode()}=${v.urlEncode()}&")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.removeSuffix("&")
|
}
|
||||||
|
.removeSuffix("&")
|
||||||
|
|
||||||
fun toLogList(): List<String> {
|
fun toLogList(): List<String> {
|
||||||
val list = mutableListOf(cleaned)
|
val list = mutableListOf(cleaned)
|
||||||
queries.forEach { (k, v) -> list.add("\n- $k\t=\t$v") }
|
queries.forEach { (k, v) -> list.add("\n- $k\t=\t$v") }
|
||||||
list.add("\n\n${toString()}")
|
list.add("\n\n${toString()}")
|
||||||
return list
|
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
|
* Items here are explicitly removed from the url Taken from FaceSlim
|
||||||
* Taken from FaceSlim
|
* https://github.com/indywidualny/FaceSlim/blob/master/app/src/main/java/org/indywidualni/fblite/util/Miscellany.java
|
||||||
* 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
|
||||||
* Note: Typically, in this case, the redirect url should have all the necessary queries
|
* unsure how Facebook reacts in all cases, so the ones after the redirect are appended on
|
||||||
* I am unsure how Facebook reacts in all cases, so the ones after the redirect are appended on afterwards
|
* afterwards That shouldn't break anything
|
||||||
* That shouldn't break anything
|
*/
|
||||||
*/
|
val discardable =
|
||||||
val discardable = arrayOf(
|
arrayOf(
|
||||||
"http://lm.facebook.com/l.php?u=",
|
"http://lm.facebook.com/l.php?u=",
|
||||||
"https://lm.facebook.com/l.php?u=",
|
"https://lm.facebook.com/l.php?u=",
|
||||||
"http://m.facebook.com/l.php?u=",
|
"http://m.facebook.com/l.php?u=",
|
||||||
"https://m.facebook.com/l.php?u=",
|
"https://m.facebook.com/l.php?u=",
|
||||||
"http://touch.facebook.com/l.php?u=",
|
"http://touch.facebook.com/l.php?u=",
|
||||||
"https://touch.facebook.com/l.php?u=",
|
"https://touch.facebook.com/l.php?u=",
|
||||||
VIDEO_REDIRECT
|
VIDEO_REDIRECT
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries that are not necessary for independent links
|
* Queries that are not necessary for independent links
|
||||||
*
|
*
|
||||||
* acontext is not required for "friends interested in" notifications
|
* acontext is not required for "friends interested in" notifications
|
||||||
*/
|
*/
|
||||||
val discardableQueries = arrayOf(
|
val discardableQueries =
|
||||||
"ref",
|
arrayOf(
|
||||||
"refid",
|
"ref",
|
||||||
"SharedWith",
|
"refid",
|
||||||
"fbclid",
|
"SharedWith",
|
||||||
"h",
|
"fbclid",
|
||||||
"_ft_",
|
"h",
|
||||||
"_tn_",
|
"_ft_",
|
||||||
"_xt_",
|
"_tn_",
|
||||||
"bacr",
|
"_xt_",
|
||||||
"frefs",
|
"bacr",
|
||||||
"hc_ref",
|
"frefs",
|
||||||
"loc_ref",
|
"hc_ref",
|
||||||
"pn_ref"
|
"loc_ref",
|
||||||
)
|
"pn_ref"
|
||||||
|
)
|
||||||
|
|
||||||
val converter = listOf(
|
val converter =
|
||||||
"\\3C " to "%3C", "\\3E " to "%3E", "\\23 " to "%23", "\\25 " to "%25",
|
listOf(
|
||||||
"\\7B " to "%7B", "\\7D " to "%7D", "\\7C " to "%7C", "\\5C " to "%5C",
|
"\\3C " to "%3C",
|
||||||
"\\5E " to "%5E", "\\7E " to "%7E", "\\5B " to "%5B", "\\5D " to "%5D",
|
"\\3E " to "%3E",
|
||||||
"\\60 " to "%60", "\\3B " to "%3B", "\\2F " to "%2F", "\\3F " to "%3F",
|
"\\23 " to "%23",
|
||||||
"\\3A " to "%3A", "\\40 " to "%40", "\\3D " to "%3D", "\\26 " to "%26",
|
"\\25 " to "%25",
|
||||||
"\\24 " to "%24", "\\2B " to "%2B", "\\22 " to "%22", "\\2C " to "%2C",
|
"\\7B " to "%7B",
|
||||||
"\\20 " to "%20"
|
"\\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()
|
object BadgeParser : FrostParser<FrostBadges> by BadgeParserImpl()
|
||||||
|
|
||||||
data class FrostBadges(
|
data class FrostBadges(
|
||||||
val feed: String?,
|
val feed: String?,
|
||||||
val friends: String?,
|
val friends: String?,
|
||||||
val messages: String?,
|
val messages: String?,
|
||||||
val notifications: String?
|
val notifications: String?
|
||||||
) : ParseData {
|
) : ParseData {
|
||||||
override val isEmpty: Boolean
|
override val isEmpty: Boolean
|
||||||
get() = feed.isNullOrEmpty() &&
|
get() =
|
||||||
friends.isNullOrEmpty() &&
|
feed.isNullOrEmpty() &&
|
||||||
messages.isNullOrEmpty() &&
|
friends.isNullOrEmpty() &&
|
||||||
notifications.isNullOrEmpty()
|
messages.isNullOrEmpty() &&
|
||||||
|
notifications.isNullOrEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private class BadgeParserImpl : FrostParserBase<FrostBadges>(false) {
|
private class BadgeParserImpl : FrostParserBase<FrostBadges>(false) {
|
||||||
// Not actually displayed
|
// Not actually displayed
|
||||||
override var nameRes: Int = R.string.frost_name
|
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? {
|
override fun parseImpl(doc: Document): FrostBadges? {
|
||||||
val header = doc.getElementById("header") ?: return null
|
val header = doc.getElementById("header") ?: return null
|
||||||
if (header.select("[data-sigil=count]").isEmpty())
|
if (header.select("[data-sigil=count]").isEmpty()) return null
|
||||||
return null
|
val (feed, requests, messages, notifications) =
|
||||||
val (feed, requests, messages, notifications) = listOf(
|
listOf("feed", "requests", "messages", "notifications")
|
||||||
"feed",
|
.map { "[data-sigil*=$it] [data-sigil=count]" }
|
||||||
"requests",
|
.map { doc.select(it) }
|
||||||
"messages",
|
.map { e -> e?.getOrNull(0)?.ownText() }
|
||||||
"notifications"
|
return FrostBadges(
|
||||||
)
|
feed = feed,
|
||||||
.map { "[data-sigil*=$it] [data-sigil=count]" }
|
friends = requests,
|
||||||
.map { doc.select(it) }
|
messages = messages,
|
||||||
.map { e -> e?.getOrNull(0)?.ownText() }
|
notifications = notifications
|
||||||
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.
|
* Created by Allan Wang on 2017-10-06.
|
||||||
*
|
*
|
||||||
* Interface for a given parser
|
* Interface for a given parser Use cases should be attached as delegates to objects that implement
|
||||||
* Use cases should be attached as delegates to objects that implement this interface
|
* this interface
|
||||||
*
|
*
|
||||||
* In all cases, parsing will be done from a JSoup document
|
* In all cases, parsing will be done from a JSoup document Variants accepting strings are also
|
||||||
* Variants accepting strings are also permitted, and they will be converted to documents accordingly
|
* permitted, and they will be converted to documents accordingly The return type must be nonnull if
|
||||||
* The return type must be nonnull if no parsing errors occurred, as null signifies a parse error
|
* no parsing errors occurred, as null signifies a parse error If null really must be allowed, use
|
||||||
* If null really must be allowed, use Optionals
|
* Optionals
|
||||||
*/
|
*/
|
||||||
interface FrostParser<out T : ParseData> {
|
interface FrostParser<out T : ParseData> {
|
||||||
|
|
||||||
/**
|
/** Name associated to parser Purely for display */
|
||||||
* Name associated to parser
|
var nameRes: Int
|
||||||
* Purely for display
|
|
||||||
*/
|
|
||||||
var nameRes: Int
|
|
||||||
|
|
||||||
/**
|
/** Url to request from */
|
||||||
* Url to request from
|
val url: String
|
||||||
*/
|
|
||||||
val url: String
|
|
||||||
|
|
||||||
/**
|
/** Call parsing with default implementation using cookie */
|
||||||
* Call parsing with default implementation using cookie
|
fun parse(cookie: String?): ParseResponse<T>?
|
||||||
*/
|
|
||||||
fun parse(cookie: String?): ParseResponse<T>?
|
|
||||||
|
|
||||||
/**
|
/** Call parsing with given document */
|
||||||
* Call parsing with given document
|
fun parse(cookie: String?, document: Document): ParseResponse<T>?
|
||||||
*/
|
|
||||||
fun parse(cookie: String?, document: Document): ParseResponse<T>?
|
|
||||||
|
|
||||||
/**
|
/** Call parsing using jsoup to fetch from given url */
|
||||||
* Call parsing using jsoup to fetch from given url
|
fun parseFromUrl(cookie: String?, url: String): ParseResponse<T>?
|
||||||
*/
|
|
||||||
fun parseFromUrl(cookie: String?, url: String): ParseResponse<T>?
|
|
||||||
|
|
||||||
/**
|
/** Call parsing with given data */
|
||||||
* Call parsing with given data
|
fun parseFromData(cookie: String?, text: String): ParseResponse<T>?
|
||||||
*/
|
|
||||||
fun parseFromData(cookie: String?, text: String): ParseResponse<T>?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const val FALLBACK_TIME_MOD = 1000000
|
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 FrostLink(val text: String, val href: String)
|
||||||
|
|
||||||
data class ParseResponse<out T : ParseData>(val cookie: String, val data: T) {
|
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 {
|
interface ParseData {
|
||||||
val isEmpty: Boolean
|
val isEmpty: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParseNotification : ParseData {
|
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 {
|
internal fun <T> List<T>.toJsonString(tag: String, indent: Int) =
|
||||||
val tabs = "\t".repeat(indent)
|
StringBuilder()
|
||||||
append("$tabs$tag: [\n\t$tabs")
|
.apply {
|
||||||
append(this@toJsonString.joinToString("\n\t$tabs"))
|
val tabs = "\t".repeat(indent)
|
||||||
append("\n$tabs]\n")
|
append("$tabs$tag: [\n\t$tabs")
|
||||||
}.toString()
|
append(this@toJsonString.joinToString("\n\t$tabs"))
|
||||||
|
append("\n$tabs]\n")
|
||||||
|
}
|
||||||
|
.toString()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* T should have a readable toString() function
|
* T should have a readable toString() function [redirectToText] dictates whether all data should be
|
||||||
* [redirectToText] dictates whether all data should be converted to text then back to document before parsing
|
* converted to text then back to document before parsing
|
||||||
*/
|
*/
|
||||||
internal abstract class FrostParserBase<out T : ParseData>(private val redirectToText: Boolean) :
|
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>? {
|
final override fun parseFromData(cookie: String?, text: String): ParseResponse<T>? {
|
||||||
cookie ?: return null
|
cookie ?: return null
|
||||||
val doc = textToDoc(text) ?: return null
|
val doc = textToDoc(text) ?: return null
|
||||||
val data = parseImpl(doc) ?: return null
|
val data = parseImpl(doc) ?: return null
|
||||||
return ParseResponse(cookie, data)
|
return ParseResponse(cookie, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
final override fun parseFromUrl(cookie: String?, url: String): ParseResponse<T>? =
|
final override fun parseFromUrl(cookie: String?, url: String): ParseResponse<T>? =
|
||||||
parse(cookie, frostJsoup(cookie, url))
|
parse(cookie, frostJsoup(cookie, url))
|
||||||
|
|
||||||
override fun parse(cookie: String?, document: Document): ParseResponse<T>? {
|
override fun parse(cookie: String?, document: Document): ParseResponse<T>? {
|
||||||
cookie ?: return null
|
cookie ?: return null
|
||||||
if (redirectToText)
|
if (redirectToText) return parseFromData(cookie, document.toString())
|
||||||
return parseFromData(cookie, document.toString())
|
val data = parseImpl(document) ?: return null
|
||||||
val data = parseImpl(document) ?: return null
|
return ParseResponse(cookie, data)
|
||||||
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
|
* Attempts to find inner <i> element with some style containing a url Returns the formatted url,
|
||||||
* Returns the formatted url, or an empty string if nothing was found
|
* or an empty string if nothing was found
|
||||||
*/
|
*/
|
||||||
protected fun Element.getInnerImgStyle(): String? =
|
protected fun Element.getInnerImgStyle(): String? = select("i.img[style*=url]").getStyleUrl()
|
||||||
select("i.img[style*=url]").getStyleUrl()
|
|
||||||
|
|
||||||
protected fun Elements.getStyleUrl(): String? =
|
protected fun Elements.getStyleUrl(): String? =
|
||||||
FB_CSS_URL_MATCHER.find(attr("style"))[1]?.formattedFbUrl
|
FB_CSS_URL_MATCHER.find(attr("style"))[1]?.formattedFbUrl
|
||||||
|
|
||||||
protected open fun textToDoc(text: String): Document? =
|
protected open fun textToDoc(text: String): Document? =
|
||||||
if (!redirectToText) Jsoup.parse(text)
|
if (!redirectToText) Jsoup.parse(text)
|
||||||
else throw RuntimeException("${this::class.java.simpleName} requires text redirect but did not implement textToDoc")
|
else
|
||||||
|
throw RuntimeException(
|
||||||
|
"${this::class.java.simpleName} requires text redirect but did not implement textToDoc"
|
||||||
|
)
|
||||||
|
|
||||||
protected fun parseLink(element: Element?): FrostLink? {
|
protected fun parseLink(element: Element?): FrostLink? {
|
||||||
val a = element?.getElementsByTag("a")?.first() ?: return null
|
val a = element?.getElementsByTag("a")?.first() ?: return null
|
||||||
return FrostLink(a.text(), a.attr("href"))
|
return FrostLink(a.text(), a.attr("href"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,126 +33,128 @@ import org.jsoup.nodes.Element
|
|||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 2017-10-06.
|
* Created by Allan Wang on 2017-10-06.
|
||||||
*
|
*
|
||||||
* In Facebook, messages are passed through scripts and loaded into view via react afterwards
|
* In Facebook, messages are passed through scripts and loaded into view via react afterwards We can
|
||||||
* We can parse out the content we want directly and load it ourselves
|
* parse out the content we want directly and load it ourselves
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
object MessageParser : FrostParser<FrostMessages> by MessageParserImpl() {
|
object MessageParser : FrostParser<FrostMessages> by MessageParserImpl() {
|
||||||
|
|
||||||
fun queryUser(cookie: String?, name: String) =
|
fun queryUser(cookie: String?, name: String) =
|
||||||
parseFromUrl(cookie, "${FbItem.MESSAGES.url}/?q=${name.urlEncode()}")
|
parseFromUrl(cookie, "${FbItem.MESSAGES.url}/?q=${name.urlEncode()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FrostMessages(
|
data class FrostMessages(
|
||||||
val threads: List<FrostThread>,
|
val threads: List<FrostThread>,
|
||||||
val seeMore: FrostLink?,
|
val seeMore: FrostLink?,
|
||||||
val extraLinks: List<FrostLink>
|
val extraLinks: List<FrostLink>
|
||||||
) : ParseNotification {
|
) : ParseNotification {
|
||||||
|
|
||||||
override val isEmpty: Boolean
|
override val isEmpty: Boolean
|
||||||
get() = threads.isEmpty()
|
get() = threads.isEmpty()
|
||||||
|
|
||||||
override fun toString() = StringBuilder().apply {
|
override fun toString() =
|
||||||
|
StringBuilder()
|
||||||
|
.apply {
|
||||||
append("FrostMessages {\n")
|
append("FrostMessages {\n")
|
||||||
append(threads.toJsonString("threads", 1))
|
append(threads.toJsonString("threads", 1))
|
||||||
append("\tsee more: $seeMore\n")
|
append("\tsee more: $seeMore\n")
|
||||||
append(extraLinks.toJsonString("extra links", 1))
|
append(extraLinks.toJsonString("extra links", 1))
|
||||||
append("}")
|
append("}")
|
||||||
}.toString()
|
}
|
||||||
|
.toString()
|
||||||
|
|
||||||
override fun getUnreadNotifications(data: CookieEntity) =
|
override fun getUnreadNotifications(data: CookieEntity) =
|
||||||
threads.asSequence().filter(FrostThread::unread).map {
|
threads
|
||||||
with(it) {
|
.asSequence()
|
||||||
NotificationContent(
|
.filter(FrostThread::unread)
|
||||||
data = data,
|
.map {
|
||||||
id = id,
|
with(it) {
|
||||||
href = url,
|
NotificationContent(
|
||||||
title = title,
|
data = data,
|
||||||
text = content ?: "",
|
id = id,
|
||||||
timestamp = time,
|
href = url,
|
||||||
profileUrl = img,
|
title = title,
|
||||||
unread = unread
|
text = content ?: "",
|
||||||
)
|
timestamp = time,
|
||||||
}
|
profileUrl = img,
|
||||||
}.toList()
|
unread = unread
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [id] user/thread id, or current time fallback
|
* [id] user/thread id, or current time fallback [img] parsed url for profile img [time] time of
|
||||||
* [img] parsed url for profile img
|
* message [url] link to thread [unread] true if image is unread, false otherwise [content] optional
|
||||||
* [time] time of message
|
* string for thread
|
||||||
* [url] link to thread
|
|
||||||
* [unread] true if image is unread, false otherwise
|
|
||||||
* [content] optional string for thread
|
|
||||||
*/
|
*/
|
||||||
data class FrostThread(
|
data class FrostThread(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val img: String?,
|
val img: String?,
|
||||||
val title: String,
|
val title: String,
|
||||||
val time: Long,
|
val time: Long,
|
||||||
val url: String,
|
val url: String,
|
||||||
val unread: Boolean,
|
val unread: Boolean,
|
||||||
val content: String?,
|
val content: String?,
|
||||||
val contentImgUrl: String?
|
val contentImgUrl: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
private class MessageParserImpl : FrostParserBase<FrostMessages>(true) {
|
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? {
|
override fun textToDoc(text: String): Document? {
|
||||||
var content = StringEscapeUtils.unescapeEcmaScript(text)
|
var content = StringEscapeUtils.unescapeEcmaScript(text)
|
||||||
val begin = content.indexOf("id=\"threadlist_rows\"")
|
val begin = content.indexOf("id=\"threadlist_rows\"")
|
||||||
if (begin <= 0) {
|
if (begin <= 0) {
|
||||||
L.d { "Threadlist not found" }
|
L.d { "Threadlist not found" }
|
||||||
return null
|
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")
|
|
||||||
}
|
}
|
||||||
|
content = content.substring(begin)
|
||||||
override fun parseImpl(doc: Document): FrostMessages? {
|
val end = content.indexOf("</script>")
|
||||||
val threadList = doc.getElementById("threadlist_rows") ?: return null
|
if (end <= 0) {
|
||||||
val threads: List<FrostThread> =
|
L.d { "Script tail not found" }
|
||||||
threadList.getElementsByAttributeValueMatching(
|
return null
|
||||||
"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(0, end).substringBeforeLast("</div>")
|
||||||
|
return Jsoup.parseBodyFragment("<div $content")
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseMessage(element: Element): FrostThread? {
|
override fun parseImpl(doc: Document): FrostMessages? {
|
||||||
val a = element.getElementsByTag("a").first() ?: return null
|
val threadList = doc.getElementById("threadlist_rows") ?: return null
|
||||||
val abbr = element.getElementsByTag("abbr")
|
val threads: List<FrostThread> =
|
||||||
val epoch = FB_EPOCH_MATCHER.find(abbr.attr("data-store"))[1]?.toLongOrNull() ?: -1L
|
threadList
|
||||||
// fetch id
|
.getElementsByAttributeValueMatching("id", ".*${FB_MESSAGE_NOTIF_ID_MATCHER.pattern}.*")
|
||||||
val id = FB_MESSAGE_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull()
|
.mapNotNull(this::parseMessage)
|
||||||
?: System.currentTimeMillis() % FALLBACK_TIME_MOD
|
val seeMore = parseLink(doc.getElementById("see_older_threads"))
|
||||||
val snippet = element.select("span.snippet").firstOrNull()
|
val extraLinks =
|
||||||
val content = snippet?.text()?.trim()
|
threadList.nextElementSibling()?.select("a")?.mapNotNull(this::parseLink) ?: emptyList()
|
||||||
val contentImg = snippet?.select("i[style*=url]")?.getStyleUrl()
|
return FrostMessages(threads, seeMore, extraLinks)
|
||||||
val img = element.getInnerImgStyle()
|
}
|
||||||
return FrostThread(
|
|
||||||
id = id,
|
private fun parseMessage(element: Element): FrostThread? {
|
||||||
img = img,
|
val a = element.getElementsByTag("a").first() ?: return null
|
||||||
title = a.text(),
|
val abbr = element.getElementsByTag("abbr")
|
||||||
time = epoch,
|
val epoch = FB_EPOCH_MATCHER.find(abbr.attr("data-store"))[1]?.toLongOrNull() ?: -1L
|
||||||
url = a.attr("href").formattedFbUrl,
|
// fetch id
|
||||||
unread = !element.hasClass("acw"),
|
val id =
|
||||||
content = content,
|
FB_MESSAGE_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull()
|
||||||
contentImgUrl = contentImg
|
?: 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.Document
|
||||||
import org.jsoup.nodes.Element
|
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()
|
object NotifParser : FrostParser<FrostNotifs> by NotifParserImpl()
|
||||||
|
|
||||||
data class FrostNotifs(
|
data class FrostNotifs(val notifs: List<FrostNotif>, val seeMore: FrostLink?) : ParseNotification {
|
||||||
val notifs: List<FrostNotif>,
|
|
||||||
val seeMore: FrostLink?
|
|
||||||
) : ParseNotification {
|
|
||||||
|
|
||||||
override val isEmpty: Boolean
|
override val isEmpty: Boolean
|
||||||
get() = notifs.isEmpty()
|
get() = notifs.isEmpty()
|
||||||
|
|
||||||
override fun toString() = StringBuilder().apply {
|
override fun toString() =
|
||||||
|
StringBuilder()
|
||||||
|
.apply {
|
||||||
append("FrostNotifs {\n")
|
append("FrostNotifs {\n")
|
||||||
append(notifs.toJsonString("notifs", 1))
|
append(notifs.toJsonString("notifs", 1))
|
||||||
append("\tsee more: $seeMore\n")
|
append("\tsee more: $seeMore\n")
|
||||||
append("}")
|
append("}")
|
||||||
}.toString()
|
}
|
||||||
|
.toString()
|
||||||
|
|
||||||
override fun getUnreadNotifications(data: CookieEntity) =
|
override fun getUnreadNotifications(data: CookieEntity) =
|
||||||
notifs.asSequence().filter(FrostNotif::unread).map {
|
notifs
|
||||||
with(it) {
|
.asSequence()
|
||||||
NotificationContent(
|
.filter(FrostNotif::unread)
|
||||||
data = data,
|
.map {
|
||||||
id = id,
|
with(it) {
|
||||||
href = url,
|
NotificationContent(
|
||||||
title = null,
|
data = data,
|
||||||
text = content,
|
id = id,
|
||||||
timestamp = time,
|
href = url,
|
||||||
profileUrl = img,
|
title = null,
|
||||||
unread = unread
|
text = content,
|
||||||
)
|
timestamp = time,
|
||||||
}
|
profileUrl = img,
|
||||||
}.toList()
|
unread = unread
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [id] notif id, or current time fallback
|
* [id] notif id, or current time fallback [img] parsed url for profile img [time] time of message
|
||||||
* [img] parsed url for profile img
|
* [url] link to thread [unread] true if image is unread, false otherwise [content] optional string
|
||||||
* [time] time of message
|
* for thread [timeString] text version of time from Facebook [thumbnailUrl] optional thumbnail url
|
||||||
* [url] link to thread
|
* if existent
|
||||||
* [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(
|
data class FrostNotif(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val img: String?,
|
val img: String?,
|
||||||
val time: Long,
|
val time: Long,
|
||||||
val url: String,
|
val url: String,
|
||||||
val unread: Boolean,
|
val unread: Boolean,
|
||||||
val content: String,
|
val content: String,
|
||||||
val timeString: String,
|
val timeString: String,
|
||||||
val thumbnailUrl: String?
|
val thumbnailUrl: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
private class NotifParserImpl : FrostParserBase<FrostNotifs>(false) {
|
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? {
|
override fun parseImpl(doc: Document): FrostNotifs? {
|
||||||
val notificationList = doc.getElementById("notifications_list") ?: return null
|
val notificationList = doc.getElementById("notifications_list") ?: return null
|
||||||
val notifications = notificationList
|
val notifications =
|
||||||
.getElementsByAttributeValueMatching("id", ".*${FB_NOTIF_ID_MATCHER.pattern}.*")
|
notificationList
|
||||||
.mapNotNull(this::parseNotif)
|
.getElementsByAttributeValueMatching("id", ".*${FB_NOTIF_ID_MATCHER.pattern}.*")
|
||||||
val seeMore =
|
.mapNotNull(this::parseNotif)
|
||||||
parseLink(doc.getElementsByAttributeValue("href", "/notifications.php?more").first())
|
val seeMore =
|
||||||
return FrostNotifs(notifications, seeMore)
|
parseLink(doc.getElementsByAttributeValue("href", "/notifications.php?more").first())
|
||||||
}
|
return FrostNotifs(notifications, seeMore)
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseNotif(element: Element): FrostNotif? {
|
private fun parseNotif(element: Element): FrostNotif? {
|
||||||
val a = element.getElementsByTag("a").first() ?: return null
|
val a = element.getElementsByTag("a").first() ?: return null
|
||||||
a.selectFirst("span.accessible_elem")?.remove()
|
a.selectFirst("span.accessible_elem")?.remove()
|
||||||
val abbr = element.getElementsByTag("abbr")
|
val abbr = element.getElementsByTag("abbr")
|
||||||
val epoch = FB_EPOCH_MATCHER.find(abbr.attr("data-store"))[1]?.toLongOrNull() ?: -1L
|
val epoch = FB_EPOCH_MATCHER.find(abbr.attr("data-store"))[1]?.toLongOrNull() ?: -1L
|
||||||
// fetch id
|
// fetch id
|
||||||
val id = FB_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull()
|
val id =
|
||||||
?: System.currentTimeMillis() % FALLBACK_TIME_MOD
|
FB_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull()
|
||||||
val img = element.getInnerImgStyle()
|
?: System.currentTimeMillis() % FALLBACK_TIME_MOD
|
||||||
val timeString = abbr.text()
|
val img = element.getInnerImgStyle()
|
||||||
val content =
|
val timeString = abbr.text()
|
||||||
a.text().replace("\u00a0", " ").removeSuffix(timeString).trim() // remove
|
val content = a.text().replace("\u00a0", " ").removeSuffix(timeString).trim() // remove
|
||||||
val thumbnail = element.selectFirst("img.thumbnail")?.attr("src")
|
val thumbnail = element.selectFirst("img.thumbnail")?.attr("src")
|
||||||
return FrostNotif(
|
return FrostNotif(
|
||||||
id = id,
|
id = id,
|
||||||
img = img,
|
img = img,
|
||||||
time = epoch,
|
time = epoch,
|
||||||
url = a.attr("href").formattedFbUrl,
|
url = a.attr("href").formattedFbUrl,
|
||||||
unread = !element.hasClass("acw"),
|
unread = !element.hasClass("acw"),
|
||||||
content = content,
|
content = content,
|
||||||
timeString = timeString,
|
timeString = timeString,
|
||||||
thumbnailUrl = if (thumbnail?.isNotEmpty() == true) thumbnail else null
|
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.Document
|
||||||
import org.jsoup.nodes.Element
|
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() {
|
object SearchParser : FrostParser<FrostSearches> by SearchParserImpl() {
|
||||||
fun query(cookie: String?, input: String): ParseResponse<FrostSearches>? {
|
fun query(cookie: String?, input: String): ParseResponse<FrostSearches>? {
|
||||||
val url =
|
val url = "${FbItem._SEARCH_PARSE.url}/?q=${if (input.isNotBlank()) input.urlEncode() else "a"}"
|
||||||
"${FbItem._SEARCH_PARSE.url}/?q=${if (input.isNotBlank()) input.urlEncode() else "a"}"
|
L._i { "Search Query $url" }
|
||||||
L._i { "Search Query $url" }
|
return parseFromUrl(cookie, url)
|
||||||
return parseFromUrl(cookie, url)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class SearchKeys(val key: String) {
|
enum class SearchKeys(val key: String) {
|
||||||
USERS("keywords_users"),
|
USERS("keywords_users"),
|
||||||
EVENTS("keywords_events")
|
EVENTS("keywords_events")
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FrostSearches(val results: List<FrostSearch>) : ParseData {
|
data class FrostSearches(val results: List<FrostSearch>) : ParseData {
|
||||||
|
|
||||||
override val isEmpty: Boolean
|
override val isEmpty: Boolean
|
||||||
get() = results.isEmpty()
|
get() = results.isEmpty()
|
||||||
|
|
||||||
override fun toString() = StringBuilder().apply {
|
override fun toString() =
|
||||||
|
StringBuilder()
|
||||||
|
.apply {
|
||||||
append("FrostSearches {\n")
|
append("FrostSearches {\n")
|
||||||
append(results.toJsonString("results", 1))
|
append(results.toJsonString("results", 1))
|
||||||
append("}")
|
append("}")
|
||||||
}.toString()
|
}
|
||||||
|
.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* As far as I'm aware, all links are independent, so the queries don't matter
|
* As far as I'm aware, all links are independent, so the queries don't matter A lot of it is
|
||||||
* A lot of it is tracking information, which I'll strip away
|
* tracking information, which I'll strip away Other text items are formatted for safety
|
||||||
* Other text items are formatted for safety
|
|
||||||
*
|
*
|
||||||
* Note that it's best to create search results from [create]
|
* Note that it's best to create search results from [create]
|
||||||
*/
|
*/
|
||||||
data class FrostSearch(val href: String, val title: String, val description: String?) {
|
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 {
|
companion object {
|
||||||
fun create(href: String, title: String, description: String?) = FrostSearch(
|
fun create(href: String, title: String, description: String?) =
|
||||||
with(href.indexOf("?")) { if (this == -1) href else href.substring(0, this) },
|
FrostSearch(
|
||||||
title.format(),
|
with(href.indexOf("?")) { if (this == -1) href else href.substring(0, this) },
|
||||||
description?.format()
|
title.format(),
|
||||||
)
|
description?.format()
|
||||||
}
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SearchParserImpl : FrostParserBase<FrostSearches>(false) {
|
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
|
private val String.formattedSearchUrl: String
|
||||||
get() = replace(FACEBOOK_MBASIC_COM, FACEBOOK_BASE_COM)
|
get() = replace(FACEBOOK_MBASIC_COM, FACEBOOK_BASE_COM)
|
||||||
|
|
||||||
override fun parseImpl(doc: Document): FrostSearches? {
|
override fun parseImpl(doc: Document): FrostSearches? {
|
||||||
val container: Element = doc.getElementById("BrowseResultsContainer")
|
val container: Element =
|
||||||
?: doc.getElementById("root")
|
doc.getElementById("BrowseResultsContainer") ?: doc.getElementById("root") ?: return null
|
||||||
?: return null
|
|
||||||
|
|
||||||
return FrostSearches(
|
return FrostSearches(
|
||||||
container.select("table[role=presentation]").mapNotNull { el ->
|
container.select("table[role=presentation]").mapNotNull { el ->
|
||||||
// Our assumption is that search entries start with an image, followed by general info
|
// 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
|
// 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
|
// Furthermore, the <td /> entry wraps a link, containing all the necessary info
|
||||||
val a = el.select("td")
|
val a = el.select("td").getOrNull(1)?.selectFirst("a") ?: return@mapNotNull null
|
||||||
.getOrNull(1)
|
val url =
|
||||||
?.selectFirst("a")
|
a.attr("href").takeIf { it.isNotEmpty() }?.formattedFbUrl?.formattedSearchUrl
|
||||||
?: return@mapNotNull null
|
?: return@mapNotNull null
|
||||||
val url =
|
// Currently, children should all be <div /> elements, where the first entry is the
|
||||||
a.attr("href").takeIf { it.isNotEmpty() }
|
// name/title
|
||||||
?.formattedFbUrl?.formattedSearchUrl
|
// And the other entries are additional info.
|
||||||
?: return@mapNotNull null
|
// There are also cases of nested tables, eg for the "join" button in groups.
|
||||||
// Currently, children should all be <div /> elements, where the first entry is the name/title
|
// Those elements have <span /> texts, so we will filter by div to ignore those
|
||||||
// And the other entries are additional info.
|
val texts =
|
||||||
// There are also cases of nested tables, eg for the "join" button in groups.
|
a.children().filter { childEl: Element ->
|
||||||
// Those elements have <span /> texts, so we will filter by div to ignore those
|
childEl.tagName() == "div" && childEl.hasText()
|
||||||
val texts =
|
}
|
||||||
a.children()
|
val title = texts.firstOrNull()?.text() ?: return@mapNotNull null
|
||||||
.filter { childEl: Element -> childEl.tagName() == "div" && childEl.hasText() }
|
val info = texts.takeIf { it.size > 1 }?.last()?.text()
|
||||||
val title = texts.firstOrNull()?.text() ?: return@mapNotNull null
|
L.e { a }
|
||||||
val info = texts.takeIf { it.size > 1 }?.last()?.text()
|
create(href = url, title = title, description = info).also { L.e { it } }
|
||||||
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
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
|
||||||
val httpClient: OkHttpClient by lazy {
|
val httpClient: OkHttpClient by lazy {
|
||||||
val builder = OkHttpClient.Builder()
|
val builder = OkHttpClient.Builder()
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
builder.addInterceptor(
|
builder.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC))
|
||||||
HttpLoggingInterceptor()
|
builder.build()
|
||||||
.setLevel(HttpLoggingInterceptor.Level.BASIC)
|
|
||||||
)
|
|
||||||
builder.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun String?.requestBuilder(): Request.Builder {
|
internal fun String?.requestBuilder(): Request.Builder {
|
||||||
val builder = Request.Builder()
|
val builder = Request.Builder().header("User-Agent", USER_AGENT)
|
||||||
.header("User-Agent", USER_AGENT)
|
if (this != null) builder.header("Cookie", this)
|
||||||
if (this != null)
|
// .cacheControl(CacheControl.FORCE_NETWORK)
|
||||||
builder.header("Cookie", this)
|
return builder
|
||||||
// .cacheControl(CacheControl.FORCE_NETWORK)
|
|
||||||
return builder
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Request.Builder.call(): Call = httpClient.newCall(build())
|
fun Request.Builder.call(): Call = httpClient.newCall(build())
|
||||||
|
@ -24,23 +24,19 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeout
|
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? =
|
suspend fun String.getFullSizedImageUrl(url: String, timeout: Long = 3000): String? =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
withTimeout(timeout) {
|
withTimeout(timeout) {
|
||||||
val redirect = requestBuilder().url(url).get().call()
|
val redirect =
|
||||||
.execute().body?.string() ?: return@withTimeout null
|
requestBuilder().url(url).get().call().execute().body?.string() ?: return@withTimeout null
|
||||||
FB_REDIRECT_URL_MATCHER.find(redirect)[1]?.formattedFbUrl
|
FB_REDIRECT_URL_MATCHER.find(redirect)[1]?.formattedFbUrl
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
L.e(e) { "Failed to load full size image url" }
|
L.e(e) { "Failed to load full size image url" }
|
||||||
null
|
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.REQUEST_TEXT_ZOOM
|
||||||
import com.pitchedapps.frost.utils.frostEvent
|
import com.pitchedapps.frost.utils.frostEvent
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 2017-11-07.
|
* Created by Allan Wang on 2017-11-07.
|
||||||
*
|
*
|
||||||
* All fragments pertaining to the main view
|
* All fragments pertaining to the main view Must be attached to activities implementing
|
||||||
* Must be attached to activities implementing [MainActivityContract]
|
* [MainActivityContract]
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
abstract class BaseFragment :
|
abstract class BaseFragment : Fragment(), CoroutineScope, FragmentContract, DynamicUiContract {
|
||||||
Fragment(),
|
|
||||||
CoroutineScope,
|
|
||||||
FragmentContract,
|
|
||||||
DynamicUiContract {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ARG_POSITION = "arg_position"
|
private const val ARG_POSITION = "arg_position"
|
||||||
private const val ARG_VALID = "arg_valid"
|
private const val ARG_VALID = "arg_valid"
|
||||||
|
|
||||||
internal operator fun invoke(
|
internal operator fun invoke(
|
||||||
base: () -> BaseFragment,
|
base: () -> BaseFragment,
|
||||||
prefs: Prefs,
|
prefs: Prefs,
|
||||||
useFallback: Boolean,
|
useFallback: Boolean,
|
||||||
data: FbItem,
|
data: FbItem,
|
||||||
position: Int
|
position: Int
|
||||||
): BaseFragment {
|
): BaseFragment {
|
||||||
val fragment = if (useFallback) WebFragment() else base()
|
val fragment = if (useFallback) WebFragment() else base()
|
||||||
val d = if (data == FbItem.FEED) FeedSort(prefs.feedSort).item else data
|
val d = if (data == FbItem.FEED) FeedSort(prefs.feedSort).item else data
|
||||||
fragment.withArguments(
|
fragment.withArguments(ARG_URL to d.url, ARG_POSITION to position)
|
||||||
ARG_URL to d.url,
|
d.put(fragment.requireArguments())
|
||||||
ARG_POSITION to position
|
return fragment
|
||||||
)
|
}
|
||||||
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
|
override var firstLoad: Boolean = true
|
||||||
protected lateinit var mainContract: MainActivityContract
|
private var onCreateRunnable: ((FragmentContract) -> Unit)? = null
|
||||||
|
|
||||||
@Inject
|
override var content: FrostContentParent? = null
|
||||||
protected lateinit var fbCookie: FbCookie
|
|
||||||
|
|
||||||
@Inject
|
protected abstract val layoutRes: Int
|
||||||
protected lateinit var prefs: Prefs
|
|
||||||
|
|
||||||
@Inject
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
protected lateinit var themeProvider: ThemeProvider
|
super.onCreate(savedInstanceState)
|
||||||
|
job = SupervisorJob()
|
||||||
|
firstLoad = true
|
||||||
|
}
|
||||||
|
|
||||||
open lateinit var job: Job
|
final override fun onCreateView(
|
||||||
override val coroutineContext: CoroutineContext
|
inflater: LayoutInflater,
|
||||||
get() = ContextHelper.dispatcher + job
|
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 fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
override val baseEnum: FbItem by lazy { FbItem[arguments]!! }
|
super.onViewCreated(view, savedInstanceState)
|
||||||
override val position: Int by lazy { requireArguments().getInt(ARG_POSITION) }
|
onCreateRunnable?.invoke(this)
|
||||||
|
onCreateRunnable = null
|
||||||
|
firstLoadRequest()
|
||||||
|
attach(mainContract)
|
||||||
|
}
|
||||||
|
|
||||||
override var valid: Boolean
|
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
||||||
get() = requireArguments().getBoolean(ARG_VALID, true)
|
super.setUserVisibleHint(isVisibleToUser)
|
||||||
set(value) {
|
firstLoadRequest()
|
||||||
if (!isActive || value || this is WebFragment) return
|
}
|
||||||
requireArguments().putBoolean(ARG_VALID, value)
|
|
||||||
frostEvent(
|
|
||||||
"Native Fallback",
|
|
||||||
"Item" to baseEnum.name
|
|
||||||
)
|
|
||||||
mainContract.reloadFragment(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override var firstLoad: Boolean = true
|
override fun firstLoadRequest() {
|
||||||
private var onCreateRunnable: ((FragmentContract) -> Unit)? = null
|
val core = core ?: return
|
||||||
|
if (userVisibleHint && isVisible && firstLoad) {
|
||||||
override var content: FrostContentParent? = null
|
core.reloadBase(true)
|
||||||
|
firstLoad = false
|
||||||
protected abstract val layoutRes: Int
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
job = SupervisorJob()
|
|
||||||
firstLoad = true
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final override fun onCreateView(
|
override fun post(action: (fragment: FragmentContract) -> Unit) {
|
||||||
inflater: LayoutInflater,
|
onCreateRunnable = action
|
||||||
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 onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun setTitle(title: String) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
mainContract.setTitle(title)
|
||||||
onCreateRunnable?.invoke(this)
|
}
|
||||||
onCreateRunnable = null
|
|
||||||
firstLoadRequest()
|
|
||||||
attach(mainContract)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
override fun attach(contract: MainActivityContract) {
|
||||||
super.setUserVisibleHint(isVisibleToUser)
|
contract.fragmentFlow
|
||||||
firstLoadRequest()
|
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
|
||||||
}
|
.onEach { flag ->
|
||||||
|
when (flag) {
|
||||||
override fun firstLoadRequest() {
|
REQUEST_REFRESH -> {
|
||||||
val core = core ?: return
|
core?.apply {
|
||||||
if (userVisibleHint && isVisible && firstLoad) {
|
clearHistory()
|
||||||
core.reloadBase(true)
|
firstLoad = true
|
||||||
firstLoad = false
|
firstLoadRequest()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
setIcon(iicon, themeProvider.iconColor)
|
position -> {
|
||||||
show()
|
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() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
L.i { "Fragment on destroy $position ${hashCode()}" }
|
L.i { "Fragment on destroy $position ${hashCode()}" }
|
||||||
content?.destroy()
|
content?.destroy()
|
||||||
content = null
|
content = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
job.cancel()
|
job.cancel()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reloadTheme() {
|
override fun reloadTheme() {
|
||||||
reloadThemeSelf()
|
reloadThemeSelf()
|
||||||
content?.reloadTextSize()
|
content?.reloadTextSize()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reloadThemeSelf() {
|
override fun reloadThemeSelf() {
|
||||||
// intentionally blank
|
// intentionally blank
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reloadTextSize() {
|
override fun reloadTextSize() {
|
||||||
reloadTextSizeSelf()
|
reloadTextSizeSelf()
|
||||||
content?.reloadTextSize()
|
content?.reloadTextSize()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reloadTextSizeSelf() {
|
override fun reloadTextSizeSelf() {
|
||||||
// intentionally blank
|
// 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.contracts.MainFabContract
|
||||||
import com.pitchedapps.frost.views.FrostRecyclerView
|
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 {
|
interface FragmentContract : FrostContentContainer {
|
||||||
|
|
||||||
val content: FrostContentParent?
|
val content: FrostContentParent?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines whether the fragment is valid in the viewpager
|
* Defines whether the fragment is valid in the viewpager or if it needs to be recreated May be
|
||||||
* or if it needs to be recreated
|
* called from any thread to toggle status. Note that calls beyond the fragment lifecycle will be
|
||||||
* May be called from any thread to toggle status.
|
* ignored
|
||||||
* Note that calls beyond the fragment lifecycle will be ignored
|
*/
|
||||||
*/
|
var valid: Boolean
|
||||||
var valid: Boolean
|
|
||||||
|
|
||||||
/**
|
/** Helper to retrieve the core from [content] */
|
||||||
* Helper to retrieve the core from [content]
|
val core: FrostContentCore?
|
||||||
*/
|
get() = content?.core
|
||||||
val core: FrostContentCore?
|
|
||||||
get() = content?.core
|
|
||||||
|
|
||||||
/**
|
/** Specifies position in Activity's viewpager */
|
||||||
* Specifies position in Activity's viewpager
|
val position: Int
|
||||||
*/
|
|
||||||
val position: Int
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specifies whether if current load
|
* Specifies whether if current load will be fragment's first load
|
||||||
* will be fragment's first load
|
*
|
||||||
*
|
* Defaults to true
|
||||||
* Defaults to true
|
*/
|
||||||
*/
|
var firstLoad: Boolean
|
||||||
var firstLoad: Boolean
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the fragment is first visible
|
* Called when the fragment is first visible Typically, if [firstLoad] is true, the fragment
|
||||||
* Typically, if [firstLoad] is true,
|
* should call [reload] and make [firstLoad] false
|
||||||
* the fragment should call [reload] and make [firstLoad] false
|
*/
|
||||||
*/
|
fun firstLoadRequest()
|
||||||
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 */
|
||||||
* Single callable action to be executed upon creation
|
fun post(action: (fragment: FragmentContract) -> Unit)
|
||||||
* Note that this call is not guaranteed
|
|
||||||
*/
|
|
||||||
fun post(action: (fragment: FragmentContract) -> Unit)
|
|
||||||
|
|
||||||
/**
|
/** Call whenever a fragment is attached so that it may listen to activity emissions. */
|
||||||
* Call whenever a fragment is attached so that it may listen
|
fun attach(contract: MainActivityContract)
|
||||||
* to activity emissions.
|
|
||||||
*/
|
|
||||||
fun attach(contract: MainActivityContract)
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -----------------------------------------
|
* -----------------------------------------
|
||||||
* Delegates
|
* Delegates
|
||||||
* -----------------------------------------
|
* -----------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun onBackPressed(): Boolean
|
fun onBackPressed(): Boolean
|
||||||
|
|
||||||
fun onTabClick()
|
fun onTabClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RecyclerContentContract {
|
interface RecyclerContentContract {
|
||||||
|
|
||||||
fun bind(recyclerView: FrostRecyclerView)
|
fun bind(recyclerView: FrostRecyclerView)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Completely handle data reloading, within a non-ui thread
|
* Completely handle data reloading, within a non-ui thread The progress function allows optional
|
||||||
* The progress function allows optional emission of progress values (between 0 and 100)
|
* emission of progress values (between 0 and 100) and can be called from any thread. Returns
|
||||||
* and can be called from any thread.
|
* [true] for success, [false] otherwise
|
||||||
* Returns [true] for success, [false] otherwise
|
*/
|
||||||
*/
|
suspend fun reload(progress: (Int) -> Unit): Boolean
|
||||||
suspend fun reload(progress: (Int) -> Unit): Boolean
|
|
||||||
}
|
}
|
||||||
|
@ -32,119 +32,113 @@ import com.pitchedapps.frost.views.FrostRecyclerView
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
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 {
|
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() {
|
override fun firstLoadRequest() {
|
||||||
val core = core ?: return
|
val core = core ?: return
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
core.reloadBase(true)
|
core.reloadBase(true)
|
||||||
firstLoad = false
|
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 =
|
protected abstract suspend fun reloadImpl(progress: (Int) -> Unit): List<T>?
|
||||||
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>?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class GenericRecyclerFragment<T, Item : GenericItem> : RecyclerFragment<T, Item>() {
|
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) {
|
final override fun bind(recyclerView: FrostRecyclerView) {
|
||||||
recyclerView.adapter = getAdapter()
|
recyclerView.adapter = getAdapter()
|
||||||
recyclerView.onReloadClear = { adapter.clear() }
|
recyclerView.onReloadClear = { adapter.clear() }
|
||||||
bindImpl(recyclerView)
|
bindImpl(recyclerView)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Anything to call for one time bindings
|
* Anything to call for one time bindings At this stage, all adapters will have FastAdapter
|
||||||
* At this stage, all adapters will have FastAdapter references
|
* references
|
||||||
*/
|
*/
|
||||||
open fun bindImpl(recyclerView: FrostRecyclerView) = Unit
|
open fun bindImpl(recyclerView: FrostRecyclerView) = Unit
|
||||||
|
|
||||||
/**
|
/** Create the fast adapter to bind to the recyclerview */
|
||||||
* Create the fast adapter to bind to the recyclerview
|
open fun getAdapter(): GenericFastAdapter = fastAdapter(this.adapter)
|
||||||
*/
|
|
||||||
open fun getAdapter(): GenericFastAdapter = fastAdapter(this.adapter)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class FrostParserFragment<T : ParseData, Item : GenericItem> :
|
abstract class FrostParserFragment<T : ParseData, Item : GenericItem> :
|
||||||
RecyclerFragment<Item, Item>() {
|
RecyclerFragment<Item, Item>() {
|
||||||
|
|
||||||
/**
|
/** The parser to make this all happen */
|
||||||
* The parser to make this all happen
|
abstract val parser: FrostParser<T>
|
||||||
*/
|
|
||||||
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) {
|
final override fun bind(recyclerView: FrostRecyclerView) {
|
||||||
recyclerView.adapter = getAdapter()
|
recyclerView.adapter = getAdapter()
|
||||||
recyclerView.onReloadClear = { adapter.clear() }
|
recyclerView.onReloadClear = { adapter.clear() }
|
||||||
bindImpl(recyclerView)
|
bindImpl(recyclerView)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Anything to call for one time bindings
|
* Anything to call for one time bindings At this stage, all adapters will have FastAdapter
|
||||||
* At this stage, all adapters will have FastAdapter references
|
* references
|
||||||
*/
|
*/
|
||||||
open fun bindImpl(recyclerView: FrostRecyclerView) = Unit
|
open fun bindImpl(recyclerView: FrostRecyclerView) = Unit
|
||||||
|
|
||||||
/**
|
/** Create the fast adapter to bind to the recyclerview */
|
||||||
* Create the fast adapter to bind to the recyclerview
|
open fun getAdapter(): GenericFastAdapter = fastAdapter(this.adapter)
|
||||||
*/
|
|
||||||
open fun getAdapter(): GenericFastAdapter = fastAdapter(this.adapter)
|
|
||||||
|
|
||||||
override suspend fun reloadImpl(progress: (Int) -> Unit): List<Item>? =
|
override suspend fun reloadImpl(progress: (Int) -> Unit): List<Item>? =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
progress(10)
|
progress(10)
|
||||||
val cookie = fbCookie.webCookie
|
val cookie = fbCookie.webCookie
|
||||||
val doc = getDoc(cookie)
|
val doc = getDoc(cookie)
|
||||||
progress(60)
|
progress(60)
|
||||||
val response = try {
|
val response =
|
||||||
parser.parse(cookie, doc)
|
try {
|
||||||
} catch (ignored: Exception) {
|
parser.parse(cookie, doc)
|
||||||
null
|
} 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
|
|
||||||
}
|
}
|
||||||
|
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.
|
* 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>() {
|
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> =
|
override fun toItems(response: ParseResponse<FrostNotifs>): List<NotificationIItem> =
|
||||||
response.data.notifs.map { NotificationIItem(it, response.cookie, themeProvider) }
|
response.data.notifs.map { NotificationIItem(it, response.cookie, themeProvider) }
|
||||||
|
|
||||||
override fun bindImpl(recyclerView: FrostRecyclerView) {
|
override fun bindImpl(recyclerView: FrostRecyclerView) {
|
||||||
NotificationIItem.bindEvents(adapter, fbCookie, prefs, themeProvider)
|
NotificationIItem.bindEvents(adapter, fbCookie, prefs, themeProvider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,34 +31,30 @@ import com.pitchedapps.frost.web.FrostWebViewClientMessenger
|
|||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 27/12/17.
|
* Created by Allan Wang on 27/12/17.
|
||||||
*
|
*
|
||||||
* Basic webfragment
|
* Basic webfragment Do not extend as this is always a fallback
|
||||||
* Do not extend as this is always a fallback
|
|
||||||
*/
|
*/
|
||||||
class WebFragment : BaseFragment() {
|
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 */
|
||||||
* Given a webview, output a client
|
fun client(web: FrostWebView) =
|
||||||
*/
|
when (baseEnum) {
|
||||||
fun client(web: FrostWebView) = when (baseEnum) {
|
FbItem.MESSENGER -> FrostWebViewClientMessenger(web)
|
||||||
FbItem.MESSENGER -> FrostWebViewClientMessenger(web)
|
FbItem.MENU -> FrostWebViewClientMenu(web)
|
||||||
FbItem.MENU -> FrostWebViewClientMenu(web)
|
else -> FrostWebViewClient(web)
|
||||||
else -> FrostWebViewClient(web)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateFab(contract: MainFabContract) {
|
override fun updateFab(contract: MainFabContract) {
|
||||||
val web = core as? WebView
|
val web = core as? WebView
|
||||||
if (web == null) {
|
if (web == null) {
|
||||||
L.e { "Webview not found in fragment $baseEnum" }
|
L.e { "Webview not found in fragment $baseEnum" }
|
||||||
return super.updateFab(contract)
|
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)
|
|
||||||
}
|
}
|
||||||
|
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.module.AppGlideModule
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import com.pitchedapps.frost.facebook.FbCookie
|
import com.pitchedapps.frost.facebook.FbCookie
|
||||||
|
import javax.inject.Inject
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 28/12/17.
|
* Created by Allan Wang on 28/12/17.
|
||||||
*
|
*
|
||||||
* Collection of transformations
|
* Collection of transformations Each caller will generate a new one upon request
|
||||||
* Each caller will generate a new one upon request
|
|
||||||
*/
|
*/
|
||||||
object FrostGlide {
|
object FrostGlide {
|
||||||
val circleCrop
|
val circleCrop
|
||||||
get() = CircleCrop()
|
get() = CircleCrop()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> RequestBuilder<T>.transform(vararg transformation: BitmapTransformation): RequestBuilder<T> =
|
fun <T> RequestBuilder<T>.transform(
|
||||||
when (transformation.size) {
|
vararg transformation: BitmapTransformation
|
||||||
0 -> this
|
): RequestBuilder<T> =
|
||||||
1 -> apply(RequestOptions.bitmapTransform(transformation[0]))
|
when (transformation.size) {
|
||||||
else -> apply(RequestOptions.bitmapTransform(MultiTransformation(*transformation)))
|
0 -> this
|
||||||
}
|
1 -> apply(RequestOptions.bitmapTransform(transformation[0]))
|
||||||
|
else -> apply(RequestOptions.bitmapTransform(MultiTransformation(*transformation)))
|
||||||
|
}
|
||||||
|
|
||||||
@GlideModule
|
@GlideModule
|
||||||
class FrostGlideModule : AppGlideModule() {
|
class FrostGlideModule : AppGlideModule() {
|
||||||
|
|
||||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||||
// registry.replace(GlideUrl::class.java,
|
// registry.replace(GlideUrl::class.java,
|
||||||
// InputStream::class.java,
|
// InputStream::class.java,
|
||||||
// OkHttpUrlLoader.Factory(getFrostHttpClient()))
|
// OkHttpUrlLoader.Factory(getFrostHttpClient()))
|
||||||
// registry.prepend(HdImageMaybe::class.java, InputStream::class.java, HdImageLoadingFactory())
|
// registry.prepend(HdImageMaybe::class.java, InputStream::class.java,
|
||||||
}
|
// HdImageLoadingFactory())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// private fun getFrostHttpClient(): OkHttpClient =
|
// private fun getFrostHttpClient(): OkHttpClient =
|
||||||
// OkHttpClient.Builder().addInterceptor(FrostCookieInterceptor()).build()
|
// OkHttpClient.Builder().addInterceptor(FrostCookieInterceptor()).build()
|
||||||
|
|
||||||
class FrostCookieInterceptor @Inject internal constructor(
|
class FrostCookieInterceptor @Inject internal constructor(private val fbCookie: FbCookie) :
|
||||||
private val fbCookie: FbCookie
|
Interceptor {
|
||||||
) : Interceptor {
|
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val origRequest = chain.request()
|
val origRequest = chain.request()
|
||||||
val cookie = fbCookie.webCookie ?: return chain.proceed(origRequest)
|
val cookie = fbCookie.webCookie ?: return chain.proceed(origRequest)
|
||||||
val request = origRequest.newBuilder().addHeader("Cookie", cookie).build()
|
val request = origRequest.newBuilder().addHeader("Cookie", cookie).build()
|
||||||
return chain.proceed(request)
|
return chain.proceed(request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,101 +32,86 @@ import com.pitchedapps.frost.injectors.ThemeProvider
|
|||||||
import com.pitchedapps.frost.prefs.Prefs
|
import com.pitchedapps.frost.prefs.Prefs
|
||||||
import com.pitchedapps.frost.utils.launchWebOverlay
|
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 {
|
interface ClickableIItemContract {
|
||||||
|
|
||||||
val url: String?
|
val url: String?
|
||||||
|
|
||||||
fun click(context: Context, fbCookie: FbCookie, prefs: Prefs) {
|
fun click(context: Context, fbCookie: FbCookie, prefs: Prefs) {
|
||||||
val url = url ?: return
|
val url = url ?: return
|
||||||
context.launchWebOverlay(url, fbCookie, prefs)
|
context.launchWebOverlay(url, fbCookie, prefs)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun bindEvents(adapter: IAdapter<GenericItem>, fbCookie: FbCookie, prefs: Prefs) {
|
fun bindEvents(adapter: IAdapter<GenericItem>, fbCookie: FbCookie, prefs: Prefs) {
|
||||||
adapter.fastAdapter?.apply {
|
adapter.fastAdapter?.apply {
|
||||||
selectExtension {
|
selectExtension { isSelectable = false }
|
||||||
isSelectable = false
|
onClickListener = { v, _, item, _ ->
|
||||||
}
|
if (item is ClickableIItemContract) {
|
||||||
onClickListener = { v, _, item, _ ->
|
item.click(v!!.context, fbCookie, prefs)
|
||||||
if (item is ClickableIItemContract) {
|
true
|
||||||
item.click(v!!.context, fbCookie, prefs)
|
} else false
|
||||||
true
|
|
||||||
} else
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Generic header item Not clickable with an accent color */
|
||||||
* Generic header item
|
|
||||||
* Not clickable with an accent color
|
|
||||||
*/
|
|
||||||
open class HeaderIItem(
|
open class HeaderIItem(
|
||||||
val text: String?,
|
val text: String?,
|
||||||
itemId: Int = R.layout.iitem_header,
|
itemId: Int = R.layout.iitem_header,
|
||||||
private val themeProvider: ThemeProvider
|
private val themeProvider: ThemeProvider
|
||||||
) : KauIItem<HeaderIItem.ViewHolder>(
|
) :
|
||||||
|
KauIItem<HeaderIItem.ViewHolder>(
|
||||||
R.layout.iitem_header,
|
R.layout.iitem_header,
|
||||||
{ ViewHolder(it, themeProvider) },
|
{ ViewHolder(it, themeProvider) },
|
||||||
itemId
|
itemId
|
||||||
) {
|
) {
|
||||||
|
|
||||||
class ViewHolder(
|
class ViewHolder(itemView: View, private val themeProvider: ThemeProvider) :
|
||||||
itemView: View,
|
FastAdapter.ViewHolder<HeaderIItem>(itemView) {
|
||||||
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>) {
|
override fun bindView(item: HeaderIItem, payloads: List<Any>) {
|
||||||
text.setTextColor(themeProvider.accentColor)
|
text.setTextColor(themeProvider.accentColor)
|
||||||
text.text = item.text
|
text.text = item.text
|
||||||
text.setBackgroundColor(themeProvider.nativeBgColor)
|
text.setBackgroundColor(themeProvider.nativeBgColor)
|
||||||
}
|
|
||||||
|
|
||||||
override fun unbindView(item: HeaderIItem) {
|
|
||||||
text.text = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
open class TextIItem(
|
||||||
val text: String?,
|
val text: String?,
|
||||||
override val url: String?,
|
override val url: String?,
|
||||||
itemId: Int = R.layout.iitem_text,
|
itemId: Int = R.layout.iitem_text,
|
||||||
private val themeProvider: ThemeProvider
|
private val themeProvider: ThemeProvider
|
||||||
) : KauIItem<TextIItem.ViewHolder>(R.layout.iitem_text, { ViewHolder(it, themeProvider) }, itemId),
|
) :
|
||||||
ClickableIItemContract {
|
KauIItem<TextIItem.ViewHolder>(R.layout.iitem_text, { ViewHolder(it, themeProvider) }, itemId),
|
||||||
|
ClickableIItemContract {
|
||||||
|
|
||||||
class ViewHolder(
|
class ViewHolder(itemView: View, private val themeProvider: ThemeProvider) :
|
||||||
itemView: View,
|
FastAdapter.ViewHolder<TextIItem>(itemView) {
|
||||||
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>) {
|
override fun bindView(item: TextIItem, payloads: List<Any>) {
|
||||||
text.setTextColor(themeProvider.textColor)
|
text.setTextColor(themeProvider.textColor)
|
||||||
text.text = item.text
|
text.text = item.text
|
||||||
text.background =
|
text.background =
|
||||||
createSimpleRippleDrawable(themeProvider.bgColor, themeProvider.nativeBgColor)
|
createSimpleRippleDrawable(themeProvider.bgColor, themeProvider.nativeBgColor)
|
||||||
}
|
|
||||||
|
|
||||||
override fun unbindView(item: TextIItem) {
|
|
||||||
text.text = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.isIndependent
|
||||||
import com.pitchedapps.frost.utils.launchWebOverlay
|
import com.pitchedapps.frost.utils.launchWebOverlay
|
||||||
|
|
||||||
/**
|
/** Created by Allan Wang on 27/12/17. */
|
||||||
* Created by Allan Wang on 27/12/17.
|
|
||||||
*/
|
|
||||||
class NotificationIItem(
|
class NotificationIItem(
|
||||||
val notification: FrostNotif,
|
val notification: FrostNotif,
|
||||||
val cookie: String,
|
val cookie: String,
|
||||||
private val themeProvider: ThemeProvider
|
private val themeProvider: ThemeProvider
|
||||||
) : KauIItem<NotificationIItem.ViewHolder>(
|
) :
|
||||||
R.layout.iitem_notification, { ViewHolder(it, themeProvider) }
|
KauIItem<NotificationIItem.ViewHolder>(
|
||||||
) {
|
R.layout.iitem_notification,
|
||||||
|
{ ViewHolder(it, themeProvider) }
|
||||||
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun bindEvents(
|
fun bindEvents(
|
||||||
adapter: ItemAdapter<NotificationIItem>,
|
adapter: ItemAdapter<NotificationIItem>,
|
||||||
fbCookie: FbCookie,
|
fbCookie: FbCookie,
|
||||||
prefs: Prefs,
|
prefs: Prefs,
|
||||||
themeProvider: ThemeProvider
|
themeProvider: ThemeProvider
|
||||||
) {
|
) {
|
||||||
adapter.fastAdapter?.apply {
|
adapter.fastAdapter?.apply {
|
||||||
selectExtension {
|
selectExtension { isSelectable = false }
|
||||||
isSelectable = false
|
onClickListener = { v, _, item, position ->
|
||||||
}
|
val notif = item.notification
|
||||||
onClickListener = { v, _, item, position ->
|
if (notif.unread) {
|
||||||
val notif = item.notification
|
adapter.set(
|
||||||
if (notif.unread) {
|
position,
|
||||||
adapter.set(
|
NotificationIItem(notif.copy(unread = false), item.cookie, themeProvider)
|
||||||
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)
|
|
||||||
)
|
)
|
||||||
content.setTextColor(themeProvider.textColor)
|
}
|
||||||
date.setTextColor(themeProvider.textColor.withAlpha(150))
|
// TODO temp fix. If url is dependent, we cannot load it directly
|
||||||
|
v!!
|
||||||
val glide = glide
|
.context
|
||||||
glide.load(notif.img)
|
.launchWebOverlay(
|
||||||
.transform(FrostGlide.circleCrop)
|
if (notif.url.isIndependent) notif.url else FbItem.NOTIFICATIONS.url,
|
||||||
.into(avatar)
|
fbCookie,
|
||||||
if (notif.thumbnailUrl != null)
|
prefs
|
||||||
glide.load(notif.thumbnailUrl).into(thumbnail.visible())
|
)
|
||||||
|
true
|
||||||
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 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.facebook.FbItem
|
||||||
import com.pitchedapps.frost.injectors.ThemeProvider
|
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) :
|
class TabIItem(val item: FbItem, private val themeProvider: ThemeProvider) :
|
||||||
KauIItem<TabIItem.ViewHolder>(
|
KauIItem<TabIItem.ViewHolder>(R.layout.iitem_tab_preview, { ViewHolder(it, themeProvider) }),
|
||||||
R.layout.iitem_tab_preview,
|
IDraggable {
|
||||||
{ ViewHolder(it, themeProvider) }
|
|
||||||
),
|
|
||||||
IDraggable {
|
|
||||||
|
|
||||||
override val isDraggable: Boolean = true
|
override val isDraggable: Boolean = true
|
||||||
|
|
||||||
class ViewHolder(
|
class ViewHolder(itemView: View, private val themeProvider: ThemeProvider) :
|
||||||
itemView: View,
|
FastAdapter.ViewHolder<TabIItem>(itemView) {
|
||||||
private val themeProvider: ThemeProvider
|
|
||||||
) : FastAdapter.ViewHolder<TabIItem>(itemView) {
|
|
||||||
|
|
||||||
val image: ImageView by bindView(R.id.image)
|
val image: ImageView by bindView(R.id.image)
|
||||||
val text: TextView by bindView(R.id.text)
|
val text: TextView by bindView(R.id.text)
|
||||||
|
|
||||||
override fun bindView(item: TabIItem, payloads: List<Any>) {
|
override fun bindView(item: TabIItem, payloads: List<Any>) {
|
||||||
val isInToolbar = adapterPosition < 4
|
val isInToolbar = adapterPosition < 4
|
||||||
val color = if (isInToolbar) themeProvider.iconColor else themeProvider.textColor
|
val color = if (isInToolbar) themeProvider.iconColor else themeProvider.textColor
|
||||||
image.setIcon(item.item.icon, 20, color)
|
image.setIcon(item.item.icon, 20, color)
|
||||||
if (isInToolbar)
|
if (isInToolbar) text.invisible()
|
||||||
text.invisible()
|
else {
|
||||||
else {
|
text.visible().setText(item.item.titleId)
|
||||||
text.visible().setText(item.item.titleId)
|
text.setTextColor(color.withAlpha(200))
|
||||||
text.setTextColor(color.withAlpha(200))
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun unbindView(item: TabIItem) {
|
|
||||||
image.setImageDrawable(null)
|
|
||||||
text.visible().text = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 android.webkit.WebView
|
||||||
import com.pitchedapps.frost.prefs.Prefs
|
import com.pitchedapps.frost.prefs.Prefs
|
||||||
|
|
||||||
/**
|
/** Small misc inline css assets */
|
||||||
* Small misc inline css assets
|
|
||||||
*/
|
|
||||||
enum class CssAsset(private val content: String) : InjectorContract {
|
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)
|
* Remove top margin and hide some contents from the top bar and home page (as it's our base url)
|
||||||
*/
|
*/
|
||||||
Menu("#bookmarks_flyout{margin-top:0 !important}#m_news_feed_stream,#MComposer{display:none !important}")
|
Menu(
|
||||||
;
|
"#bookmarks_flyout{margin-top:0 !important}#m_news_feed_stream,#MComposer{display:none !important}"
|
||||||
|
);
|
||||||
|
|
||||||
val injector: JsInjector by lazy {
|
val injector: JsInjector by lazy {
|
||||||
JsBuilder().css(content).single("css-small-assets-$name").build()
|
JsBuilder().css(content).single("css-small-assets-$name").build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun inject(webView: WebView, prefs: Prefs) {
|
override fun inject(webView: WebView, prefs: Prefs) {
|
||||||
injector.inject(webView, prefs)
|
injector.inject(webView, prefs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,41 +25,34 @@ import com.pitchedapps.frost.prefs.Prefs
|
|||||||
* List of elements to hide
|
* List of elements to hide
|
||||||
*/
|
*/
|
||||||
enum class CssHider(private vararg val items: String) : InjectorContract {
|
enum class CssHider(private vararg val items: String) : InjectorContract {
|
||||||
CORE("[data-sigil=m_login_upsell]", "[role=progressbar]"),
|
CORE("[data-sigil=m_login_upsell]", "[role=progressbar]"),
|
||||||
HEADER(
|
HEADER(
|
||||||
"#header:not(.mFuturePageHeader):not(.titled)",
|
"#header:not(.mFuturePageHeader):not(.titled)",
|
||||||
"#mJewelNav",
|
"#mJewelNav",
|
||||||
"[data-sigil=MTopBlueBarHeader]",
|
"[data-sigil=MTopBlueBarHeader]",
|
||||||
"#header-notices",
|
"#header-notices",
|
||||||
"[data-sigil*=m-promo-jewel-header]"
|
"[data-sigil*=m-promo-jewel-header]"
|
||||||
),
|
),
|
||||||
ADS(
|
ADS("article[data-xt*=sponsor]", "article[data-store*=sponsor]"),
|
||||||
"article[data-xt*=sponsor]",
|
PEOPLE_YOU_MAY_KNOW("article._d2r"),
|
||||||
"article[data-store*=sponsor]"
|
SUGGESTED_GROUPS("article[data-ft*=\"ei\":]"),
|
||||||
),
|
COMPOSER("#MComposer"),
|
||||||
PEOPLE_YOU_MAY_KNOW("article._d2r"),
|
MESSENGER("._s15", "[data-testid=info_panel]", "js_i"),
|
||||||
SUGGESTED_GROUPS("article[data-ft*=\"ei\":]"),
|
NON_RECENT("article:not([data-store*=actor_name])"),
|
||||||
COMPOSER("#MComposer"),
|
STORIES(
|
||||||
MESSENGER("._s15", "[data-testid=info_panel]", "js_i"),
|
"#MStoriesTray",
|
||||||
NON_RECENT("article:not([data-store*=actor_name])"),
|
// Sub element with just the tray; title is not a part of this
|
||||||
STORIES(
|
"[data-testid=story_tray]"
|
||||||
"#MStoriesTray",
|
),
|
||||||
// Sub element with just the tray; title is not a part of this
|
POST_ACTIONS("footer [data-sigil=\"ufi-inline-actions\"]"),
|
||||||
"[data-testid=story_tray]"
|
POST_REACTIONS("footer [data-sigil=\"reactions-bling-bar\"]");
|
||||||
),
|
|
||||||
POST_ACTIONS(
|
|
||||||
"footer [data-sigil=\"ufi-inline-actions\"]"
|
|
||||||
),
|
|
||||||
POST_REACTIONS(
|
|
||||||
"footer [data-sigil=\"reactions-bling-bar\"]"
|
|
||||||
)
|
|
||||||
;
|
|
||||||
|
|
||||||
val injector: JsInjector by lazy {
|
val injector: JsInjector by lazy {
|
||||||
JsBuilder().css("${items.joinToString(separator = ",")}{display:none !important}")
|
JsBuilder()
|
||||||
.single("css-hider-$name").build()
|
.css("${items.joinToString(separator = ",")}{display:none !important}")
|
||||||
}
|
.single("css-hider-$name")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
override fun inject(webView: WebView, prefs: Prefs) =
|
override fun inject(webView: WebView, prefs: Prefs) = injector.inject(webView, prefs)
|
||||||
injector.inject(webView, prefs)
|
|
||||||
}
|
}
|
||||||
|
@ -26,27 +26,26 @@ import com.pitchedapps.frost.prefs.Prefs
|
|||||||
* Collection of short js functions that are embedded directly
|
* Collection of short js functions that are embedded directly
|
||||||
*/
|
*/
|
||||||
enum class JsActions(body: String) : InjectorContract {
|
enum class JsActions(body: String) : InjectorContract {
|
||||||
/**
|
/**
|
||||||
* Redirects to login activity if create account is found
|
* Redirects to login activity if create account is found see
|
||||||
* see [com.pitchedapps.frost.web.FrostJSI.loadLogin]
|
* [com.pitchedapps.frost.web.FrostJSI.loadLogin]
|
||||||
*/
|
*/
|
||||||
LOGIN_CHECK("document.getElementById('signup-button')&&Frost.loadLogin();"),
|
LOGIN_CHECK("document.getElementById('signup-button')&&Frost.loadLogin();"),
|
||||||
BASE_HREF("""document.write("<base href='$FB_URL_BASE'/>");"""),
|
BASE_HREF("""document.write("<base href='$FB_URL_BASE'/>");"""),
|
||||||
FETCH_BODY("""setTimeout(function(){var e=document.querySelector("main");e||(e=document.querySelector("body")),Frost.handleHtml(e.outerHTML)},1e2);"""),
|
FETCH_BODY(
|
||||||
RETURN_BODY("return(document.getElementsByTagName('html')[0].innerHTML);"),
|
"""setTimeout(function(){var e=document.querySelector("main");e||(e=document.querySelector("body")),Frost.handleHtml(e.outerHTML)},1e2);"""
|
||||||
CREATE_POST(clickBySelector("#MComposer [onclick]")),
|
),
|
||||||
// CREATE_MSG(clickBySelector("a[rel=dialog]")),
|
RETURN_BODY("return(document.getElementsByTagName('html')[0].innerHTML);"),
|
||||||
/**
|
CREATE_POST(clickBySelector("#MComposer [onclick]")),
|
||||||
* Used as a pseudoinjector for maybe functions
|
// CREATE_MSG(clickBySelector("a[rel=dialog]")),
|
||||||
*/
|
/** Used as a pseudoinjector for maybe functions */
|
||||||
EMPTY("");
|
EMPTY("");
|
||||||
|
|
||||||
val function = "(function(){$body})();"
|
val function = "(function(){$body})();"
|
||||||
|
|
||||||
override fun inject(webView: WebView, prefs: Prefs) =
|
override fun inject(webView: WebView, prefs: Prefs) = JsInjector(function).inject(webView, prefs)
|
||||||
JsInjector(function).inject(webView, prefs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
private inline fun clickBySelector(selector: String): String =
|
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 ca.allanwang.kau.kotlin.lazyContext
|
||||||
import com.pitchedapps.frost.prefs.Prefs
|
import com.pitchedapps.frost.prefs.Prefs
|
||||||
import com.pitchedapps.frost.utils.L
|
import com.pitchedapps.frost.utils.L
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 2017-05-31.
|
* Created by Allan Wang on 2017-05-31. Mapping of the available assets The enum name must match the
|
||||||
* Mapping of the available assets
|
* css file name
|
||||||
* The enum name must match the css file name
|
|
||||||
*/
|
*/
|
||||||
enum class JsAssets(private val singleLoad: Boolean = true) : InjectorContract {
|
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,
|
MENU,
|
||||||
DOCUMENT_WATCHER, HORIZONTAL_SCROLLING, AUTO_RESIZE_TEXTAREA(singleLoad = false), SCROLL_STOP,
|
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
|
@VisibleForTesting internal val file = "${name.toLowerCase(Locale.CANADA)}.js"
|
||||||
internal val file = "${name.toLowerCase(Locale.CANADA)}.js"
|
private val injector = lazyContext {
|
||||||
private val injector = lazyContext {
|
try {
|
||||||
try {
|
val content = it.assets.open("js/$file").bufferedReader().use(BufferedReader::readText)
|
||||||
val content = it.assets.open("js/$file").bufferedReader().use(BufferedReader::readText)
|
JsBuilder().js(content).run { if (singleLoad) single(name) else this }.build()
|
||||||
JsBuilder().js(content).run { if (singleLoad) single(name) else this }.build()
|
} catch (e: FileNotFoundException) {
|
||||||
} catch (e: FileNotFoundException) {
|
L.e(e) { "JsAssets file not found" }
|
||||||
L.e(e) { "JsAssets file not found" }
|
JsInjector(JsActions.EMPTY.function)
|
||||||
JsInjector(JsActions.EMPTY.function)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun inject(webView: WebView, prefs: Prefs) =
|
override fun inject(webView: WebView, prefs: Prefs) =
|
||||||
injector(webView.context).inject(webView, prefs)
|
injector(webView.context).inject(webView, prefs)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// Ensures that all non themes and the selected theme are loaded
|
// Ensures that all non themes and the selected theme are loaded
|
||||||
suspend fun load(context: Context) {
|
suspend fun load(context: Context) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) { values().forEach { it.injector.invoke(context) } }
|
||||||
values().forEach { it.injector.invoke(context) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,125 +21,115 @@ import androidx.annotation.VisibleForTesting
|
|||||||
import com.pitchedapps.frost.prefs.Prefs
|
import com.pitchedapps.frost.prefs.Prefs
|
||||||
import com.pitchedapps.frost.utils.L
|
import com.pitchedapps.frost.utils.L
|
||||||
import com.pitchedapps.frost.web.FrostWebViewClient
|
import com.pitchedapps.frost.web.FrostWebViewClient
|
||||||
import org.apache.commons.text.StringEscapeUtils
|
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
import org.apache.commons.text.StringEscapeUtils
|
||||||
|
|
||||||
class JsBuilder {
|
class JsBuilder {
|
||||||
private val css = StringBuilder()
|
private val css = StringBuilder()
|
||||||
private val js = StringBuilder()
|
private val js = StringBuilder()
|
||||||
|
|
||||||
private var tag: String? = null
|
private var tag: String? = null
|
||||||
|
|
||||||
fun css(css: String): JsBuilder {
|
fun css(css: String): JsBuilder {
|
||||||
this.css.append(StringEscapeUtils.escapeEcmaScript(css))
|
this.css.append(StringEscapeUtils.escapeEcmaScript(css))
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun js(content: String): JsBuilder {
|
fun js(content: String): JsBuilder {
|
||||||
this.js.append(content)
|
this.js.append(content)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun single(tag: String): JsBuilder {
|
fun single(tag: String): JsBuilder {
|
||||||
this.tag = TagObfuscator.obfuscateTag(tag)
|
this.tag = TagObfuscator.obfuscateTag(tag)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun build() = JsInjector(toString())
|
fun build() = JsInjector(toString())
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
val tag = this.tag
|
val tag = this.tag
|
||||||
val builder = StringBuilder().apply {
|
val builder =
|
||||||
append("!function(){")
|
StringBuilder().apply {
|
||||||
if (css.isNotBlank()) {
|
append("!function(){")
|
||||||
val cssMin = css.replace(Regex("\\s*\n\\s*"), "")
|
if (css.isNotBlank()) {
|
||||||
append("var a=document.createElement('style');")
|
val cssMin = css.replace(Regex("\\s*\n\\s*"), "")
|
||||||
append("a.innerHTML='$cssMin';")
|
append("var a=document.createElement('style');")
|
||||||
if (tag != null) {
|
append("a.innerHTML='$cssMin';")
|
||||||
append("a.id='$tag';")
|
if (tag != null) {
|
||||||
}
|
append("a.id='$tag';")
|
||||||
append("document.head.appendChild(a);")
|
}
|
||||||
}
|
append("document.head.appendChild(a);")
|
||||||
if (js.isNotBlank()) {
|
|
||||||
append(js)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var content = builder.append("}()").toString()
|
if (js.isNotBlank()) {
|
||||||
if (tag != null) {
|
append(js)
|
||||||
content = singleInjector(tag, content)
|
|
||||||
}
|
}
|
||||||
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("if (!window.hasOwnProperty(\"$tag\")) {")
|
||||||
append("console.log(\"Registering $tag\");")
|
append("console.log(\"Registering $tag\");")
|
||||||
append("window.$tag = true;")
|
append("window.$tag = true;")
|
||||||
append(content)
|
append(content)
|
||||||
append("}")
|
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 {
|
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 */
|
||||||
* Toggle the injector (usually through Prefs
|
fun maybe(enable: Boolean): InjectorContract = if (enable) this else JsActions.EMPTY
|
||||||
* 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) {
|
fun WebView.jsInject(vararg injectors: InjectorContract, prefs: Prefs) {
|
||||||
injectors.asSequence().filter { it != JsActions.EMPTY }.forEach {
|
injectors.asSequence().filter { it != JsActions.EMPTY }.forEach { it.inject(this, prefs) }
|
||||||
it.inject(this, prefs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun FrostWebViewClient.jsInject(vararg injectors: InjectorContract, prefs: 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 {
|
class JsInjector(val function: String) : InjectorContract {
|
||||||
override fun inject(webView: WebView, prefs: Prefs) =
|
override fun inject(webView: WebView, prefs: Prefs) = webView.evaluateJavascript(function, null)
|
||||||
webView.evaluateJavascript(function, null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Helper object to obfuscate window tags for JS injection. */
|
||||||
* Helper object to obfuscate window tags for JS injection.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal object TagObfuscator {
|
internal object TagObfuscator {
|
||||||
|
|
||||||
fun obfuscateTag(tag: String): String {
|
fun obfuscateTag(tag: String): String {
|
||||||
val rnd = Random(tag.hashCode() + salt)
|
val rnd = Random(tag.hashCode() + salt)
|
||||||
val obfuscated = buildString {
|
val obfuscated = buildString {
|
||||||
append(prefix)
|
append(prefix)
|
||||||
append('_')
|
append('_')
|
||||||
appendRandomChars(rnd, 16)
|
appendRandomChars(rnd, 16)
|
||||||
}
|
|
||||||
L.v { "TagObfuscator: Obfuscating tag '$tag' to '$obfuscated'" }
|
|
||||||
return obfuscated
|
|
||||||
}
|
}
|
||||||
|
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 {
|
private val prefix: String by lazy {
|
||||||
val rnd = Random(System.currentTimeMillis())
|
val rnd = Random(System.currentTimeMillis())
|
||||||
buildString { appendRandomChars(rnd, 8) }
|
buildString { appendRandomChars(rnd, 8) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Appendable.appendRandomChars(random: Random, count: Int) {
|
private fun Appendable.appendRandomChars(random: Random, count: Int) {
|
||||||
for (i in 1..count) {
|
for (i in 1..count) {
|
||||||
append('a' + random.nextInt(26))
|
append('a' + random.nextInt(26))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,157 +35,151 @@ import dagger.Module
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
interface ThemeProvider {
|
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]] */
|
||||||
* Note that while this can be loaded from any thread, it is typically done through [preload]]
|
fun injector(category: ThemeCategory): InjectorContract
|
||||||
*/
|
|
||||||
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].
|
* Provides [InjectorContract] for each [ThemeCategory]. Can be reloaded to take in changes from
|
||||||
* Can be reloaded to take in changes from [Prefs]
|
* [Prefs]
|
||||||
*/
|
*/
|
||||||
class ThemeProviderImpl @Inject internal constructor(
|
class ThemeProviderImpl
|
||||||
@ApplicationContext private val context: Context,
|
@Inject
|
||||||
private val prefs: Prefs
|
internal constructor(@ApplicationContext private val context: Context, private val prefs: Prefs) :
|
||||||
) : ThemeProvider {
|
ThemeProvider {
|
||||||
|
|
||||||
private var theme: Theme = Theme.values[prefs.theme]
|
private var theme: Theme = Theme.values[prefs.theme]
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
prefs.theme = value.ordinal
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setTheme(id: Int) {
|
private val injectors: MutableMap<ThemeCategory, InjectorContract> = mutableMapOf()
|
||||||
if (theme.ordinal == id) return
|
|
||||||
theme = Theme.values[id]
|
|
||||||
reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun reset() {
|
override val textColor: Int
|
||||||
injectors.clear()
|
get() = theme.textColorGetter(prefs)
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun preload() {
|
override val accentColor: Int
|
||||||
withContext(Dispatchers.IO) {
|
get() = theme.accentColorGetter(prefs)
|
||||||
reset()
|
|
||||||
ThemeCategory.values().forEach { injector(it) }
|
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
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface ThemeProviderModule {
|
interface ThemeProviderModule {
|
||||||
@Binds
|
@Binds @Singleton fun themeProvider(to: ThemeProviderImpl): ThemeProvider
|
||||||
@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.databinding.IntroThemeBinding
|
||||||
import com.pitchedapps.frost.enums.Theme
|
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) {
|
class IntroFragmentTheme : BaseIntroFragment(R.layout.intro_theme) {
|
||||||
|
|
||||||
private lateinit var binding: IntroThemeBinding
|
private lateinit var binding: IntroThemeBinding
|
||||||
|
|
||||||
val themeList
|
val themeList
|
||||||
get() = with(binding) {
|
get() =
|
||||||
listOf(introThemeLight, introThemeDark, introThemeAmoled, introThemeGlass)
|
with(binding) { listOf(introThemeLight, introThemeDark, introThemeAmoled, introThemeGlass) }
|
||||||
}
|
|
||||||
|
|
||||||
override fun viewArray(): Array<Array<out View>> = with(binding) {
|
override fun viewArray(): Array<Array<out View>> =
|
||||||
arrayOf(
|
with(binding) {
|
||||||
arrayOf(title),
|
arrayOf(
|
||||||
arrayOf(introThemeLight, introThemeDark),
|
arrayOf(title),
|
||||||
arrayOf(introThemeAmoled, introThemeGlass)
|
arrayOf(introThemeLight, introThemeDark),
|
||||||
)
|
arrayOf(introThemeAmoled, introThemeGlass)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
binding = IntroThemeBinding.bind(view)
|
binding = IntroThemeBinding.bind(view)
|
||||||
binding.init()
|
binding.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun IntroThemeBinding.init() {
|
private fun IntroThemeBinding.init() {
|
||||||
introThemeLight.setThemeClick(Theme.LIGHT)
|
introThemeLight.setThemeClick(Theme.LIGHT)
|
||||||
introThemeDark.setThemeClick(Theme.DARK)
|
introThemeDark.setThemeClick(Theme.DARK)
|
||||||
introThemeAmoled.setThemeClick(Theme.AMOLED)
|
introThemeAmoled.setThemeClick(Theme.AMOLED)
|
||||||
introThemeGlass.setThemeClick(Theme.GLASS)
|
introThemeGlass.setThemeClick(Theme.GLASS)
|
||||||
val currentTheme = prefs.theme - 1
|
val currentTheme = prefs.theme - 1
|
||||||
if (currentTheme in 0..3)
|
if (currentTheme in 0..3)
|
||||||
themeList.forEachIndexed { index, v ->
|
themeList.forEachIndexed { index, v -> v.scaleXY = if (index == currentTheme) 1.6f else 0.8f }
|
||||||
v.scaleXY = if (index == currentTheme) 1.6f else 0.8f
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun View.setThemeClick(theme: Theme) {
|
private fun View.setThemeClick(theme: Theme) {
|
||||||
setOnClickListener { v ->
|
setOnClickListener { v ->
|
||||||
themeProvider.setTheme(theme.ordinal)
|
themeProvider.setTheme(theme.ordinal)
|
||||||
(activity as IntroActivity).apply {
|
(activity as IntroActivity).apply {
|
||||||
binding.ripple.ripple(themeProvider.bgColor, v.x + v.pivotX, v.y + v.pivotY)
|
binding.ripple.ripple(themeProvider.bgColor, v.x + v.pivotX, v.y + v.pivotY)
|
||||||
theme()
|
theme()
|
||||||
}
|
}
|
||||||
themeList.forEach { it.animate().scaleXY(if (it == this) 1.6f else 0.8f).start() }
|
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 com.pitchedapps.frost.utils.launchTabCustomizerActivity
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
/**
|
/** Created by Allan Wang on 2017-07-28. */
|
||||||
* 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) {
|
||||||
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 imageDrawable: LayerDrawable by lazyResettableRegistered { image.drawable as LayerDrawable }
|
||||||
val phone: Drawable by lazyResettableRegistered { imageDrawable.findDrawableByLayerId(R.id.intro_phone) }
|
val phone: Drawable by lazyResettableRegistered {
|
||||||
val screen: Drawable by lazyResettableRegistered { imageDrawable.findDrawableByLayerId(R.id.intro_phone_screen) }
|
imageDrawable.findDrawableByLayerId(R.id.intro_phone)
|
||||||
val icon: ImageView by bindViewResettable(R.id.intro_button)
|
}
|
||||||
|
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?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
title.setText(titleRes)
|
title.setText(titleRes)
|
||||||
image.setImageResource(imageRes)
|
image.setImageResource(imageRes)
|
||||||
desc.setText(descRes)
|
desc.setText(descRes)
|
||||||
super.onViewCreated(view, savedInstanceState)
|
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() {
|
fun firstImageFragmentTransition(offset: Float) {
|
||||||
super.themeFragmentImpl()
|
if (offset < 0) image.alpha = 1 + offset
|
||||||
title.setTextColor(themeProvider.textColor)
|
}
|
||||||
desc.setTextColor(themeProvider.textColor)
|
|
||||||
phone.tint(themeProvider.textColor)
|
|
||||||
screen.tint(themeProvider.bgColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun themeImageComponent(color: Int, vararg id: Int) {
|
fun lastImageFragmentTransition(offset: Float) {
|
||||||
id.forEach { imageDrawable.findDrawableByLayerId(it).tint(color) }
|
if (offset > 0) image.alpha = 1 - offset
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class IntroAccountFragment : BaseImageIntroFragment(
|
class IntroAccountFragment :
|
||||||
|
BaseImageIntroFragment(
|
||||||
R.string.intro_multiple_accounts,
|
R.string.intro_multiple_accounts,
|
||||||
R.drawable.intro_phone_nav,
|
R.drawable.intro_phone_nav,
|
||||||
R.string.intro_multiple_accounts_desc
|
R.string.intro_multiple_accounts_desc
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun themeFragmentImpl() {
|
override fun themeFragmentImpl() {
|
||||||
super.themeFragmentImpl()
|
super.themeFragmentImpl()
|
||||||
themeImageComponent(themeProvider.iconColor, R.id.intro_phone_avatar_1, R.id.intro_phone_avatar_2)
|
themeImageComponent(
|
||||||
themeImageComponent(themeProvider.bgColor.colorToForeground(), R.id.intro_phone_nav)
|
themeProvider.iconColor,
|
||||||
themeImageComponent(themeProvider.headerColor, R.id.intro_phone_header)
|
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) {
|
override fun onPageScrolledImpl(positionOffset: Float) {
|
||||||
super.onPageScrolledImpl(positionOffset)
|
super.onPageScrolledImpl(positionOffset)
|
||||||
firstImageFragmentTransition(positionOffset)
|
firstImageFragmentTransition(positionOffset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class IntroTabTouchFragment : BaseImageIntroFragment(
|
class IntroTabTouchFragment :
|
||||||
R.string.intro_easy_navigation, R.drawable.intro_phone_tab, R.string.intro_easy_navigation_desc
|
BaseImageIntroFragment(
|
||||||
) {
|
R.string.intro_easy_navigation,
|
||||||
|
R.drawable.intro_phone_tab,
|
||||||
|
R.string.intro_easy_navigation_desc
|
||||||
|
) {
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
icon.visible().setIcon(GoogleMaterial.Icon.gmd_edit, 24)
|
icon.visible().setIcon(GoogleMaterial.Icon.gmd_edit, 24)
|
||||||
icon.setOnClickListener {
|
icon.setOnClickListener { activity?.launchTabCustomizerActivity() }
|
||||||
activity?.launchTabCustomizerActivity()
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun themeFragmentImpl() {
|
override fun themeFragmentImpl() {
|
||||||
super.themeFragmentImpl()
|
super.themeFragmentImpl()
|
||||||
themeImageComponent(
|
themeImageComponent(
|
||||||
themeProvider.iconColor,
|
themeProvider.iconColor,
|
||||||
R.id.intro_phone_icon_1,
|
R.id.intro_phone_icon_1,
|
||||||
R.id.intro_phone_icon_2,
|
R.id.intro_phone_icon_2,
|
||||||
R.id.intro_phone_icon_3,
|
R.id.intro_phone_icon_3,
|
||||||
R.id.intro_phone_icon_4
|
R.id.intro_phone_icon_4
|
||||||
)
|
)
|
||||||
themeImageComponent(themeProvider.headerColor, R.id.intro_phone_tab)
|
themeImageComponent(themeProvider.headerColor, R.id.intro_phone_tab)
|
||||||
themeImageComponent(themeProvider.textColor.withAlpha(80), R.id.intro_phone_icon_ripple)
|
themeImageComponent(themeProvider.textColor.withAlpha(80), R.id.intro_phone_icon_ripple)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class IntroTabContextFragment : BaseImageIntroFragment(
|
class IntroTabContextFragment :
|
||||||
|
BaseImageIntroFragment(
|
||||||
R.string.intro_context_aware,
|
R.string.intro_context_aware,
|
||||||
R.drawable.intro_phone_long_press,
|
R.drawable.intro_phone_long_press,
|
||||||
R.string.intro_context_aware_desc
|
R.string.intro_context_aware_desc
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun themeFragmentImpl() {
|
override fun themeFragmentImpl() {
|
||||||
super.themeFragmentImpl()
|
super.themeFragmentImpl()
|
||||||
themeImageComponent(themeProvider.headerColor, R.id.intro_phone_toolbar)
|
themeImageComponent(themeProvider.headerColor, R.id.intro_phone_toolbar)
|
||||||
themeImageComponent(themeProvider.bgColor.colorToForeground(0.1f), R.id.intro_phone_image)
|
themeImageComponent(themeProvider.bgColor.colorToForeground(0.1f), R.id.intro_phone_image)
|
||||||
themeImageComponent(
|
themeImageComponent(
|
||||||
themeProvider.bgColor.colorToForeground(0.2f),
|
themeProvider.bgColor.colorToForeground(0.2f),
|
||||||
R.id.intro_phone_like,
|
R.id.intro_phone_like,
|
||||||
R.id.intro_phone_share
|
R.id.intro_phone_share
|
||||||
)
|
)
|
||||||
themeImageComponent(themeProvider.bgColor.colorToForeground(0.3f), R.id.intro_phone_comment)
|
themeImageComponent(themeProvider.bgColor.colorToForeground(0.3f), R.id.intro_phone_comment)
|
||||||
themeImageComponent(
|
themeImageComponent(
|
||||||
themeProvider.bgColor.colorToForeground(0.1f),
|
themeProvider.bgColor.colorToForeground(0.1f),
|
||||||
R.id.intro_phone_card_1,
|
R.id.intro_phone_card_1,
|
||||||
R.id.intro_phone_card_2
|
R.id.intro_phone_card_2
|
||||||
)
|
)
|
||||||
themeImageComponent(
|
themeImageComponent(
|
||||||
themeProvider.textColor,
|
themeProvider.textColor,
|
||||||
R.id.intro_phone_image_indicator,
|
R.id.intro_phone_image_indicator,
|
||||||
R.id.intro_phone_comment_indicator,
|
R.id.intro_phone_comment_indicator,
|
||||||
R.id.intro_phone_card_indicator
|
R.id.intro_phone_card_indicator
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPageScrolledImpl(positionOffset: Float) {
|
override fun onPageScrolledImpl(positionOffset: Float) {
|
||||||
super.onPageScrolledImpl(positionOffset)
|
super.onPageScrolledImpl(positionOffset)
|
||||||
lastImageFragmentTransition(positionOffset)
|
lastImageFragmentTransition(positionOffset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,122 +45,118 @@ import kotlin.math.abs
|
|||||||
* Contains the base, start, and end fragments
|
* Contains the base, start, and end fragments
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/** The core intro fragment for all other fragments */
|
||||||
* The core intro fragment for all other fragments
|
|
||||||
*/
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
abstract class BaseIntroFragment(val layoutRes: Int) : Fragment() {
|
abstract class BaseIntroFragment(val layoutRes: Int) : Fragment() {
|
||||||
|
|
||||||
@Inject
|
@Inject protected lateinit var prefs: Prefs
|
||||||
protected lateinit var prefs: Prefs
|
|
||||||
|
|
||||||
@Inject
|
@Inject protected lateinit var themeProvider: ThemeProvider
|
||||||
protected lateinit var themeProvider: ThemeProvider
|
|
||||||
|
|
||||||
val screenWidth
|
val screenWidth
|
||||||
get() = resources.displayMetrics.widthPixels
|
get() = resources.displayMetrics.widthPixels
|
||||||
|
|
||||||
val lazyRegistry = LazyResettableRegistry()
|
val lazyRegistry = LazyResettableRegistry()
|
||||||
|
|
||||||
protected fun translate(offset: Float, views: Array<Array<out View>>) {
|
protected fun translate(offset: Float, views: Array<Array<out View>>) {
|
||||||
val maxTranslation = offset * screenWidth
|
val maxTranslation = offset * screenWidth
|
||||||
val increment = maxTranslation / views.size
|
val increment = maxTranslation / views.size
|
||||||
views.forEachIndexed { i, group ->
|
views.forEachIndexed { i, group ->
|
||||||
group.forEach {
|
group.forEach {
|
||||||
it.translationX =
|
it.translationX = if (offset > 0) -maxTranslation + i * increment else -(i + 1) * increment
|
||||||
if (offset > 0) -maxTranslation + i * increment else -(i + 1) * increment
|
it.alpha = 1 - abs(offset)
|
||||||
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
|
* Note that these ids aren't actually inside all layouts
|
||||||
* However, they are in most of them, so they are added here
|
* However, they are in most of them, so they are added here
|
||||||
* for convenience
|
* for convenience
|
||||||
*/
|
*/
|
||||||
protected val title: TextView by bindViewResettable(R.id.intro_title)
|
protected val title: TextView by bindViewResettable(R.id.intro_title)
|
||||||
protected val image: ImageView by bindViewResettable(R.id.intro_image)
|
protected val image: ImageView by bindViewResettable(R.id.intro_image)
|
||||||
protected val desc: TextView by bindViewResettable(R.id.intro_desc)
|
protected val desc: TextView by bindViewResettable(R.id.intro_desc)
|
||||||
|
|
||||||
protected fun defaultViewArray(): Array<Array<out View>> =
|
protected fun defaultViewArray(): Array<Array<out View>> =
|
||||||
arrayOf(arrayOf(title), arrayOf(image), arrayOf(desc))
|
arrayOf(arrayOf(title), arrayOf(image), arrayOf(desc))
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
return inflater.inflate(layoutRes, container, false)
|
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?) {
|
protected val viewArray: Array<Array<out View>> by lazyResettableRegistered { viewArray() }
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
themeFragment()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
protected abstract fun viewArray(): Array<Array<out View>>
|
||||||
Kotterknife.reset(this)
|
|
||||||
lazyRegistry.invalidateAll()
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun themeFragment() {
|
fun onPageScrolled(positionOffset: Float) {
|
||||||
if (view != null) themeFragmentImpl()
|
if (view != null) onPageScrolledImpl(positionOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun themeFragmentImpl() {
|
protected open fun onPageScrolledImpl(positionOffset: Float) {
|
||||||
(view as? ViewGroup)?.children?.forEach { (it as? TextView)?.setTextColor(themeProvider.textColor) }
|
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>>
|
protected open fun onPageSelectedImpl() {}
|
||||||
|
|
||||||
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() {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class IntroFragmentWelcome : BaseIntroFragment(R.layout.intro_welcome) {
|
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() {
|
override fun themeFragmentImpl() {
|
||||||
super.themeFragmentImpl()
|
super.themeFragmentImpl()
|
||||||
image.imageTintList = ColorStateList.valueOf(themeProvider.textColor)
|
image.imageTintList = ColorStateList.valueOf(themeProvider.textColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class IntroFragmentEnd : BaseIntroFragment(R.layout.intro_end) {
|
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() {
|
override fun themeFragmentImpl() {
|
||||||
super.themeFragmentImpl()
|
super.themeFragmentImpl()
|
||||||
image.imageTintList = ColorStateList.valueOf(themeProvider.textColor)
|
image.imageTintList = ColorStateList.valueOf(themeProvider.textColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
container.setOnSingleTapListener { _, event ->
|
container.setOnSingleTapListener { _, event ->
|
||||||
(activity as IntroActivity).finish(event.x, event.y)
|
(activity as IntroActivity).finish(event.x, event.y)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,109 +31,109 @@ import javax.inject.Inject
|
|||||||
*/
|
*/
|
||||||
@Deprecated(level = DeprecationLevel.WARNING, message = "Use pref segments")
|
@Deprecated(level = DeprecationLevel.WARNING, message = "Use pref segments")
|
||||||
class OldPrefs @Inject internal constructor(factory: KPrefFactory) :
|
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.
|
* Despite the naming, this toggle currently only enables debug logging. Verbose is never logged
|
||||||
* Verbose is never logged in release builds.
|
* in release builds.
|
||||||
*/
|
*/
|
||||||
var verboseLogging: Boolean by kpref("verbose_logging", false)
|
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
|
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 {
|
interface PrefsBase {
|
||||||
fun reset()
|
fun reset()
|
||||||
fun deleteKeys(vararg keys: String)
|
fun deleteKeys(vararg keys: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Prefs :
|
interface Prefs :
|
||||||
BehaviourPrefs,
|
BehaviourPrefs, CorePrefs, FeedPrefs, NotifPrefs, ThemePrefs, ShowcasePrefs, PrefsBase
|
||||||
CorePrefs,
|
|
||||||
FeedPrefs,
|
|
||||||
NotifPrefs,
|
|
||||||
ThemePrefs,
|
|
||||||
ShowcasePrefs,
|
|
||||||
PrefsBase
|
|
||||||
|
|
||||||
class PrefsImpl @Inject internal constructor(
|
class PrefsImpl
|
||||||
private val behaviourPrefs: BehaviourPrefs,
|
@Inject
|
||||||
private val corePrefs: CorePrefs,
|
internal constructor(
|
||||||
private val feedPrefs: FeedPrefs,
|
private val behaviourPrefs: BehaviourPrefs,
|
||||||
private val notifPrefs: NotifPrefs,
|
private val corePrefs: CorePrefs,
|
||||||
private val themePrefs: ThemePrefs,
|
private val feedPrefs: FeedPrefs,
|
||||||
private val showcasePrefs: ShowcasePrefs
|
private val notifPrefs: NotifPrefs,
|
||||||
) : Prefs,
|
private val themePrefs: ThemePrefs,
|
||||||
BehaviourPrefs by behaviourPrefs,
|
private val showcasePrefs: ShowcasePrefs
|
||||||
CorePrefs by corePrefs,
|
) :
|
||||||
FeedPrefs by feedPrefs,
|
Prefs,
|
||||||
NotifPrefs by notifPrefs,
|
BehaviourPrefs by behaviourPrefs,
|
||||||
ThemePrefs by themePrefs,
|
CorePrefs by corePrefs,
|
||||||
ShowcasePrefs by showcasePrefs {
|
FeedPrefs by feedPrefs,
|
||||||
|
NotifPrefs by notifPrefs,
|
||||||
|
ThemePrefs by themePrefs,
|
||||||
|
ShowcasePrefs by showcasePrefs {
|
||||||
|
|
||||||
override fun reset() {
|
override fun reset() {
|
||||||
behaviourPrefs.reset()
|
behaviourPrefs.reset()
|
||||||
corePrefs.reset()
|
corePrefs.reset()
|
||||||
feedPrefs.reset()
|
feedPrefs.reset()
|
||||||
notifPrefs.reset()
|
notifPrefs.reset()
|
||||||
themePrefs.reset()
|
themePrefs.reset()
|
||||||
showcasePrefs.reset()
|
showcasePrefs.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteKeys(vararg keys: String) {
|
override fun deleteKeys(vararg keys: String) {
|
||||||
behaviourPrefs.deleteKeys()
|
behaviourPrefs.deleteKeys()
|
||||||
corePrefs.deleteKeys()
|
corePrefs.deleteKeys()
|
||||||
feedPrefs.deleteKeys()
|
feedPrefs.deleteKeys()
|
||||||
notifPrefs.deleteKeys()
|
notifPrefs.deleteKeys()
|
||||||
themePrefs.deleteKeys()
|
themePrefs.deleteKeys()
|
||||||
showcasePrefs.deleteKeys()
|
showcasePrefs.deleteKeys()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface PrefModule {
|
interface PrefModule {
|
||||||
@Binds
|
@Binds @Singleton fun behaviour(to: BehaviourPrefsImpl): BehaviourPrefs
|
||||||
@Singleton
|
|
||||||
fun behaviour(to: BehaviourPrefsImpl): BehaviourPrefs
|
|
||||||
|
|
||||||
@Binds
|
@Binds @Singleton fun core(to: CorePrefsImpl): CorePrefs
|
||||||
@Singleton
|
|
||||||
fun core(to: CorePrefsImpl): CorePrefs
|
|
||||||
|
|
||||||
@Binds
|
@Binds @Singleton fun feed(to: FeedPrefsImpl): FeedPrefs
|
||||||
@Singleton
|
|
||||||
fun feed(to: FeedPrefsImpl): FeedPrefs
|
|
||||||
|
|
||||||
@Binds
|
@Binds @Singleton fun notif(to: NotifPrefsImpl): NotifPrefs
|
||||||
@Singleton
|
|
||||||
fun notif(to: NotifPrefsImpl): NotifPrefs
|
|
||||||
|
|
||||||
@Binds
|
@Binds @Singleton fun theme(to: ThemePrefsImpl): ThemePrefs
|
||||||
@Singleton
|
|
||||||
fun theme(to: ThemePrefsImpl): ThemePrefs
|
|
||||||
|
|
||||||
@Binds
|
@Binds @Singleton fun showcase(to: ShowcasePrefsImpl): ShowcasePrefs
|
||||||
@Singleton
|
|
||||||
fun showcase(to: ShowcasePrefsImpl): ShowcasePrefs
|
|
||||||
|
|
||||||
@Binds
|
@Binds @Singleton fun prefs(to: PrefsImpl): Prefs
|
||||||
@Singleton
|
|
||||||
fun prefs(to: PrefsImpl): Prefs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object PrefFactoryModule {
|
object PrefFactoryModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun factory(@ApplicationContext context: Context): KPrefFactory = KPrefFactoryAndroid(context)
|
fun factory(@ApplicationContext context: Context): KPrefFactory = KPrefFactoryAndroid(context)
|
||||||
}
|
}
|
||||||
|
@ -24,87 +24,67 @@ import com.pitchedapps.frost.prefs.PrefsBase
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
interface BehaviourPrefs : PrefsBase {
|
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(
|
class BehaviourPrefsImpl
|
||||||
factory: KPrefFactory,
|
@Inject
|
||||||
oldPrefs: OldPrefs,
|
internal constructor(
|
||||||
|
factory: KPrefFactory,
|
||||||
|
oldPrefs: OldPrefs,
|
||||||
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.behaviour", factory), BehaviourPrefs {
|
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.behaviour", factory), BehaviourPrefs {
|
||||||
|
|
||||||
override var biometricsEnabled: Boolean by kpref(
|
override var biometricsEnabled: Boolean by
|
||||||
"biometrics_enabled",
|
kpref("biometrics_enabled", oldPrefs.biometricsEnabled /* false */)
|
||||||
oldPrefs.biometricsEnabled /* false */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var overlayEnabled: Boolean by kpref(
|
override var overlayEnabled: Boolean by
|
||||||
"overlay_enabled",
|
kpref("overlay_enabled", oldPrefs.overlayEnabled /* true */)
|
||||||
oldPrefs.overlayEnabled /* true */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var overlayFullScreenSwipe: Boolean by kpref(
|
override var overlayFullScreenSwipe: Boolean by
|
||||||
"overlay_full_screen_swipe",
|
kpref("overlay_full_screen_swipe", oldPrefs.overlayFullScreenSwipe /* true */)
|
||||||
oldPrefs.overlayFullScreenSwipe /* true */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var viewpagerSwipe: Boolean by kpref(
|
override var viewpagerSwipe: Boolean by
|
||||||
"viewpager_swipe",
|
kpref("viewpager_swipe", oldPrefs.viewpagerSwipe /* true */)
|
||||||
oldPrefs.viewpagerSwipe /* true */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var loadMediaOnMeteredNetwork: Boolean by kpref(
|
override var loadMediaOnMeteredNetwork: Boolean by
|
||||||
"media_on_metered_network",
|
kpref("media_on_metered_network", oldPrefs.loadMediaOnMeteredNetwork /* true */)
|
||||||
oldPrefs.loadMediaOnMeteredNetwork /* true */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var debugSettings: Boolean by kpref(
|
override var debugSettings: Boolean by kpref("debug_settings", oldPrefs.debugSettings /* false */)
|
||||||
"debug_settings",
|
|
||||||
oldPrefs.debugSettings /* false */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var linksInDefaultApp: Boolean by kpref(
|
override var linksInDefaultApp: Boolean by
|
||||||
"link_in_default_app",
|
kpref("link_in_default_app", oldPrefs.linksInDefaultApp /* false */)
|
||||||
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(
|
override var autoRefreshFeed: Boolean by
|
||||||
"auto_refresh_feed",
|
kpref("auto_refresh_feed", oldPrefs.autoRefreshFeed /* false */)
|
||||||
oldPrefs.autoRefreshFeed /* false */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var showCreateFab: Boolean by kpref(
|
override var showCreateFab: Boolean by kpref("show_create_fab", oldPrefs.showCreateFab /* true */)
|
||||||
"show_create_fab",
|
|
||||||
oldPrefs.showCreateFab /* true */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var fullSizeImage: Boolean by kpref(
|
override var fullSizeImage: Boolean by
|
||||||
"full_size_image",
|
kpref("full_size_image", oldPrefs.fullSizeImage /* false */)
|
||||||
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
interface CorePrefs : PrefsBase {
|
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.
|
* Despite the naming, this toggle currently only enables debug logging. Verbose is never logged
|
||||||
* Verbose is never logged in release builds.
|
* in release builds.
|
||||||
*/
|
*/
|
||||||
var verboseLogging: Boolean
|
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(
|
class CorePrefsImpl
|
||||||
factory: KPrefFactory,
|
@Inject
|
||||||
oldPrefs: OldPrefs,
|
internal constructor(
|
||||||
|
factory: KPrefFactory,
|
||||||
|
oldPrefs: OldPrefs,
|
||||||
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.core", factory), CorePrefs {
|
) : 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
|
override val frostId: String
|
||||||
get() = "$installDate-$identifier"
|
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(
|
override var prevVersionCode: Int by kpref("prev_version_code", oldPrefs.prevVersionCode /* -1 */)
|
||||||
"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(
|
override var verboseLogging: Boolean by
|
||||||
"verbose_logging",
|
kpref("verbose_logging", oldPrefs.verboseLogging /* false */)
|
||||||
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(
|
override var exitConfirmation: Boolean by
|
||||||
"exit_confirmation",
|
kpref("exit_confirmation", oldPrefs.exitConfirmation /* true */)
|
||||||
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(
|
override var messageScrollToBottom: Boolean by
|
||||||
"message_scroll_to_bottom",
|
kpref("message_scroll_to_bottom", oldPrefs.messageScrollToBottom /* false */)
|
||||||
oldPrefs.messageScrollToBottom /* false */
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -25,79 +25,62 @@ import com.pitchedapps.frost.prefs.PrefsBase
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
interface FeedPrefs : PrefsBase {
|
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(
|
class FeedPrefsImpl @Inject internal constructor(factory: KPrefFactory, oldPrefs: OldPrefs) :
|
||||||
factory: KPrefFactory,
|
KPref("${BuildConfig.APPLICATION_ID}.prefs.feed", factory), FeedPrefs {
|
||||||
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(
|
override var feedSort: Int by kpref("feed_sort", oldPrefs.feedSort /* FeedSort.DEFAULT.ordinal */)
|
||||||
"feed_sort",
|
|
||||||
oldPrefs.feedSort /* FeedSort.DEFAULT.ordinal */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var aggressiveRecents: Boolean by kpref(
|
override var aggressiveRecents: Boolean by
|
||||||
"aggressive_recents",
|
kpref("aggressive_recents", oldPrefs.aggressiveRecents /* false */)
|
||||||
oldPrefs.aggressiveRecents /* false */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var showComposer: Boolean by kpref(
|
override var showComposer: Boolean by
|
||||||
"status_composer_feed",
|
kpref("status_composer_feed", oldPrefs.showComposer /* true */)
|
||||||
oldPrefs.showComposer /* true */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var showSuggestedFriends: Boolean by kpref(
|
override var showSuggestedFriends: Boolean by
|
||||||
"suggested_friends_feed",
|
kpref("suggested_friends_feed", oldPrefs.showSuggestedFriends /* true */)
|
||||||
oldPrefs.showSuggestedFriends /* true */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var showSuggestedGroups: Boolean by kpref(
|
override var showSuggestedGroups: Boolean by
|
||||||
"suggested_groups_feed",
|
kpref("suggested_groups_feed", oldPrefs.showSuggestedGroups /* true */)
|
||||||
oldPrefs.showSuggestedGroups /* true */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var showFacebookAds: Boolean by kpref(
|
override var showFacebookAds: Boolean by
|
||||||
"facebook_ads",
|
kpref("facebook_ads", oldPrefs.showFacebookAds /* false */)
|
||||||
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(
|
override var mainActivityLayoutType: Int by
|
||||||
"main_activity_layout_type",
|
kpref("main_activity_layout_type", oldPrefs.mainActivityLayoutType /* 0 */)
|
||||||
oldPrefs.mainActivityLayoutType /* 0 */
|
|
||||||
)
|
|
||||||
|
|
||||||
override val mainActivityLayout: MainActivityLayout
|
override val mainActivityLayout: MainActivityLayout
|
||||||
get() = MainActivityLayout(mainActivityLayoutType)
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
interface NotifPrefs : PrefsBase {
|
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(
|
class NotifPrefsImpl
|
||||||
factory: KPrefFactory,
|
@Inject
|
||||||
oldPrefs: OldPrefs,
|
internal constructor(
|
||||||
|
factory: KPrefFactory,
|
||||||
|
oldPrefs: OldPrefs,
|
||||||
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.notif", factory), NotifPrefs {
|
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.notif", factory), NotifPrefs {
|
||||||
|
|
||||||
override var notificationKeywords: Set<String> by kpref(
|
override var notificationKeywords: Set<String> by
|
||||||
"notification_keywords",
|
kpref("notification_keywords", oldPrefs.notificationKeywords /* mutableSetOf() */)
|
||||||
oldPrefs.notificationKeywords /* mutableSetOf() */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var notificationsGeneral: Boolean by kpref(
|
override var notificationsGeneral: Boolean by
|
||||||
"notification_general",
|
kpref("notification_general", oldPrefs.notificationsGeneral /* true */)
|
||||||
oldPrefs.notificationsGeneral /* true */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var notificationAllAccounts: Boolean by kpref(
|
override var notificationAllAccounts: Boolean by
|
||||||
"notification_all_accounts",
|
kpref("notification_all_accounts", oldPrefs.notificationAllAccounts /* true */)
|
||||||
oldPrefs.notificationAllAccounts /* true */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var notificationsInstantMessages: Boolean by kpref(
|
override var notificationsInstantMessages: Boolean by
|
||||||
"notification_im",
|
kpref("notification_im", oldPrefs.notificationsInstantMessages /* true */)
|
||||||
oldPrefs.notificationsInstantMessages /* true */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var notificationsImAllAccounts: Boolean by kpref(
|
override var notificationsImAllAccounts: Boolean by
|
||||||
"notification_im_all_accounts",
|
kpref("notification_im_all_accounts", oldPrefs.notificationsImAllAccounts /* false */)
|
||||||
oldPrefs.notificationsImAllAccounts /* false */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var notificationVibrate: Boolean by kpref(
|
override var notificationVibrate: Boolean by
|
||||||
"notification_vibrate",
|
kpref("notification_vibrate", oldPrefs.notificationVibrate /* true */)
|
||||||
oldPrefs.notificationVibrate /* true */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var notificationSound: Boolean by kpref(
|
override var notificationSound: Boolean by
|
||||||
"notification_sound",
|
kpref("notification_sound", oldPrefs.notificationSound /* true */)
|
||||||
oldPrefs.notificationSound /* true */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var notificationRingtone: String by kpref(
|
override var notificationRingtone: String by
|
||||||
"notification_ringtone",
|
kpref("notification_ringtone", oldPrefs.notificationRingtone /* "" */)
|
||||||
oldPrefs.notificationRingtone /* "" */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var messageRingtone: String by kpref(
|
override var messageRingtone: String by
|
||||||
"message_ringtone",
|
kpref("message_ringtone", oldPrefs.messageRingtone /* "" */)
|
||||||
oldPrefs.messageRingtone /* "" */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var notificationLights: Boolean by kpref(
|
override var notificationLights: Boolean by
|
||||||
"notification_lights",
|
kpref("notification_lights", oldPrefs.notificationLights /* true */)
|
||||||
oldPrefs.notificationLights /* true */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var notificationFreq: Long by kpref(
|
override var notificationFreq: Long by
|
||||||
"notification_freq",
|
kpref("notification_freq", oldPrefs.notificationFreq /* 15L */)
|
||||||
oldPrefs.notificationFreq /* 15L */
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -23,12 +23,10 @@ import com.pitchedapps.frost.prefs.PrefsBase
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
interface ShowcasePrefs : PrefsBase {
|
interface ShowcasePrefs : PrefsBase {
|
||||||
/**
|
/** Check if this is the first time launching the web overlay; show snackbar if true */
|
||||||
* Check if this is the first time launching the web overlay; show snackbar if true
|
val firstWebOverlay: Boolean
|
||||||
*/
|
|
||||||
val 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
|
* Showcase prefs that offer one time helpers to guide new users
|
||||||
*/
|
*/
|
||||||
class ShowcasePrefsImpl @Inject internal constructor(
|
class ShowcasePrefsImpl @Inject internal constructor(factory: KPrefFactory) :
|
||||||
factory: KPrefFactory
|
KPref("${BuildConfig.APPLICATION_ID}.showcase", factory), ShowcasePrefs {
|
||||||
) : 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
|
import javax.inject.Inject
|
||||||
|
|
||||||
interface ThemePrefs : PrefsBase {
|
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(
|
class ThemePrefsImpl
|
||||||
factory: KPrefFactory,
|
@Inject
|
||||||
oldPrefs: OldPrefs,
|
internal constructor(
|
||||||
|
factory: KPrefFactory,
|
||||||
|
oldPrefs: OldPrefs,
|
||||||
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.theme", factory), ThemePrefs {
|
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.theme", factory), ThemePrefs {
|
||||||
|
|
||||||
/**
|
/** Note that this is purely for the pref storage. Updating themes should use ThemeProvider */
|
||||||
* Note that this is purely for the pref storage. Updating themes should use
|
override var theme: Int by kpref("theme", oldPrefs.theme /* 0 */)
|
||||||
* ThemeProvider
|
|
||||||
*/
|
|
||||||
override var theme: Int by kpref("theme", oldPrefs.theme /* 0 */)
|
|
||||||
|
|
||||||
override var customTextColor: Int by kpref(
|
override var customTextColor: Int by
|
||||||
"color_text",
|
kpref("color_text", oldPrefs.customTextColor /* 0xffeceff1.toInt() */)
|
||||||
oldPrefs.customTextColor /* 0xffeceff1.toInt() */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var customAccentColor: Int by kpref(
|
override var customAccentColor: Int by
|
||||||
"color_accent",
|
kpref("color_accent", oldPrefs.customAccentColor /* 0xff0288d1.toInt() */)
|
||||||
oldPrefs.customAccentColor /* 0xff0288d1.toInt() */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var customBackgroundColor: Int by kpref(
|
override var customBackgroundColor: Int by
|
||||||
"color_bg",
|
kpref("color_bg", oldPrefs.customBackgroundColor /* 0xff212121.toInt() */)
|
||||||
oldPrefs.customBackgroundColor /* 0xff212121.toInt() */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var customHeaderColor: Int by kpref(
|
override var customHeaderColor: Int by
|
||||||
"color_header",
|
kpref("color_header", oldPrefs.customHeaderColor /* 0xff01579b.toInt() */)
|
||||||
oldPrefs.customHeaderColor /* 0xff01579b.toInt() */
|
|
||||||
)
|
|
||||||
|
|
||||||
override var customIconColor: Int by kpref(
|
override var customIconColor: Int by
|
||||||
"color_icons",
|
kpref("color_icons", oldPrefs.customIconColor /* 0xffeceff1.toInt() */)
|
||||||
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 android.app.job.JobService
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import ca.allanwang.kau.utils.ContextHelper
|
import ca.allanwang.kau.utils.ContextHelper
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
abstract class BaseJobService : JobService(), CoroutineScope {
|
abstract class BaseJobService : JobService(), CoroutineScope {
|
||||||
|
|
||||||
private lateinit var job: Job
|
private lateinit var job: Job
|
||||||
override val coroutineContext: CoroutineContext
|
override val coroutineContext: CoroutineContext
|
||||||
get() = ContextHelper.dispatcher + job
|
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 */
|
||||||
* Note that if a job plans on running asynchronously, it should return true
|
@CallSuper
|
||||||
*/
|
override fun onStartJob(params: JobParameters?): Boolean {
|
||||||
@CallSuper
|
job = Job()
|
||||||
override fun onStartJob(params: JobParameters?): Boolean {
|
return false
|
||||||
job = Job()
|
}
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onStopJob(params: JobParameters?): Boolean {
|
override fun onStopJob(params: JobParameters?): Boolean {
|
||||||
job.cancel()
|
job.cancel()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -60,283 +60,271 @@ import kotlin.math.abs
|
|||||||
private val _40_DP = 40.dpToPx
|
private val _40_DP = 40.dpToPx
|
||||||
|
|
||||||
private val pendingIntentFlagUpdateCurrent: Int
|
private val pendingIntentFlagUpdateCurrent: Int
|
||||||
get() = PendingIntent.FLAG_UPDATE_CURRENT or
|
get() =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
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(
|
enum class NotificationType(
|
||||||
val channelId: String,
|
val channelId: String,
|
||||||
private val overlayContext: OverlayContext,
|
private val overlayContext: OverlayContext,
|
||||||
private val fbItem: FbItem,
|
private val fbItem: FbItem,
|
||||||
private val parser: FrostParser<ParseNotification>,
|
private val parser: FrostParser<ParseNotification>,
|
||||||
private val ringtoneProvider: (Prefs) -> String
|
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(
|
private val groupPrefix = "frost_${name.toLowerCase(Locale.CANADA)}"
|
||||||
NOTIF_CHANNEL_GENERAL,
|
|
||||||
OverlayContext.NOTIFICATION,
|
|
||||||
FbItem.NOTIFICATIONS,
|
|
||||||
NotifParser,
|
|
||||||
{ it.notificationRingtone }
|
|
||||||
),
|
|
||||||
|
|
||||||
MESSAGE(
|
/** Optional binder to return the request bundle builder */
|
||||||
NOTIF_CHANNEL_MESSAGES,
|
internal open fun bindRequest(
|
||||||
OverlayContext.MESSAGE,
|
content: NotificationContent,
|
||||||
FbItem.MESSAGES,
|
cookie: String
|
||||||
MessageParser,
|
): (BaseBundle.() -> Unit)? = null
|
||||||
{ it.messageRingtone }
|
|
||||||
);
|
|
||||||
|
|
||||||
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
|
* Get unread data from designated parser Display notifications for those after old epoch Save new
|
||||||
*/
|
* epoch
|
||||||
internal open fun bindRequest(
|
*
|
||||||
content: NotificationContent,
|
* Returns the number of notifications generated, or -1 if an error occurred
|
||||||
cookie: String
|
*/
|
||||||
): (BaseBundle.() -> Unit)? = null
|
suspend fun fetch(
|
||||||
|
context: Context,
|
||||||
private fun bindRequest(intent: Intent, content: NotificationContent) {
|
data: CookieEntity,
|
||||||
val cookie = content.data.cookie ?: return
|
prefs: Prefs,
|
||||||
val binder = bindRequest(content, cookie) ?: return
|
notifDao: NotificationDao
|
||||||
val bundle = Bundle()
|
): Int {
|
||||||
bundle.binder()
|
val response =
|
||||||
intent.putExtras(bundle)
|
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 */
|
||||||
* Get unread data from designated parser
|
fun validText(text: String?): Boolean {
|
||||||
* Display notifications for those after old epoch
|
val t = text ?: return true
|
||||||
* Save new epoch
|
return prefs.notificationKeywords.none { t.contains(it, true) }
|
||||||
*
|
|
||||||
* 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun debugNotification(context: Context, data: CookieEntity) {
|
val notifContents =
|
||||||
val content = NotificationContent(
|
response.data.getUnreadNotifications(data).filter { notif ->
|
||||||
data,
|
validText(notif.title) && validText(notif.text)
|
||||||
System.currentTimeMillis(),
|
}
|
||||||
"https://github.com/AllanWang/Frost-for-Facebook",
|
if (notifContents.isEmpty()) return 0
|
||||||
"Debug Notif",
|
|
||||||
"Test 123",
|
val userId = data.id
|
||||||
System.currentTimeMillis() / 1000,
|
val prevLatestEpoch = notifDao.latestEpoch(userId, channelId)
|
||||||
"https://www.iconexperience.com/_img/v_collection_png/256x256/shadow/dog.png",
|
L.v { "Notif $name prev epoch $prevLatestEpoch" }
|
||||||
false
|
|
||||||
)
|
if (!notifDao.saveNotifications(channelId, notifContents)) {
|
||||||
createNotification(context, content).notify(context)
|
L.d { "Skip notifs for $name as saving failed" }
|
||||||
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
if (prevLatestEpoch == -1L && !BuildConfig.DEBUG) {
|
||||||
* Attach content related data to an intent
|
L.d { "Skipping first notification fetch" }
|
||||||
*/
|
return 0 // do not notify the first time
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
val newNotifContents = notifContents.filter { it.timestamp > prevLatestEpoch }
|
||||||
* Create a generic content for the provided type and user id.
|
|
||||||
* No content related data is added
|
if (newNotifContents.isEmpty()) {
|
||||||
*/
|
L.d { "No new notifs found for $name" }
|
||||||
fun createCommonIntent(context: Context, userId: Long): Intent {
|
return 0
|
||||||
val intent = Intent(context, FrostWebActivity::class.java)
|
|
||||||
intent.putExtra(ARG_USER_ID, userId)
|
|
||||||
overlayContext.put(intent)
|
|
||||||
return intent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
L.d { "${newNotifContents.size} new notifs found for $name" }
|
||||||
* 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)
|
val notifs = newNotifContents.map { createNotification(context, it) }
|
||||||
L.v { "Notif load $content" }
|
|
||||||
|
|
||||||
if (profileUrl != null) {
|
frostEvent("Notifications", "Type" to name, "Count" to notifs.size)
|
||||||
try {
|
if (notifs.size > 1) summaryNotification(context, userId, notifs.size).notify(context)
|
||||||
val profileImg = GlideApp.with(context)
|
val ringtone = ringtoneProvider(prefs)
|
||||||
.asBitmap()
|
notifs.forEachIndexed { i, notif ->
|
||||||
.load(profileUrl)
|
// Ring at most twice
|
||||||
.transform(FrostGlide.circleCrop)
|
notif.withAlert(context, i < 2, ringtone, prefs).notify(context)
|
||||||
.submit(_40_DP, _40_DP)
|
}
|
||||||
.get()
|
return notifs.size
|
||||||
notifBuilder.setLargeIcon(profileImg)
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
L.e { "Failed to get image $profileUrl" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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" }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
data class NotificationContent(
|
||||||
// TODO replace data with userId?
|
// TODO replace data with userId?
|
||||||
val data: CookieEntity,
|
val data: CookieEntity,
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val href: String,
|
val href: String,
|
||||||
val title: String? = null, // defaults to frost title
|
val title: String? = null, // defaults to frost title
|
||||||
val text: String,
|
val text: String,
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
val profileUrl: String?,
|
val profileUrl: String?,
|
||||||
val unread: Boolean
|
val unread: Boolean
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val notifId = abs(id.toInt())
|
val notifId = abs(id.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper for a complete notification builder and identifier
|
* Wrapper for a complete notification builder and identifier which can be immediately notified when
|
||||||
* which can be immediately notified when given a [Context]
|
* given a [Context]
|
||||||
*/
|
*/
|
||||||
data class FrostNotification(
|
data class FrostNotification(
|
||||||
private val tag: String,
|
private val tag: String,
|
||||||
private val id: Int,
|
private val id: Int,
|
||||||
val notif: NotificationCompat.Builder
|
val notif: NotificationCompat.Builder
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun withAlert(
|
fun withAlert(
|
||||||
context: Context,
|
context: Context,
|
||||||
enable: Boolean,
|
enable: Boolean,
|
||||||
ringtone: String,
|
ringtone: String,
|
||||||
prefs: Prefs
|
prefs: Prefs
|
||||||
): FrostNotification {
|
): FrostNotification {
|
||||||
notif.setFrostAlert(context, enable, ringtone, prefs)
|
notif.setFrostAlert(context, enable, ringtone, prefs)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun notify(context: Context) =
|
fun notify(context: Context) =
|
||||||
NotificationManagerCompat.from(context).notify(tag, id, notif.build())
|
NotificationManagerCompat.from(context).notify(tag, id, notif.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.scheduleNotificationsFromPrefs(prefs: Prefs): Boolean {
|
fun Context.scheduleNotificationsFromPrefs(prefs: Prefs): Boolean {
|
||||||
val shouldSchedule = prefs.hasNotifications
|
val shouldSchedule = prefs.hasNotifications
|
||||||
return if (shouldSchedule) scheduleNotifications(prefs.notificationFreq)
|
return if (shouldSchedule) scheduleNotifications(prefs.notificationFreq)
|
||||||
else scheduleNotifications(-1)
|
else scheduleNotifications(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.scheduleNotifications(minutes: Long): Boolean =
|
fun Context.scheduleNotifications(minutes: Long): Boolean =
|
||||||
scheduleJob<NotificationService>(NOTIFICATION_PERIODIC_JOB, minutes)
|
scheduleJob<NotificationService>(NOTIFICATION_PERIODIC_JOB, minutes)
|
||||||
|
|
||||||
fun Context.fetchNotifications(): Boolean =
|
fun Context.fetchNotifications(): Boolean = fetchJob<NotificationService>(NOTIFICATION_JOB_NOW)
|
||||||
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.utils.frostEvent
|
||||||
import com.pitchedapps.frost.widgets.NotificationWidget
|
import com.pitchedapps.frost.widgets.NotificationWidget
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 2017-06-14.
|
* Created by Allan Wang on 2017-06-14.
|
||||||
*
|
*
|
||||||
* Service to manage notifications
|
* Service to manage notifications Will periodically check through all accounts in the db and send
|
||||||
* Will periodically check through all accounts in the db and send notifications when appropriate
|
* notifications when appropriate
|
||||||
*
|
*
|
||||||
* All fetching is done through parsers
|
* All fetching is done through parsers
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NotificationService : BaseJobService() {
|
class NotificationService : BaseJobService() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var prefs: Prefs
|
||||||
lateinit var prefs: Prefs
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var notifDao: NotificationDao
|
||||||
lateinit var notifDao: NotificationDao
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var cookieDao: CookieDao
|
||||||
lateinit var cookieDao: CookieDao
|
|
||||||
|
|
||||||
override fun onStopJob(params: JobParameters?): Boolean {
|
override fun onStopJob(params: JobParameters?): Boolean {
|
||||||
super.onStopJob(params)
|
super.onStopJob(params)
|
||||||
prepareFinish(true)
|
prepareFinish(true)
|
||||||
return false
|
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
|
/**
|
||||||
|
* Implemented fetch to also notify when an error occurs Also normalized the output to return the
|
||||||
private fun prepareFinish(abrupt: Boolean) {
|
* number of notifications received
|
||||||
if (preparedFinish)
|
*/
|
||||||
return
|
private suspend fun fetch(jobId: Int, type: NotificationType, cookie: CookieEntity): Int {
|
||||||
preparedFinish = true
|
val count = type.fetch(this, cookie, prefs, notifDao)
|
||||||
val time = System.currentTimeMillis() - startTime
|
if (count < 0) {
|
||||||
L.i { "Notification service has ${if (abrupt) "finished abruptly" else "finished"} in $time ms" }
|
if (jobId == NOTIFICATION_JOB_NOW)
|
||||||
frostEvent(
|
generalNotification(666, R.string.error_notification, BuildConfig.DEBUG)
|
||||||
"NotificationTime",
|
return 0
|
||||||
"Type" to (if (abrupt) "Service force stop" else "Service"),
|
|
||||||
"IM Included" to prefs.notificationsInstantMessages,
|
|
||||||
"Duration" to time
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStartJob(params: JobParameters?): Boolean {
|
private fun logNotif(text: String): NotificationContent? {
|
||||||
super.onStartJob(params)
|
L.eThrow("NotificationService: $text")
|
||||||
L.i { "Fetching notifications" }
|
return null
|
||||||
launch {
|
}
|
||||||
try {
|
|
||||||
sendNotifications(params)
|
|
||||||
} finally {
|
|
||||||
if (!isActive)
|
|
||||||
prepareFinish(false)
|
|
||||||
jobFinished(params, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun sendNotifications(params: JobParameters?): Unit =
|
private fun generalNotification(id: Int, textRes: Int, withDefaults: Boolean) {
|
||||||
withContext(Dispatchers.Default) {
|
val notifBuilder =
|
||||||
val currentId = prefs.userId
|
frostNotification(NOTIF_CHANNEL_GENERAL)
|
||||||
val cookies = cookieDao.selectAll()
|
.setFrostAlert(this, withDefaults, prefs.notificationRingtone, prefs)
|
||||||
yield()
|
.setContentTitle(string(R.string.frost_name))
|
||||||
val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1
|
.setContentText(string(textRes))
|
||||||
var notifCount = 0
|
NotificationManagerCompat.from(this).notify(id, notifBuilder.build())
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -36,84 +36,76 @@ import com.pitchedapps.frost.prefs.Prefs
|
|||||||
import com.pitchedapps.frost.utils.L
|
import com.pitchedapps.frost.utils.L
|
||||||
import com.pitchedapps.frost.utils.frostUri
|
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_GENERAL = "general"
|
||||||
const val NOTIF_CHANNEL_MESSAGES = "messages"
|
const val NOTIF_CHANNEL_MESSAGES = "messages"
|
||||||
|
|
||||||
fun setupNotificationChannels(c: Context, themeProvider: ThemeProvider) {
|
fun setupNotificationChannels(c: Context, themeProvider: ThemeProvider) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
val manager = c.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val manager = c.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
val appName = c.string(R.string.frost_name)
|
val appName = c.string(R.string.frost_name)
|
||||||
val msg = c.string(R.string.messages)
|
val msg = c.string(R.string.messages)
|
||||||
manager.createNotificationChannel(NOTIF_CHANNEL_GENERAL, appName, themeProvider)
|
manager.createNotificationChannel(NOTIF_CHANNEL_GENERAL, appName, themeProvider)
|
||||||
manager.createNotificationChannel(NOTIF_CHANNEL_MESSAGES, "$appName: $msg", themeProvider)
|
manager.createNotificationChannel(NOTIF_CHANNEL_MESSAGES, "$appName: $msg", themeProvider)
|
||||||
manager.notificationChannels
|
manager.notificationChannels
|
||||||
.filter {
|
.filter { it.id != NOTIF_CHANNEL_GENERAL && it.id != NOTIF_CHANNEL_MESSAGES }
|
||||||
it.id != NOTIF_CHANNEL_GENERAL &&
|
.forEach { manager.deleteNotificationChannel(it.id) }
|
||||||
it.id != NOTIF_CHANNEL_MESSAGES
|
L.d {
|
||||||
}
|
"Created notification channels: ${manager.notificationChannels.size} channels, ${manager.notificationChannelGroups.size} groups"
|
||||||
.forEach { manager.deleteNotificationChannel(it.id) }
|
}
|
||||||
L.d { "Created notification channels: ${manager.notificationChannels.size} channels, ${manager.notificationChannelGroups.size} groups" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
private fun NotificationManager.createNotificationChannel(
|
private fun NotificationManager.createNotificationChannel(
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
themeProvider: ThemeProvider
|
themeProvider: ThemeProvider
|
||||||
): NotificationChannel {
|
): NotificationChannel {
|
||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT)
|
||||||
id,
|
channel.enableLights(true)
|
||||||
name, NotificationManager.IMPORTANCE_DEFAULT
|
channel.lightColor = themeProvider.accentColor
|
||||||
)
|
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||||
channel.enableLights(true)
|
createNotificationChannel(channel)
|
||||||
channel.lightColor = themeProvider.accentColor
|
return channel
|
||||||
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
||||||
createNotificationChannel(channel)
|
|
||||||
return channel
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.frostNotification(id: String) =
|
fun Context.frostNotification(id: String) =
|
||||||
NotificationCompat.Builder(this, id)
|
NotificationCompat.Builder(this, id).apply {
|
||||||
.apply {
|
setSmallIcon(R.drawable.frost_f_24)
|
||||||
setSmallIcon(R.drawable.frost_f_24)
|
setAutoCancel(true)
|
||||||
setAutoCancel(true)
|
setOnlyAlertOnce(true)
|
||||||
setOnlyAlertOnce(true)
|
setStyle(NotificationCompat.BigTextStyle())
|
||||||
setStyle(NotificationCompat.BigTextStyle())
|
color = color(R.color.frost_notification_accent)
|
||||||
color = color(R.color.frost_notification_accent)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dictates whether a notification should have sound/vibration/lights or not
|
* Dictates whether a notification should have sound/vibration/lights or not Delegates to channels
|
||||||
* Delegates to channels if Android O and up
|
* if Android O and up Otherwise uses our provided preferences
|
||||||
* Otherwise uses our provided preferences
|
|
||||||
*/
|
*/
|
||||||
fun NotificationCompat.Builder.setFrostAlert(
|
fun NotificationCompat.Builder.setFrostAlert(
|
||||||
context: Context,
|
context: Context,
|
||||||
enable: Boolean,
|
enable: Boolean,
|
||||||
ringtone: String,
|
ringtone: String,
|
||||||
prefs: Prefs
|
prefs: Prefs
|
||||||
): NotificationCompat.Builder {
|
): NotificationCompat.Builder {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
setGroupAlertBehavior(
|
setGroupAlertBehavior(
|
||||||
if (enable) NotificationCompat.GROUP_ALERT_CHILDREN
|
if (enable) NotificationCompat.GROUP_ALERT_CHILDREN
|
||||||
else NotificationCompat.GROUP_ALERT_SUMMARY
|
else NotificationCompat.GROUP_ALERT_SUMMARY
|
||||||
)
|
)
|
||||||
} else if (!enable) {
|
} else if (!enable) {
|
||||||
setDefaults(0)
|
setDefaults(0)
|
||||||
} else {
|
} else {
|
||||||
var defaults = 0
|
var defaults = 0
|
||||||
if (prefs.notificationVibrate) defaults = defaults or Notification.DEFAULT_VIBRATE
|
if (prefs.notificationVibrate) defaults = defaults or Notification.DEFAULT_VIBRATE
|
||||||
if (prefs.notificationSound) {
|
if (prefs.notificationSound) {
|
||||||
if (ringtone.isNotBlank()) setSound(context.frostUri(ringtone))
|
if (ringtone.isNotBlank()) setSound(context.frostUri(ringtone))
|
||||||
else defaults = defaults or Notification.DEFAULT_SOUND
|
else defaults = defaults or Notification.DEFAULT_SOUND
|
||||||
}
|
|
||||||
if (prefs.notificationLights) defaults = defaults or Notification.DEFAULT_LIGHTS
|
|
||||||
setDefaults(defaults)
|
|
||||||
}
|
}
|
||||||
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"
|
const val NOTIFICATION_PARAM_ID = "notif_param_id"
|
||||||
|
|
||||||
fun JobInfo.Builder.setExtras(id: Int): JobInfo.Builder {
|
fun JobInfo.Builder.setExtras(id: Int): JobInfo.Builder {
|
||||||
val bundle = PersistableBundle()
|
val bundle = PersistableBundle()
|
||||||
bundle.putInt(NOTIFICATION_PARAM_ID, id)
|
bundle.putInt(NOTIFICATION_PARAM_ID, id)
|
||||||
return setExtras(bundle)
|
return setExtras(bundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* interval is # of min, which must be at least 15
|
* interval is # of min, which must be at least 15 returns false if an error occurs; true otherwise
|
||||||
* returns false if an error occurs; true otherwise
|
|
||||||
*/
|
*/
|
||||||
inline fun <reified T : JobService> Context.scheduleJob(id: Int, minutes: Long): Boolean {
|
inline fun <reified T : JobService> Context.scheduleJob(id: Int, minutes: Long): Boolean {
|
||||||
val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
||||||
scheduler.cancel(id)
|
scheduler.cancel(id)
|
||||||
if (minutes < 0L) return true
|
if (minutes < 0L) return true
|
||||||
val serviceComponent = ComponentName(this, T::class.java)
|
val serviceComponent = ComponentName(this, T::class.java)
|
||||||
val builder = JobInfo.Builder(id, serviceComponent)
|
val builder =
|
||||||
.setPeriodic(minutes * 60000)
|
JobInfo.Builder(id, serviceComponent)
|
||||||
.setExtras(id)
|
.setPeriodic(minutes * 60000)
|
||||||
.setPersisted(true)
|
.setExtras(id)
|
||||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) // TODO add options
|
.setPersisted(true)
|
||||||
val result = scheduler.schedule(builder.build())
|
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) // TODO add options
|
||||||
if (result <= 0) {
|
val result = scheduler.schedule(builder.build())
|
||||||
L.eThrow("${T::class.java.simpleName} scheduler failed")
|
if (result <= 0) {
|
||||||
return false
|
L.eThrow("${T::class.java.simpleName} scheduler failed")
|
||||||
}
|
return false
|
||||||
return true
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Run notification job right now */
|
||||||
* Run notification job right now
|
|
||||||
*/
|
|
||||||
inline fun <reified T : JobService> Context.fetchJob(id: Int): Boolean {
|
inline fun <reified T : JobService> Context.fetchJob(id: Int): Boolean {
|
||||||
val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
||||||
val serviceComponent = ComponentName(this, T::class.java)
|
val serviceComponent = ComponentName(this, T::class.java)
|
||||||
val builder = JobInfo.Builder(id, serviceComponent)
|
val builder =
|
||||||
.setMinimumLatency(0L)
|
JobInfo.Builder(id, serviceComponent)
|
||||||
.setExtras(id)
|
.setMinimumLatency(0L)
|
||||||
.setOverrideDeadline(2000L)
|
.setExtras(id)
|
||||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
.setOverrideDeadline(2000L)
|
||||||
val result = scheduler.schedule(builder.build())
|
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||||
if (result <= 0) {
|
val result = scheduler.schedule(builder.build())
|
||||||
L.eThrow("${T::class.java.simpleName} instant scheduler failed")
|
if (result <= 0) {
|
||||||
return false
|
L.eThrow("${T::class.java.simpleName} instant scheduler failed")
|
||||||
}
|
return false
|
||||||
return true
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
@ -32,12 +32,11 @@ import javax.inject.Inject
|
|||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class UpdateReceiver : BroadcastReceiver() {
|
class UpdateReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var prefs: Prefs
|
||||||
lateinit var prefs: Prefs
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
|
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
|
||||||
L.d { "Frost has updated" }
|
L.d { "Frost has updated" }
|
||||||
context.scheduleNotifications(prefs.notificationFreq) // Update notifications
|
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.utils.launchTabCustomizerActivity
|
||||||
import com.pitchedapps.frost.views.KPrefTextSeekbar
|
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 = {
|
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 = {
|
||||||
text(R.string.theme, prefs::theme, { themeProvider.setTheme(it) }) {
|
materialDialog {
|
||||||
onClick = {
|
title(R.string.theme)
|
||||||
materialDialog {
|
listItemsSingleChoice(
|
||||||
title(R.string.theme)
|
items = Theme.values().map { string(it.textRes) },
|
||||||
listItemsSingleChoice(
|
initialSelection = item.pref
|
||||||
items = Theme.values().map { string(it.textRes) },
|
) { _, index, _ ->
|
||||||
initialSelection = item.pref
|
if (item.pref != index) {
|
||||||
) { _, index, _ ->
|
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()
|
|
||||||
shouldRestartMain()
|
shouldRestartMain()
|
||||||
}
|
|
||||||
) {
|
|
||||||
dependsOnCustom()
|
|
||||||
allowCustomAlpha = false
|
|
||||||
}
|
|
||||||
|
|
||||||
colorPicker(
|
|
||||||
R.string.accent_color, prefs::customAccentColor,
|
|
||||||
{
|
|
||||||
prefs.customAccentColor = it
|
|
||||||
reload()
|
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)
|
activityThemer.setFrostTheme(forceTransparent = true)
|
||||||
shouldRestartMain()
|
themeExterior()
|
||||||
}
|
|
||||||
) {
|
|
||||||
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()
|
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()
|
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(
|
checkbox(
|
||||||
R.string.main_activity_layout,
|
R.string.tint_nav,
|
||||||
prefs::mainActivityLayoutType,
|
prefs::tintNavBar,
|
||||||
{ prefs.mainActivityLayoutType = it }
|
{
|
||||||
) {
|
prefs.tintNavBar = it
|
||||||
textGetter = { string(prefs.mainActivityLayout.titleRes) }
|
frostNavigationBar(prefs, themeProvider)
|
||||||
onClick = {
|
setFrostResult(REQUEST_NAV)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
descRes = R.string.tint_nav_desc
|
||||||
|
}
|
||||||
|
|
||||||
plainText(R.string.main_tabs) {
|
list.add(
|
||||||
descRes = R.string.main_tabs_desc
|
KPrefTextSeekbar(
|
||||||
onClick = { launchTabCustomizerActivity() }
|
KPrefSeekbar.KPrefSeekbarBuilder(
|
||||||
}
|
globalOptions,
|
||||||
|
R.string.web_text_scaling,
|
||||||
checkbox(
|
prefs::webTextScaling
|
||||||
R.string.tint_nav, prefs::tintNavBar,
|
) {
|
||||||
{
|
prefs.webTextScaling = it
|
||||||
prefs.tintNavBar = it
|
setFrostResult(REQUEST_TEXT_ZOOM)
|
||||||
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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
checkbox(
|
checkbox(R.string.enforce_black_media_bg, prefs::blackMediaBg, { prefs.blackMediaBg = it }) {
|
||||||
R.string.enforce_black_media_bg, prefs::blackMediaBg,
|
descRes = R.string.enforce_black_media_bg_desc
|
||||||
{
|
}
|
||||||
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.R
|
||||||
import com.pitchedapps.frost.activities.SettingsActivity
|
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 = {
|
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 }) {
|
checkbox(
|
||||||
descRes = R.string.auto_refresh_feed_desc
|
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 }) {
|
checkbox(
|
||||||
descRes = R.string.fancy_animations_desc
|
R.string.overlay_swipe,
|
||||||
|
prefs::overlayEnabled,
|
||||||
|
{
|
||||||
|
prefs.overlayEnabled = it
|
||||||
|
shouldRefreshMain()
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
descRes = R.string.overlay_swipe_desc
|
||||||
|
}
|
||||||
|
|
||||||
checkbox(
|
checkbox(
|
||||||
R.string.overlay_swipe,
|
R.string.overlay_full_screen_swipe,
|
||||||
prefs::overlayEnabled,
|
prefs::overlayFullScreenSwipe,
|
||||||
{ prefs.overlayEnabled = it; shouldRefreshMain() }
|
{ prefs.overlayFullScreenSwipe = it }
|
||||||
) {
|
) {
|
||||||
descRes = R.string.overlay_swipe_desc
|
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(
|
checkbox(R.string.enable_pip, prefs::enablePip, { prefs.enablePip = it }) {
|
||||||
R.string.overlay_full_screen_swipe,
|
descRes = R.string.enable_pip_desc
|
||||||
prefs::overlayFullScreenSwipe,
|
}
|
||||||
{ prefs.overlayFullScreenSwipe = it }
|
|
||||||
) {
|
|
||||||
descRes = R.string.overlay_full_screen_swipe_desc
|
|
||||||
}
|
|
||||||
|
|
||||||
checkbox(
|
// Not available for desktop user agent for now
|
||||||
R.string.open_links_in_default,
|
// plainText(R.string.autoplay_settings) {
|
||||||
prefs::linksInDefaultApp,
|
// descRes = R.string.autoplay_settings_desc
|
||||||
{ prefs.linksInDefaultApp = it }
|
// onClick = {
|
||||||
) {
|
// launchWebOverlay("${FB_URL_BASE}settings/videos/")
|
||||||
descRes = R.string.open_links_in_default_desc
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
checkbox(R.string.viewpager_swipe, prefs::viewpagerSwipe, { prefs.viewpagerSwipe = it }) {
|
checkbox(R.string.exit_confirmation, prefs::exitConfirmation, { prefs.exitConfirmation = it }) {
|
||||||
descRes = R.string.viewpager_swipe_desc
|
descRes = R.string.exit_confirmation_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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -40,123 +40,114 @@ import com.pitchedapps.frost.prefs.Prefs
|
|||||||
import com.pitchedapps.frost.utils.L
|
import com.pitchedapps.frost.utils.L
|
||||||
import com.pitchedapps.frost.utils.frostUriFromFile
|
import com.pitchedapps.frost.utils.frostUriFromFile
|
||||||
import com.pitchedapps.frost.utils.sendFrostEmail
|
import com.pitchedapps.frost.utils.sendFrostEmail
|
||||||
|
import java.io.File
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 2017-06-30.
|
* Created by Allan Wang on 2017-06-30.
|
||||||
*
|
*
|
||||||
* A sub pref section that is enabled through a hidden preference
|
* A sub pref section that is enabled through a hidden preference Each category will load a page,
|
||||||
* Each category will load a page, extract the contents, remove private info, and create a report
|
* extract the contents, remove private info, and create a report
|
||||||
*/
|
*/
|
||||||
fun SettingsActivity.getDebugPrefs(): KPrefAdapterBuilder.() -> Unit = {
|
fun SettingsActivity.getDebugPrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||||
|
plainText(R.string.disclaimer) { descRes = R.string.debug_disclaimer_info }
|
||||||
|
|
||||||
plainText(R.string.disclaimer) {
|
plainText(R.string.debug_web) {
|
||||||
descRes = R.string.debug_disclaimer_info
|
descRes = R.string.debug_web_desc
|
||||||
}
|
onClick = { this@getDebugPrefs.startActivityForResult<DebugActivity>(ACTIVITY_REQUEST_DEBUG) }
|
||||||
|
}
|
||||||
|
|
||||||
plainText(R.string.debug_web) {
|
plainText(R.string.debug_parsers) {
|
||||||
descRes = R.string.debug_web_desc
|
descRes = R.string.debug_parsers_desc
|
||||||
onClick =
|
onClick = {
|
||||||
{ this@getDebugPrefs.startActivityForResult<DebugActivity>(ACTIVITY_REQUEST_DEBUG) }
|
val parsers = arrayOf(NotifParser, MessageParser, SearchParser)
|
||||||
}
|
|
||||||
|
|
||||||
plainText(R.string.debug_parsers) {
|
materialDialog {
|
||||||
descRes = R.string.debug_parsers_desc
|
// noinspection CheckResult
|
||||||
onClick = {
|
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)
|
attempt =
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
materialDialog {
|
try {
|
||||||
// noinspection CheckResult
|
val data = parser.parse(fbCookie.webCookie)
|
||||||
listItems(items = parsers.map { string(it.nameRes) }) { dialog, position, _ ->
|
withMainContext {
|
||||||
dialog.dismiss()
|
loading.dismiss()
|
||||||
val parser = parsers[position]
|
createEmail(parser, data?.data, prefs)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
createEmail(parser, "Error: ${e.message}", prefs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Context.createEmail(parser: FrostParser<*>, content: Any?, prefs: Prefs) =
|
private fun Context.createEmail(parser: FrostParser<*>, content: Any?, prefs: Prefs) =
|
||||||
sendFrostEmail(
|
sendFrostEmail(
|
||||||
"${string(R.string.debug_report)}: ${parser::class.java.simpleName}",
|
"${string(R.string.debug_report)}: ${parser::class.java.simpleName}",
|
||||||
prefs = prefs
|
prefs = prefs
|
||||||
) {
|
) {
|
||||||
addItem("Url", parser.url)
|
addItem("Url", parser.url)
|
||||||
addItem("Contents", "$content")
|
addItem("Contents", "$content")
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val ZIP_NAME = "debug"
|
private const val ZIP_NAME = "debug"
|
||||||
|
|
||||||
fun SettingsActivity.sendDebug(url: String, html: String?) {
|
fun SettingsActivity.sendDebug(url: String, html: String?) {
|
||||||
|
|
||||||
val downloader = OfflineWebsite(
|
val downloader =
|
||||||
url,
|
OfflineWebsite(
|
||||||
cookie = fbCookie.webCookie ?: "",
|
url,
|
||||||
baseUrl = FB_URL_BASE,
|
cookie = fbCookie.webCookie ?: "",
|
||||||
html = html,
|
baseUrl = FB_URL_BASE,
|
||||||
baseDir = DebugActivity.baseDir(this)
|
html = html,
|
||||||
|
baseDir = DebugActivity.baseDir(this)
|
||||||
)
|
)
|
||||||
|
|
||||||
val job = Job()
|
val job = Job()
|
||||||
|
|
||||||
val md = materialDialog {
|
val md = materialDialog {
|
||||||
title(R.string.parsing_data)
|
title(R.string.parsing_data)
|
||||||
// TODO remove dialog? No progress ui
|
// TODO remove dialog? No progress ui
|
||||||
negativeButton(R.string.kau_cancel) { it.dismiss() }
|
negativeButton(R.string.kau_cancel) { it.dismiss() }
|
||||||
cancelOnTouchOutside(false)
|
cancelOnTouchOutside(false)
|
||||||
onDismiss { job.cancel() }
|
onDismiss { job.cancel() }
|
||||||
}
|
}
|
||||||
|
|
||||||
val progressFlow = MutableStateFlow(0)
|
val progressFlow = MutableStateFlow(0)
|
||||||
|
|
||||||
// progressFlow.onEach { md.setProgress(it) }.launchIn(this)
|
// progressFlow.onEach { md.setProgress(it) }.launchIn(this)
|
||||||
|
|
||||||
launchMain {
|
launchMain {
|
||||||
val success = downloader.loadAndZip(ZIP_NAME) {
|
val success = downloader.loadAndZip(ZIP_NAME) { progressFlow.tryEmit(it) }
|
||||||
progressFlow.tryEmit(it)
|
md.dismiss()
|
||||||
}
|
if (success) {
|
||||||
md.dismiss()
|
val zipUri = frostUriFromFile(File(downloader.baseDir, "$ZIP_NAME.zip"))
|
||||||
if (success) {
|
L.i { "Sending debug zip with uri $zipUri" }
|
||||||
val zipUri = frostUriFromFile(
|
sendFrostEmail(R.string.debug_report_email_title, prefs = prefs) {
|
||||||
File(downloader.baseDir, "$ZIP_NAME.zip")
|
addItem("Url", url)
|
||||||
)
|
addAttachment(zipUri)
|
||||||
L.i { "Sending debug zip with uri $zipUri" }
|
extras = { type = "application/zip" }
|
||||||
sendFrostEmail(R.string.debug_report_email_title, prefs = prefs) {
|
}
|
||||||
addItem("Url", url)
|
} else {
|
||||||
addAttachment(zipUri)
|
toast(R.string.error_generic)
|
||||||
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