mirror of
https://github.com/AllanWang/Frost-for-Facebook.git
synced 2024-11-10 04:52:38 +01:00
Merge dev
This commit is contained in:
commit
58f4f9298b
41
.travis.yml
41
.travis.yml
@ -1,32 +1,23 @@
|
|||||||
language: android
|
language: java
|
||||||
jdk:
|
services:
|
||||||
- oraclejdk8
|
- docker
|
||||||
android:
|
|
||||||
components:
|
|
||||||
- tools
|
|
||||||
- platform-tools
|
|
||||||
- build-tools-28.0.3
|
|
||||||
- android-28
|
|
||||||
- extra-android-support
|
|
||||||
- extra-android-m2repository
|
|
||||||
- extra-google-m2repository
|
|
||||||
licenses:
|
|
||||||
- ".+"
|
|
||||||
git:
|
git:
|
||||||
depth: 500
|
depth: 500
|
||||||
before_install:
|
before_install:
|
||||||
- openssl aes-256-cbc -K $encrypted_0454d0cf846c_key -iv $encrypted_0454d0cf846c_iv
|
- openssl aes-256-cbc -K $encrypted_0454d0cf846c_key -iv $encrypted_0454d0cf846c_iv
|
||||||
-in files/frost.tar.enc -out files/frost.tar -d
|
-in files/frost.tar.enc -out files/frost.tar -d
|
||||||
- tar xvf files/frost.tar
|
- tar xvf files/frost.tar
|
||||||
- yes | sdkmanager "platforms;android-28"
|
- docker build -q -t frost .
|
||||||
|
- docker volume create -o device=$HOME/.gradle/caches/ -o o=bind gradle_caches
|
||||||
|
- docker volume create -o device=$HOME/.gradle/wrapper/ -o o=bind gradle_wrapper
|
||||||
|
install: true
|
||||||
after_success:
|
after_success:
|
||||||
- chmod +x ./generate-apk-release.sh; ./generate-apk-release.sh
|
- ./generate-apk-release.sh
|
||||||
script:
|
script:
|
||||||
- cd $TRAVIS_BUILD_DIR/
|
- cd $TRAVIS_BUILD_DIR
|
||||||
- printf "Starting script\n"
|
- docker run --name frost_container -v gradle_caches:/root/.gradle/caches/ -v gradle_wrapper:/root/.gradle/wrapper/ frost
|
||||||
- chmod +x gradlew
|
- mkdir $HOME/Frost
|
||||||
- "./gradlew --quiet androidGitVersion"
|
- docker cp frost_container:/frost/app/build/outputs/apk/releaseTest/. $HOME/Frost
|
||||||
- "./gradlew lintReleaseTest testReleaseUnitTest assembleReleaseTest"
|
|
||||||
notifications:
|
notifications:
|
||||||
email: false
|
email: false
|
||||||
slack:
|
slack:
|
||||||
@ -47,13 +38,11 @@ branches:
|
|||||||
- l10n_dev
|
- l10n_dev
|
||||||
before_cache:
|
before_cache:
|
||||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
- rm -rf $HOME/.gradle/caches/*/plugin-resolution/
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
- "$HOME/.gradle/caches/"
|
- $HOME/.gradle/caches/
|
||||||
- "$HOME/.gradle/wrapper/"
|
- $HOME/.gradle/wrapper/
|
||||||
- "$HOME/.android/build-cache"
|
|
||||||
- "$HOME/.m2s"
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- secure: X3J97ccW+8K0bXPXhX608vPx7Pr/G4ju7quxydqMaYGgClHxoL/WpXOBAyyllde5P28PY4kioaqcI21BEhnAw0QUbmnzVLA1Qd5VS7aMPHpEnInKuOxGZ2d570OZd1f+ozFVt05vzG0VBJlBAkVhz2GWNxQdmIV1sO28MH526JMuYaEREuuywVSZmAeY7AAbW9MeCC2wvHvNmhk2nk6NLRQcsrDHcBsimy9fnnQ9lT/QsvToi1ZJd/MN7YkGDUULR+YmaotBzG546UJ1EiZQX91bFEJfP0oL43Pk7t5snzmHnKjLOr8Mt5QsIUXaiy/uzhUVmuDh1i0GEpZmhqM7nz/T6P7ogaLbbyJeauNmf15nu+e3hSvNiTzKyIwfSSflv8Do3g8/Eo3dKfIi3I8/OKF/uZ76kywh2LRqtZAqxRDiAMDZVwsRgD4aztoWm5AWa3tSoGy1J7i1eoqX6bNqokRbjgheTqcjN13kCdSZi3pZX7UBYm2Vumhn4izhTume19Rh9SqTmRgQ8jM7ynxHh7vVsJPPJG0HbQ623xz+d9mtXGy1fAb0dcUJMXdOhFN3m6AnKuHiF7cmsqje7Euk/TOZyqZmu0xEhTkugMbNKwGrklJiwRr3IoLtPdhLE38u3/auloUqBQ4K/iA9ZdhAreTSHEaI9d3J4N6kqCj3U30=
|
- secure: X3J97ccW+8K0bXPXhX608vPx7Pr/G4ju7quxydqMaYGgClHxoL/WpXOBAyyllde5P28PY4kioaqcI21BEhnAw0QUbmnzVLA1Qd5VS7aMPHpEnInKuOxGZ2d570OZd1f+ozFVt05vzG0VBJlBAkVhz2GWNxQdmIV1sO28MH526JMuYaEREuuywVSZmAeY7AAbW9MeCC2wvHvNmhk2nk6NLRQcsrDHcBsimy9fnnQ9lT/QsvToi1ZJd/MN7YkGDUULR+YmaotBzG546UJ1EiZQX91bFEJfP0oL43Pk7t5snzmHnKjLOr8Mt5QsIUXaiy/uzhUVmuDh1i0GEpZmhqM7nz/T6P7ogaLbbyJeauNmf15nu+e3hSvNiTzKyIwfSSflv8Do3g8/Eo3dKfIi3I8/OKF/uZ76kywh2LRqtZAqxRDiAMDZVwsRgD4aztoWm5AWa3tSoGy1J7i1eoqX6bNqokRbjgheTqcjN13kCdSZi3pZX7UBYm2Vumhn4izhTume19Rh9SqTmRgQ8jM7ynxHh7vVsJPPJG0HbQ623xz+d9mtXGy1fAb0dcUJMXdOhFN3m6AnKuHiF7cmsqje7Euk/TOZyqZmu0xEhTkugMbNKwGrklJiwRr3IoLtPdhLE38u3/auloUqBQ4K/iA9ZdhAreTSHEaI9d3J4N6kqCj3U30=
|
||||||
|
49
Dockerfile
Normal file
49
Dockerfile
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
FROM openjdk:8
|
||||||
|
|
||||||
|
# Android SDK
|
||||||
|
|
||||||
|
ENV ANDROID_HOME /opt/android-sdk-linux
|
||||||
|
|
||||||
|
# Download Android SDK into $ANDROID_HOME
|
||||||
|
# You can find URL to the current version at: https://developer.android.com/studio/index.html
|
||||||
|
# Or https://github.com/Homebrew/homebrew-cask/blob/master/Casks/android-sdk.rb
|
||||||
|
|
||||||
|
RUN mkdir -p ${ANDROID_HOME} && \
|
||||||
|
cd ${ANDROID_HOME} && \
|
||||||
|
wget -q https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip -O android_tools.zip && \
|
||||||
|
unzip android_tools.zip && \
|
||||||
|
rm android_tools.zip
|
||||||
|
|
||||||
|
ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:${ANDROID_HOME}/platform-tools
|
||||||
|
|
||||||
|
# Accept Android SDK licenses && install other elements
|
||||||
|
# For full list; see sdkmanager --list --verbose
|
||||||
|
RUN yes | sdkmanager --licenses && \
|
||||||
|
sdkmanager 'platform-tools' && \
|
||||||
|
sdkmanager 'extras;google;m2repository' && \
|
||||||
|
sdkmanager 'extras;android;m2repository'
|
||||||
|
|
||||||
|
# SDK Specific
|
||||||
|
|
||||||
|
RUN sdkmanager 'platforms;android-28' && \
|
||||||
|
sdkmanager 'build-tools;28.0.3'
|
||||||
|
|
||||||
|
# Install Node.js
|
||||||
|
|
||||||
|
ENV NODEJS_VERSION=11.12.0 \
|
||||||
|
PATH=$PATH:/opt/node/bin
|
||||||
|
|
||||||
|
WORKDIR "/opt/node"
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y curl git ca-certificates --no-install-recommends && \
|
||||||
|
curl -sL https://nodejs.org/dist/v${NODEJS_VERSION}/node-v${NODEJS_VERSION}-linux-x64.tar.gz | tar xz --strip-components=1 && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
apt-get clean
|
||||||
|
|
||||||
|
RUN mkdir -p /frost/
|
||||||
|
|
||||||
|
WORKDIR /frost/
|
||||||
|
|
||||||
|
COPY . /frost/
|
||||||
|
|
||||||
|
CMD ["./docker_build.sh"]
|
@ -1,7 +1,7 @@
|
|||||||
# Frost-for-Facebook
|
# Frost-for-Facebook
|
||||||
|
|
||||||
[![Releaes Version](https://img.shields.io/github/release/AllanWang/Frost-for-Facebook.svg)](https://github.com/AllanWang/Frost-for-Facebook/releases)
|
[![Releaes Version](https://img.shields.io/github/release/AllanWang/Frost-for-Facebook.svg)](https://github.com/AllanWang/Frost-for-Facebook/releases)
|
||||||
[![Build Status](https://travis-ci.org/AllanWang/Frost-for-Facebook.svg?branch=dev)](https://travis-ci.org/AllanWang/Frost-for-Facebook)
|
[![Build Status](https://travis-ci.com/AllanWang/Frost-for-Facebook.svg?branch=dev)](https://travis-ci.com/AllanWang/Frost-for-Facebook)
|
||||||
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/frost-for-facebook/localized.svg)](https://crowdin.com/project/frost-for-facebook)
|
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/frost-for-facebook/localized.svg)](https://crowdin.com/project/frost-for-facebook)
|
||||||
[![ZenHub](https://img.shields.io/badge/Shipping%20faster%20with-ZenHub-45529A.svg)](https://app.zenhub.com/workspace/o/allanwang/frost-for-facebook/boards)
|
[![ZenHub](https://img.shields.io/badge/Shipping%20faster%20with-ZenHub-45529A.svg)](https://app.zenhub.com/workspace/o/allanwang/frost-for-facebook/boards)
|
||||||
[![BugSnag](https://img.shields.io/badge/Bug%20tracking%20with-BugSnag-37C2D9.svg)](https://www.bugsnag.com/)
|
[![BugSnag](https://img.shields.io/badge/Bug%20tracking%20with-BugSnag-37C2D9.svg)](https://www.bugsnag.com/)
|
||||||
|
68
_layouts/default.html
Normal file
68
_layouts/default.html
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<!--See https://github.com/pages-themes/minimal/blob/master/_layouts/default.html-->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ site.lang | default: "en-US" }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
{% seo %}
|
||||||
|
<link rel="stylesheet" href="{{ "/assets/css/style.css?v=" | append: site.github.build_revision | relative_url }}">
|
||||||
|
|
||||||
|
<!--Begin favicon-->
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ '/favicon/apple-touch-icon.png' | relative_url }}">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ '/favicon/favicon-32x32.png' | relative_url }}">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ '/favicon/favicon-16x16.png' | relative_url }}">
|
||||||
|
<link rel="manifest" href="{{ '/favicon/site.webmanifest' | relative_url }}">
|
||||||
|
<link rel="mask-icon" href="{{ '/favicon/safari-pinned-tab.svg' | relative_url }}" color="#3b5998">
|
||||||
|
<link rel="shortcut icon" href="{{ '/favicon/favicon.ico' | relative_url }}">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="msapplication-config" content="{{ '/favicon/browserconfig.xml' | relative_url }}">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<!--End favicon-->
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrapper">
|
||||||
|
<header>
|
||||||
|
<h1><a href="{{ "/" | absolute_url }}">{{ site.title | default: site.github.repository_name }}</a></h1>
|
||||||
|
|
||||||
|
{% if site.logo %}
|
||||||
|
<img src="{{site.logo | relative_url}}" alt="Logo" />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>{{ site.description | default: site.github.project_tagline }}</p>
|
||||||
|
|
||||||
|
{% if site.github.is_project_page %}
|
||||||
|
<p class="view"><a href="{{ site.github.repository_url }}">View the Project on GitHub <small>{{ site.github.repository_nwo }}</small></a></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if site.github.is_user_page %}
|
||||||
|
<p class="view"><a href="{{ site.github.owner_url }}">View My GitHub Profile</a></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if site.show_downloads %}
|
||||||
|
<ul class="downloads">
|
||||||
|
<li><a href="{{ site.github.zip_url }}">Download <strong>ZIP File</strong></a></li>
|
||||||
|
<li><a href="{{ site.github.tar_url }}">Download <strong>TAR Ball</strong></a></li>
|
||||||
|
<li><a href="{{ site.github.repository_url }}">View On <strong>GitHub</strong></a></li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
|
||||||
|
{{ content }}
|
||||||
|
|
||||||
|
</section>
|
||||||
|
<footer>
|
||||||
|
{% if site.github.is_project_page %}
|
||||||
|
<p>This project is maintained by <a href="{{ site.github.owner_url }}">{{ site.github.owner_name }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
<p><small>Hosted on GitHub Pages — Theme by <a href="https://github.com/orderedlist">orderedlist</a></small></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script src="{{ "/assets/js/scale.fix.js" | relative_url }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -27,6 +27,11 @@ android {
|
|||||||
versionName androidGitVersion.name()
|
versionName androidGitVersion.name()
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
javaCompileOptions {
|
||||||
|
annotationProcessorOptions {
|
||||||
|
arguments = ["room.schemaLocation": "$projectDir/src/schemas".toString()]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationVariants.all { variant ->
|
applicationVariants.all { variant ->
|
||||||
@ -174,6 +179,7 @@ dependencies {
|
|||||||
androidTestImplementation kauDependency.espresso
|
androidTestImplementation kauDependency.espresso
|
||||||
androidTestImplementation kauDependency.testRules
|
androidTestImplementation kauDependency.testRules
|
||||||
androidTestImplementation kauDependency.testRunner
|
androidTestImplementation kauDependency.testRunner
|
||||||
|
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:${KOTLIN}"
|
||||||
|
|
||||||
testImplementation kauDependency.kotlinTest
|
testImplementation kauDependency.kotlinTest
|
||||||
testImplementation "org.jetbrains.kotlin:kotlin-reflect:${KOTLIN}"
|
testImplementation "org.jetbrains.kotlin:kotlin-reflect:${KOTLIN}"
|
||||||
@ -202,9 +208,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation "androidx.biometric:biometric:${ANDX_BIOMETRIC}"
|
implementation "androidx.biometric:biometric:${ANDX_BIOMETRIC}"
|
||||||
|
|
||||||
// implementation "org.koin:koin-android:${KOIN}"
|
implementation "org.koin:koin-android:${KOIN}"
|
||||||
// testImplementation "org.koin:koin-test:${KOIN}"
|
testImplementation "org.koin:koin-test:${KOIN}"
|
||||||
// androidTestImplementation "org.koin:koin-test:${KOIN}"
|
androidTestImplementation "org.koin:koin-test:${KOIN}"
|
||||||
|
|
||||||
// androidTestImplementation "io.mockk:mockk:${MOCKK}"
|
// androidTestImplementation "io.mockk:mockk:${MOCKK}"
|
||||||
|
|
||||||
@ -255,6 +261,11 @@ dependencies {
|
|||||||
|
|
||||||
implementation "com.sothree.slidinguppanel:library:${SLIDING_PANEL}"
|
implementation "com.sothree.slidinguppanel:library:${SLIDING_PANEL}"
|
||||||
|
|
||||||
|
implementation "androidx.room:room-coroutines:${ROOM}"
|
||||||
|
implementation "androidx.room:room-runtime:${ROOM}"
|
||||||
|
kapt "androidx.room:room-compiler:${ROOM}"
|
||||||
|
testImplementation "androidx.room:room-testing:${ROOM}"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validates code and generates apk
|
// Validates code and generates apk
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.db
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import kotlin.test.AfterTest
|
||||||
|
import kotlin.test.BeforeTest
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
abstract class BaseDbTest {
|
||||||
|
|
||||||
|
protected lateinit var db: FrostDatabase
|
||||||
|
|
||||||
|
@BeforeTest
|
||||||
|
fun before() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
val privateDb = Room.inMemoryDatabaseBuilder(
|
||||||
|
context, FrostPrivateDatabase::class.java
|
||||||
|
).build()
|
||||||
|
val publicDb = Room.inMemoryDatabaseBuilder(
|
||||||
|
context, FrostPublicDatabase::class.java
|
||||||
|
).build()
|
||||||
|
db = FrostDatabase(privateDb, publicDb)
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterTest
|
||||||
|
fun after() {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.db
|
||||||
|
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import kotlin.test.fail
|
||||||
|
|
||||||
|
class CacheDbTest : BaseDbTest() {
|
||||||
|
|
||||||
|
private val dao get() = db.cacheDao()
|
||||||
|
private val cookieDao get() = db.cookieDao()
|
||||||
|
|
||||||
|
private fun cookie(id: Long) = CookieEntity(id, "name$id", "cookie$id")
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun save() {
|
||||||
|
val cookie = cookie(1L)
|
||||||
|
val type = "test"
|
||||||
|
val content = "long test".repeat(10000)
|
||||||
|
runBlocking {
|
||||||
|
cookieDao.save(cookie)
|
||||||
|
dao.save(cookie.id, type, content)
|
||||||
|
val cache = dao.select(cookie.id, type) ?: fail("Cache not found")
|
||||||
|
assertEquals(content, cache.contents, "Content mismatch")
|
||||||
|
assertTrue(
|
||||||
|
System.currentTimeMillis() - cache.lastUpdated < 500,
|
||||||
|
"Cache retrieval took over 500ms (${System.currentTimeMillis() - cache.lastUpdated})"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.db
|
||||||
|
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
class CookieDbTest : BaseDbTest() {
|
||||||
|
|
||||||
|
private val dao get() = db.cookieDao()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun basicCookie() {
|
||||||
|
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||||
|
runBlocking {
|
||||||
|
dao.save(cookie)
|
||||||
|
val cookies = dao.selectAll()
|
||||||
|
assertEquals(listOf(cookie), cookies, "Cookie mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deleteCookie() {
|
||||||
|
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
dao.save(cookie)
|
||||||
|
dao.deleteById(cookie.id + 1)
|
||||||
|
assertEquals(
|
||||||
|
listOf(cookie),
|
||||||
|
dao.selectAll(),
|
||||||
|
"Cookie list should be the same after inexistent deletion"
|
||||||
|
)
|
||||||
|
dao.deleteById(cookie.id)
|
||||||
|
assertEquals(emptyList(), dao.selectAll(), "Cookie list should be empty after deletion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insertReplaceCookie() {
|
||||||
|
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||||
|
runBlocking {
|
||||||
|
dao.save(cookie)
|
||||||
|
assertEquals(listOf(cookie), dao.selectAll(), "Cookie insertion failed")
|
||||||
|
dao.save(cookie.copy(name = "testName2"))
|
||||||
|
assertEquals(
|
||||||
|
listOf(cookie.copy(name = "testName2")),
|
||||||
|
dao.selectAll(),
|
||||||
|
"Cookie replacement failed"
|
||||||
|
)
|
||||||
|
dao.save(cookie.copy(id = 123L))
|
||||||
|
assertEquals(
|
||||||
|
setOf(cookie.copy(id = 123L), cookie.copy(name = "testName2")),
|
||||||
|
dao.selectAll().toSet(),
|
||||||
|
"New cookie insertion failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun selectCookie() {
|
||||||
|
val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
|
||||||
|
runBlocking {
|
||||||
|
dao.save(cookie)
|
||||||
|
assertEquals(cookie, dao.selectById(cookie.id), "Cookie selection failed")
|
||||||
|
assertNull(dao.selectById(cookie.id + 1), "Inexistent cookie selection failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.db
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.koin.error.NoBeanDefFoundException
|
||||||
|
import org.koin.standalone.get
|
||||||
|
import org.koin.test.KoinTest
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.full.functions
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class DatabaseTest : KoinTest {
|
||||||
|
|
||||||
|
inline fun <reified T : Any> hasKoin() = hasKoin(T::class)
|
||||||
|
|
||||||
|
fun <T : Any> hasKoin(klazz: KClass<T>): Boolean =
|
||||||
|
try {
|
||||||
|
get<T>(clazz = klazz)
|
||||||
|
true
|
||||||
|
} catch (e: NoBeanDefFoundException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database and all daos should be loaded as components
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun testKoins() {
|
||||||
|
hasKoin<FrostDatabase>()
|
||||||
|
val members = FrostDatabase::class.java.kotlin.functions.filter { it.name.endsWith("Dao") }
|
||||||
|
.mapNotNull { it.returnType.classifier as? KClass<*> }
|
||||||
|
assertTrue(members.isNotEmpty(), "Failed to find dao interfaces")
|
||||||
|
val missingKoins = (members + FrostDatabase::class).filter { !hasKoin(it) }
|
||||||
|
assertTrue(missingKoins.isEmpty(), "Missing koins: $missingKoins")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.db
|
||||||
|
|
||||||
|
import com.pitchedapps.frost.facebook.FbItem
|
||||||
|
import com.pitchedapps.frost.facebook.defaultTabs
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class GenericDbTest : BaseDbTest() {
|
||||||
|
|
||||||
|
private val dao get() = db.genericDao()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note that order is also preserved here
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun save() {
|
||||||
|
val tabs = listOf(FbItem.ACTIVITY_LOG, FbItem.BIRTHDAYS, FbItem.EVENTS, FbItem.MARKETPLACE, FbItem.ACTIVITY_LOG)
|
||||||
|
runBlocking {
|
||||||
|
dao.saveTabs(tabs)
|
||||||
|
assertEquals(tabs, dao.getTabs(), "Tab saving failed")
|
||||||
|
val newTabs = listOf(FbItem.PAGES, FbItem.MENU)
|
||||||
|
dao.saveTabs(newTabs)
|
||||||
|
assertEquals(newTabs, dao.getTabs(), "Tab overwrite failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun defaultRetrieve() {
|
||||||
|
runBlocking {
|
||||||
|
assertEquals(defaultTabs(), dao.getTabs(), "Default retrieval failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun ignoreErrors() {
|
||||||
|
runBlocking {
|
||||||
|
dao.save(GenericEntity(GenericDao.TYPE_TABS, "${FbItem.ACTIVITY_LOG.name},unknown,${FbItem.EVENTS.name}"))
|
||||||
|
assertEquals(
|
||||||
|
listOf(FbItem.ACTIVITY_LOG, FbItem.EVENTS),
|
||||||
|
dao.getTabs(),
|
||||||
|
"Tab fetching does not ignore unknown names"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.db
|
||||||
|
|
||||||
|
import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL
|
||||||
|
import com.pitchedapps.frost.services.NOTIF_CHANNEL_MESSAGES
|
||||||
|
import com.pitchedapps.frost.services.NotificationContent
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class NotificationDbTest : BaseDbTest() {
|
||||||
|
|
||||||
|
private val dao get() = db.notifDao()
|
||||||
|
|
||||||
|
private fun cookie(id: Long) = CookieEntity(id, "name$id", "cookie$id")
|
||||||
|
|
||||||
|
private fun notifContent(id: Long, cookie: CookieEntity, time: Long = id) = NotificationContent(
|
||||||
|
data = cookie,
|
||||||
|
id = id,
|
||||||
|
href = "",
|
||||||
|
title = null,
|
||||||
|
text = "",
|
||||||
|
timestamp = time,
|
||||||
|
profileUrl = null,
|
||||||
|
unread = true
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveAndRetrieve() {
|
||||||
|
val cookie = cookie(12345L)
|
||||||
|
// Unique unsorted ids
|
||||||
|
val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
|
||||||
|
runBlocking {
|
||||||
|
db.cookieDao().save(cookie)
|
||||||
|
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
|
||||||
|
val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL)
|
||||||
|
assertEquals(notifs.sortedByDescending { it.timestamp }, dbNotifs, "Incorrect notification list received")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun selectConditions() {
|
||||||
|
runBlocking {
|
||||||
|
val cookie1 = cookie(12345L)
|
||||||
|
val cookie2 = cookie(12L)
|
||||||
|
val notifs1 = (0L..2L).map { notifContent(it, cookie1) }
|
||||||
|
val notifs2 = (5L..10L).map { notifContent(it, cookie2) }
|
||||||
|
db.cookieDao().save(cookie1)
|
||||||
|
db.cookieDao().save(cookie2)
|
||||||
|
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1)
|
||||||
|
dao.saveNotifications(NOTIF_CHANNEL_MESSAGES, notifs2)
|
||||||
|
assertEquals(
|
||||||
|
emptyList(),
|
||||||
|
dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_MESSAGES),
|
||||||
|
"Filtering by type did not work for cookie1"
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
notifs1.sortedByDescending { it.timestamp },
|
||||||
|
dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_GENERAL),
|
||||||
|
"Selection for cookie1 failed"
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
emptyList(),
|
||||||
|
dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_GENERAL),
|
||||||
|
"Filtering by type did not work for cookie2"
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
notifs2.sortedByDescending { it.timestamp },
|
||||||
|
dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_MESSAGES),
|
||||||
|
"Selection for cookie2 failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary key is both id and userId, in the event that the same notification to multiple users has the same id
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun primaryKeyCheck() {
|
||||||
|
runBlocking {
|
||||||
|
val cookie1 = cookie(12345L)
|
||||||
|
val cookie2 = cookie(12L)
|
||||||
|
val notifs1 = (0L..2L).map { notifContent(it, cookie1) }
|
||||||
|
val notifs2 = notifs1.map { it.copy(data = cookie2) }
|
||||||
|
db.cookieDao().save(cookie1)
|
||||||
|
db.cookieDao().save(cookie2)
|
||||||
|
assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1), "Notif1 save failed")
|
||||||
|
assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs2), "Notif2 save failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cascadeDeletion() {
|
||||||
|
val cookie = cookie(12345L)
|
||||||
|
// Unique unsorted ids
|
||||||
|
val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
|
||||||
|
runBlocking {
|
||||||
|
db.cookieDao().save(cookie)
|
||||||
|
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
|
||||||
|
db.cookieDao().deleteById(cookie.id)
|
||||||
|
val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL)
|
||||||
|
assertTrue(dbNotifs.isEmpty(), "Cascade deletion failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun latestEpoch() {
|
||||||
|
val cookie = cookie(12345L)
|
||||||
|
// Unique unsorted ids
|
||||||
|
val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
|
||||||
|
runBlocking {
|
||||||
|
assertEquals(-1L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Default epoch failed")
|
||||||
|
db.cookieDao().save(cookie)
|
||||||
|
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
|
||||||
|
assertEquals(99L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Latest epoch failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insertionWithInvalidCookies() {
|
||||||
|
runBlocking {
|
||||||
|
assertFalse(
|
||||||
|
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, listOf(notifContent(1L, cookie(2L)))),
|
||||||
|
"Notif save should not have passed without relevant cookie entries"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -174,6 +174,20 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<!--Widgets-->
|
||||||
|
<receiver android:name=".widgets.NotificationWidget">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/notification_widget_info" />
|
||||||
|
</receiver>
|
||||||
|
<service
|
||||||
|
android:name=".widgets.NotificationWidgetService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
|
@ -29,9 +29,10 @@ import com.bumptech.glide.request.RequestOptions
|
|||||||
import com.bumptech.glide.signature.ApplicationVersionSignature
|
import com.bumptech.glide.signature.ApplicationVersionSignature
|
||||||
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
|
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
|
||||||
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
||||||
import com.pitchedapps.frost.dbflow.CookiesDb
|
import com.pitchedapps.frost.db.CookiesDb
|
||||||
import com.pitchedapps.frost.dbflow.FbTabsDb
|
import com.pitchedapps.frost.db.FbTabsDb
|
||||||
import com.pitchedapps.frost.dbflow.NotificationDb
|
import com.pitchedapps.frost.db.FrostDatabase
|
||||||
|
import com.pitchedapps.frost.db.NotificationDb
|
||||||
import com.pitchedapps.frost.glide.GlideApp
|
import com.pitchedapps.frost.glide.GlideApp
|
||||||
import com.pitchedapps.frost.services.scheduleNotifications
|
import com.pitchedapps.frost.services.scheduleNotifications
|
||||||
import com.pitchedapps.frost.services.setupNotificationChannels
|
import com.pitchedapps.frost.services.setupNotificationChannels
|
||||||
@ -44,6 +45,7 @@ import com.raizlabs.android.dbflow.config.DatabaseConfig
|
|||||||
import com.raizlabs.android.dbflow.config.FlowConfig
|
import com.raizlabs.android.dbflow.config.FlowConfig
|
||||||
import com.raizlabs.android.dbflow.config.FlowManager
|
import com.raizlabs.android.dbflow.config.FlowManager
|
||||||
import com.raizlabs.android.dbflow.runtime.ContentResolverNotifier
|
import com.raizlabs.android.dbflow.runtime.ContentResolverNotifier
|
||||||
|
import org.koin.android.ext.android.startKoin
|
||||||
import java.util.Random
|
import java.util.Random
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
@ -132,6 +134,7 @@ class FrostApp : Application() {
|
|||||||
L.d { "Activity ${activity.localClassName} created" }
|
L.d { "Activity ${activity.localClassName} created" }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
startKoin(this, listOf(FrostDatabase.module(this)))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initBugsnag() {
|
private fun initBugsnag() {
|
||||||
|
@ -32,8 +32,15 @@ import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
|||||||
import com.pitchedapps.frost.activities.LoginActivity
|
import com.pitchedapps.frost.activities.LoginActivity
|
||||||
import com.pitchedapps.frost.activities.MainActivity
|
import com.pitchedapps.frost.activities.MainActivity
|
||||||
import com.pitchedapps.frost.activities.SelectorActivity
|
import com.pitchedapps.frost.activities.SelectorActivity
|
||||||
import com.pitchedapps.frost.dbflow.CookieModel
|
import com.pitchedapps.frost.db.CookieDao
|
||||||
import com.pitchedapps.frost.dbflow.loadFbCookiesSync
|
import com.pitchedapps.frost.db.CookieEntity
|
||||||
|
import com.pitchedapps.frost.db.CookieModel
|
||||||
|
import com.pitchedapps.frost.db.FbTabModel
|
||||||
|
import com.pitchedapps.frost.db.GenericDao
|
||||||
|
import com.pitchedapps.frost.db.getTabs
|
||||||
|
import com.pitchedapps.frost.db.save
|
||||||
|
import com.pitchedapps.frost.db.saveTabs
|
||||||
|
import com.pitchedapps.frost.db.selectAll
|
||||||
import com.pitchedapps.frost.facebook.FbCookie
|
import com.pitchedapps.frost.facebook.FbCookie
|
||||||
import com.pitchedapps.frost.utils.BiometricUtils
|
import com.pitchedapps.frost.utils.BiometricUtils
|
||||||
import com.pitchedapps.frost.utils.EXTRA_COOKIES
|
import com.pitchedapps.frost.utils.EXTRA_COOKIES
|
||||||
@ -41,9 +48,12 @@ import com.pitchedapps.frost.utils.L
|
|||||||
import com.pitchedapps.frost.utils.Prefs
|
import com.pitchedapps.frost.utils.Prefs
|
||||||
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 com.raizlabs.android.dbflow.kotlinextensions.from
|
||||||
|
import com.raizlabs.android.dbflow.kotlinextensions.select
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,6 +61,9 @@ import java.util.ArrayList
|
|||||||
*/
|
*/
|
||||||
class StartActivity : KauBaseActivity() {
|
class StartActivity : KauBaseActivity() {
|
||||||
|
|
||||||
|
private val cookieDao: CookieDao by inject()
|
||||||
|
private val genericDao: GenericDao by inject()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@ -69,12 +82,11 @@ class StartActivity : KauBaseActivity() {
|
|||||||
launch {
|
launch {
|
||||||
val authDefer = BiometricUtils.authenticate(this@StartActivity)
|
val authDefer = BiometricUtils.authenticate(this@StartActivity)
|
||||||
try {
|
try {
|
||||||
|
migrate()
|
||||||
FbCookie.switchBackUser()
|
FbCookie.switchBackUser()
|
||||||
val cookies = ArrayList(withContext(Dispatchers.IO) {
|
val cookies = ArrayList(cookieDao.selectAll())
|
||||||
loadFbCookiesSync()
|
|
||||||
})
|
|
||||||
L.i { "Cookies loaded at time ${System.currentTimeMillis()}" }
|
L.i { "Cookies loaded at time ${System.currentTimeMillis()}" }
|
||||||
L._d { "Cookies: ${cookies.joinToString("\t", transform = CookieModel::toSensitiveString)}" }
|
L._d { "Cookies: ${cookies.joinToString("\t", transform = CookieEntity::toSensitiveString)}" }
|
||||||
loadAssets()
|
loadAssets()
|
||||||
authDefer.await()
|
authDefer.await()
|
||||||
when {
|
when {
|
||||||
@ -88,11 +100,32 @@ class StartActivity : KauBaseActivity() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
L._e(e) { "Load start failed" }
|
||||||
showInvalidWebView()
|
showInvalidWebView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate from dbflow to room
|
||||||
|
* TODO delete dbflow data
|
||||||
|
*/
|
||||||
|
private suspend fun migrate() = withContext(Dispatchers.IO) {
|
||||||
|
if (cookieDao.selectAll().isNotEmpty()) return@withContext
|
||||||
|
val cookies = (select from CookieModel::class).queryList().map { CookieEntity(it.id, it.name, it.cookie) }
|
||||||
|
if (cookies.isNotEmpty()) {
|
||||||
|
cookieDao.save(cookies)
|
||||||
|
L._d { "Migrated cookies ${cookieDao.selectAll()}" }
|
||||||
|
}
|
||||||
|
val tabs = (select from FbTabModel::class).queryList().map(FbTabModel::tab)
|
||||||
|
if (tabs.isNotEmpty()) {
|
||||||
|
genericDao.saveTabs(tabs)
|
||||||
|
L._d { "Migrated tabs ${genericDao.getTabs()}" }
|
||||||
|
}
|
||||||
|
deleteDatabase("Cookies.db")
|
||||||
|
deleteDatabase("FrostTabs.db")
|
||||||
|
}
|
||||||
|
|
||||||
private fun showInvalidWebView() =
|
private fun showInvalidWebView() =
|
||||||
showInvalidView(R.string.error_webview)
|
showInvalidView(R.string.error_webview)
|
||||||
|
|
||||||
|
@ -69,9 +69,10 @@ import com.pitchedapps.frost.contracts.FileChooserContract
|
|||||||
import com.pitchedapps.frost.contracts.FileChooserDelegate
|
import com.pitchedapps.frost.contracts.FileChooserDelegate
|
||||||
import com.pitchedapps.frost.contracts.MainActivityContract
|
import com.pitchedapps.frost.contracts.MainActivityContract
|
||||||
import com.pitchedapps.frost.contracts.VideoViewHolder
|
import com.pitchedapps.frost.contracts.VideoViewHolder
|
||||||
import com.pitchedapps.frost.dbflow.TAB_COUNT
|
import com.pitchedapps.frost.db.CookieDao
|
||||||
import com.pitchedapps.frost.dbflow.loadFbCookie
|
import com.pitchedapps.frost.db.GenericDao
|
||||||
import com.pitchedapps.frost.dbflow.loadFbTabs
|
import com.pitchedapps.frost.db.currentCookie
|
||||||
|
import com.pitchedapps.frost.db.getTabs
|
||||||
import com.pitchedapps.frost.enums.MainActivityLayout
|
import com.pitchedapps.frost.enums.MainActivityLayout
|
||||||
import com.pitchedapps.frost.facebook.FbCookie
|
import com.pitchedapps.frost.facebook.FbCookie
|
||||||
import com.pitchedapps.frost.facebook.FbItem
|
import com.pitchedapps.frost.facebook.FbItem
|
||||||
@ -103,12 +104,14 @@ import com.pitchedapps.frost.utils.setFrostColors
|
|||||||
import com.pitchedapps.frost.views.BadgedIcon
|
import com.pitchedapps.frost.views.BadgedIcon
|
||||||
import com.pitchedapps.frost.views.FrostVideoViewer
|
import com.pitchedapps.frost.views.FrostVideoViewer
|
||||||
import com.pitchedapps.frost.views.FrostViewPager
|
import com.pitchedapps.frost.views.FrostViewPager
|
||||||
|
import com.pitchedapps.frost.widgets.NotificationWidget
|
||||||
import kotlinx.android.synthetic.main.activity_frame_wrapper.*
|
import kotlinx.android.synthetic.main.activity_frame_wrapper.*
|
||||||
import kotlinx.android.synthetic.main.view_main_fab.*
|
import kotlinx.android.synthetic.main.view_main_fab.*
|
||||||
import kotlinx.android.synthetic.main.view_main_toolbar.*
|
import kotlinx.android.synthetic.main.view_main_toolbar.*
|
||||||
import kotlinx.android.synthetic.main.view_main_viewpager.*
|
import kotlinx.android.synthetic.main.view_main_viewpager.*
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 20/12/17.
|
* Created by Allan Wang on 20/12/17.
|
||||||
@ -120,9 +123,14 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
|
|||||||
FileChooserContract by FileChooserDelegate(),
|
FileChooserContract by FileChooserDelegate(),
|
||||||
VideoViewHolder, SearchViewHolder {
|
VideoViewHolder, SearchViewHolder {
|
||||||
|
|
||||||
protected lateinit var adapter: SectionsPagerAdapter
|
/**
|
||||||
|
* Note that tabs themselves are initialized through a coroutine during onCreate
|
||||||
|
*/
|
||||||
|
protected val adapter: SectionsPagerAdapter = SectionsPagerAdapter()
|
||||||
override val frameWrapper: FrameLayout get() = frame_wrapper
|
override val frameWrapper: FrameLayout get() = frame_wrapper
|
||||||
val viewPager: FrostViewPager get() = container
|
val viewPager: FrostViewPager get() = container
|
||||||
|
val cookieDao: CookieDao by inject()
|
||||||
|
val genericDao: GenericDao by inject()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Components with the same id in multiple layout files
|
* Components with the same id in multiple layout files
|
||||||
@ -131,6 +139,8 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
|
|||||||
val appBar: AppBarLayout by bindView(R.id.appbar)
|
val appBar: AppBarLayout by bindView(R.id.appbar)
|
||||||
val coordinator: CoordinatorLayout by bindView(R.id.main_content)
|
val coordinator: CoordinatorLayout by bindView(R.id.main_content)
|
||||||
|
|
||||||
|
protected var lastPosition = -1
|
||||||
|
|
||||||
override var videoViewer: FrostVideoViewer? = null
|
override var videoViewer: FrostVideoViewer? = null
|
||||||
private lateinit var drawer: Drawer
|
private lateinit var drawer: Drawer
|
||||||
private lateinit var drawerHeader: AccountHeader
|
private lateinit var drawerHeader: AccountHeader
|
||||||
@ -151,12 +161,13 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
|
|||||||
background(viewPager)
|
background(viewPager)
|
||||||
}
|
}
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
adapter = SectionsPagerAdapter(loadFbTabs())
|
|
||||||
viewPager.adapter = adapter
|
viewPager.adapter = adapter
|
||||||
viewPager.offscreenPageLimit = TAB_COUNT
|
|
||||||
tabs.setBackgroundColor(Prefs.mainActivityLayout.backgroundColor())
|
tabs.setBackgroundColor(Prefs.mainActivityLayout.backgroundColor())
|
||||||
onNestedCreate(savedInstanceState)
|
onNestedCreate(savedInstanceState)
|
||||||
L.i { "Main finished loading UI in ${System.currentTimeMillis() - start} ms" }
|
L.i { "Main finished loading UI in ${System.currentTimeMillis() - start} ms" }
|
||||||
|
launch {
|
||||||
|
adapter.setPages(genericDao.getTabs())
|
||||||
|
}
|
||||||
controlWebview = WebView(this)
|
controlWebview = WebView(this)
|
||||||
if (BuildConfig.VERSION_CODE > Prefs.versionCode) {
|
if (BuildConfig.VERSION_CODE > Prefs.versionCode) {
|
||||||
Prefs.prevVersionCode = Prefs.versionCode
|
Prefs.prevVersionCode = Prefs.versionCode
|
||||||
@ -274,36 +285,37 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
|
|||||||
if (current) launchWebOverlay(FbItem.PROFILE.url)
|
if (current) launchWebOverlay(FbItem.PROFILE.url)
|
||||||
else when (profile.identifier) {
|
else when (profile.identifier) {
|
||||||
-2L -> {
|
-2L -> {
|
||||||
val currentCookie = loadFbCookie(Prefs.userId)
|
// TODO no backpressure support
|
||||||
|
this@BaseMainActivity.launch {
|
||||||
|
val currentCookie = cookieDao.currentCookie()
|
||||||
if (currentCookie == null) {
|
if (currentCookie == null) {
|
||||||
toast(R.string.account_not_found)
|
toast(R.string.account_not_found)
|
||||||
launch {
|
|
||||||
FbCookie.reset()
|
FbCookie.reset()
|
||||||
launchLogin(cookies(), true)
|
launchLogin(cookies(), true)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
materialDialogThemed {
|
materialDialogThemed {
|
||||||
title(R.string.kau_logout)
|
title(R.string.kau_logout)
|
||||||
content(
|
content(
|
||||||
String.format(
|
String.format(
|
||||||
string(R.string.kau_logout_confirm_as_x), currentCookie.name
|
string(R.string.kau_logout_confirm_as_x),
|
||||||
?: Prefs.userId.toString()
|
currentCookie.name ?: Prefs.userId.toString()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
positiveText(R.string.kau_yes)
|
positiveText(R.string.kau_yes)
|
||||||
negativeText(R.string.kau_no)
|
negativeText(R.string.kau_no)
|
||||||
onPositive { _, _ ->
|
onPositive { _, _ ->
|
||||||
launch {
|
this@BaseMainActivity.launch {
|
||||||
FbCookie.logout(this@BaseMainActivity)
|
FbCookie.logout(this@BaseMainActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
-3L -> launchNewTask<LoginActivity>(clearStack = false)
|
-3L -> launchNewTask<LoginActivity>(clearStack = false)
|
||||||
-4L -> launchNewTask<SelectorActivity>(cookies(), false)
|
-4L -> launchNewTask<SelectorActivity>(cookies(), false)
|
||||||
else -> {
|
else -> {
|
||||||
launch {
|
this@BaseMainActivity.launch {
|
||||||
FbCookie.switchUser(profile.identifier)
|
FbCookie.switchUser(profile.identifier)
|
||||||
tabsForEachView { _, view -> view.badgeText = null }
|
tabsForEachView { _, view -> view.badgeText = null }
|
||||||
refreshAll()
|
refreshAll()
|
||||||
@ -371,6 +383,11 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
|
|||||||
R.id.action_settings to GoogleMaterial.Icon.gmd_settings,
|
R.id.action_settings to GoogleMaterial.Icon.gmd_settings,
|
||||||
R.id.action_search to GoogleMaterial.Icon.gmd_search
|
R.id.action_search to GoogleMaterial.Icon.gmd_search
|
||||||
)
|
)
|
||||||
|
bindSearchView(menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindSearchView(menu: Menu) {
|
||||||
searchViewBindIfNull {
|
searchViewBindIfNull {
|
||||||
bindSearchView(menu, R.id.action_search, Prefs.iconColor) {
|
bindSearchView(menu, R.id.action_search, Prefs.iconColor) {
|
||||||
textCallback = { query, searchView ->
|
textCallback = { query, searchView ->
|
||||||
@ -402,7 +419,6 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
|
|||||||
onItemClick = { _, key, _, _ -> launchWebOverlay(key) }
|
onItemClick = { _, key, _, _ -> launchWebOverlay(key) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@ -439,7 +455,11 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
|
|||||||
Runtime.getRuntime().exit(0)
|
Runtime.getRuntime().exit(0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (resultCode and REQUEST_RESTART > 0) return restart()
|
if (resultCode and REQUEST_RESTART > 0) {
|
||||||
|
NotificationWidget.forceUpdate(this)
|
||||||
|
restart()
|
||||||
|
return
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
* These results can be stacked
|
* These results can be stacked
|
||||||
*/
|
*/
|
||||||
@ -454,16 +474,12 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
|
|||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
outState.putStringArrayList(STATE_FORCE_FALLBACK, ArrayList(adapter.forcedFallbacks))
|
adapter.saveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
super.onRestoreInstanceState(savedInstanceState)
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
adapter.forcedFallbacks.clear()
|
adapter.restoreInstanceState(savedInstanceState)
|
||||||
adapter.forcedFallbacks.addAll(
|
|
||||||
savedInstanceState.getStringArrayList(STATE_FORCE_FALLBACK)
|
|
||||||
?: emptyList()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@ -496,6 +512,10 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun backConsumer(): Boolean {
|
override fun backConsumer(): Boolean {
|
||||||
|
if (drawer.isDrawerOpen) {
|
||||||
|
drawer.closeDrawer()
|
||||||
|
return true
|
||||||
|
}
|
||||||
if (currentFragment.onBackPressed()) return true
|
if (currentFragment.onBackPressed()) return true
|
||||||
if (Prefs.exitConfirmation) {
|
if (Prefs.exitConfirmation) {
|
||||||
materialDialogThemed {
|
materialDialogThemed {
|
||||||
@ -518,9 +538,48 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
|
|||||||
runOnUiThread { adapter.reloadFragment(fragment) }
|
runOnUiThread { adapter.reloadFragment(fragment) }
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class SectionsPagerAdapter(val pages: List<FbItem>) : FragmentPagerAdapter(supportFragmentManager) {
|
inner class SectionsPagerAdapter : FragmentPagerAdapter(supportFragmentManager) {
|
||||||
|
|
||||||
val forcedFallbacks = mutableSetOf<String>()
|
private val pages: MutableList<FbItem> = mutableListOf()
|
||||||
|
|
||||||
|
private val forcedFallbacks = mutableSetOf<String>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update page list and prompt reload
|
||||||
|
*/
|
||||||
|
fun setPages(pages: List<FbItem>) {
|
||||||
|
this.pages.clear()
|
||||||
|
this.pages.addAll(pages)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
tabs.removeAllTabs()
|
||||||
|
this.pages.forEachIndexed { index, fbItem ->
|
||||||
|
tabs.addTab(
|
||||||
|
tabs.newTab()
|
||||||
|
.setCustomView(BadgedIcon(this@BaseMainActivity).apply { iicon = fbItem.icon }.also {
|
||||||
|
it.setAllAlpha(if (index == 0) SELECTED_TAB_ALPHA else UNSELECTED_TAB_ALPHA)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
lastPosition = 0
|
||||||
|
viewPager.setCurrentItem(0, false)
|
||||||
|
viewPager.offscreenPageLimit = pages.size
|
||||||
|
viewPager.post {
|
||||||
|
if (!fragmentChannel.isClosedForSend)
|
||||||
|
fragmentChannel.offer(0)
|
||||||
|
} //trigger hook so title is set
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveInstanceState(outState: Bundle) {
|
||||||
|
outState.putStringArrayList(STATE_FORCE_FALLBACK, ArrayList(forcedFallbacks))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreInstanceState(savedInstanceState: Bundle) {
|
||||||
|
forcedFallbacks.clear()
|
||||||
|
forcedFallbacks.addAll(
|
||||||
|
savedInstanceState.getStringArrayList(STATE_FORCE_FALLBACK)
|
||||||
|
?: emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun reloadFragment(fragment: BaseFragment) {
|
fun reloadFragment(fragment: BaseFragment) {
|
||||||
if (fragment is WebFragment) return
|
if (fragment is WebFragment) return
|
||||||
@ -559,4 +618,9 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
|
|||||||
PointF(0f, toolbar.height.toFloat())
|
PointF(0f, toolbar.height.toFloat())
|
||||||
else
|
else
|
||||||
PointF(0f, 0f)
|
PointF(0f, 0f)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SELECTED_TAB_ALPHA = 255f
|
||||||
|
const val UNSELECTED_TAB_ALPHA = 128f
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,7 @@ import com.pitchedapps.frost.utils.Prefs
|
|||||||
import com.pitchedapps.frost.utils.cookies
|
import com.pitchedapps.frost.utils.cookies
|
||||||
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 com.pitchedapps.frost.widgets.NotificationWidget
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@ -171,6 +172,7 @@ class IntroActivity : KauBaseActivity(), ViewPager.PageTransformer, ViewPager.On
|
|||||||
override fun finish() {
|
override fun finish() {
|
||||||
launch(NonCancellable) {
|
launch(NonCancellable) {
|
||||||
loadAssets()
|
loadAssets()
|
||||||
|
NotificationWidget.forceUpdate(this@IntroActivity)
|
||||||
launchNewTask<MainActivity>(cookies(), false)
|
launchNewTask<MainActivity>(cookies(), false)
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
}
|
||||||
|
@ -32,9 +32,10 @@ import com.bumptech.glide.load.engine.GlideException
|
|||||||
import com.bumptech.glide.request.RequestListener
|
import com.bumptech.glide.request.RequestListener
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.bumptech.glide.request.target.Target
|
||||||
import com.pitchedapps.frost.R
|
import com.pitchedapps.frost.R
|
||||||
import com.pitchedapps.frost.dbflow.CookieModel
|
import com.pitchedapps.frost.db.CookieDao
|
||||||
import com.pitchedapps.frost.dbflow.loadFbCookiesSuspend
|
import com.pitchedapps.frost.db.CookieEntity
|
||||||
import com.pitchedapps.frost.dbflow.saveFbCookie
|
import com.pitchedapps.frost.db.save
|
||||||
|
import com.pitchedapps.frost.db.selectAll
|
||||||
import com.pitchedapps.frost.facebook.FbCookie
|
import com.pitchedapps.frost.facebook.FbCookie
|
||||||
import com.pitchedapps.frost.facebook.FbItem
|
import com.pitchedapps.frost.facebook.FbItem
|
||||||
import com.pitchedapps.frost.facebook.profilePictureUrl
|
import com.pitchedapps.frost.facebook.profilePictureUrl
|
||||||
@ -58,6 +59,7 @@ 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 org.koin.android.ext.android.inject
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
@ -71,6 +73,7 @@ class LoginActivity : BaseActivity() {
|
|||||||
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 val cookieDao: CookieDao by inject()
|
||||||
|
|
||||||
private lateinit var profileLoader: RequestManager
|
private lateinit var profileLoader: RequestManager
|
||||||
private val refreshChannel = Channel<Boolean>(10)
|
private val refreshChannel = Channel<Boolean>(10)
|
||||||
@ -109,13 +112,13 @@ class LoginActivity : BaseActivity() {
|
|||||||
refreshChannel.offer(refreshing)
|
refreshChannel.offer(refreshing)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadInfo(cookie: CookieModel): Unit = withMainContext {
|
private suspend fun loadInfo(cookie: CookieEntity): Unit = withMainContext {
|
||||||
refresh(true)
|
refresh(true)
|
||||||
|
|
||||||
val imageDeferred = async { loadProfile(cookie.id) }
|
val imageDeferred = async { loadProfile(cookie.id) }
|
||||||
val nameDeferred = async { loadUsername(cookie) }
|
val nameDeferred = async { loadUsername(cookie) }
|
||||||
|
|
||||||
val name: String = nameDeferred.await()
|
val name: String? = nameDeferred.await()
|
||||||
val foundImage: Boolean = imageDeferred.await()
|
val foundImage: Boolean = imageDeferred.await()
|
||||||
|
|
||||||
L._d { "Logged in and received data" }
|
L._d { "Logged in and received data" }
|
||||||
@ -126,7 +129,7 @@ class LoginActivity : BaseActivity() {
|
|||||||
L._i { cookie }
|
L._i { cookie }
|
||||||
}
|
}
|
||||||
|
|
||||||
textview.text = String.format(getString(R.string.welcome), name)
|
textview.text = String.format(getString(R.string.welcome), name ?: "")
|
||||||
textview.fadeIn()
|
textview.fadeIn()
|
||||||
frostEvent("Login", "success" to true)
|
frostEvent("Login", "success" to true)
|
||||||
|
|
||||||
@ -134,7 +137,7 @@ class LoginActivity : BaseActivity() {
|
|||||||
* The user may have logged into an account that is already in the database
|
* 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
|
* We will let the db handle duplicates and load it now after the new account has been saved
|
||||||
*/
|
*/
|
||||||
val cookies = ArrayList(loadFbCookiesSuspend())
|
val cookies = ArrayList(cookieDao.selectAll())
|
||||||
delay(1000)
|
delay(1000)
|
||||||
if (Showcase.intro)
|
if (Showcase.intro)
|
||||||
launchNewTask<IntroActivity>(cookies, true)
|
launchNewTask<IntroActivity>(cookies, true)
|
||||||
@ -171,23 +174,23 @@ class LoginActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadUsername(cookie: CookieModel): String = withContext(Dispatchers.IO) {
|
private suspend fun loadUsername(cookie: CookieEntity): String? = withContext(Dispatchers.IO) {
|
||||||
val result: String = try {
|
val result: String? = try {
|
||||||
withTimeout(5000) {
|
withTimeout(5000) {
|
||||||
frostJsoup(cookie.cookie, FbItem.PROFILE.url).title()
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cookie.name?.isNotBlank() == false && result != cookie.name) {
|
if (result != null) {
|
||||||
cookie.name = result
|
cookieDao.save(cookie.copy(name = result))
|
||||||
saveFbCookie(cookie)
|
return@withContext result
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie.name ?: ""
|
return@withContext cookie.name
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun backConsumer(): Boolean {
|
override fun backConsumer(): Boolean {
|
||||||
|
@ -35,7 +35,6 @@ class MainActivity : BaseMainActivity() {
|
|||||||
|
|
||||||
override val fragmentChannel = BroadcastChannel<Int>(10)
|
override val fragmentChannel = BroadcastChannel<Int>(10)
|
||||||
override val headerBadgeChannel = BroadcastChannel<String>(Channel.CONFLATED)
|
override val headerBadgeChannel = BroadcastChannel<String>(Channel.CONFLATED)
|
||||||
var lastPosition = -1
|
|
||||||
|
|
||||||
override fun onNestedCreate(savedInstanceState: Bundle?) {
|
override fun onNestedCreate(savedInstanceState: Bundle?) {
|
||||||
setupTabs()
|
setupTabs()
|
||||||
@ -54,23 +53,18 @@ class MainActivity : BaseMainActivity() {
|
|||||||
|
|
||||||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
||||||
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
|
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
|
||||||
val delta = positionOffset * (255 - 128).toFloat()
|
val delta = positionOffset * (SELECTED_TAB_ALPHA - UNSELECTED_TAB_ALPHA)
|
||||||
tabsForEachView { tabPosition, view ->
|
tabsForEachView { tabPosition, view ->
|
||||||
view.setAllAlpha(
|
view.setAllAlpha(
|
||||||
when (tabPosition) {
|
when (tabPosition) {
|
||||||
position -> 255.0f - delta
|
position -> SELECTED_TAB_ALPHA - delta
|
||||||
position + 1 -> 128.0f + delta
|
position + 1 -> UNSELECTED_TAB_ALPHA + delta
|
||||||
else -> 128f
|
else -> UNSELECTED_TAB_ALPHA
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
viewPager.post {
|
|
||||||
if (!fragmentChannel.isClosedForSend)
|
|
||||||
fragmentChannel.offer(0)
|
|
||||||
lastPosition = 0
|
|
||||||
} //trigger hook so title is set
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupTabs() {
|
private fun setupTabs() {
|
||||||
@ -115,11 +109,5 @@ class MainActivity : BaseMainActivity() {
|
|||||||
L.e(e) { "Header badge error" }
|
L.e(e) { "Header badge error" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
adapter.pages.forEach {
|
|
||||||
tabs.addTab(
|
|
||||||
tabs.newTab()
|
|
||||||
.setCustomView(BadgedIcon(this).apply { iicon = it.icon })
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import androidx.recyclerview.widget.GridLayoutManager
|
|||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ca.allanwang.kau.kotlin.lazyContext
|
import ca.allanwang.kau.kotlin.lazyContext
|
||||||
|
import ca.allanwang.kau.utils.launchMain
|
||||||
import ca.allanwang.kau.utils.scaleXY
|
import ca.allanwang.kau.utils.scaleXY
|
||||||
import ca.allanwang.kau.utils.setIcon
|
import ca.allanwang.kau.utils.setIcon
|
||||||
import ca.allanwang.kau.utils.withAlpha
|
import ca.allanwang.kau.utils.withAlpha
|
||||||
@ -33,14 +34,19 @@ import com.mikepenz.fastadapter_extensions.drag.ItemTouchCallback
|
|||||||
import com.mikepenz.fastadapter_extensions.drag.SimpleDragCallback
|
import com.mikepenz.fastadapter_extensions.drag.SimpleDragCallback
|
||||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||||
import com.pitchedapps.frost.R
|
import com.pitchedapps.frost.R
|
||||||
import com.pitchedapps.frost.dbflow.TAB_COUNT
|
import com.pitchedapps.frost.db.GenericDao
|
||||||
import com.pitchedapps.frost.dbflow.loadFbTabs
|
import com.pitchedapps.frost.db.TAB_COUNT
|
||||||
import com.pitchedapps.frost.dbflow.save
|
import com.pitchedapps.frost.db.getTabs
|
||||||
|
import com.pitchedapps.frost.db.saveTabs
|
||||||
import com.pitchedapps.frost.facebook.FbItem
|
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.Prefs
|
import com.pitchedapps.frost.utils.Prefs
|
||||||
import com.pitchedapps.frost.utils.setFrostColors
|
import com.pitchedapps.frost.utils.setFrostColors
|
||||||
import kotlinx.android.synthetic.main.activity_tab_customizer.*
|
import kotlinx.android.synthetic.main.activity_tab_customizer.*
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,6 +54,8 @@ import java.util.Collections
|
|||||||
*/
|
*/
|
||||||
class TabCustomizerActivity : BaseActivity() {
|
class TabCustomizerActivity : BaseActivity() {
|
||||||
|
|
||||||
|
private val genericDao: GenericDao by inject()
|
||||||
|
|
||||||
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) }
|
||||||
@ -65,25 +73,31 @@ class TabCustomizerActivity : BaseActivity() {
|
|||||||
divider.setBackgroundColor(Prefs.textColor.withAlpha(30))
|
divider.setBackgroundColor(Prefs.textColor.withAlpha(30))
|
||||||
instructions.setTextColor(Prefs.textColor)
|
instructions.setTextColor(Prefs.textColor)
|
||||||
|
|
||||||
val tabs = loadFbTabs().toMutableList()
|
launch {
|
||||||
|
val tabs = genericDao.getTabs().toMutableList()
|
||||||
|
L.d { "Tabs $tabs" }
|
||||||
val remaining = FbItem.values().filter { it.name[0] != '_' }.toMutableList()
|
val remaining = FbItem.values().filter { it.name[0] != '_' }.toMutableList()
|
||||||
remaining.removeAll(tabs)
|
remaining.removeAll(tabs)
|
||||||
tabs.addAll(remaining)
|
tabs.addAll(remaining)
|
||||||
|
adapter.set(tabs.map(::TabIItem))
|
||||||
|
|
||||||
adapter.add(tabs.map(::TabIItem))
|
|
||||||
bindSwapper(adapter, tab_recycler)
|
bindSwapper(adapter, tab_recycler)
|
||||||
|
|
||||||
adapter.withOnClickListener { view, _, _, _ -> view!!.wobble(); true }
|
adapter.withOnClickListener { view, _, _, _ -> view!!.wobble(); true }
|
||||||
|
}
|
||||||
|
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
|
||||||
fab_save.setIcon(GoogleMaterial.Icon.gmd_check, Prefs.iconColor)
|
fab_save.setIcon(GoogleMaterial.Icon.gmd_check, Prefs.iconColor)
|
||||||
fab_save.backgroundTintList = ColorStateList.valueOf(Prefs.accentColor)
|
fab_save.backgroundTintList = ColorStateList.valueOf(Prefs.accentColor)
|
||||||
fab_save.setOnClickListener {
|
fab_save.setOnClickListener {
|
||||||
adapter.adapterItems.subList(0, TAB_COUNT).map(TabIItem::item).save()
|
launchMain(NonCancellable) {
|
||||||
|
val tabs = adapter.adapterItems.subList(0, TAB_COUNT).map(TabIItem::item)
|
||||||
|
genericDao.saveTabs(tabs)
|
||||||
setResult(Activity.RESULT_OK)
|
setResult(Activity.RESULT_OK)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fab_cancel.setIcon(GoogleMaterial.Icon.gmd_close, Prefs.iconColor)
|
fab_cancel.setIcon(GoogleMaterial.Icon.gmd_close, Prefs.iconColor)
|
||||||
fab_cancel.backgroundTintList = ColorStateList.valueOf(Prefs.accentColor)
|
fab_cancel.backgroundTintList = ColorStateList.valueOf(Prefs.accentColor)
|
||||||
fab_cancel.setOnClickListener { finish() }
|
fab_cancel.setOnClickListener { finish() }
|
||||||
|
@ -315,8 +315,8 @@ open class WebOverlayActivityBase(private val forceDesktopAgent: Boolean) : Base
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_copy_link -> copyToClipboard(web.currentUrl)
|
R.id.action_copy_link -> copyToClipboard(web.currentUrl.formattedFbUrl)
|
||||||
R.id.action_share -> shareText(web.currentUrl)
|
R.id.action_share -> shareText(web.currentUrl.formattedFbUrl)
|
||||||
else -> if (!OverlayContext.onOptionsItemSelected(web, item.itemId))
|
else -> if (!OverlayContext.onOptionsItemSelected(web, item.itemId))
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
86
app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt
Normal file
86
app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.db
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.pitchedapps.frost.utils.L
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Allan Wang on 2017-05-30.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic cache to store serialized content
|
||||||
|
*/
|
||||||
|
@Entity(
|
||||||
|
tableName = "frost_cache",
|
||||||
|
primaryKeys = ["id", "type"],
|
||||||
|
foreignKeys = [ForeignKey(
|
||||||
|
entity = CookieEntity::class,
|
||||||
|
parentColumns = ["cookie_id"],
|
||||||
|
childColumns = ["id"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
@Parcelize
|
||||||
|
data class CacheEntity(
|
||||||
|
val id: Long,
|
||||||
|
val type: String,
|
||||||
|
val lastUpdated: Long,
|
||||||
|
val contents: String
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface CacheDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM frost_cache WHERE id = :id AND type = :type")
|
||||||
|
fun _select(id: Long, type: String): CacheEntity?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun _insertCache(cache: CacheEntity)
|
||||||
|
|
||||||
|
@Query("DELETE FROM frost_cache WHERE id = :id AND type = :type")
|
||||||
|
fun _delete(id: Long, type: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun CacheDao.select(id: Long, type: String) = dao {
|
||||||
|
_select(id, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun CacheDao.delete(id: Long, type: String) = dao {
|
||||||
|
_delete(id, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if successful, given that there are constraints to the insertion
|
||||||
|
*/
|
||||||
|
suspend fun CacheDao.save(id: Long, type: String, contents: String): Boolean = dao {
|
||||||
|
try {
|
||||||
|
_insertCache(CacheEntity(id, type, System.currentTimeMillis(), contents))
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
L.e(e) { "Cache save failed for $type" }
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
90
app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt
Normal file
90
app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.db
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.pitchedapps.frost.utils.Prefs
|
||||||
|
import com.raizlabs.android.dbflow.annotation.ConflictAction
|
||||||
|
import com.raizlabs.android.dbflow.annotation.Database
|
||||||
|
import com.raizlabs.android.dbflow.annotation.PrimaryKey
|
||||||
|
import com.raizlabs.android.dbflow.annotation.Table
|
||||||
|
import com.raizlabs.android.dbflow.structure.BaseModel
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Allan Wang on 2017-05-30.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Entity(tableName = "cookies")
|
||||||
|
@Parcelize
|
||||||
|
data class CookieEntity(
|
||||||
|
@androidx.room.PrimaryKey
|
||||||
|
@ColumnInfo(name = "cookie_id")
|
||||||
|
val id: Long,
|
||||||
|
val name: String?,
|
||||||
|
val cookie: String?
|
||||||
|
) : Parcelable {
|
||||||
|
override fun toString(): String = "CookieEntity(${hashCode()})"
|
||||||
|
|
||||||
|
fun toSensitiveString(): String = "CookieEntity(id=$id, name=$name, cookie=$cookie)"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface CookieDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM cookies")
|
||||||
|
fun _selectAll(): List<CookieEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM cookies WHERE cookie_id = :id")
|
||||||
|
fun _selectById(id: Long): CookieEntity?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun _save(cookie: CookieEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun _save(cookies: List<CookieEntity>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM cookies WHERE cookie_id = :id")
|
||||||
|
fun _deleteById(id: Long)
|
||||||
|
}
|
||||||
|
|
||||||
|
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() = selectById(Prefs.userId)
|
||||||
|
|
||||||
|
@Database(version = CookiesDb.VERSION)
|
||||||
|
object CookiesDb {
|
||||||
|
const val NAME = "Cookies"
|
||||||
|
const val VERSION = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
@Table(database = CookiesDb::class, allFields = true, primaryKeyConflict = ConflictAction.REPLACE)
|
||||||
|
data class CookieModel(@PrimaryKey var id: Long = -1L, var name: String? = null, var cookie: String? = null) :
|
||||||
|
BaseModel(), Parcelable {
|
||||||
|
|
||||||
|
override fun toString(): String = "CookieModel(${hashCode()})"
|
||||||
|
}
|
28
app/src/main/kotlin/com/pitchedapps/frost/db/DaoUtils.kt
Normal file
28
app/src/main/kotlin/com/pitchedapps/frost/db/DaoUtils.kt
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.db
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps dao calls to work with coroutines
|
||||||
|
* Non transactional queries were supposed to be fixed in https://issuetracker.google.com/issues/69474692,
|
||||||
|
* but it still requires dispatch from a non ui thread.
|
||||||
|
* This avoids that constraint
|
||||||
|
*/
|
||||||
|
suspend inline fun <T> dao(crossinline block: () -> T) = withContext(Dispatchers.IO) { block() }
|
106
app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt
Normal file
106
app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.db
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import com.pitchedapps.frost.BuildConfig
|
||||||
|
import org.koin.dsl.module.module
|
||||||
|
import org.koin.standalone.StandAloneContext
|
||||||
|
|
||||||
|
interface FrostPrivateDao {
|
||||||
|
fun cookieDao(): CookieDao
|
||||||
|
fun notifDao(): NotificationDao
|
||||||
|
fun cacheDao(): CacheDao
|
||||||
|
}
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [CookieEntity::class, NotificationEntity::class, CacheEntity::class],
|
||||||
|
version = 1,
|
||||||
|
exportSchema = true
|
||||||
|
)
|
||||||
|
abstract class FrostPrivateDatabase : RoomDatabase(), FrostPrivateDao {
|
||||||
|
companion object {
|
||||||
|
const val DATABASE_NAME = "frost-priv-db"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FrostPublicDao {
|
||||||
|
fun genericDao(): GenericDao
|
||||||
|
}
|
||||||
|
|
||||||
|
@Database(entities = [GenericEntity::class], version = 1, exportSchema = true)
|
||||||
|
abstract class FrostPublicDatabase : RoomDatabase(), FrostPublicDao {
|
||||||
|
companion object {
|
||||||
|
const val DATABASE_NAME = "frost-db"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FrostDao : FrostPrivateDao, FrostPublicDao {
|
||||||
|
fun close()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composition of all database interfaces
|
||||||
|
*/
|
||||||
|
class FrostDatabase(private val privateDb: FrostPrivateDatabase, private val publicDb: FrostPublicDatabase) :
|
||||||
|
FrostDao,
|
||||||
|
FrostPrivateDao by privateDb,
|
||||||
|
FrostPublicDao by publicDb {
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
privateDb.close()
|
||||||
|
publicDb.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private fun <T : RoomDatabase> RoomDatabase.Builder<T>.frostBuild() = if (BuildConfig.DEBUG) {
|
||||||
|
fallbackToDestructiveMigration().build()
|
||||||
|
} else {
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun create(context: Context): FrostDatabase {
|
||||||
|
val privateDb = Room.databaseBuilder(
|
||||||
|
context, FrostPrivateDatabase::class.java,
|
||||||
|
FrostPrivateDatabase.DATABASE_NAME
|
||||||
|
).frostBuild()
|
||||||
|
val publicDb = Room.databaseBuilder(
|
||||||
|
context, FrostPublicDatabase::class.java,
|
||||||
|
FrostPublicDatabase.DATABASE_NAME
|
||||||
|
).frostBuild()
|
||||||
|
return FrostDatabase(privateDb, publicDb)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun module(context: Context) = module {
|
||||||
|
single { create(context) }
|
||||||
|
single { get<FrostDatabase>().cookieDao() }
|
||||||
|
single { get<FrostDatabase>().cacheDao() }
|
||||||
|
single { get<FrostDatabase>().notifDao() }
|
||||||
|
single { get<FrostDatabase>().genericDao() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get from koin
|
||||||
|
* For the most part, you can retrieve directly from other koin components
|
||||||
|
*/
|
||||||
|
fun get(): FrostDatabase = StandAloneContext.getKoin().koinContext.get()
|
||||||
|
}
|
||||||
|
}
|
@ -14,23 +14,18 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package com.pitchedapps.frost.dbflow
|
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.utils.L
|
|
||||||
import com.raizlabs.android.dbflow.annotation.Database
|
import com.raizlabs.android.dbflow.annotation.Database
|
||||||
import com.raizlabs.android.dbflow.annotation.PrimaryKey
|
import com.raizlabs.android.dbflow.annotation.PrimaryKey
|
||||||
import com.raizlabs.android.dbflow.annotation.Table
|
import com.raizlabs.android.dbflow.annotation.Table
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.database
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.fastSave
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.from
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.select
|
|
||||||
import com.raizlabs.android.dbflow.structure.BaseModel
|
import com.raizlabs.android.dbflow.structure.BaseModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 2017-05-30.
|
* Created by Allan Wang on 2017-05-30.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const val TAB_COUNT = 4
|
const val TAB_COUNT = 4
|
||||||
|
|
||||||
@Database(version = FbTabsDb.VERSION)
|
@Database(version = FbTabsDb.VERSION)
|
||||||
@ -41,18 +36,3 @@ object FbTabsDb {
|
|||||||
|
|
||||||
@Table(database = FbTabsDb::class, allFields = true)
|
@Table(database = FbTabsDb::class, allFields = true)
|
||||||
data class FbTabModel(@PrimaryKey var position: Int = -1, var tab: FbItem = FbItem.FEED) : BaseModel()
|
data class FbTabModel(@PrimaryKey var position: Int = -1, var tab: FbItem = FbItem.FEED) : BaseModel()
|
||||||
|
|
||||||
/**
|
|
||||||
* Load tabs synchronously
|
|
||||||
* Note that tab length should never be a big number anyways
|
|
||||||
*/
|
|
||||||
fun loadFbTabs(): List<FbItem> {
|
|
||||||
val tabs: List<FbTabModel>? = (select from (FbTabModel::class)).orderBy(FbTabModel_Table.position, true).queryList()
|
|
||||||
if (tabs?.size == TAB_COUNT) return tabs.map(FbTabModel::tab)
|
|
||||||
L.d { "No tabs (${tabs?.size}); loading default" }
|
|
||||||
return defaultTabs()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun List<FbItem>.save() {
|
|
||||||
database<FbTabsDb>().beginTransactionAsync(mapIndexed(::FbTabModel).fastSave().build()).execute()
|
|
||||||
}
|
|
71
app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt
Normal file
71
app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.db
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.pitchedapps.frost.facebook.FbItem
|
||||||
|
import com.pitchedapps.frost.facebook.defaultTabs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Allan Wang on 2017-05-30.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic cache to store serialized content
|
||||||
|
*/
|
||||||
|
@Entity(tableName = "frost_generic")
|
||||||
|
data class GenericEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
val type: String,
|
||||||
|
val contents: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface GenericDao {
|
||||||
|
|
||||||
|
@Query("SELECT contents FROM frost_generic WHERE type = :type")
|
||||||
|
fun _select(type: String): String?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun _save(entity: GenericEntity)
|
||||||
|
|
||||||
|
@Query("DELETE FROM frost_generic WHERE type = :type")
|
||||||
|
fun _delete(type: String)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TYPE_TABS = "generic_tabs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun GenericDao.saveTabs(tabs: List<FbItem>) = dao {
|
||||||
|
val content = tabs.joinToString(",") { it.name }
|
||||||
|
_save(GenericEntity(GenericDao.TYPE_TABS, content))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun GenericDao.getTabs(): List<FbItem> = dao {
|
||||||
|
val allTabs = FbItem.values.map { it.name to it }.toMap()
|
||||||
|
_select(GenericDao.TYPE_TABS)
|
||||||
|
?.split(",")
|
||||||
|
?.mapNotNull { allTabs[it] }
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
?: defaultTabs()
|
||||||
|
}
|
202
app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt
Normal file
202
app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.db
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL
|
||||||
|
import com.pitchedapps.frost.services.NOTIF_CHANNEL_MESSAGES
|
||||||
|
import com.pitchedapps.frost.services.NotificationContent
|
||||||
|
import com.pitchedapps.frost.utils.L
|
||||||
|
import com.raizlabs.android.dbflow.annotation.ConflictAction
|
||||||
|
import com.raizlabs.android.dbflow.annotation.Database
|
||||||
|
import com.raizlabs.android.dbflow.annotation.Migration
|
||||||
|
import com.raizlabs.android.dbflow.annotation.PrimaryKey
|
||||||
|
import com.raizlabs.android.dbflow.annotation.Table
|
||||||
|
import com.raizlabs.android.dbflow.kotlinextensions.eq
|
||||||
|
import com.raizlabs.android.dbflow.kotlinextensions.from
|
||||||
|
import com.raizlabs.android.dbflow.kotlinextensions.select
|
||||||
|
import com.raizlabs.android.dbflow.kotlinextensions.where
|
||||||
|
import com.raizlabs.android.dbflow.sql.SQLiteType
|
||||||
|
import com.raizlabs.android.dbflow.sql.migration.AlterTableMigration
|
||||||
|
import com.raizlabs.android.dbflow.structure.BaseModel
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "notifications",
|
||||||
|
primaryKeys = ["notif_id", "userId"],
|
||||||
|
foreignKeys = [ForeignKey(
|
||||||
|
entity = CookieEntity::class,
|
||||||
|
parentColumns = ["cookie_id"],
|
||||||
|
childColumns = ["userId"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)],
|
||||||
|
indices = [Index("notif_id"), Index("userId")]
|
||||||
|
)
|
||||||
|
data class NotificationEntity(
|
||||||
|
@ColumnInfo(name = "notif_id")
|
||||||
|
val id: Long,
|
||||||
|
val userId: Long,
|
||||||
|
val href: String,
|
||||||
|
val title: String?,
|
||||||
|
val text: String,
|
||||||
|
val timestamp: Long,
|
||||||
|
val profileUrl: String?,
|
||||||
|
// Type essentially refers to channel
|
||||||
|
val type: String,
|
||||||
|
val unread: Boolean
|
||||||
|
) {
|
||||||
|
constructor(
|
||||||
|
type: String,
|
||||||
|
content: NotificationContent
|
||||||
|
) : this(
|
||||||
|
content.id,
|
||||||
|
content.data.id,
|
||||||
|
content.href,
|
||||||
|
content.title,
|
||||||
|
content.text,
|
||||||
|
content.timestamp,
|
||||||
|
content.profileUrl,
|
||||||
|
type,
|
||||||
|
content.unread
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class NotificationContentEntity(
|
||||||
|
@Embedded
|
||||||
|
val cookie: CookieEntity,
|
||||||
|
@Embedded
|
||||||
|
val notif: NotificationEntity
|
||||||
|
) {
|
||||||
|
fun toNotifContent() = NotificationContent(
|
||||||
|
data = cookie,
|
||||||
|
id = notif.id,
|
||||||
|
href = notif.href,
|
||||||
|
title = notif.title,
|
||||||
|
text = notif.text,
|
||||||
|
timestamp = notif.timestamp,
|
||||||
|
profileUrl = notif.profileUrl,
|
||||||
|
unread = notif.unread
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface NotificationDao {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note that notifications are guaranteed to be ordered by descending timestamp
|
||||||
|
*/
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM cookies INNER JOIN notifications ON cookie_id = userId WHERE userId = :userId AND type = :type ORDER BY timestamp DESC")
|
||||||
|
fun _selectNotifications(userId: Long, type: String): List<NotificationContentEntity>
|
||||||
|
|
||||||
|
@Query("SELECT timestamp FROM notifications WHERE userId = :userId AND type = :type ORDER BY timestamp DESC LIMIT 1")
|
||||||
|
fun _selectEpoch(userId: Long, type: String): Long?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun _insertNotifications(notifs: List<NotificationEntity>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM notifications WHERE userId = :userId AND type = :type")
|
||||||
|
fun _deleteNotifications(userId: Long, type: String)
|
||||||
|
|
||||||
|
@Query("DELETE FROM notifications")
|
||||||
|
fun _deleteAll()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is assumed that the notification batch comes from the same user
|
||||||
|
*/
|
||||||
|
@Transaction
|
||||||
|
fun _saveNotifications(type: String, notifs: List<NotificationContent>) {
|
||||||
|
val userId = notifs.firstOrNull()?.data?.id ?: return
|
||||||
|
val entities = notifs.map { NotificationEntity(type, it) }
|
||||||
|
_deleteNotifications(userId, type)
|
||||||
|
_insertNotifications(entities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun NotificationDao.deleteAll() = dao { _deleteAll() }
|
||||||
|
|
||||||
|
fun NotificationDao.selectNotificationsSync(userId: Long, type: String): List<NotificationContent> =
|
||||||
|
_selectNotifications(userId, type).map { it.toNotifContent() }
|
||||||
|
|
||||||
|
suspend fun NotificationDao.selectNotifications(userId: Long, type: String): List<NotificationContent> = dao {
|
||||||
|
selectNotificationsSync(userId, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if successful, given that there are constraints to the insertion
|
||||||
|
*/
|
||||||
|
suspend fun NotificationDao.saveNotifications(type: String, notifs: List<NotificationContent>): Boolean {
|
||||||
|
if (notifs.isEmpty()) return true
|
||||||
|
return dao {
|
||||||
|
try {
|
||||||
|
_saveNotifications(type, notifs)
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
L.e(e) { "Notif save failed for $type" }
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun NotificationDao.latestEpoch(userId: Long, type: String): Long = dao {
|
||||||
|
_selectEpoch(userId, type) ?: lastNotificationTime(userId).let {
|
||||||
|
when (type) {
|
||||||
|
NOTIF_CHANNEL_GENERAL -> it.epoch
|
||||||
|
NOTIF_CHANNEL_MESSAGES -> it.epochIm
|
||||||
|
else -> -1L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Allan Wang on 2017-05-30.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Database(version = NotificationDb.VERSION)
|
||||||
|
object NotificationDb {
|
||||||
|
const val NAME = "Notifications"
|
||||||
|
const val VERSION = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@Migration(version = 2, database = NotificationDb::class)
|
||||||
|
class NotificationMigration2(modelClass: Class<NotificationModel>) :
|
||||||
|
AlterTableMigration<NotificationModel>(modelClass) {
|
||||||
|
override fun onPreMigrate() {
|
||||||
|
super.onPreMigrate()
|
||||||
|
addColumn(SQLiteType.INTEGER, "epochIm")
|
||||||
|
L.d { "Added column" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Table(database = NotificationDb::class, allFields = true, primaryKeyConflict = ConflictAction.REPLACE)
|
||||||
|
data class NotificationModel(
|
||||||
|
@PrimaryKey var id: Long = -1L,
|
||||||
|
var epoch: Long = -1L,
|
||||||
|
var epochIm: Long = -1L
|
||||||
|
) : BaseModel()
|
||||||
|
|
||||||
|
internal fun lastNotificationTime(id: Long): NotificationModel =
|
||||||
|
(select from NotificationModel::class where (NotificationModel_Table.id eq id)).querySingle()
|
||||||
|
?: NotificationModel(id = id)
|
@ -1,92 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 Allan Wang
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.pitchedapps.frost.dbflow
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.pitchedapps.frost.utils.L
|
|
||||||
import com.raizlabs.android.dbflow.annotation.ConflictAction
|
|
||||||
import com.raizlabs.android.dbflow.annotation.Database
|
|
||||||
import com.raizlabs.android.dbflow.annotation.PrimaryKey
|
|
||||||
import com.raizlabs.android.dbflow.annotation.Table
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.async
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.delete
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.eq
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.from
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.save
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.select
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.where
|
|
||||||
import com.raizlabs.android.dbflow.structure.BaseModel
|
|
||||||
import kotlinx.android.parcel.Parcelize
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Allan Wang on 2017-05-30.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Database(version = CookiesDb.VERSION)
|
|
||||||
object CookiesDb {
|
|
||||||
const val NAME = "Cookies"
|
|
||||||
const val VERSION = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
@Table(database = CookiesDb::class, allFields = true, primaryKeyConflict = ConflictAction.REPLACE)
|
|
||||||
data class CookieModel(@PrimaryKey var id: Long = -1L, var name: String? = null, var cookie: String? = null) :
|
|
||||||
BaseModel(), Parcelable {
|
|
||||||
|
|
||||||
override fun toString(): String = "CookieModel(${hashCode()})"
|
|
||||||
|
|
||||||
fun toSensitiveString(): String = "CookieModel(id=$id, name=$name, cookie=$cookie)"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadFbCookie(id: Long): CookieModel? =
|
|
||||||
(select from CookieModel::class where (CookieModel_Table.id eq id)).querySingle()
|
|
||||||
|
|
||||||
fun loadFbCookie(name: String): CookieModel? =
|
|
||||||
(select from CookieModel::class where (CookieModel_Table.name eq name)).querySingle()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads cookies sorted by name
|
|
||||||
*/
|
|
||||||
fun loadFbCookiesAsync(callback: (cookies: List<CookieModel>) -> Unit) {
|
|
||||||
(select from CookieModel::class).orderBy(CookieModel_Table.name, true).async()
|
|
||||||
.queryListResultCallback { _, tResult -> callback(tResult) }.execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadFbCookiesSync(): List<CookieModel> =
|
|
||||||
(select from CookieModel::class).orderBy(CookieModel_Table.name, true).queryList()
|
|
||||||
|
|
||||||
// TODO temp method until dbflow supports coroutines
|
|
||||||
suspend fun loadFbCookiesSuspend(): List<CookieModel> = withContext(Dispatchers.IO) {
|
|
||||||
loadFbCookiesSync()
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun saveFbCookie(cookie: CookieModel, crossinline callback: (() -> Unit) = {}) {
|
|
||||||
cookie.async save {
|
|
||||||
L.d { "Fb cookie saved" }
|
|
||||||
L._d { cookie.toSensitiveString() }
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeCookie(id: Long) {
|
|
||||||
loadFbCookie(id)?.async?.delete {
|
|
||||||
L.d { "Fb cookie deleted" }
|
|
||||||
L._d { id }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 Allan Wang
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.pitchedapps.frost.dbflow
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.pitchedapps.frost.utils.L
|
|
||||||
import com.raizlabs.android.dbflow.config.FlowManager
|
|
||||||
import com.raizlabs.android.dbflow.structure.database.transaction.FastStoreModelTransaction
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Allan Wang on 2017-05-30.
|
|
||||||
*/
|
|
||||||
|
|
||||||
object DbUtils {
|
|
||||||
|
|
||||||
fun db(name: String) = FlowManager.getDatabase(name)
|
|
||||||
fun dbName(name: String) = "$name.db"
|
|
||||||
fun deleteDatabase(c: Context, name: String) = c.deleteDatabase(dbName(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T : Any> List<T>.replace(dbName: String) {
|
|
||||||
L.d { "Replacing $dbName.db" }
|
|
||||||
DbUtils.db(dbName).reset()
|
|
||||||
FastStoreModelTransaction.saveBuilder(FlowManager.getModelAdapter(T::class.java)).addAll(this).build()
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 Allan Wang
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.pitchedapps.frost.dbflow
|
|
||||||
|
|
||||||
import com.pitchedapps.frost.utils.L
|
|
||||||
import com.raizlabs.android.dbflow.annotation.ConflictAction
|
|
||||||
import com.raizlabs.android.dbflow.annotation.Database
|
|
||||||
import com.raizlabs.android.dbflow.annotation.Migration
|
|
||||||
import com.raizlabs.android.dbflow.annotation.PrimaryKey
|
|
||||||
import com.raizlabs.android.dbflow.annotation.Table
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.async
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.eq
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.from
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.save
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.select
|
|
||||||
import com.raizlabs.android.dbflow.kotlinextensions.where
|
|
||||||
import com.raizlabs.android.dbflow.sql.SQLiteType
|
|
||||||
import com.raizlabs.android.dbflow.sql.migration.AlterTableMigration
|
|
||||||
import com.raizlabs.android.dbflow.structure.BaseModel
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Allan Wang on 2017-05-30.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Database(version = NotificationDb.VERSION)
|
|
||||||
object NotificationDb {
|
|
||||||
const val NAME = "Notifications"
|
|
||||||
const val VERSION = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
@Migration(version = 2, database = NotificationDb::class)
|
|
||||||
class NotificationMigration2(modelClass: Class<NotificationModel>) :
|
|
||||||
AlterTableMigration<NotificationModel>(modelClass) {
|
|
||||||
override fun onPreMigrate() {
|
|
||||||
super.onPreMigrate()
|
|
||||||
addColumn(SQLiteType.INTEGER, "epochIm")
|
|
||||||
L.d { "Added column" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table(database = NotificationDb::class, allFields = true, primaryKeyConflict = ConflictAction.REPLACE)
|
|
||||||
data class NotificationModel(
|
|
||||||
@PrimaryKey var id: Long = -1L,
|
|
||||||
var epoch: Long = -1L,
|
|
||||||
var epochIm: Long = -1L
|
|
||||||
) : BaseModel()
|
|
||||||
|
|
||||||
fun lastNotificationTime(id: Long): NotificationModel =
|
|
||||||
(select from NotificationModel::class where (NotificationModel_Table.id eq id)).querySingle()
|
|
||||||
?: NotificationModel(id = id)
|
|
||||||
|
|
||||||
fun saveNotificationTime(notificationModel: NotificationModel, callback: (() -> Unit)? = null) {
|
|
||||||
notificationModel.async save {
|
|
||||||
L.d { "Fb notification model saved" }
|
|
||||||
L._d { notificationModel }
|
|
||||||
callback?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
@ -19,10 +19,12 @@ package com.pitchedapps.frost.facebook
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import com.pitchedapps.frost.dbflow.CookieModel
|
import com.pitchedapps.frost.db.CookieDao
|
||||||
import com.pitchedapps.frost.dbflow.loadFbCookie
|
import com.pitchedapps.frost.db.CookieEntity
|
||||||
import com.pitchedapps.frost.dbflow.removeCookie
|
import com.pitchedapps.frost.db.FrostDatabase
|
||||||
import com.pitchedapps.frost.dbflow.saveFbCookie
|
import com.pitchedapps.frost.db.deleteById
|
||||||
|
import com.pitchedapps.frost.db.save
|
||||||
|
import com.pitchedapps.frost.db.selectById
|
||||||
import com.pitchedapps.frost.utils.L
|
import com.pitchedapps.frost.utils.L
|
||||||
import com.pitchedapps.frost.utils.Prefs
|
import com.pitchedapps.frost.utils.Prefs
|
||||||
import com.pitchedapps.frost.utils.cookies
|
import com.pitchedapps.frost.utils.cookies
|
||||||
@ -50,6 +52,10 @@ object FbCookie {
|
|||||||
inline val webCookie: String?
|
inline val webCookie: String?
|
||||||
get() = CookieManager.getInstance().getCookie(COOKIE_DOMAIN)
|
get() = CookieManager.getInstance().getCookie(COOKIE_DOMAIN)
|
||||||
|
|
||||||
|
private val cookieDao: CookieDao by lazy {
|
||||||
|
FrostDatabase.get().cookieDao()
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun CookieManager.suspendSetWebCookie(cookie: String?): Boolean {
|
private suspend fun CookieManager.suspendSetWebCookie(cookie: String?): Boolean {
|
||||||
cookie ?: return true
|
cookie ?: return true
|
||||||
return withContext(NonCancellable) {
|
return withContext(NonCancellable) {
|
||||||
@ -77,12 +83,12 @@ object FbCookie {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun save(id: Long) {
|
suspend fun save(id: Long) {
|
||||||
L.d { "New cookie found" }
|
L.d { "New cookie found" }
|
||||||
Prefs.userId = id
|
Prefs.userId = id
|
||||||
CookieManager.getInstance().flush()
|
CookieManager.getInstance().flush()
|
||||||
val cookie = CookieModel(Prefs.userId, "", webCookie)
|
val cookie = CookieEntity(Prefs.userId, null, webCookie)
|
||||||
saveFbCookie(cookie)
|
cookieDao.save(cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun reset() {
|
suspend fun reset() {
|
||||||
@ -93,11 +99,12 @@ object FbCookie {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun switchUser(id: Long) = switchUser(loadFbCookie(id))
|
suspend fun switchUser(id: Long) {
|
||||||
|
val cookie = cookieDao.selectById(id) ?: return L.e { "No cookie for id" }
|
||||||
|
switchUser(cookie)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun switchUser(name: String) = switchUser(loadFbCookie(name))
|
suspend fun switchUser(cookie: CookieEntity?) {
|
||||||
|
|
||||||
suspend fun switchUser(cookie: CookieModel?) {
|
|
||||||
if (cookie == null) {
|
if (cookie == null) {
|
||||||
L.d { "Switching User; null cookie" }
|
L.d { "Switching User; null cookie" }
|
||||||
return
|
return
|
||||||
@ -114,7 +121,7 @@ object FbCookie {
|
|||||||
* and launch the proper login page
|
* and launch the proper login page
|
||||||
*/
|
*/
|
||||||
suspend fun logout(context: Context) {
|
suspend fun logout(context: Context) {
|
||||||
val cookies = arrayListOf<CookieModel>()
|
val cookies = arrayListOf<CookieEntity>()
|
||||||
if (context is Activity)
|
if (context is Activity)
|
||||||
cookies.addAll(context.cookies().filter { it.id != Prefs.userId })
|
cookies.addAll(context.cookies().filter { it.id != Prefs.userId })
|
||||||
logout(Prefs.userId)
|
logout(Prefs.userId)
|
||||||
@ -126,7 +133,9 @@ object FbCookie {
|
|||||||
*/
|
*/
|
||||||
suspend fun logout(id: Long) {
|
suspend fun logout(id: Long) {
|
||||||
L.d { "Logging out user" }
|
L.d { "Logging out user" }
|
||||||
removeCookie(id)
|
cookieDao.deleteById(id)
|
||||||
|
L.d { "Fb cookie deleted" }
|
||||||
|
L._d { id }
|
||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +56,9 @@ enum class FbItem(
|
|||||||
PHOTOS(R.string.photos, GoogleMaterial.Icon.gmd_photo, "me/photos"),
|
PHOTOS(R.string.photos, GoogleMaterial.Icon.gmd_photo, "me/photos"),
|
||||||
PROFILE(R.string.profile, CommunityMaterial.Icon.cmd_account, "me"),
|
PROFILE(R.string.profile, CommunityMaterial.Icon.cmd_account, "me"),
|
||||||
SAVED(R.string.saved, GoogleMaterial.Icon.gmd_bookmark, "saved"),
|
SAVED(R.string.saved, GoogleMaterial.Icon.gmd_bookmark, "saved"),
|
||||||
|
/**
|
||||||
|
* Note that this url only works if a query (?q=) is provided
|
||||||
|
*/
|
||||||
_SEARCH(R.string.kau_search, GoogleMaterial.Icon.gmd_search, "search/top"),
|
_SEARCH(R.string.kau_search, GoogleMaterial.Icon.gmd_search, "search/top"),
|
||||||
SETTINGS(R.string.settings, GoogleMaterial.Icon.gmd_settings, "settings"),
|
SETTINGS(R.string.settings, GoogleMaterial.Icon.gmd_settings, "settings"),
|
||||||
;
|
;
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.pitchedapps.frost.facebook.parsers
|
package com.pitchedapps.frost.facebook.parsers
|
||||||
|
|
||||||
import com.pitchedapps.frost.dbflow.CookieModel
|
import com.pitchedapps.frost.db.CookieEntity
|
||||||
import com.pitchedapps.frost.facebook.FB_CSS_URL_MATCHER
|
import com.pitchedapps.frost.facebook.FB_CSS_URL_MATCHER
|
||||||
import com.pitchedapps.frost.facebook.formattedFbUrl
|
import com.pitchedapps.frost.facebook.formattedFbUrl
|
||||||
import com.pitchedapps.frost.facebook.get
|
import com.pitchedapps.frost.facebook.get
|
||||||
@ -38,7 +38,7 @@ import org.jsoup.select.Elements
|
|||||||
* The return type must be nonnull if no parsing errors occurred, as null signifies a parse error
|
* The return type must be nonnull if no parsing errors occurred, as null signifies a parse error
|
||||||
* If null really must be allowed, use Optionals
|
* If null really must be allowed, use Optionals
|
||||||
*/
|
*/
|
||||||
interface FrostParser<out T : Any> {
|
interface FrostParser<out T : ParseData> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name associated to parser
|
* Name associated to parser
|
||||||
@ -76,12 +76,16 @@ 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>(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 ParseNotification {
|
interface ParseData {
|
||||||
fun getUnreadNotifications(data: CookieModel): List<NotificationContent>
|
val isEmpty: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParseNotification : ParseData {
|
||||||
|
fun getUnreadNotifications(data: CookieEntity): List<NotificationContent>
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun <T> List<T>.toJsonString(tag: String, indent: Int) = StringBuilder().apply {
|
internal fun <T> List<T>.toJsonString(tag: String, indent: Int) = StringBuilder().apply {
|
||||||
@ -95,7 +99,7 @@ internal fun <T> List<T>.toJsonString(tag: String, indent: Int) = StringBuilder(
|
|||||||
* T should have a readable toString() function
|
* T should have a readable toString() function
|
||||||
* [redirectToText] dictates whether all data should be converted to text then back to document before parsing
|
* [redirectToText] dictates whether all data should be converted to text then back to document before parsing
|
||||||
*/
|
*/
|
||||||
internal abstract class FrostParserBase<out T : Any>(private val redirectToText: Boolean) : FrostParser<T> {
|
internal abstract class FrostParserBase<out T : ParseData>(private val redirectToText: Boolean) : FrostParser<T> {
|
||||||
|
|
||||||
final override fun parse(cookie: String?) = parseFromUrl(cookie, url)
|
final override fun parse(cookie: String?) = parseFromUrl(cookie, url)
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.pitchedapps.frost.facebook.parsers
|
package com.pitchedapps.frost.facebook.parsers
|
||||||
|
|
||||||
import com.pitchedapps.frost.dbflow.CookieModel
|
import com.pitchedapps.frost.db.CookieEntity
|
||||||
import com.pitchedapps.frost.facebook.FB_EPOCH_MATCHER
|
import com.pitchedapps.frost.facebook.FB_EPOCH_MATCHER
|
||||||
import com.pitchedapps.frost.facebook.FB_MESSAGE_NOTIF_ID_MATCHER
|
import com.pitchedapps.frost.facebook.FB_MESSAGE_NOTIF_ID_MATCHER
|
||||||
import com.pitchedapps.frost.facebook.FbItem
|
import com.pitchedapps.frost.facebook.FbItem
|
||||||
@ -46,6 +46,10 @@ data class FrostMessages(
|
|||||||
val seeMore: FrostLink?,
|
val seeMore: FrostLink?,
|
||||||
val extraLinks: List<FrostLink>
|
val extraLinks: List<FrostLink>
|
||||||
) : ParseNotification {
|
) : ParseNotification {
|
||||||
|
|
||||||
|
override val isEmpty: Boolean
|
||||||
|
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))
|
||||||
@ -54,7 +58,7 @@ data class FrostMessages(
|
|||||||
append("}")
|
append("}")
|
||||||
}.toString()
|
}.toString()
|
||||||
|
|
||||||
override fun getUnreadNotifications(data: CookieModel) =
|
override fun getUnreadNotifications(data: CookieEntity) =
|
||||||
threads.asSequence().filter(FrostThread::unread).map {
|
threads.asSequence().filter(FrostThread::unread).map {
|
||||||
with(it) {
|
with(it) {
|
||||||
NotificationContent(
|
NotificationContent(
|
||||||
@ -64,7 +68,8 @@ data class FrostMessages(
|
|||||||
title = title,
|
title = title,
|
||||||
text = content ?: "",
|
text = content ?: "",
|
||||||
timestamp = time,
|
timestamp = time,
|
||||||
profileUrl = img
|
profileUrl = img,
|
||||||
|
unread = unread
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.toList()
|
}.toList()
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.pitchedapps.frost.facebook.parsers
|
package com.pitchedapps.frost.facebook.parsers
|
||||||
|
|
||||||
import com.pitchedapps.frost.dbflow.CookieModel
|
import com.pitchedapps.frost.db.CookieEntity
|
||||||
import com.pitchedapps.frost.facebook.FB_EPOCH_MATCHER
|
import com.pitchedapps.frost.facebook.FB_EPOCH_MATCHER
|
||||||
import com.pitchedapps.frost.facebook.FB_NOTIF_ID_MATCHER
|
import com.pitchedapps.frost.facebook.FB_NOTIF_ID_MATCHER
|
||||||
import com.pitchedapps.frost.facebook.FbItem
|
import com.pitchedapps.frost.facebook.FbItem
|
||||||
@ -36,6 +36,10 @@ data class FrostNotifs(
|
|||||||
val notifs: List<FrostNotif>,
|
val notifs: List<FrostNotif>,
|
||||||
val seeMore: FrostLink?
|
val seeMore: FrostLink?
|
||||||
) : ParseNotification {
|
) : ParseNotification {
|
||||||
|
|
||||||
|
override val isEmpty: Boolean
|
||||||
|
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))
|
||||||
@ -43,7 +47,7 @@ data class FrostNotifs(
|
|||||||
append("}")
|
append("}")
|
||||||
}.toString()
|
}.toString()
|
||||||
|
|
||||||
override fun getUnreadNotifications(data: CookieModel) =
|
override fun getUnreadNotifications(data: CookieEntity) =
|
||||||
notifs.asSequence().filter(FrostNotif::unread).map {
|
notifs.asSequence().filter(FrostNotif::unread).map {
|
||||||
with(it) {
|
with(it) {
|
||||||
NotificationContent(
|
NotificationContent(
|
||||||
@ -53,7 +57,8 @@ data class FrostNotifs(
|
|||||||
title = null,
|
title = null,
|
||||||
text = content,
|
text = content,
|
||||||
timestamp = time,
|
timestamp = time,
|
||||||
profileUrl = img
|
profileUrl = img,
|
||||||
|
unread = unread
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.toList()
|
}.toList()
|
||||||
|
@ -40,7 +40,10 @@ enum class SearchKeys(val key: String) {
|
|||||||
EVENTS("keywords_events")
|
EVENTS("keywords_events")
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FrostSearches(val results: List<FrostSearch>) {
|
data class FrostSearches(val results: List<FrostSearch>) : ParseData {
|
||||||
|
|
||||||
|
override val isEmpty: Boolean
|
||||||
|
get() = results.isEmpty()
|
||||||
|
|
||||||
override fun toString() = StringBuilder().apply {
|
override fun toString() = StringBuilder().apply {
|
||||||
append("FrostSearches {\n")
|
append("FrostSearches {\n")
|
||||||
|
@ -25,6 +25,7 @@ import com.mikepenz.fastadapter.adapters.ModelAdapter
|
|||||||
import com.pitchedapps.frost.R
|
import com.pitchedapps.frost.R
|
||||||
import com.pitchedapps.frost.facebook.FbCookie
|
import com.pitchedapps.frost.facebook.FbCookie
|
||||||
import com.pitchedapps.frost.facebook.parsers.FrostParser
|
import com.pitchedapps.frost.facebook.parsers.FrostParser
|
||||||
|
import com.pitchedapps.frost.facebook.parsers.ParseData
|
||||||
import com.pitchedapps.frost.facebook.parsers.ParseResponse
|
import com.pitchedapps.frost.facebook.parsers.ParseResponse
|
||||||
import com.pitchedapps.frost.utils.L
|
import com.pitchedapps.frost.utils.L
|
||||||
import com.pitchedapps.frost.utils.frostJsoup
|
import com.pitchedapps.frost.utils.frostJsoup
|
||||||
@ -94,7 +95,7 @@ abstract class GenericRecyclerFragment<T, Item : IItem<*, *>> : RecyclerFragment
|
|||||||
open fun getAdapter(): FastAdapter<IItem<*, *>> = fastAdapter(this.adapter)
|
open fun getAdapter(): FastAdapter<IItem<*, *>> = fastAdapter(this.adapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class FrostParserFragment<T : Any, Item : IItem<*, *>> : RecyclerFragment<Item, Item>() {
|
abstract class FrostParserFragment<T : ParseData, Item : IItem<*, *>> : RecyclerFragment<Item, Item>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The parser to make this all happen
|
* The parser to make this all happen
|
||||||
|
@ -31,9 +31,10 @@ import ca.allanwang.kau.utils.string
|
|||||||
import com.pitchedapps.frost.BuildConfig
|
import com.pitchedapps.frost.BuildConfig
|
||||||
import com.pitchedapps.frost.R
|
import com.pitchedapps.frost.R
|
||||||
import com.pitchedapps.frost.activities.FrostWebActivity
|
import com.pitchedapps.frost.activities.FrostWebActivity
|
||||||
import com.pitchedapps.frost.dbflow.CookieModel
|
import com.pitchedapps.frost.db.CookieEntity
|
||||||
import com.pitchedapps.frost.dbflow.NotificationModel
|
import com.pitchedapps.frost.db.FrostDatabase
|
||||||
import com.pitchedapps.frost.dbflow.lastNotificationTime
|
import com.pitchedapps.frost.db.latestEpoch
|
||||||
|
import com.pitchedapps.frost.db.saveNotifications
|
||||||
import com.pitchedapps.frost.enums.OverlayContext
|
import com.pitchedapps.frost.enums.OverlayContext
|
||||||
import com.pitchedapps.frost.facebook.FbItem
|
import com.pitchedapps.frost.facebook.FbItem
|
||||||
import com.pitchedapps.frost.facebook.parsers.FrostParser
|
import com.pitchedapps.frost.facebook.parsers.FrostParser
|
||||||
@ -60,12 +61,10 @@ private val _40_DP = 40.dpToPx
|
|||||||
* Enum to handle notification creations
|
* Enum to handle notification creations
|
||||||
*/
|
*/
|
||||||
enum class NotificationType(
|
enum class NotificationType(
|
||||||
private 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 getTime: (notif: NotificationModel) -> Long,
|
|
||||||
private val putTime: (notif: NotificationModel, time: Long) -> NotificationModel,
|
|
||||||
private val ringtone: () -> String
|
private val ringtone: () -> String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -74,8 +73,6 @@ enum class NotificationType(
|
|||||||
OverlayContext.NOTIFICATION,
|
OverlayContext.NOTIFICATION,
|
||||||
FbItem.NOTIFICATIONS,
|
FbItem.NOTIFICATIONS,
|
||||||
NotifParser,
|
NotifParser,
|
||||||
NotificationModel::epoch,
|
|
||||||
{ notif, time -> notif.copy(epoch = time) },
|
|
||||||
Prefs::notificationRingtone
|
Prefs::notificationRingtone
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -88,8 +85,6 @@ enum class NotificationType(
|
|||||||
OverlayContext.MESSAGE,
|
OverlayContext.MESSAGE,
|
||||||
FbItem.MESSAGES,
|
FbItem.MESSAGES,
|
||||||
MessageParser,
|
MessageParser,
|
||||||
NotificationModel::epochIm,
|
|
||||||
{ notif, time -> notif.copy(epochIm = time) },
|
|
||||||
Prefs::messageRingtone
|
Prefs::messageRingtone
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -100,8 +95,8 @@ enum class NotificationType(
|
|||||||
*/
|
*/
|
||||||
internal open fun bindRequest(content: NotificationContent, cookie: String): (BaseBundle.() -> Unit)? = null
|
internal open fun bindRequest(content: NotificationContent, cookie: String): (BaseBundle.() -> Unit)? = null
|
||||||
|
|
||||||
private fun bindRequest(intent: Intent, content: NotificationContent, cookie: String?) {
|
private fun bindRequest(intent: Intent, content: NotificationContent) {
|
||||||
cookie ?: return
|
val cookie = content.data.cookie ?: return
|
||||||
val binder = bindRequest(content, cookie) ?: return
|
val binder = bindRequest(content, cookie) ?: return
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.binder()
|
bundle.binder()
|
||||||
@ -116,7 +111,8 @@ enum class NotificationType(
|
|||||||
* Returns the number of notifications generated,
|
* Returns the number of notifications generated,
|
||||||
* or -1 if an error occurred
|
* or -1 if an error occurred
|
||||||
*/
|
*/
|
||||||
fun fetch(context: Context, data: CookieModel): Int {
|
suspend fun fetch(context: Context, data: CookieEntity): Int {
|
||||||
|
val notifDao = FrostDatabase.get().notifDao()
|
||||||
val response = try {
|
val response = try {
|
||||||
parser.parse(data.cookie)
|
parser.parse(data.cookie)
|
||||||
} catch (ignored: Exception) {
|
} catch (ignored: Exception) {
|
||||||
@ -142,36 +138,42 @@ enum class NotificationType(
|
|||||||
}
|
}
|
||||||
if (notifContents.isEmpty()) return 0
|
if (notifContents.isEmpty()) return 0
|
||||||
val userId = data.id
|
val userId = data.id
|
||||||
val prevNotifTime = lastNotificationTime(userId)
|
// Legacy, remove with dbflow
|
||||||
val prevLatestEpoch = getTime(prevNotifTime)
|
val prevLatestEpoch = notifDao.latestEpoch(userId, channelId)
|
||||||
L.v { "Notif $name prev epoch $prevLatestEpoch" }
|
L.v { "Notif $name prev epoch $prevLatestEpoch" }
|
||||||
var newLatestEpoch = prevLatestEpoch
|
|
||||||
val notifs = mutableListOf<FrostNotification>()
|
|
||||||
notifContents.forEach { notif ->
|
|
||||||
L.v { "Notif timestamp ${notif.timestamp}" }
|
|
||||||
if (notif.timestamp <= prevLatestEpoch) return@forEach
|
|
||||||
notifs.add(createNotification(context, notif))
|
|
||||||
if (notif.timestamp > newLatestEpoch)
|
|
||||||
newLatestEpoch = notif.timestamp
|
|
||||||
}
|
|
||||||
if (newLatestEpoch > prevLatestEpoch)
|
|
||||||
putTime(prevNotifTime, newLatestEpoch).save()
|
|
||||||
L.d { "Notif $name new epoch ${getTime(lastNotificationTime(userId))}" }
|
|
||||||
if (prevLatestEpoch == -1L && !BuildConfig.DEBUG) {
|
if (prevLatestEpoch == -1L && !BuildConfig.DEBUG) {
|
||||||
L.d { "Skipping first notification fetch" }
|
L.d { "Skipping first notification fetch" }
|
||||||
return 0 // do not notify the first time
|
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" }
|
||||||
|
|
||||||
|
if (!notifDao.saveNotifications(channelId, newNotifContents)) {
|
||||||
|
L.d { "Skip notifs for $name as saving failed" }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val notifs = newNotifContents.map { createNotification(context, it) }
|
||||||
|
|
||||||
frostEvent("Notifications", "Type" to name, "Count" to notifs.size)
|
frostEvent("Notifications", "Type" to name, "Count" to notifs.size)
|
||||||
if (notifs.size > 1)
|
if (notifs.size > 1)
|
||||||
summaryNotification(context, userId, notifs.size).notify(context)
|
summaryNotification(context, userId, notifs.size).notify(context)
|
||||||
val ringtone = ringtone()
|
val ringtone = ringtone()
|
||||||
notifs.forEachIndexed { i, notif ->
|
notifs.forEachIndexed { i, notif ->
|
||||||
|
// Ring at most twice
|
||||||
notif.withAlert(i < 2, ringtone).notify(context)
|
notif.withAlert(i < 2, ringtone).notify(context)
|
||||||
}
|
}
|
||||||
return notifs.size
|
return notifs.size
|
||||||
}
|
}
|
||||||
|
|
||||||
fun debugNotification(context: Context, data: CookieModel) {
|
fun debugNotification(context: Context, data: CookieEntity) {
|
||||||
val content = NotificationContent(
|
val content = NotificationContent(
|
||||||
data,
|
data,
|
||||||
System.currentTimeMillis(),
|
System.currentTimeMillis(),
|
||||||
@ -179,23 +181,40 @@ enum class NotificationType(
|
|||||||
"Debug Notif",
|
"Debug Notif",
|
||||||
"Test 123",
|
"Test 123",
|
||||||
System.currentTimeMillis() / 1000,
|
System.currentTimeMillis() / 1000,
|
||||||
"https://www.iconexperience.com/_img/v_collection_png/256x256/shadow/dog.png"
|
"https://www.iconexperience.com/_img/v_collection_png/256x256/shadow/dog.png",
|
||||||
|
false
|
||||||
)
|
)
|
||||||
createNotification(context, content).notify(context)
|
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]
|
* Create and submit a new notification with the given [content]
|
||||||
*/
|
*/
|
||||||
private fun createNotification(context: Context, content: NotificationContent): FrostNotification =
|
private fun createNotification(context: Context, content: NotificationContent): FrostNotification =
|
||||||
with(content) {
|
with(content) {
|
||||||
val intent = Intent(context, FrostWebActivity::class.java)
|
val intent = createCommonIntent(context, content.data.id)
|
||||||
// TODO temp fix; we will show notification page for dependent urls. We can trigger a click next time
|
putContentExtra(intent, content)
|
||||||
intent.data = Uri.parse(if (href.isIndependent) href else FbItem.NOTIFICATIONS.url)
|
|
||||||
intent.putExtra(ARG_USER_ID, data.id)
|
|
||||||
overlayContext.put(intent)
|
|
||||||
bindRequest(intent, content, data.cookie)
|
|
||||||
|
|
||||||
val group = "${groupPrefix}_${data.id}"
|
val group = "${groupPrefix}_${data.id}"
|
||||||
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
val notifBuilder = context.frostNotification(channelId)
|
val notifBuilder = context.frostNotification(channelId)
|
||||||
@ -257,13 +276,15 @@ enum class NotificationType(
|
|||||||
* Notification data holder
|
* Notification data holder
|
||||||
*/
|
*/
|
||||||
data class NotificationContent(
|
data class NotificationContent(
|
||||||
val data: CookieModel,
|
// TODO replace data with userId?
|
||||||
|
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 notifId = Math.abs(id.toInt())
|
val notifId = Math.abs(id.toInt())
|
||||||
|
@ -21,16 +21,19 @@ import androidx.core.app.NotificationManagerCompat
|
|||||||
import ca.allanwang.kau.utils.string
|
import ca.allanwang.kau.utils.string
|
||||||
import com.pitchedapps.frost.BuildConfig
|
import com.pitchedapps.frost.BuildConfig
|
||||||
import com.pitchedapps.frost.R
|
import com.pitchedapps.frost.R
|
||||||
import com.pitchedapps.frost.dbflow.CookieModel
|
import com.pitchedapps.frost.db.CookieDao
|
||||||
import com.pitchedapps.frost.dbflow.loadFbCookiesSync
|
import com.pitchedapps.frost.db.CookieEntity
|
||||||
|
import com.pitchedapps.frost.db.selectAll
|
||||||
import com.pitchedapps.frost.utils.L
|
import com.pitchedapps.frost.utils.L
|
||||||
import com.pitchedapps.frost.utils.Prefs
|
import com.pitchedapps.frost.utils.Prefs
|
||||||
import com.pitchedapps.frost.utils.frostEvent
|
import com.pitchedapps.frost.utils.frostEvent
|
||||||
|
import com.pitchedapps.frost.widgets.NotificationWidget
|
||||||
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 org.koin.android.ext.android.inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 2017-06-14.
|
* Created by Allan Wang on 2017-06-14.
|
||||||
@ -42,6 +45,8 @@ import kotlinx.coroutines.yield
|
|||||||
*/
|
*/
|
||||||
class NotificationService : BaseJobService() {
|
class NotificationService : BaseJobService() {
|
||||||
|
|
||||||
|
val cookieDao: CookieDao by inject()
|
||||||
|
|
||||||
override fun onStopJob(params: JobParameters?): Boolean {
|
override fun onStopJob(params: JobParameters?): Boolean {
|
||||||
super.onStopJob(params)
|
super.onStopJob(params)
|
||||||
prepareFinish(true)
|
prepareFinish(true)
|
||||||
@ -81,7 +86,7 @@ class NotificationService : BaseJobService() {
|
|||||||
|
|
||||||
private suspend fun sendNotifications(params: JobParameters?): Unit = withContext(Dispatchers.Default) {
|
private suspend fun sendNotifications(params: JobParameters?): Unit = withContext(Dispatchers.Default) {
|
||||||
val currentId = Prefs.userId
|
val currentId = Prefs.userId
|
||||||
val cookies = loadFbCookiesSync()
|
val cookies = cookieDao.selectAll()
|
||||||
yield()
|
yield()
|
||||||
val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1
|
val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1
|
||||||
var notifCount = 0
|
var notifCount = 0
|
||||||
@ -101,13 +106,16 @@ class NotificationService : BaseJobService() {
|
|||||||
L.i { "Sent $notifCount notifications" }
|
L.i { "Sent $notifCount notifications" }
|
||||||
if (notifCount == 0 && jobId == NOTIFICATION_JOB_NOW)
|
if (notifCount == 0 && jobId == NOTIFICATION_JOB_NOW)
|
||||||
generalNotification(665, R.string.no_new_notifications, BuildConfig.DEBUG)
|
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
|
* Implemented fetch to also notify when an error occurs
|
||||||
* Also normalized the output to return the number of notifications received
|
* Also normalized the output to return the number of notifications received
|
||||||
*/
|
*/
|
||||||
private fun fetch(jobId: Int, type: NotificationType, cookie: CookieModel): Int {
|
private suspend fun fetch(jobId: Int, type: NotificationType, cookie: CookieEntity): Int {
|
||||||
val count = type.fetch(this, cookie)
|
val count = type.fetch(this, cookie)
|
||||||
if (count < 0) {
|
if (count < 0) {
|
||||||
if (jobId == NOTIFICATION_JOB_NOW)
|
if (jobId == NOTIFICATION_JOB_NOW)
|
||||||
|
@ -29,14 +29,15 @@ import ca.allanwang.kau.utils.string
|
|||||||
import com.pitchedapps.frost.BuildConfig
|
import com.pitchedapps.frost.BuildConfig
|
||||||
import com.pitchedapps.frost.R
|
import com.pitchedapps.frost.R
|
||||||
import com.pitchedapps.frost.activities.SettingsActivity
|
import com.pitchedapps.frost.activities.SettingsActivity
|
||||||
import com.pitchedapps.frost.dbflow.NotificationModel
|
import com.pitchedapps.frost.db.FrostDatabase
|
||||||
import com.pitchedapps.frost.dbflow.loadFbCookiesAsync
|
import com.pitchedapps.frost.db.deleteAll
|
||||||
import com.pitchedapps.frost.services.fetchNotifications
|
import com.pitchedapps.frost.services.fetchNotifications
|
||||||
import com.pitchedapps.frost.services.scheduleNotifications
|
import com.pitchedapps.frost.services.scheduleNotifications
|
||||||
import com.pitchedapps.frost.utils.Prefs
|
import com.pitchedapps.frost.utils.Prefs
|
||||||
import com.pitchedapps.frost.utils.frostSnackbar
|
import com.pitchedapps.frost.utils.frostSnackbar
|
||||||
import com.pitchedapps.frost.utils.materialDialogThemed
|
import com.pitchedapps.frost.utils.materialDialogThemed
|
||||||
import com.pitchedapps.frost.views.Keywords
|
import com.pitchedapps.frost.views.Keywords
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 2017-06-29.
|
* Created by Allan Wang on 2017-06-29.
|
||||||
@ -171,8 +172,8 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = {
|
|||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
plainText(R.string.reset_notif_epoch) {
|
plainText(R.string.reset_notif_epoch) {
|
||||||
onClick = {
|
onClick = {
|
||||||
loadFbCookiesAsync { cookies ->
|
launch {
|
||||||
cookies.map { NotificationModel(it.id) }.forEach { it.save() }
|
FrostDatabase.get().notifDao().deleteAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ import ca.allanwang.kau.utils.showAppInfo
|
|||||||
import ca.allanwang.kau.utils.string
|
import ca.allanwang.kau.utils.string
|
||||||
import ca.allanwang.kau.utils.toast
|
import ca.allanwang.kau.utils.toast
|
||||||
import com.pitchedapps.frost.R
|
import com.pitchedapps.frost.R
|
||||||
import com.pitchedapps.frost.dbflow.loadFbCookie
|
import com.pitchedapps.frost.db.CookieEntity
|
||||||
import com.pitchedapps.frost.facebook.USER_AGENT_DESKTOP
|
import com.pitchedapps.frost.facebook.USER_AGENT_DESKTOP
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,6 +38,7 @@ import com.pitchedapps.frost.facebook.USER_AGENT_DESKTOP
|
|||||||
* With reference to <a href="https://stackoverflow.com/questions/33434532/android-webview-download-files-like-browsers-do">Stack Overflow</a>
|
* With reference to <a href="https://stackoverflow.com/questions/33434532/android-webview-download-files-like-browsers-do">Stack Overflow</a>
|
||||||
*/
|
*/
|
||||||
fun Context.frostDownload(
|
fun Context.frostDownload(
|
||||||
|
cookie: CookieEntity,
|
||||||
url: String?,
|
url: String?,
|
||||||
userAgent: String = USER_AGENT_DESKTOP,
|
userAgent: String = USER_AGENT_DESKTOP,
|
||||||
contentDisposition: String? = null,
|
contentDisposition: String? = null,
|
||||||
@ -45,10 +46,11 @@ fun Context.frostDownload(
|
|||||||
contentLength: Long = 0L
|
contentLength: Long = 0L
|
||||||
) {
|
) {
|
||||||
url ?: return
|
url ?: return
|
||||||
frostDownload(Uri.parse(url), userAgent, contentDisposition, mimeType, contentLength)
|
frostDownload(cookie, Uri.parse(url), userAgent, contentDisposition, mimeType, contentLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.frostDownload(
|
fun Context.frostDownload(
|
||||||
|
cookie: CookieEntity,
|
||||||
uri: Uri?,
|
uri: Uri?,
|
||||||
userAgent: String = USER_AGENT_DESKTOP,
|
userAgent: String = USER_AGENT_DESKTOP,
|
||||||
contentDisposition: String? = null,
|
contentDisposition: String? = null,
|
||||||
@ -75,7 +77,6 @@ fun Context.frostDownload(
|
|||||||
if (!granted) return@kauRequestPermissions
|
if (!granted) return@kauRequestPermissions
|
||||||
val request = DownloadManager.Request(uri)
|
val request = DownloadManager.Request(uri)
|
||||||
request.setMimeType(mimeType)
|
request.setMimeType(mimeType)
|
||||||
val cookie = loadFbCookie(Prefs.userId) ?: return@kauRequestPermissions
|
|
||||||
val title = URLUtil.guessFileName(uri.toString(), contentDisposition, mimeType)
|
val title = URLUtil.guessFileName(uri.toString(), contentDisposition, mimeType)
|
||||||
request.addRequestHeader("Cookie", cookie.cookie)
|
request.addRequestHeader("Cookie", cookie.cookie)
|
||||||
request.addRequestHeader("User-Agent", userAgent)
|
request.addRequestHeader("User-Agent", userAgent)
|
||||||
|
@ -50,6 +50,11 @@ object L : KauLogger("Frost", {
|
|||||||
d(message)
|
d(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun _e(e: Throwable?, message: () -> Any?) {
|
||||||
|
if (BuildConfig.DEBUG)
|
||||||
|
e(e, message)
|
||||||
|
}
|
||||||
|
|
||||||
override fun logImpl(priority: Int, message: String?, t: Throwable?) {
|
override fun logImpl(priority: Int, message: String?, t: Throwable?) {
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
super.logImpl(priority, message, t)
|
super.logImpl(priority, message, t)
|
||||||
|
50
app/src/main/kotlin/com/pitchedapps/frost/utils/TimeUtils.kt
Normal file
50
app/src/main/kotlin/com/pitchedapps/frost/utils/TimeUtils.kt
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import ca.allanwang.kau.utils.string
|
||||||
|
import com.pitchedapps.frost.R
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts time in millis to readable date,
|
||||||
|
* eg Apr 24 at 7:32 PM
|
||||||
|
*
|
||||||
|
* With regards to date modifications in calendars,
|
||||||
|
* it appears to respect calendar rules;
|
||||||
|
* see https://stackoverflow.com/a/43227817/4407321
|
||||||
|
*/
|
||||||
|
fun Long.toReadableTime(context: Context): String {
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
cal.timeInMillis = this
|
||||||
|
val timeFormatter = SimpleDateFormat.getTimeInstance(DateFormat.SHORT)
|
||||||
|
val time = timeFormatter.format(Date(this))
|
||||||
|
val day = when {
|
||||||
|
cal >= Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -1) } -> context.string(R.string.today)
|
||||||
|
cal >= Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -2) } -> context.string(R.string.yesterday)
|
||||||
|
else -> {
|
||||||
|
val dayFormatter = SimpleDateFormat("MMM dd", Locale.getDefault())
|
||||||
|
dayFormatter.format(Date(this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return context.getString(R.string.time_template, day, time)
|
||||||
|
}
|
@ -62,7 +62,7 @@ import com.pitchedapps.frost.activities.TabCustomizerActivity
|
|||||||
import com.pitchedapps.frost.activities.WebOverlayActivity
|
import com.pitchedapps.frost.activities.WebOverlayActivity
|
||||||
import com.pitchedapps.frost.activities.WebOverlayActivityBase
|
import com.pitchedapps.frost.activities.WebOverlayActivityBase
|
||||||
import com.pitchedapps.frost.activities.WebOverlayDesktopActivity
|
import com.pitchedapps.frost.activities.WebOverlayDesktopActivity
|
||||||
import com.pitchedapps.frost.dbflow.CookieModel
|
import com.pitchedapps.frost.db.CookieEntity
|
||||||
import com.pitchedapps.frost.facebook.FACEBOOK_COM
|
import com.pitchedapps.frost.facebook.FACEBOOK_COM
|
||||||
import com.pitchedapps.frost.facebook.FBCDN_NET
|
import com.pitchedapps.frost.facebook.FBCDN_NET
|
||||||
import com.pitchedapps.frost.facebook.FbCookie
|
import com.pitchedapps.frost.facebook.FbCookie
|
||||||
@ -103,7 +103,7 @@ internal inline val Context.ctxCoroutine: CoroutineScope
|
|||||||
get() = this as? CoroutineScope ?: GlobalScope
|
get() = this as? CoroutineScope ?: GlobalScope
|
||||||
|
|
||||||
inline fun <reified T : Activity> Context.launchNewTask(
|
inline fun <reified T : Activity> Context.launchNewTask(
|
||||||
cookieList: ArrayList<CookieModel> = arrayListOf(),
|
cookieList: ArrayList<CookieEntity> = arrayListOf(),
|
||||||
clearStack: Boolean = false
|
clearStack: Boolean = false
|
||||||
) {
|
) {
|
||||||
startActivity<T>(clearStack, intentBuilder = {
|
startActivity<T>(clearStack, intentBuilder = {
|
||||||
@ -111,13 +111,13 @@ inline fun <reified T : Activity> Context.launchNewTask(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.launchLogin(cookieList: ArrayList<CookieModel>, clearStack: Boolean = true) {
|
fun Context.launchLogin(cookieList: ArrayList<CookieEntity>, clearStack: Boolean = true) {
|
||||||
if (cookieList.isNotEmpty()) launchNewTask<SelectorActivity>(cookieList, clearStack)
|
if (cookieList.isNotEmpty()) launchNewTask<SelectorActivity>(cookieList, clearStack)
|
||||||
else launchNewTask<LoginActivity>(clearStack = clearStack)
|
else launchNewTask<LoginActivity>(clearStack = clearStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Activity.cookies(): ArrayList<CookieModel> {
|
fun Activity.cookies(): ArrayList<CookieEntity> {
|
||||||
return intent?.getParcelableArrayListExtra<CookieModel>(EXTRA_COOKIES) ?: arrayListOf()
|
return intent?.getParcelableArrayListExtra<CookieEntity>(EXTRA_COOKIES) ?: arrayListOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -186,7 +186,7 @@ fun MaterialDialog.Builder.theme(): MaterialDialog.Builder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun Activity.setFrostTheme(forceTransparent: Boolean = false) {
|
fun Activity.setFrostTheme(forceTransparent: Boolean = false) {
|
||||||
val isTransparent = (Color.alpha(Prefs.bgColor) != 255) || forceTransparent
|
val isTransparent = (Color.alpha(Prefs.bgColor) != 255) || (Color.alpha(Prefs.headerColor) != 255) || forceTransparent
|
||||||
if (Prefs.bgColor.isColorDark)
|
if (Prefs.bgColor.isColorDark)
|
||||||
setTheme(if (isTransparent) R.style.FrostTheme_Transparent else R.style.FrostTheme)
|
setTheme(if (isTransparent) R.style.FrostTheme_Transparent else R.style.FrostTheme)
|
||||||
else
|
else
|
||||||
|
@ -33,7 +33,7 @@ import com.bumptech.glide.request.RequestListener
|
|||||||
import com.bumptech.glide.request.target.Target
|
import com.bumptech.glide.request.target.Target
|
||||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||||
import com.pitchedapps.frost.R
|
import com.pitchedapps.frost.R
|
||||||
import com.pitchedapps.frost.dbflow.CookieModel
|
import com.pitchedapps.frost.db.CookieEntity
|
||||||
import com.pitchedapps.frost.facebook.profilePictureUrl
|
import com.pitchedapps.frost.facebook.profilePictureUrl
|
||||||
import com.pitchedapps.frost.glide.FrostGlide
|
import com.pitchedapps.frost.glide.FrostGlide
|
||||||
import com.pitchedapps.frost.glide.GlideApp
|
import com.pitchedapps.frost.glide.GlideApp
|
||||||
@ -42,7 +42,7 @@ import com.pitchedapps.frost.utils.Prefs
|
|||||||
/**
|
/**
|
||||||
* Created by Allan Wang on 2017-06-05.
|
* Created by Allan Wang on 2017-06-05.
|
||||||
*/
|
*/
|
||||||
class AccountItem(val cookie: CookieModel?) : KauIItem<AccountItem, AccountItem.ViewHolder>
|
class AccountItem(val cookie: CookieEntity?) : KauIItem<AccountItem, AccountItem.ViewHolder>
|
||||||
(R.layout.view_account, { ViewHolder(it) }, R.id.item_account) {
|
(R.layout.view_account, { ViewHolder(it) }, R.id.item_account) {
|
||||||
|
|
||||||
override fun bindView(viewHolder: ViewHolder, payloads: MutableList<Any>) {
|
override fun bindView(viewHolder: ViewHolder, payloads: MutableList<Any>) {
|
||||||
|
@ -32,6 +32,7 @@ import ca.allanwang.kau.utils.inflate
|
|||||||
import ca.allanwang.kau.utils.isColorDark
|
import ca.allanwang.kau.utils.isColorDark
|
||||||
import ca.allanwang.kau.utils.isGone
|
import ca.allanwang.kau.utils.isGone
|
||||||
import ca.allanwang.kau.utils.isVisible
|
import ca.allanwang.kau.utils.isVisible
|
||||||
|
import ca.allanwang.kau.utils.launchMain
|
||||||
import ca.allanwang.kau.utils.setIcon
|
import ca.allanwang.kau.utils.setIcon
|
||||||
import ca.allanwang.kau.utils.setMenuIcons
|
import ca.allanwang.kau.utils.setMenuIcons
|
||||||
import ca.allanwang.kau.utils.visible
|
import ca.allanwang.kau.utils.visible
|
||||||
@ -39,8 +40,11 @@ import ca.allanwang.kau.utils.withMinAlpha
|
|||||||
import com.devbrackets.android.exomedia.listener.VideoControlsVisibilityListener
|
import com.devbrackets.android.exomedia.listener.VideoControlsVisibilityListener
|
||||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||||
import com.pitchedapps.frost.R
|
import com.pitchedapps.frost.R
|
||||||
|
import com.pitchedapps.frost.db.FrostDatabase
|
||||||
|
import com.pitchedapps.frost.db.currentCookie
|
||||||
import com.pitchedapps.frost.utils.L
|
import com.pitchedapps.frost.utils.L
|
||||||
import com.pitchedapps.frost.utils.Prefs
|
import com.pitchedapps.frost.utils.Prefs
|
||||||
|
import com.pitchedapps.frost.utils.ctxCoroutine
|
||||||
import com.pitchedapps.frost.utils.frostDownload
|
import com.pitchedapps.frost.utils.frostDownload
|
||||||
import kotlinx.android.synthetic.main.view_video.view.*
|
import kotlinx.android.synthetic.main.view_video.view.*
|
||||||
|
|
||||||
@ -96,7 +100,10 @@ class FrostVideoViewer @JvmOverloads constructor(
|
|||||||
video_toolbar.setOnMenuItemClickListener {
|
video_toolbar.setOnMenuItemClickListener {
|
||||||
when (it.itemId) {
|
when (it.itemId) {
|
||||||
R.id.action_pip -> video.isExpanded = false
|
R.id.action_pip -> video.isExpanded = false
|
||||||
R.id.action_download -> context.frostDownload(video.videoUri)
|
R.id.action_download -> context.ctxCoroutine.launchMain {
|
||||||
|
val cookie = FrostDatabase.get().cookieDao().currentCookie() ?: return@launchMain
|
||||||
|
context.frostDownload(cookie, video.videoUri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -24,15 +24,19 @@ import android.util.AttributeSet
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import ca.allanwang.kau.utils.AnimHolder
|
import ca.allanwang.kau.utils.AnimHolder
|
||||||
|
import ca.allanwang.kau.utils.launchMain
|
||||||
import com.pitchedapps.frost.contracts.FrostContentContainer
|
import com.pitchedapps.frost.contracts.FrostContentContainer
|
||||||
import com.pitchedapps.frost.contracts.FrostContentCore
|
import com.pitchedapps.frost.contracts.FrostContentCore
|
||||||
import com.pitchedapps.frost.contracts.FrostContentParent
|
import com.pitchedapps.frost.contracts.FrostContentParent
|
||||||
|
import com.pitchedapps.frost.db.FrostDatabase
|
||||||
|
import com.pitchedapps.frost.db.currentCookie
|
||||||
import com.pitchedapps.frost.facebook.FB_HOME_URL
|
import com.pitchedapps.frost.facebook.FB_HOME_URL
|
||||||
import com.pitchedapps.frost.facebook.FbItem
|
import com.pitchedapps.frost.facebook.FbItem
|
||||||
import com.pitchedapps.frost.facebook.USER_AGENT_DESKTOP
|
import com.pitchedapps.frost.facebook.USER_AGENT_DESKTOP
|
||||||
import com.pitchedapps.frost.facebook.USER_AGENT_MOBILE
|
import com.pitchedapps.frost.facebook.USER_AGENT_MOBILE
|
||||||
import com.pitchedapps.frost.fragments.WebFragment
|
import com.pitchedapps.frost.fragments.WebFragment
|
||||||
import com.pitchedapps.frost.utils.Prefs
|
import com.pitchedapps.frost.utils.Prefs
|
||||||
|
import com.pitchedapps.frost.utils.ctxCoroutine
|
||||||
import com.pitchedapps.frost.utils.frostDownload
|
import com.pitchedapps.frost.utils.frostDownload
|
||||||
import com.pitchedapps.frost.web.FrostChromeClient
|
import com.pitchedapps.frost.web.FrostChromeClient
|
||||||
import com.pitchedapps.frost.web.FrostJSI
|
import com.pitchedapps.frost.web.FrostJSI
|
||||||
@ -81,7 +85,13 @@ class FrostWebView @JvmOverloads constructor(
|
|||||||
webChromeClient = FrostChromeClient(this)
|
webChromeClient = FrostChromeClient(this)
|
||||||
addJavascriptInterface(FrostJSI(this), "Frost")
|
addJavascriptInterface(FrostJSI(this), "Frost")
|
||||||
setBackgroundColor(Color.TRANSPARENT)
|
setBackgroundColor(Color.TRANSPARENT)
|
||||||
setDownloadListener(context::frostDownload)
|
val db = FrostDatabase.get()
|
||||||
|
setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
|
||||||
|
context.ctxCoroutine.launchMain {
|
||||||
|
val cookie = db.cookieDao().currentCookie() ?: return@launchMain
|
||||||
|
context.frostDownload(cookie, url, userAgent, contentDisposition, mimetype, contentLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ class DebugWebView @JvmOverloads constructor(
|
|||||||
settings.userAgentString = USER_AGENT_MOBILE
|
settings.userAgentString = USER_AGENT_MOBILE
|
||||||
setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
||||||
webViewClient = DebugClient()
|
webViewClient = DebugClient()
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
isDrawingCacheEnabled = true
|
isDrawingCacheEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,6 +73,7 @@ class DebugWebView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
output.outputStream().use {
|
output.outputStream().use {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
drawingCache.compress(Bitmap.CompressFormat.PNG, 100, it)
|
drawingCache.compress(Bitmap.CompressFormat.PNG, 100, it)
|
||||||
}
|
}
|
||||||
L.d { "Created screenshot at ${output.absolutePath}" }
|
L.d { "Created screenshot at ${output.absolutePath}" }
|
||||||
|
@ -21,7 +21,7 @@ import android.webkit.JavascriptInterface
|
|||||||
import com.pitchedapps.frost.activities.MainActivity
|
import com.pitchedapps.frost.activities.MainActivity
|
||||||
import com.pitchedapps.frost.contracts.MainActivityContract
|
import com.pitchedapps.frost.contracts.MainActivityContract
|
||||||
import com.pitchedapps.frost.contracts.VideoViewHolder
|
import com.pitchedapps.frost.contracts.VideoViewHolder
|
||||||
import com.pitchedapps.frost.dbflow.CookieModel
|
import com.pitchedapps.frost.db.CookieEntity
|
||||||
import com.pitchedapps.frost.facebook.FbCookie
|
import com.pitchedapps.frost.facebook.FbCookie
|
||||||
import com.pitchedapps.frost.utils.L
|
import com.pitchedapps.frost.utils.L
|
||||||
import com.pitchedapps.frost.utils.Prefs
|
import com.pitchedapps.frost.utils.Prefs
|
||||||
@ -44,7 +44,7 @@ class FrostJSI(val web: FrostWebView) {
|
|||||||
private val activity: MainActivity? = context as? MainActivity
|
private val activity: MainActivity? = context as? MainActivity
|
||||||
private val header: SendChannel<String>? = activity?.headerBadgeChannel
|
private val header: SendChannel<String>? = activity?.headerBadgeChannel
|
||||||
private val refresh: SendChannel<Boolean> = web.parent.refreshChannel
|
private val refresh: SendChannel<Boolean> = web.parent.refreshChannel
|
||||||
private val cookies: List<CookieModel> = activity?.cookies() ?: arrayListOf()
|
private val cookies: List<CookieEntity> = activity?.cookies() ?: arrayListOf()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to load the url in an overlay
|
* Attempts to load the url in an overlay
|
||||||
|
@ -29,7 +29,7 @@ import android.webkit.WebView
|
|||||||
import ca.allanwang.kau.utils.fadeIn
|
import ca.allanwang.kau.utils.fadeIn
|
||||||
import ca.allanwang.kau.utils.isVisible
|
import ca.allanwang.kau.utils.isVisible
|
||||||
import ca.allanwang.kau.utils.launchMain
|
import ca.allanwang.kau.utils.launchMain
|
||||||
import com.pitchedapps.frost.dbflow.CookieModel
|
import com.pitchedapps.frost.db.CookieEntity
|
||||||
import com.pitchedapps.frost.facebook.FB_LOGIN_URL
|
import com.pitchedapps.frost.facebook.FB_LOGIN_URL
|
||||||
import com.pitchedapps.frost.facebook.FB_USER_MATCHER
|
import com.pitchedapps.frost.facebook.FB_USER_MATCHER
|
||||||
import com.pitchedapps.frost.facebook.FbCookie
|
import com.pitchedapps.frost.facebook.FbCookie
|
||||||
@ -51,7 +51,7 @@ class LoginWebView @JvmOverloads constructor(
|
|||||||
defStyleAttr: Int = 0
|
defStyleAttr: Int = 0
|
||||||
) : WebView(context, attrs, defStyleAttr) {
|
) : WebView(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
private val completable: CompletableDeferred<CookieModel> = CompletableDeferred()
|
private val completable: CompletableDeferred<CookieEntity> = CompletableDeferred()
|
||||||
private lateinit var progressCallback: (Int) -> Unit
|
private lateinit var progressCallback: (Int) -> Unit
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
@ -62,7 +62,7 @@ class LoginWebView @JvmOverloads constructor(
|
|||||||
webChromeClient = LoginChromeClient()
|
webChromeClient = LoginChromeClient()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadLogin(progressCallback: (Int) -> Unit): CompletableDeferred<CookieModel> = coroutineScope {
|
suspend fun loadLogin(progressCallback: (Int) -> Unit): CompletableDeferred<CookieEntity> = coroutineScope {
|
||||||
this@LoginWebView.progressCallback = progressCallback
|
this@LoginWebView.progressCallback = progressCallback
|
||||||
L.d { "Begin loading login" }
|
L.d { "Begin loading login" }
|
||||||
launchMain {
|
launchMain {
|
||||||
@ -77,18 +77,18 @@ class LoginWebView @JvmOverloads constructor(
|
|||||||
|
|
||||||
override fun onPageFinished(view: WebView, url: String?) {
|
override fun onPageFinished(view: WebView, url: String?) {
|
||||||
super.onPageFinished(view, url)
|
super.onPageFinished(view, url)
|
||||||
val cookieModel = checkForLogin(url)
|
val cookie = checkForLogin(url)
|
||||||
if (cookieModel != null)
|
if (cookie != null)
|
||||||
completable.complete(cookieModel)
|
completable.complete(cookie)
|
||||||
if (!view.isVisible) view.fadeIn()
|
if (!view.isVisible) view.fadeIn()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkForLogin(url: String?): CookieModel? {
|
fun checkForLogin(url: String?): CookieEntity? {
|
||||||
if (!url.isFacebookUrl) return null
|
if (!url.isFacebookUrl) return null
|
||||||
val cookie = CookieManager.getInstance().getCookie(url) ?: return null
|
val cookie = CookieManager.getInstance().getCookie(url) ?: return null
|
||||||
L.d { "Checking cookie for login" }
|
L.d { "Checking cookie for login" }
|
||||||
val id = FB_USER_MATCHER.find(cookie)[1]?.toLong() ?: return null
|
val id = FB_USER_MATCHER.find(cookie)[1]?.toLong() ?: return null
|
||||||
return CookieModel(id, "", cookie)
|
return CookieEntity(id, null, cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPageCommitVisible(view: WebView, url: String?) {
|
override fun onPageCommitVisible(view: WebView, url: String?) {
|
||||||
|
@ -0,0 +1,194 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.widgets
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.appwidget.AppWidgetProvider
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.graphics.PorterDuffColorFilter
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import android.widget.RemoteViewsService
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import ca.allanwang.kau.utils.dimenPixelSize
|
||||||
|
import ca.allanwang.kau.utils.withAlpha
|
||||||
|
import com.pitchedapps.frost.R
|
||||||
|
import com.pitchedapps.frost.activities.MainActivity
|
||||||
|
import com.pitchedapps.frost.db.NotificationDao
|
||||||
|
import com.pitchedapps.frost.db.selectNotificationsSync
|
||||||
|
import com.pitchedapps.frost.glide.FrostGlide
|
||||||
|
import com.pitchedapps.frost.glide.GlideApp
|
||||||
|
import com.pitchedapps.frost.services.NotificationContent
|
||||||
|
import com.pitchedapps.frost.services.NotificationType
|
||||||
|
import com.pitchedapps.frost.utils.Prefs
|
||||||
|
import com.pitchedapps.frost.utils.toReadableTime
|
||||||
|
import org.koin.standalone.KoinComponent
|
||||||
|
import org.koin.standalone.inject
|
||||||
|
|
||||||
|
class NotificationWidget : AppWidgetProvider() {
|
||||||
|
|
||||||
|
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||||
|
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||||
|
val type = NotificationType.GENERAL
|
||||||
|
val userId = Prefs.userId
|
||||||
|
val intent = NotificationWidgetService.createIntent(context, type, userId)
|
||||||
|
for (id in appWidgetIds) {
|
||||||
|
val views = RemoteViews(context.packageName, R.layout.widget_notifications)
|
||||||
|
|
||||||
|
views.setBackgroundColor(R.id.widget_layout_toolbar, Prefs.headerColor)
|
||||||
|
views.setIcon(R.id.img_frost, context, R.drawable.frost_f_24, Prefs.iconColor)
|
||||||
|
views.setOnClickPendingIntent(
|
||||||
|
R.id.img_frost,
|
||||||
|
PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
views.setBackgroundColor(R.id.widget_notification_list, Prefs.bgColor)
|
||||||
|
views.setRemoteAdapter(R.id.widget_notification_list, intent)
|
||||||
|
|
||||||
|
val pendingIntentTemplate = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
type.createCommonIntent(context, userId),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
|
||||||
|
views.setPendingIntentTemplate(R.id.widget_notification_list, pendingIntentTemplate)
|
||||||
|
|
||||||
|
appWidgetManager.updateAppWidget(id, views)
|
||||||
|
}
|
||||||
|
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_notification_list)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun forceUpdate(context: Context) {
|
||||||
|
val manager = AppWidgetManager.getInstance(context)
|
||||||
|
val ids = manager.getAppWidgetIds(ComponentName(context, NotificationWidget::class.java))
|
||||||
|
val intent = Intent().apply {
|
||||||
|
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||||
|
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
||||||
|
}
|
||||||
|
context.sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val NOTIF_WIDGET_TYPE = "notif_widget_type"
|
||||||
|
private const val NOTIF_WIDGET_USER_ID = "notif_widget_user_id"
|
||||||
|
|
||||||
|
private fun RemoteViews.setBackgroundColor(@IdRes viewId: Int, @ColorInt color: Int) {
|
||||||
|
setInt(viewId, "setBackgroundColor", color)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds backward compatibility to setting tinted icons
|
||||||
|
*/
|
||||||
|
private fun RemoteViews.setIcon(@IdRes viewId: Int, context: Context, @DrawableRes res: Int, @ColorInt color: Int) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val icon = Icon.createWithResource(context, res).setTint(color).setTintMode(PorterDuff.Mode.SRC_IN)
|
||||||
|
setImageViewIcon(viewId, icon)
|
||||||
|
} else {
|
||||||
|
val bitmap = BitmapFactory.decodeResource(context.resources, res)
|
||||||
|
if (bitmap != null) {
|
||||||
|
val paint = Paint()
|
||||||
|
paint.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
||||||
|
val result = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(result)
|
||||||
|
canvas.drawBitmap(bitmap, 0f, 0f, paint)
|
||||||
|
setImageViewBitmap(viewId, result)
|
||||||
|
} else {
|
||||||
|
// Fallback to just icon
|
||||||
|
setImageViewResource(viewId, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationWidgetService : RemoteViewsService() {
|
||||||
|
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = NotificationWidgetDataProvider(this, intent)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun createIntent(context: Context, type: NotificationType, userId: Long): Intent =
|
||||||
|
Intent(context, NotificationWidgetService::class.java)
|
||||||
|
.putExtra(NOTIF_WIDGET_TYPE, type.name)
|
||||||
|
.putExtra(NOTIF_WIDGET_USER_ID, userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationWidgetDataProvider(val context: Context, val intent: Intent) : RemoteViewsService.RemoteViewsFactory,
|
||||||
|
KoinComponent {
|
||||||
|
|
||||||
|
private val notifDao: NotificationDao by inject()
|
||||||
|
@Volatile
|
||||||
|
private var content: List<NotificationContent> = emptyList()
|
||||||
|
|
||||||
|
private val type = NotificationType.valueOf(intent.getStringExtra(NOTIF_WIDGET_TYPE))
|
||||||
|
|
||||||
|
private val userId = intent.getLongExtra(NOTIF_WIDGET_USER_ID, -1)
|
||||||
|
|
||||||
|
private val avatarSize = context.dimenPixelSize(R.dimen.avatar_image_size)
|
||||||
|
|
||||||
|
private val glide = GlideApp.with(context).asBitmap()
|
||||||
|
|
||||||
|
private fun loadNotifications() {
|
||||||
|
content = notifDao.selectNotificationsSync(userId, type.channelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDataSetChanged() {
|
||||||
|
loadNotifications()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLoadingView(): RemoteViews? = null
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long = content[position].id
|
||||||
|
|
||||||
|
override fun hasStableIds(): Boolean = true
|
||||||
|
|
||||||
|
override fun getViewAt(position: Int): RemoteViews {
|
||||||
|
val views = RemoteViews(context.packageName, R.layout.widget_notification_item)
|
||||||
|
val notif = content[position]
|
||||||
|
views.setBackgroundColor(R.id.item_frame, Prefs.nativeBgColor(notif.unread))
|
||||||
|
views.setTextColor(R.id.item_content, Prefs.textColor)
|
||||||
|
views.setTextViewText(R.id.item_content, notif.text)
|
||||||
|
views.setTextColor(R.id.item_date, Prefs.textColor.withAlpha(150))
|
||||||
|
views.setTextViewText(R.id.item_date, notif.timestamp.toReadableTime(context))
|
||||||
|
|
||||||
|
val avatar = glide.load(notif.profileUrl).transform(FrostGlide.circleCrop).submit(avatarSize, avatarSize).get()
|
||||||
|
views.setImageViewBitmap(R.id.item_avatar, avatar)
|
||||||
|
views.setOnClickFillInIntent(R.id.item_frame, type.putContentExtra(Intent(), notif))
|
||||||
|
return views
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCount(): Int = content.size
|
||||||
|
|
||||||
|
override fun getViewTypeCount(): Int = 1
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
}
|
||||||
|
}
|
48
app/src/main/res/drawable/notification_widget_preview.xml
Normal file
48
app/src/main/res/drawable/notification_widget_preview.xml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="30dp"
|
||||||
|
android:height="40dp"
|
||||||
|
android:viewportWidth="300"
|
||||||
|
android:viewportHeight="400">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h300v400H0V0z"
|
||||||
|
android:fillColor="#fafafa"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h300v50H0V0z"
|
||||||
|
android:fillColor="@color/facebook_blue"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M65,170a20,20 0,1 1,-40 0,20 20,0 0,1 40,0z"
|
||||||
|
android:fillColor="#DE000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M85,150h184v11H85v-11z"
|
||||||
|
android:fillColor="#DE000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M85,179h146v11H85v-11z"
|
||||||
|
android:fillColor="#DE000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M65,95a20,20 0,1 1,-40 0,20 20,0 0,1 40,0z"
|
||||||
|
android:fillColor="#DE000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M85,75h184v11H85V75z"
|
||||||
|
android:fillColor="#DE000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M85,104h146v11H85v-11z"
|
||||||
|
android:fillColor="#DE000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M65,245a20,20 0,1 1,-40 0,20 20,0 0,1 40,0z"
|
||||||
|
android:fillColor="#DE000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M85,225h184v11H85v-11z"
|
||||||
|
android:fillColor="#DE000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M85,254h146v11H85v-11z"
|
||||||
|
android:fillColor="#DE000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M65,320a20,20 0,1 1,-40 0,20 20,0 0,1 40,0z"
|
||||||
|
android:fillColor="#DE000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M85,300h184v11H85v-11z"
|
||||||
|
android:fillColor="#DE000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M85,329h146v11H85v-11z"
|
||||||
|
android:fillColor="#DE000000"/>
|
||||||
|
</vector>
|
46
app/src/main/res/layout/widget_notification_item.xml
Normal file
46
app/src/main/res/layout/widget_notification_item.xml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/item_frame"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:selectableItemBackground"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingStart="@dimen/kau_activity_horizontal_margin"
|
||||||
|
android:paddingTop="@dimen/kau_activity_vertical_margin"
|
||||||
|
android:paddingEnd="@dimen/kau_activity_horizontal_margin"
|
||||||
|
android:paddingBottom="@dimen/kau_activity_vertical_margin">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/item_avatar"
|
||||||
|
android:layout_width="@dimen/avatar_image_size"
|
||||||
|
android:layout_height="@dimen/avatar_image_size" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Unlike the actual notification panel,
|
||||||
|
we do not show thumbnails, and we limit the title length
|
||||||
|
-->
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/kau_padding_normal"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/item_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="2" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/item_date"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
31
app/src/main/res/layout/widget_notifications.xml
Normal file
31
app/src/main/res/layout/widget_notifications.xml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/widget_layout_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/widget_layout_toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingStart="@dimen/kau_padding_small"
|
||||||
|
android:paddingEnd="@dimen/kau_padding_small">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/img_frost"
|
||||||
|
android:layout_width="@dimen/toolbar_icon_size"
|
||||||
|
android:layout_height="@dimen/toolbar_icon_size"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_margin="@dimen/kau_padding_small"
|
||||||
|
android:background="?android:selectableItemBackgroundBorderless" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/widget_notification_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
</LinearLayout>
|
@ -46,4 +46,12 @@ plně funkční náhrada za oficiální aplikaci Facebooku, vytvořena od nuly a
|
|||||||
<string name="options">Nastavení</string>
|
<string name="options">Nastavení</string>
|
||||||
<string name="tab_customizer_instructions">Dlouhým stiskem přeuspořádejte horní ikony.</string>
|
<string name="tab_customizer_instructions">Dlouhým stiskem přeuspořádejte horní ikony.</string>
|
||||||
<string name="no_new_notifications">Žádné nové oznámení</string>
|
<string name="no_new_notifications">Žádné nové oznámení</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -45,4 +45,12 @@
|
|||||||
<string name="options">Valgmuligheder</string>
|
<string name="options">Valgmuligheder</string>
|
||||||
<string name="tab_customizer_instructions">Hold nede og træk for at flytte de øverste ikoner.</string>
|
<string name="tab_customizer_instructions">Hold nede og træk for at flytte de øverste ikoner.</string>
|
||||||
<string name="no_new_notifications">Ingen nye notifikationer</string>
|
<string name="no_new_notifications">Ingen nye notifikationer</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
<string name="birthdays">Geburtstage</string>
|
<string name="birthdays">Geburtstage</string>
|
||||||
<string name="chat">Chat</string>
|
<string name="chat">Chat</string>
|
||||||
<string name="photos">Fotos</string>
|
<string name="photos">Fotos</string>
|
||||||
|
<string name="marketplace">Marktplatz</string>
|
||||||
<string name="notes">Notizen</string>
|
<string name="notes">Notizen</string>
|
||||||
<string name="on_this_day">An diesem Tag</string>
|
<string name="on_this_day">An diesem Tag</string>
|
||||||
<string name="loading_account">Alles wird vorbereitet…</string>
|
<string name="loading_account">Alles wird vorbereitet…</string>
|
||||||
@ -45,4 +46,12 @@
|
|||||||
<string name="options">Optionen</string>
|
<string name="options">Optionen</string>
|
||||||
<string name="tab_customizer_instructions">Durch langes Drücken und Ziehen können Sie die oberen Symbole neu anordnen.</string>
|
<string name="tab_customizer_instructions">Durch langes Drücken und Ziehen können Sie die oberen Symbole neu anordnen.</string>
|
||||||
<string name="no_new_notifications">Keine neue Benachrichtigungen gefunden</string>
|
<string name="no_new_notifications">Keine neue Benachrichtigungen gefunden</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
<string name="suggested_friends_desc">Zeige \"Leute die du vielleicht kennst\" im Feed</string>
|
<string name="suggested_friends_desc">Zeige \"Leute die du vielleicht kennst\" im Feed</string>
|
||||||
<string name="suggested_groups">Empfohlene Gruppen</string>
|
<string name="suggested_groups">Empfohlene Gruppen</string>
|
||||||
<string name="suggested_groups_desc">Zeige \"Empfohlene Gruppen\" im Feed</string>
|
<string name="suggested_groups_desc">Zeige \"Empfohlene Gruppen\" im Feed</string>
|
||||||
|
<string name="show_stories">Story\'s anzeigen</string>
|
||||||
|
<string name="show_stories_desc">Story\'s in den Feed anzeigen</string>
|
||||||
<string name="facebook_ads">Facebook Werbung</string>
|
<string name="facebook_ads">Facebook Werbung</string>
|
||||||
<string name="facebook_ads_desc">Zeige native Facebook Werbung</string>
|
<string name="facebook_ads_desc">Zeige native Facebook Werbung</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
<string name="birthdays">Cumpleaños</string>
|
<string name="birthdays">Cumpleaños</string>
|
||||||
<string name="chat">Chat</string>
|
<string name="chat">Chat</string>
|
||||||
<string name="photos">Fotos</string>
|
<string name="photos">Fotos</string>
|
||||||
|
<string name="marketplace">Marketplace</string>
|
||||||
<string name="notes">Notas</string>
|
<string name="notes">Notas</string>
|
||||||
<string name="on_this_day">En este día</string>
|
<string name="on_this_day">En este día</string>
|
||||||
<string name="loading_account">Preparando todo…</string>
|
<string name="loading_account">Preparando todo…</string>
|
||||||
@ -45,4 +46,14 @@
|
|||||||
<string name="options">Opciones</string>
|
<string name="options">Opciones</string>
|
||||||
<string name="tab_customizer_instructions">Mantén pulsado y arrastra para reorganizar los iconos superiores.</string>
|
<string name="tab_customizer_instructions">Mantén pulsado y arrastra para reorganizar los iconos superiores.</string>
|
||||||
<string name="no_new_notifications">No se han encontrado Notificaciones</string>
|
<string name="no_new_notifications">No se han encontrado Notificaciones</string>
|
||||||
|
<string name="today">Hoy</string>
|
||||||
|
<string name="yesterday">Ayer</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
<string name="force_message_bottom_desc">Al cargar un hilo de mensaje, activa un desplazamiento hacia la parte inferior de la página en lugar de cargar la página tal como es.</string>
|
<string name="force_message_bottom_desc">Al cargar un hilo de mensaje, activa un desplazamiento hacia la parte inferior de la página en lugar de cargar la página tal como es.</string>
|
||||||
<string name="enable_pip">Activar PIP</string>
|
<string name="enable_pip">Activar PIP</string>
|
||||||
<string name="enable_pip_desc">Activar función de video en miniatura</string>
|
<string name="enable_pip_desc">Activar función de video en miniatura</string>
|
||||||
|
<string name="autoplay_settings">Configuración de jugadas automáticas</string>
|
||||||
|
<string name="autoplay_settings_desc">Abra configuración de juego de auto de Facebook. Tenga en cuenta que debe estar desactivada para que PIP trabajar.</string>
|
||||||
<string name="exit_confirmation">Confirmar salida</string>
|
<string name="exit_confirmation">Confirmar salida</string>
|
||||||
<string name="exit_confirmation_desc">Muestra un diálogo de confirmación antes de salir de la app</string>
|
<string name="exit_confirmation_desc">Muestra un diálogo de confirmación antes de salir de la app</string>
|
||||||
<string name="analytics">Analytics</string>
|
<string name="analytics">Analytics</string>
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
<string name="suggested_friends_desc">Mostrar \"Gente que quizá conozcas\" en el feed</string>
|
<string name="suggested_friends_desc">Mostrar \"Gente que quizá conozcas\" en el feed</string>
|
||||||
<string name="suggested_groups">Grupos sugeridos</string>
|
<string name="suggested_groups">Grupos sugeridos</string>
|
||||||
<string name="suggested_groups_desc">Mostrar \"grupos sugeridos\" en el feed</string>
|
<string name="suggested_groups_desc">Mostrar \"grupos sugeridos\" en el feed</string>
|
||||||
|
<string name="show_stories">Historias destacadas</string>
|
||||||
|
<string name="show_stories_desc">Mostrar historias en el feed</string>
|
||||||
<string name="facebook_ads">Anuncios de Facebook</string>
|
<string name="facebook_ads">Anuncios de Facebook</string>
|
||||||
<string name="facebook_ads_desc">Mostrar anuncios nativos de Facebook</string>
|
<string name="facebook_ads_desc">Mostrar anuncios nativos de Facebook</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--Generated by crowdin.com-->
|
|
||||||
<resources>
|
|
||||||
<string name="newsfeed_sort">Orden de las Publicaciones</string>
|
|
||||||
<string name="newsfeed_sort_desc">Define el orden en que aparecen las publicaciones</string>
|
|
||||||
<string name="aggressive_recents">Modo \"Más Recientes\" agresivo</string>
|
|
||||||
<string name="aggressive_recents_desc">Filtra de manera adicional las publicaciones más antiguas de Facebook de las noticias recientes. Deshabilita esta opción si el feed se encuentra vacio.</string>
|
|
||||||
<string name="composer">Escritor de Estado</string>
|
|
||||||
<string name="composer_desc">Mostrar escritor de estado en el feed</string>
|
|
||||||
<string name="suggested_friends">Sugerencias de Amigos</string>
|
|
||||||
<string name="suggested_friends_desc">Mostrar \"Gente que quizá conozcas\" en el feed</string>
|
|
||||||
<string name="suggested_groups">Grupos sugeridos</string>
|
|
||||||
<string name="suggested_groups_desc">Mostrar \"grupos sugeridos\" en el feed</string>
|
|
||||||
<string name="facebook_ads">Publicidad de Facebook</string>
|
|
||||||
<string name="facebook_ads_desc">Mostrar Publicidad Nativa de Facebook</string>
|
|
||||||
</resources>
|
|
@ -17,6 +17,7 @@
|
|||||||
<string name="birthdays">Anniversaires</string>
|
<string name="birthdays">Anniversaires</string>
|
||||||
<string name="chat">Conversations</string>
|
<string name="chat">Conversations</string>
|
||||||
<string name="photos">Photos</string>
|
<string name="photos">Photos</string>
|
||||||
|
<string name="marketplace">Marketplace</string>
|
||||||
<string name="notes">Notes</string>
|
<string name="notes">Notes</string>
|
||||||
<string name="on_this_day">Aujourd\'hui</string>
|
<string name="on_this_day">Aujourd\'hui</string>
|
||||||
<string name="loading_account">Tout se prépare…</string>
|
<string name="loading_account">Tout se prépare…</string>
|
||||||
@ -45,4 +46,15 @@
|
|||||||
<string name="options">Options</string>
|
<string name="options">Options</string>
|
||||||
<string name="tab_customizer_instructions">Appuyez longuement et faites glisser pour réorganiser les icônes du haut.</string>
|
<string name="tab_customizer_instructions">Appuyez longuement et faites glisser pour réorganiser les icônes du haut.</string>
|
||||||
<string name="no_new_notifications">Pas de nouvelles notifications trouvées</string>
|
<string name="no_new_notifications">Pas de nouvelles notifications trouvées</string>
|
||||||
|
<string name="today">Aujourd\'hui</string>
|
||||||
|
<string name="yesterday">Hier</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
|
<string name="time_template">%1s à %2s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
<string name="force_message_bottom_desc">Lors du chargement d’un fil de message, déclencher un défilement vers le bas de la page au lieu de charger la page telle quelle.</string>
|
<string name="force_message_bottom_desc">Lors du chargement d’un fil de message, déclencher un défilement vers le bas de la page au lieu de charger la page telle quelle.</string>
|
||||||
<string name="enable_pip">Activer le PIP</string>
|
<string name="enable_pip">Activer le PIP</string>
|
||||||
<string name="enable_pip_desc">Activer les vidéos Picture In Picture</string>
|
<string name="enable_pip_desc">Activer les vidéos Picture In Picture</string>
|
||||||
|
<string name="autoplay_settings">Paramètres de lecture automatique</string>
|
||||||
|
<string name="autoplay_settings_desc">Ouvrir les paramètres de lecture automatique de Facebook. Notez qu\'il doit être désactivé pour que PIP fonctionne.</string>
|
||||||
<string name="exit_confirmation">Confirmation de la sortie</string>
|
<string name="exit_confirmation">Confirmation de la sortie</string>
|
||||||
<string name="exit_confirmation_desc">Afficher la boîte de dialogue de confirmation avant de quitter l’application</string>
|
<string name="exit_confirmation_desc">Afficher la boîte de dialogue de confirmation avant de quitter l’application</string>
|
||||||
<string name="analytics">Analytics</string>
|
<string name="analytics">Analytics</string>
|
||||||
|
@ -4,13 +4,15 @@
|
|||||||
<string name="newsfeed_sort">Ordre du fil d\'actualité</string>
|
<string name="newsfeed_sort">Ordre du fil d\'actualité</string>
|
||||||
<string name="newsfeed_sort_desc">Définit l’ordre dans lequel les messages sont affichés</string>
|
<string name="newsfeed_sort_desc">Définit l’ordre dans lequel les messages sont affichés</string>
|
||||||
<string name="aggressive_recents">Récents agressifs</string>
|
<string name="aggressive_recents">Récents agressifs</string>
|
||||||
<string name="aggressive_recents_desc">Filtrer les vieilles publications additionnelles du fil d\'actualité les plus récentes de Facebook. Désactivez cette option si votre fil d\'actualités est vide.</string>
|
<string name="aggressive_recents_desc">Éliminer les anciennes publications additionnelles du fil d\'actualité récentes de Facebook. Désactivez cette option si votre fil d\'actualités est vide.</string>
|
||||||
<string name="composer">Compositeur de statut</string>
|
<string name="composer">Compositeur de statut</string>
|
||||||
<string name="composer_desc">Montrer le compositeur de statut dans le fil d\'actualité</string>
|
<string name="composer_desc">Montrer le compositeur de statut dans le fil d\'actualité</string>
|
||||||
<string name="suggested_friends">Amis suggérés</string>
|
<string name="suggested_friends">Amis suggérés</string>
|
||||||
<string name="suggested_friends_desc">Afficher les «Personnes que vous pouvez connaître» dans le fil d\'actualité</string>
|
<string name="suggested_friends_desc">Afficher les «Personnes que vous pouvez connaître» dans le fil d\'actualité</string>
|
||||||
<string name="suggested_groups">Groupes Suggérés</string>
|
<string name="suggested_groups">Groupes Suggérés</string>
|
||||||
<string name="suggested_groups_desc">Afficher les «Groupes Suggérés» dans le fil d\'actualité</string>
|
<string name="suggested_groups_desc">Afficher les «Groupes Suggérés» dans le fil d\'actualité</string>
|
||||||
|
<string name="show_stories">Montrer les Top Stories</string>
|
||||||
|
<string name="show_stories_desc">Montrer les stories dans le fil d\'actualité</string>
|
||||||
<string name="facebook_ads">Publicités Facebook</string>
|
<string name="facebook_ads">Publicités Facebook</string>
|
||||||
<string name="facebook_ads_desc">Afficher les publicités Facebook</string>
|
<string name="facebook_ads_desc">Afficher les publicités Facebook</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -48,4 +48,12 @@
|
|||||||
<string name="options">Opcións</string>
|
<string name="options">Opcións</string>
|
||||||
<string name="tab_customizer_instructions">Toque longo e arrastra para reorganizar as iconas superiores.</string>
|
<string name="tab_customizer_instructions">Toque longo e arrastra para reorganizar as iconas superiores.</string>
|
||||||
<string name="no_new_notifications">Non se atopou ningunha nova notificación</string>
|
<string name="no_new_notifications">Non se atopou ningunha nova notificación</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
<string name="birthdays">Születésnapok</string>
|
<string name="birthdays">Születésnapok</string>
|
||||||
<string name="chat">Chat</string>
|
<string name="chat">Chat</string>
|
||||||
<string name="photos">Fényképek</string>
|
<string name="photos">Fényképek</string>
|
||||||
|
<string name="marketplace">Piactér</string>
|
||||||
<string name="notes">Jegyzetek</string>
|
<string name="notes">Jegyzetek</string>
|
||||||
<string name="on_this_day">Ezen a napon</string>
|
<string name="on_this_day">Ezen a napon</string>
|
||||||
<string name="loading_account">Előkészítés…</string>
|
<string name="loading_account">Előkészítés…</string>
|
||||||
@ -45,4 +46,12 @@
|
|||||||
<string name="options">Beállítások</string>
|
<string name="options">Beállítások</string>
|
||||||
<string name="tab_customizer_instructions">Tartsd nyomva és húzd a felső ikonokat az átrendezéshez.</string>
|
<string name="tab_customizer_instructions">Tartsd nyomva és húzd a felső ikonokat az átrendezéshez.</string>
|
||||||
<string name="no_new_notifications">Nem találhatók új értesítések</string>
|
<string name="no_new_notifications">Nem találhatók új értesítések</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
<string name="suggested_friends_desc">\"Emberek, akiket ismerhetsz\" megjelenítése a hírcsatornában</string>
|
<string name="suggested_friends_desc">\"Emberek, akiket ismerhetsz\" megjelenítése a hírcsatornában</string>
|
||||||
<string name="suggested_groups">Javasolt csoportok</string>
|
<string name="suggested_groups">Javasolt csoportok</string>
|
||||||
<string name="suggested_groups_desc">\"Javasolt csoportok\" megjelenítése a hírcsatornában</string>
|
<string name="suggested_groups_desc">\"Javasolt csoportok\" megjelenítése a hírcsatornában</string>
|
||||||
|
<string name="show_stories">Történetek megjelenítése</string>
|
||||||
|
<string name="show_stories_desc">Történetek megjelenítése a hírfolyamban</string>
|
||||||
<string name="facebook_ads">Facebook hirdetések</string>
|
<string name="facebook_ads">Facebook hirdetések</string>
|
||||||
<string name="facebook_ads_desc">Natív Facebook-hirdetések megjelenítése</string>
|
<string name="facebook_ads_desc">Natív Facebook-hirdetések megjelenítése</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -45,4 +45,12 @@
|
|||||||
<string name="preview">Pratinjau</string>
|
<string name="preview">Pratinjau</string>
|
||||||
<string name="options">Pilihan</string>
|
<string name="options">Pilihan</string>
|
||||||
<string name="tab_customizer_instructions">Tekan lama dan tarik untuk mengatur ulang ikon atas.</string>
|
<string name="tab_customizer_instructions">Tekan lama dan tarik untuk mengatur ulang ikon atas.</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -46,4 +46,12 @@
|
|||||||
<string name="options">Opzioni</string>
|
<string name="options">Opzioni</string>
|
||||||
<string name="tab_customizer_instructions">Per riordinare un\'icona tienila premuta e trascinala.</string>
|
<string name="tab_customizer_instructions">Per riordinare un\'icona tienila premuta e trascinala.</string>
|
||||||
<string name="no_new_notifications">Nessuna nuova notifica trovata</string>
|
<string name="no_new_notifications">Nessuna nuova notifica trovata</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--Generated by crowdin.com-->
|
|
||||||
<resources>
|
|
||||||
<string name="newsfeed_sort">Ordine della Sezione Notizie</string>
|
|
||||||
<string name="newsfeed_sort_desc">Definisce l\'ordine in cui sono mostrati i post</string>
|
|
||||||
<string name="aggressive_recents">Recenti Aggressivi</string>
|
|
||||||
<string name="aggressive_recents_desc">Filtra ulteriori post vecchi dalla sezione originale più recenti di Facebook. Disabilita se il tuo feed è vuoto.</string>
|
|
||||||
<string name="composer">Compositore di Stato</string>
|
|
||||||
<string name="composer_desc">Mostra la casella per comporre uno stato nelle Notizie</string>
|
|
||||||
<string name="suggested_friends">Amici Suggeriti</string>
|
|
||||||
<string name="suggested_friends_desc">Mostra \"Persone Che Potresti Conoscere\" nel feed</string>
|
|
||||||
<string name="suggested_groups">Gruppi Suggeriti</string>
|
|
||||||
<string name="suggested_groups_desc">Mostra \"Gruppi Suggeriti\" nel feed</string>
|
|
||||||
<string name="facebook_ads">Pubblicità Facebook</string>
|
|
||||||
<string name="facebook_ads_desc">Mostra le pubblicità native di Facebook</string>
|
|
||||||
</resources>
|
|
@ -42,4 +42,12 @@
|
|||||||
<string name="file_chooser_not_found">파일 선택기를 찾을 수 없습니다.</string>
|
<string name="file_chooser_not_found">파일 선택기를 찾을 수 없습니다.</string>
|
||||||
<string name="top_bar">상단 바</string>
|
<string name="top_bar">상단 바</string>
|
||||||
<string name="bottom_bar">하단 바</string>
|
<string name="bottom_bar">하단 바</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -46,4 +46,12 @@
|
|||||||
<string name="options">Opties</string>
|
<string name="options">Opties</string>
|
||||||
<string name="tab_customizer_instructions">Klik en houd vast om de iconen in de gewenste volgorde te slepen.</string>
|
<string name="tab_customizer_instructions">Klik en houd vast om de iconen in de gewenste volgorde te slepen.</string>
|
||||||
<string name="no_new_notifications">Geen nieuwe notificaties</string>
|
<string name="no_new_notifications">Geen nieuwe notificaties</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -44,4 +44,12 @@
|
|||||||
<string name="preview">Forhåndsvisning</string>
|
<string name="preview">Forhåndsvisning</string>
|
||||||
<string name="options">Alternativer</string>
|
<string name="options">Alternativer</string>
|
||||||
<string name="tab_customizer_instructions">Langt trykk og dra for å endre topp ikonene.</string>
|
<string name="tab_customizer_instructions">Langt trykk og dra for å endre topp ikonene.</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -45,4 +45,12 @@
|
|||||||
<string name="options">Opcje</string>
|
<string name="options">Opcje</string>
|
||||||
<string name="tab_customizer_instructions">Długie naciśnięcie i przeciągnięcie, aby zmienić kolejność ikon.</string>
|
<string name="tab_customizer_instructions">Długie naciśnięcie i przeciągnięcie, aby zmienić kolejność ikon.</string>
|
||||||
<string name="no_new_notifications">Brak nowych powiadomień</string>
|
<string name="no_new_notifications">Brak nowych powiadomień</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
<string name="birthdays">Aniversários</string>
|
<string name="birthdays">Aniversários</string>
|
||||||
<string name="chat">Amigos online</string>
|
<string name="chat">Amigos online</string>
|
||||||
<string name="photos">Fotos</string>
|
<string name="photos">Fotos</string>
|
||||||
|
<string name="marketplace">Marketplace</string>
|
||||||
<string name="notes">Notas</string>
|
<string name="notes">Notas</string>
|
||||||
<string name="on_this_day">Neste Dia</string>
|
<string name="on_this_day">Neste Dia</string>
|
||||||
<string name="loading_account">Preparando tudo…</string>
|
<string name="loading_account">Preparando tudo…</string>
|
||||||
@ -46,4 +47,15 @@
|
|||||||
<string name="options">Opções</string>
|
<string name="options">Opções</string>
|
||||||
<string name="tab_customizer_instructions">Mantenha pressionado e arraste para reorganizar os ícones superiores.</string>
|
<string name="tab_customizer_instructions">Mantenha pressionado e arraste para reorganizar os ícones superiores.</string>
|
||||||
<string name="no_new_notifications">Nenhuma nova notificação encontrada</string>
|
<string name="no_new_notifications">Nenhuma nova notificação encontrada</string>
|
||||||
|
<string name="today">Hoje</string>
|
||||||
|
<string name="yesterday">Ontem</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
|
<string name="time_template">%1s às %2s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
<string name="force_message_bottom_desc">Ao carregar um tópico de mensagem, aciona uma rolagem para a parte inferior da página em vez de carregar a página como está.</string>
|
<string name="force_message_bottom_desc">Ao carregar um tópico de mensagem, aciona uma rolagem para a parte inferior da página em vez de carregar a página como está.</string>
|
||||||
<string name="enable_pip">Habilitar o PIP</string>
|
<string name="enable_pip">Habilitar o PIP</string>
|
||||||
<string name="enable_pip_desc">Habilita o Picture in Picture (janelas flutuantes de vídeos)</string>
|
<string name="enable_pip_desc">Habilita o Picture in Picture (janelas flutuantes de vídeos)</string>
|
||||||
|
<string name="autoplay_settings">Configurações de reprodução automática</string>
|
||||||
|
<string name="autoplay_settings_desc">Abra as configurações de reprodução automática do Facebook. Observe que ele deve ser desativado para que o PIP funcione.</string>
|
||||||
<string name="exit_confirmation">Confirmação de Saída</string>
|
<string name="exit_confirmation">Confirmação de Saída</string>
|
||||||
<string name="exit_confirmation_desc">Mostrar caixa de diálogo de confirmação antes de sair do aplicativo</string>
|
<string name="exit_confirmation_desc">Mostrar caixa de diálogo de confirmação antes de sair do aplicativo</string>
|
||||||
<string name="analytics">Telemetria</string>
|
<string name="analytics">Telemetria</string>
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
<string name="suggested_friends_desc">Mostra \"Pessoas Que Talvez Você Conheça\" no Feed</string>
|
<string name="suggested_friends_desc">Mostra \"Pessoas Que Talvez Você Conheça\" no Feed</string>
|
||||||
<string name="suggested_groups">Grupos Sugeridos</string>
|
<string name="suggested_groups">Grupos Sugeridos</string>
|
||||||
<string name="suggested_groups_desc">Mostra \"Grupos Sugeridos\" no Feed</string>
|
<string name="suggested_groups_desc">Mostra \"Grupos Sugeridos\" no Feed</string>
|
||||||
|
<string name="show_stories">Mostrar Histórias</string>
|
||||||
|
<string name="show_stories_desc">Mostrar histórias no feed</string>
|
||||||
<string name="facebook_ads">Anúncios do Facebook</string>
|
<string name="facebook_ads">Anúncios do Facebook</string>
|
||||||
<string name="facebook_ads_desc">Mostrar anúncios nativos do Facebook</string>
|
<string name="facebook_ads_desc">Mostrar anúncios nativos do Facebook</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -45,4 +45,12 @@
|
|||||||
<string name="options">Opções</string>
|
<string name="options">Opções</string>
|
||||||
<string name="tab_customizer_instructions">Toque longo e arraste para dispor os ícones superiores.</string>
|
<string name="tab_customizer_instructions">Toque longo e arraste para dispor os ícones superiores.</string>
|
||||||
<string name="no_new_notifications">Sem notificações novas</string>
|
<string name="no_new_notifications">Sem notificações novas</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -46,4 +46,12 @@
|
|||||||
<string name="options">Opțiuni</string>
|
<string name="options">Opțiuni</string>
|
||||||
<string name="tab_customizer_instructions">Apasă lung și trage să rearanjezi.</string>
|
<string name="tab_customizer_instructions">Apasă lung și trage să rearanjezi.</string>
|
||||||
<string name="no_new_notifications">Nu s-au găsit notificări</string>
|
<string name="no_new_notifications">Nu s-au găsit notificări</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
<string name="birthdays">Дни рождения</string>
|
<string name="birthdays">Дни рождения</string>
|
||||||
<string name="chat">Написать</string>
|
<string name="chat">Написать</string>
|
||||||
<string name="photos">Фотографии</string>
|
<string name="photos">Фотографии</string>
|
||||||
|
<string name="marketplace">Marketplace</string>
|
||||||
<string name="notes">Заметки</string>
|
<string name="notes">Заметки</string>
|
||||||
<string name="on_this_day">В этот день</string>
|
<string name="on_this_day">В этот день</string>
|
||||||
<string name="loading_account">Почти готово…</string>
|
<string name="loading_account">Почти готово…</string>
|
||||||
@ -45,4 +46,14 @@
|
|||||||
<string name="options">Опции</string>
|
<string name="options">Опции</string>
|
||||||
<string name="tab_customizer_instructions">Долго нажмите и перетащите чтобы переставить иконки</string>
|
<string name="tab_customizer_instructions">Долго нажмите и перетащите чтобы переставить иконки</string>
|
||||||
<string name="no_new_notifications">Новые уведомления отсутствуют</string>
|
<string name="no_new_notifications">Новые уведомления отсутствуют</string>
|
||||||
|
<string name="today">Сегодня</string>
|
||||||
|
<string name="yesterday">Вчера</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
<string name="suggested_friends_desc">Смотреть «Люди которых вы можете знать» в канале</string>
|
<string name="suggested_friends_desc">Смотреть «Люди которых вы можете знать» в канале</string>
|
||||||
<string name="suggested_groups">Предлагаемые группы</string>
|
<string name="suggested_groups">Предлагаемые группы</string>
|
||||||
<string name="suggested_groups_desc">Смотреть «Предложения групп» в канале</string>
|
<string name="suggested_groups_desc">Смотреть «Предложения групп» в канале</string>
|
||||||
|
<string name="show_stories">Показывать Истории</string>
|
||||||
|
<string name="show_stories_desc">Показывать Истории в ленте</string>
|
||||||
<string name="facebook_ads">- Реклама в Facebook</string>
|
<string name="facebook_ads">- Реклама в Facebook</string>
|
||||||
<string name="facebook_ads_desc">Показать родной Facebook объявления</string>
|
<string name="facebook_ads_desc">Показать родной Facebook объявления</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -45,4 +45,12 @@
|
|||||||
<string name="options">Опције</string>
|
<string name="options">Опције</string>
|
||||||
<string name="tab_customizer_instructions">Задржите и превуците да би прерасподелили горње иконице.</string>
|
<string name="tab_customizer_instructions">Задржите и превуците да би прерасподелили горње иконице.</string>
|
||||||
<string name="no_new_notifications">Нема нових обавештења</string>
|
<string name="no_new_notifications">Нема нових обавештења</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -46,4 +46,12 @@
|
|||||||
<string name="options">Inställningar</string>
|
<string name="options">Inställningar</string>
|
||||||
<string name="tab_customizer_instructions">Tryck och håll kvar för att arrangera om topp-ikonerna.</string>
|
<string name="tab_customizer_instructions">Tryck och håll kvar för att arrangera om topp-ikonerna.</string>
|
||||||
<string name="no_new_notifications">Inga nya notifikationer hittades</string>
|
<string name="no_new_notifications">Inga nya notifikationer hittades</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -44,4 +44,12 @@
|
|||||||
<string name="preview">แสดงตัวอย่าง</string>
|
<string name="preview">แสดงตัวอย่าง</string>
|
||||||
<string name="options">ตัวเลือก</string>
|
<string name="options">ตัวเลือก</string>
|
||||||
<string name="tab_customizer_instructions">กดค้างและลากเพื่อจัดเรียงไอคอนด้านบน</string>
|
<string name="tab_customizer_instructions">กดค้างและลากเพื่อจัดเรียงไอคอนด้านบน</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -45,4 +45,12 @@
|
|||||||
<string name="preview">Pribyu</string>
|
<string name="preview">Pribyu</string>
|
||||||
<string name="options">Ang mga opsyon</string>
|
<string name="options">Ang mga opsyon</string>
|
||||||
<string name="tab_customizer_instructions">Pindutin ng matagal at hilahin para mabago ang ayos ng pangunahing imahe.</string>
|
<string name="tab_customizer_instructions">Pindutin ng matagal at hilahin para mabago ang ayos ng pangunahing imahe.</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
<string name="birthdays">Doğum Günleri</string>
|
<string name="birthdays">Doğum Günleri</string>
|
||||||
<string name="chat">Sohbet</string>
|
<string name="chat">Sohbet</string>
|
||||||
<string name="photos">Fotoğraflar</string>
|
<string name="photos">Fotoğraflar</string>
|
||||||
|
<string name="marketplace">Pazar yeri</string>
|
||||||
<string name="notes">Notlar</string>
|
<string name="notes">Notlar</string>
|
||||||
<string name="on_this_day">Bu günde</string>
|
<string name="on_this_day">Bu günde</string>
|
||||||
<string name="loading_account">Her şey hazır alınıyor…</string>
|
<string name="loading_account">Her şey hazır alınıyor…</string>
|
||||||
@ -45,4 +46,14 @@
|
|||||||
<string name="options">Seçenekler</string>
|
<string name="options">Seçenekler</string>
|
||||||
<string name="tab_customizer_instructions">Üstteki simgeleri yeniden düzenlemek için uzun basın ve sonra sürükleyin.</string>
|
<string name="tab_customizer_instructions">Üstteki simgeleri yeniden düzenlemek için uzun basın ve sonra sürükleyin.</string>
|
||||||
<string name="no_new_notifications">Yeni bildirim bulunmadı</string>
|
<string name="no_new_notifications">Yeni bildirim bulunmadı</string>
|
||||||
|
<string name="today">Bugün</string>
|
||||||
|
<string name="yesterday">Dün</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
<string name="force_message_bottom_desc">Birileti dizisi yüklerken, sayfayı olduğu gibi yüklemek yerine, sayfanın altına kaydırma yapın.</string>
|
<string name="force_message_bottom_desc">Birileti dizisi yüklerken, sayfayı olduğu gibi yüklemek yerine, sayfanın altına kaydırma yapın.</string>
|
||||||
<string name="enable_pip">PIP\'i etkinleştir</string>
|
<string name="enable_pip">PIP\'i etkinleştir</string>
|
||||||
<string name="enable_pip_desc">PIP (Picture in Picture) videolarını etkinleştir</string>
|
<string name="enable_pip_desc">PIP (Picture in Picture) videolarını etkinleştir</string>
|
||||||
|
<string name="autoplay_settings">Otomatik oynatma ayarları</string>
|
||||||
<string name="exit_confirmation">Çıkış Onayı</string>
|
<string name="exit_confirmation">Çıkış Onayı</string>
|
||||||
<string name="exit_confirmation_desc">Uygulamadan çıkmadanönce onay iletişim kutusunu göster</string>
|
<string name="exit_confirmation_desc">Uygulamadan çıkmadanönce onay iletişim kutusunu göster</string>
|
||||||
<string name="analytics">Analiz</string>
|
<string name="analytics">Analiz</string>
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
<string name="suggested_friends_desc">Özet akışında \"Tanıdığınız İnsanları\" gösterin</string>
|
<string name="suggested_friends_desc">Özet akışında \"Tanıdığınız İnsanları\" gösterin</string>
|
||||||
<string name="suggested_groups">Önerilen gruplar</string>
|
<string name="suggested_groups">Önerilen gruplar</string>
|
||||||
<string name="suggested_groups_desc">Özet akışında \"önerilen grup\" ları göster</string>
|
<string name="suggested_groups_desc">Özet akışında \"önerilen grup\" ları göster</string>
|
||||||
|
<string name="show_stories">Hikayeleri göster</string>
|
||||||
|
<string name="show_stories_desc">Akışda ki hikayeleri göster</string>
|
||||||
<string name="facebook_ads">Facebook reklamları</string>
|
<string name="facebook_ads">Facebook reklamları</string>
|
||||||
<string name="facebook_ads_desc">Yerli Facebook reklamlarını göster</string>
|
<string name="facebook_ads_desc">Yerli Facebook reklamlarını göster</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
<string name="birthdays">Дні народження</string>
|
<string name="birthdays">Дні народження</string>
|
||||||
<string name="chat">Чат</string>
|
<string name="chat">Чат</string>
|
||||||
<string name="photos">Фотографії</string>
|
<string name="photos">Фотографії</string>
|
||||||
|
<string name="marketplace">Магазин</string>
|
||||||
<string name="notes">Замітки</string>
|
<string name="notes">Замітки</string>
|
||||||
<string name="on_this_day">Цього дня</string>
|
<string name="on_this_day">Цього дня</string>
|
||||||
<string name="loading_account">Отримання всього готове…</string>
|
<string name="loading_account">Отримання всього готове…</string>
|
||||||
@ -45,4 +46,12 @@
|
|||||||
<string name="options">Опції</string>
|
<string name="options">Опції</string>
|
||||||
<string name="tab_customizer_instructions">Довге натискання та перетягніть, щоб переставити верхній значок.</string>
|
<string name="tab_customizer_instructions">Довге натискання та перетягніть, щоб переставити верхній значок.</string>
|
||||||
<string name="no_new_notifications">Нових повідомлень не знайдено</string>
|
<string name="no_new_notifications">Нових повідомлень не знайдено</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
<string name="suggested_friends_desc">Показати \"Люди, яких ви можете знати\" у новинній стрічці</string>
|
<string name="suggested_friends_desc">Показати \"Люди, яких ви можете знати\" у новинній стрічці</string>
|
||||||
<string name="suggested_groups">Пропоновані групи</string>
|
<string name="suggested_groups">Пропоновані групи</string>
|
||||||
<string name="suggested_groups_desc">Показати \"Пропоновані групи\" у новинній стрічці</string>
|
<string name="suggested_groups_desc">Показати \"Пропоновані групи\" у новинній стрічці</string>
|
||||||
|
<string name="show_stories">Показати Історії</string>
|
||||||
|
<string name="show_stories_desc">Показувати історії у стрічці</string>
|
||||||
<string name="facebook_ads">Реклама у Facebook</string>
|
<string name="facebook_ads">Реклама у Facebook</string>
|
||||||
<string name="facebook_ads_desc">Показувати вбудовану рекламу Facebook</string>
|
<string name="facebook_ads_desc">Показувати вбудовану рекламу Facebook</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -46,4 +46,12 @@
|
|||||||
<string name="options">Tuỳ chọn</string>
|
<string name="options">Tuỳ chọn</string>
|
||||||
<string name="tab_customizer_instructions">Bấm giữ và kéo để sắp xếp biểu tượng trên cùng.</string>
|
<string name="tab_customizer_instructions">Bấm giữ và kéo để sắp xếp biểu tượng trên cùng.</string>
|
||||||
<string name="no_new_notifications">Không có thông báo mới</string>
|
<string name="no_new_notifications">Không có thông báo mới</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -40,4 +40,12 @@
|
|||||||
<string name="file_chooser_not_found">未找到文件选择程序</string>
|
<string name="file_chooser_not_found">未找到文件选择程序</string>
|
||||||
<string name="top_bar">顶栏</string>
|
<string name="top_bar">顶栏</string>
|
||||||
<string name="bottom_bar">底栏</string>
|
<string name="bottom_bar">底栏</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -45,4 +45,12 @@
|
|||||||
<string name="options">選項</string>
|
<string name="options">選項</string>
|
||||||
<string name="tab_customizer_instructions">長按及拖曳頂部圖標可重新排列位置</string>
|
<string name="tab_customizer_instructions">長按及拖曳頂部圖標可重新排列位置</string>
|
||||||
<string name="no_new_notifications">沒有新通知。</string>
|
<string name="no_new_notifications">沒有新通知。</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -9,4 +9,6 @@
|
|||||||
<dimen name="tab_bar_height">50dp</dimen>
|
<dimen name="tab_bar_height">50dp</dimen>
|
||||||
<dimen name="intro_bar_height">64dp</dimen>
|
<dimen name="intro_bar_height">64dp</dimen>
|
||||||
<dimen name="badge_icon_size">20dp</dimen>
|
<dimen name="badge_icon_size">20dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="toolbar_icon_size">24dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -64,4 +64,15 @@
|
|||||||
|
|
||||||
<!--Biometrics-->
|
<!--Biometrics-->
|
||||||
<string name="biometrics_prompt_title">Authenticate Frost</string>
|
<string name="biometrics_prompt_title">Authenticate Frost</string>
|
||||||
|
<string name="today">Today</string>
|
||||||
|
<string name="yesterday">Yesterday</string>
|
||||||
|
<!--
|
||||||
|
Template used to display human readable string;
|
||||||
|
For instance:
|
||||||
|
Today at 1:23 PM
|
||||||
|
Mar 13 at 9:00 AM
|
||||||
|
|
||||||
|
The first element is the day, and the second element is the time
|
||||||
|
-->
|
||||||
|
<string name="time_template">%1s at %2s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -6,12 +6,22 @@
|
|||||||
<item text="" />
|
<item text="" />
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<version title="v2.3.0" />
|
||||||
|
<item text="Converted internals of Facebook data storage; auto migration will only work from 2.2.x to 2.3.x" />
|
||||||
|
<item text="Added notification widget" />
|
||||||
|
<item text="" />
|
||||||
|
<item text="" />
|
||||||
|
<item text="" />
|
||||||
|
<item text="" />
|
||||||
|
<item text="" />
|
||||||
|
<item text="" />
|
||||||
|
<item text="" />
|
||||||
|
|
||||||
<version title="v2.2.4" />
|
<version title="v2.2.4" />
|
||||||
<item text="Show top bar to allow sharing posts" />
|
<item text="Show top bar to allow sharing posts" />
|
||||||
<item text="Fix unmuting videos when autoplay is enabled" />
|
<item text="Fix unmuting videos when autoplay is enabled" />
|
||||||
<item text="Add shortcut to toggle autoplay in settings > behaviour" />
|
<item text="Add shortcut to toggle autoplay in settings > behaviour" />
|
||||||
<item text="Update theme" />
|
<item text="Update theme" />
|
||||||
<item text="" />
|
|
||||||
|
|
||||||
<version title="v2.2.3" />
|
<version title="v2.2.3" />
|
||||||
<item text="Add ability to hide stories" />
|
<item text="Add ability to hide stories" />
|
||||||
|
10
app/src/main/res/xml/notification_widget_info.xml
Normal file
10
app/src/main/res/xml/notification_widget_info.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
For sizing see:
|
||||||
|
https://developer.android.com/guide/practices/ui_guidelines/widget_design.html#anatomy_determining_size
|
||||||
|
-->
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:initialKeyguardLayout="@layout/widget_notifications"
|
||||||
|
android:initialLayout="@layout/widget_notifications"
|
||||||
|
android:minWidth="180dp"
|
||||||
|
android:minHeight="250dp"
|
||||||
|
android:previewImage="@drawable/notification_widget_preview" />
|
@ -0,0 +1,195 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "fe8f5b6c27f48d7e0733ee6819f06f40",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "cookies",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cookie_id` INTEGER NOT NULL, `name` TEXT, `cookie` TEXT, PRIMARY KEY(`cookie_id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "cookie_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "cookie",
|
||||||
|
"columnName": "cookie",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"cookie_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "notifications",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notif_id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `href` TEXT NOT NULL, `title` TEXT, `text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `profileUrl` TEXT, `type` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`notif_id`, `userId`), FOREIGN KEY(`userId`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "notif_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "userId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "href",
|
||||||
|
"columnName": "href",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "text",
|
||||||
|
"columnName": "text",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "profileUrl",
|
||||||
|
"columnName": "profileUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"notif_id",
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_notifications_notif_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"notif_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_notifications_notif_id` ON `${TABLE_NAME}` (`notif_id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_notifications_userId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_notifications_userId` ON `${TABLE_NAME}` (`userId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "cookies",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"cookie_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "frost_cache",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, `contents` TEXT NOT NULL, PRIMARY KEY(`id`, `type`), FOREIGN KEY(`id`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "lastUpdated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contents",
|
||||||
|
"columnName": "contents",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "cookies",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"cookie_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"fe8f5b6c27f48d7e0733ee6819f06f40\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "ee4d2fe4052ad3a1892be17681816c2c",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "frost_generic",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `contents` TEXT NOT NULL, PRIMARY KEY(`type`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contents",
|
||||||
|
"columnName": "contents",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"ee4d2fe4052ad3a1892be17681816c2c\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user