Initial commit

This commit is contained in:
MM20
2021-09-18 23:37:52 +02:00
commit 749e4e3073
938 changed files with 50475 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+57
View File
@@ -0,0 +1,57 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-android-extensions")
}
android {
compileSdk = sdk.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = sdk.versions.minSdk.get().toInt()
targetSdk = sdk.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.browser)
implementation(libs.androidx.palette)
implementation(libs.bundles.androidx.lifecycle)
implementation(libs.okhttp)
implementation(libs.glide)
implementation(libs.textdrawable)
implementation(libs.jsoup)
implementation(project(":preferences"))
implementation(project(":search"))
implementation(project(":base"))
implementation(project(":ktx"))
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.kts.kts.kts.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+5
View File
@@ -0,0 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.mm20.launcher2.websites">
/
</manifest>
@@ -0,0 +1,175 @@
package de.mm20.launcher2.search.data
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.util.Log
import android.webkit.URLUtil
import androidx.core.graphics.drawable.toBitmap
import androidx.core.graphics.toColorInt
import androidx.palette.graphics.Palette
import com.amulyakhare.textdrawable.TextDrawable
import com.bumptech.glide.Glide
import de.mm20.launcher2.helper.NetworkUtils
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.preferences.WebsiteProtocols
import de.mm20.launcher2.websites.R
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import org.jsoup.Jsoup
import org.jsoup.UncheckedIOException
import java.io.IOException
import java.net.MalformedURLException
import java.net.URISyntaxException
import java.net.URL
import java.util.concurrent.ExecutionException
class Website(
override val label: String,
val url: String,
val description: String,
val image: String,
val favicon: String,
val color: Int
) : Searchable() {
override fun serialize(): String {
return jsonObjectOf(
"label" to label,
"url" to url,
"description" to description,
"image" to image,
"favicon" to favicon,
"color" to color
).toString()
}
override val key = "web://$url"
override suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? {
if (favicon.isEmpty()) return null
try {
val icon = Glide.with(context)
.asDrawable()
.load(favicon)
.submit()
.get()
val color = if (color != 0) color else {
Palette
.from(icon.toBitmap())
.generate()
.getLightMutedColor(Color.WHITE)
}
return LauncherIcon(
foreground = icon,
background = ColorDrawable(color),
foregroundScale = 0.7f
)
} catch (e: ExecutionException) {
return null
}
}
override fun getPlaceholderIcon(context: Context): LauncherIcon {
val drawable = if (label.isNotEmpty()) {
TextDrawable.builder().buildRect(label[0].toString(), 0)
} else context.getDrawable(R.drawable.ic_website)!!
return LauncherIcon(
foreground = drawable,
background = ColorDrawable(if (color != 0) color else Color.LTGRAY),
foregroundScale = 1f
)
}
override fun getLaunchIntent(context: Context): Intent? {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
return intent
}
companion object {
fun search(context: Context, query: String, client: OkHttpClient): Website? {
var url = query
val prefs = LauncherPreferences.instance
if (!prefs.searchWebsite ||
NetworkUtils.isOffline(context, prefs.searchWebsitesMobileData)) return null
val protocol = if (LauncherPreferences.instance.searchWebsitesProtocol == WebsiteProtocols.HTTPS) "https://" else "http://"
if (!query.startsWith("https://") && !query.startsWith("http://")) url = "$protocol$query"
if (!URLUtil.isValidUrl(url)) return null
try {
val request = Request.Builder()
.url(URL(url))
.get()
.tag("onlinesearch")
.build()
val response = client.newCall(request).execute()
url = response.request.url.toString()
val body = response.body?.string() ?: return null
val doc = Jsoup.parse(body)
var title = doc.select("meta[property=og:title]").attr("content")
if (title.isBlank()) title = doc.title()
if (title.isBlank()) title = url
var description = doc.select("meta[property=og:description]").attr("content")
if (description.isBlank()) description = doc.select("meta[name=description]").attr("content")
val color = try {
val colorString = doc.select("meta[name=theme-color]").attr("content")
if (colorString.isNotEmpty()) colorString.toColorInt()
else 0
} catch (e: IllegalArgumentException) {
0
}
var image = doc.select("meta[property=og:image]").attr("content")
var favicon = doc.select("link[rel=apple-touch-icon]").attr("href")
if (favicon.isBlank()) favicon = doc.head().select("meta[itemprop=image]").attr("content")
if (favicon.isBlank()) favicon = doc.select("link[rel=icon]").attr("href")
if (favicon.isBlank()) favicon = doc.head().select("link[href~=.*\\.(ico|png)]").attr("href")
if (!favicon.isBlank()) favicon = resolve(response.request.url, favicon)
if (!image.isBlank()) image = resolve(response.request.url, image)
return Website(
label = title,
url = url,
description = description,
image = image,
favicon = favicon,
color = color)
} catch (e: IOException) {
//Ignore. Not a HTML page or no connection. No result for this query
} catch (e: UncheckedIOException) {
} catch (e: URISyntaxException) {
} catch (e: RuntimeException) {
} catch (e: IllegalArgumentException) {
}
return null
}
private fun resolve(url: HttpUrl, link: String): String {
/*if(link.startsWith("http://") || link.startsWith("https://")) return link
if(link.startsWith("//")) return "${urlTemplate.scheme()}:$link"
if(link.startsWith("/")) return "${urlTemplate.scheme()}:$link"*/
return try {
URL(url.toUrl(), link).toString()
} catch (e: MalformedURLException) {
""
}
}
fun deserialize(serialized: String): Website {
val json = JSONObject(serialized)
return Website(
label = json.getString("label"),
favicon = json.getString("favicon"),
image = json.getString("image"),
description = json.getString("description"),
url = json.getString("url"),
color = json.getInt("color")
)
}
}
}
@@ -0,0 +1,52 @@
package de.mm20.launcher2.websites
import android.content.Context
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.data.Website
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
class WebsiteRepository private constructor(val context: Context): BaseSearchableRepository() {
val website = MutableLiveData<Website?>()
private val httpClient = OkHttpClient
.Builder()
.connectTimeout(200, TimeUnit.MILLISECONDS)
.readTimeout(3000, TimeUnit.MILLISECONDS)
.writeTimeout(1000, TimeUnit.MILLISECONDS)
.build()
override fun onCancel() {
super.onCancel()
httpClient.dispatcher.run {
runningCalls().forEach {
it.cancel()
}
queuedCalls().forEach {
it.cancel()
}
}
}
override suspend fun search(query: String) {
website.value = null
if (query.isBlank()) return
val wiki = withContext(Dispatchers.IO) {
Website.search(context, query, httpClient)
}
website.value = wiki
}
companion object {
private lateinit var instance: WebsiteRepository
fun getInstance(context: Context): WebsiteRepository {
if(!::instance.isInitialized) instance = WebsiteRepository(context.applicationContext)
return instance
}
}
}
@@ -0,0 +1,10 @@
package de.mm20.launcher2.websites
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import de.mm20.launcher2.search.data.Website
class WebsiteViewModel(val app: Application): AndroidViewModel(app) {
val website: LiveData<Website?> = WebsiteRepository.getInstance(app).website
}
@@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"/>
</vector>