Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@
</intent-filter>
</activity>

<activity
android:name=".ui.SearchableActivity"
android:label="@string/activity_search_title"
android:exported="true"
>
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>

<activity
android:name=".ui.BookmarkActivity"
android:label="@string/activity_bookmark_title"
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/java/de/timbolender/fefereader/db/DataRepository.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package de.timbolender.fefereader.db

import android.content.Context
import androidx.paging.DataSource
import androidx.sqlite.db.SimpleSQLiteQuery

/**
* Central data access class. Automatically triggers updates if required.
Expand Down Expand Up @@ -32,4 +34,19 @@ class DataRepository(application: Context) {
fun markPostAsReadSync(postId: String) = postDao.markAsReadSync(postId)

fun togglePostBookmarkSync(postId: String) = postDao.toggleBookmarkSync(postId)

// Tries to yield the same results as /?q= on fefes blog:
// 1. Search is case sensitive
// 2. Space equals AND: All words must be contained anywhere in the result.
fun findPostsByContent(sentence: String): DataSource.Factory<Int, Post> {
val words = sentence.split(" ").map { "*$it*" }
if(words.size == 1) {
return postDao.findPostsPaged(words.first())
}
var sqlStatement = "SELECT * FROM post WHERE contents GLOB ?"
words.drop(1).forEach { _ -> sqlStatement += " AND contents GLOB ?" }
sqlStatement += " ORDER BY date DESC, timestampId DESC"
return postDao.findPostsPaged(SimpleSQLiteQuery(sqlStatement, words.toTypedArray()))
}

}
19 changes: 15 additions & 4 deletions app/src/main/java/de/timbolender/fefereader/db/PostDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RawQuery
import androidx.sqlite.db.SupportSQLiteQuery

@Dao
interface PostDao {
companion object {
const val POSTS_QUERY: String = "SELECT * FROM post ORDER BY timestampId DESC"
const val UNREAD_POSTS_QUERY: String = "SELECT * FROM post WHERE isRead = 0 ORDER BY timestampId DESC"
const val BOOKMARK_POSTS_QUERY: String = "SELECT * FROM post WHERE isBookmarked = 1 ORDER BY timestampId DESC"

const val POSTS_QUERY: String = "SELECT * FROM post ORDER BY date DESC, timestampId DESC"
const val UNREAD_POSTS_QUERY: String = "SELECT * FROM post WHERE isRead = 0 ORDER BY date DESC, timestampId DESC"
const val BOOKMARK_POSTS_QUERY: String = "SELECT * FROM post WHERE isBookmarked = 1 ORDER BY date DESC, timestampId DESC"
const val SINGLE_POST_QUERY: String = "SELECT * FROM post WHERE id = :postId"
const val SEARCH_POSTS_QUERY: String = "SELECT * FROM post WHERE contents GLOB :query ORDER BY date DESC, timestampId DESC"
}

// Full data
Expand Down Expand Up @@ -59,4 +61,13 @@ interface PostDao {

@Query("UPDATE post SET isBookmarked = (1 - isBookmarked) WHERE id = :postId")
fun toggleBookmarkSync(postId: String)

// Search data

@Query(SEARCH_POSTS_QUERY)
fun findPostsPaged(query: String): DataSource.Factory<Int, Post>

@RawQuery(observedEntities = [Post::class])
fun findPostsPaged(query: SupportSQLiteQuery): DataSource.Factory<Int, Post>

}
32 changes: 24 additions & 8 deletions app/src/main/java/de/timbolender/fefereader/network/Fetcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,40 @@ import java.text.ParseException
/**
* Fetched current front page of Fefe's blog.
*/
class Fetcher(private val client: OkHttpClient, private val parser: Parser) {
class Fetcher(private val client: OkHttpClient) {
companion object {
private const val URL = "https://blog.fefe.de/"
}

/**
* Retrieve blog posts in current thread.
* @return All parsed blog posts.
* @return All raw blog posts.
* @throws IOException Error during retrieval.
* @throws ParseException Error when parsing the posts.
*/
@Throws(IOException::class, ParseException::class)
fun fetch(): List<RawPost> {
val request = Request.Builder()
.url(URL)
.build()
fun fetch(): String {
val request = createRequest(URL)
return get(request)
}

/**
* Retrieve blog posts that contain the search phrase.
* The search is case sensitive, meaning Fnord != fnord.
* @return All raw blog posts that match $query (case sensitive).
* @throws IOException Error during retrieval.
*/
@Throws(IOException::class, ParseException::class)
fun fetch(query: String): String {
val request = createRequest("$URL?q=$query")
return get(request)
}

private fun createRequest(url: String): Request {
return Request.Builder().url(url).build()
}

private fun get(request: Request): String {
val response = client.newCall(request).execute()
return parser.parse(response.body!!.string())
return response.body!!.string()
}
}
32 changes: 17 additions & 15 deletions app/src/main/java/de/timbolender/fefereader/network/Parser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import java.util.*
class Parser {
companion object {
private val TAG: String = Parser::class.simpleName!!

private val DATE_FORMAT = SimpleDateFormat("EEE MMM d yyyy", Locale.US)

private const val MAX_POSTS = 9999
}

/**
Expand All @@ -26,17 +27,17 @@ class Parser {
fun parse(content: String): List<RawPost> {
val document = Jsoup.parse(content)

var currentDate: Date? = null
var dateOfCurrentSection: Date? = null
val postList = ArrayList<RawPost>()

val baseTime = System.currentTimeMillis() * 1000
var indexCounter: Long = 999
var postIndexCounter = MAX_POSTS

for (element in document.select("body > h3, body > ul")) {
if (element.tagName() == "h3") {
currentDate = DATE_FORMAT.parse(element.text())
dateOfCurrentSection = DATE_FORMAT.parse(element.text())
} else {
if (currentDate == null) {
if (dateOfCurrentSection == null) {
throw ParseException("No date available for first post!", -1)
}

Expand All @@ -45,20 +46,21 @@ class Parser {
if (entry.parent() != element)
continue

val entryContent = entry.children()
val idElement = entryContent[0]
val id = idElement.attr("href").replace("?ts=", "")
val postContent = entry.html().replace(idElement.outerHtml(), "")
val post = RawPost(id, baseTime + indexCounter, postContent, currentDate.time)
Log.v(TAG, "Parsed $post")
postList.add(post)
val idElement = entry.children()[0]
postList.add(RawPost(
id = idElement.attr("href").replace("?ts=", ""),
timestampId = baseTime + postIndexCounter,
contents = entry.html().replace(idElement.outerHtml(), ""),
date = dateOfCurrentSection.time)
)
Log.v(TAG, "Parsed ${postList.last()}")

// Decrement index counter
if (indexCounter == 0L) {
throw IllegalArgumentException("Parser supports only 999 posts per parsing")
if (postIndexCounter == 0) {
throw IllegalArgumentException("Parser supports only $MAX_POSTS posts per parsing")
}

indexCounter--
postIndexCounter--
}
}
}
Expand Down
59 changes: 34 additions & 25 deletions app/src/main/java/de/timbolender/fefereader/network/Updater.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,44 @@ class Updater(private val repository: DataRepository) {
private var TAG: String = Updater::class.simpleName!!
}

private val client = OkHttpClient.Builder()
.addNetworkInterceptor(StethoInterceptor())
.build()
private val parser = Parser()
private val fetcher = Fetcher(client)

@Throws(ParseException::class, IOException::class)
fun update() {
val client = OkHttpClient.Builder()
.addNetworkInterceptor(StethoInterceptor())
.build()

val parser = Parser()
val fetcher = Fetcher(client, parser)
val posts = fetcher.fetch()
val postSoup = fetcher.fetch()
val rawPosts = parser.parse(postSoup)
rawPosts.forEach { createOrUpdate(it) }
}

for (post in posts) {
val existingPost = repository.getPostSync(post.id)
if(existingPost == null) {
Log.d(TAG, "Inserted new post ${post.id}")
val dbPost = Post(post.id, post.timestampId,
isRead = false,
isUpdated = false,
isBookmarked = false,
contents = post.contents,
date = Date(post.date)
)
repository.createOrUpdatePostSync(dbPost)
}
else if(existingPost.contents != post.contents) {
Log.d(TAG, "Updated post ${post.id}")
val dbPost = existingPost.copy(contents = post.contents, isRead = false, isUpdated = true)
repository.createOrUpdatePostSync(dbPost)
}
@Throws(ParseException::class, IOException::class)
fun update(query: String) {
val postSoup = fetcher.fetch(query)
val rawPosts = parser.parse(postSoup)
Log.d(TAG, "Found ${rawPosts.size} posts matching '$query'")
rawPosts.forEach { createOrUpdate(it) }
}

private fun createOrUpdate(post: RawPost) {
val existingPost = repository.getPostSync(post.id)
if(existingPost == null) {
Log.d(TAG, "Inserted new post ${post.id}")
val dbPost = Post(post.id, post.timestampId,
isRead = false,
isUpdated = false,
isBookmarked = false,
contents = post.contents,
date = Date(post.date)
)
repository.createOrUpdatePostSync(dbPost)
}
else if(existingPost.contents != post.contents) {
Log.d(TAG, "Updated post ${post.id}")
val dbPost = existingPost.copy(contents = post.contents, isRead = false, isUpdated = true)
repository.createOrUpdatePostSync(dbPost)
}
}
}
11 changes: 11 additions & 0 deletions app/src/main/java/de/timbolender/fefereader/ui/MainActivity.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package de.timbolender.fefereader.ui;

import android.app.SearchManager;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.SearchView;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
Expand Down Expand Up @@ -58,9 +61,17 @@ boolean isRefreshGestureEnabled() {
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_main, menu);
setUpSearch(menu);
return true;
}

private void setUpSearch(Menu menu) {
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
SearchView searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
ComponentName searchComponent = new ComponentName(this, SearchableActivity.class);
searchView.setSearchableInfo(searchManager.getSearchableInfo(searchComponent));
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ protected void onCreate(Bundle savedInstanceState) {
if(isRefreshGestureEnabled()) {
binding.refreshLayout.setOnRefreshListener(() -> UpdateWorker.Companion.startManualUpdate(this));
binding.refreshLayout.setColorSchemeResources(R.color.colorAccent);
} else {
binding.refreshLayout.setEnabled(false);
}

// Create receiver to consume update notifications
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package de.timbolender.fefereader.ui

import android.app.SearchManager
import android.content.Intent
import android.os.Build
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModelProvider
import androidx.paging.PagedList
import de.timbolender.fefereader.db.DataRepository
import de.timbolender.fefereader.db.Post
import de.timbolender.fefereader.network.Updater
import de.timbolender.fefereader.viewmodel.SearchViewModel
import java.io.IOException
import java.text.ParseException
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executor
import java.util.concurrent.Executors

class SearchableActivity : PostListActivity() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The activity has no back arrow in the bar. I would like to keep it consistent with the DetailsActivity.

public override fun getPostPagedList(): LiveData<PagedList<Post>> {
// Get the intent, verify the action and get the query
if (Intent.ACTION_SEARCH != intent.action) {
throw RuntimeException()
}
val query = intent.getStringExtra(SearchManager.QUERY) ?: throw RuntimeException()

title = String.format("Suchergebnisse: %s", query)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move static text to translatable strings

val repository = DataRepository(this)
val updater = Updater(repository)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
CompletableFuture.runAsync {
try {
updater.update(query)
} catch (e: ParseException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
}
} else {
val executor: Executor = Executors.newSingleThreadExecutor()
executor.execute {
try {
updater.update(query)
} catch (e: ParseException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
val vm = ViewModelProvider(this)[SearchViewModel::class.java]
return vm.getPostsPaged(query)
}

public override fun isUpdateOnStartEnabled(): Boolean {
return true
}

public override fun isRefreshGestureEnabled(): Boolean {
return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package de.timbolender.fefereader.viewmodel

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import androidx.paging.toLiveData
import de.timbolender.fefereader.db.DataRepository
import de.timbolender.fefereader.db.Post

class SearchViewModel(app: Application): AndroidViewModel(app) {
private val repository: DataRepository = DataRepository(app)

fun getPostsPaged(query: String): LiveData<PagedList<Post>> {
return repository.findPostsByContent(query).toLiveData(20)
}

}
Loading