Migrate search settings

This commit is contained in:
MM20 2022-01-10 21:41:13 +01:00
parent d91fc525f8
commit 5834e7e8c4
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
22 changed files with 964 additions and 371 deletions

View File

@ -1,57 +1,73 @@
package de.mm20.launcher2.calculator
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Calculator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.mariuszgromada.math.mxparser.Expression
interface CalculatorRepository {
fun search(query: String): Flow<Calculator?>
}
class CalculatorRepositoryImpl : CalculatorRepository {
class CalculatorRepositoryImpl : CalculatorRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
override fun search(query: String): Flow<Calculator?> = channelFlow {
if (query.isBlank()) {
send(null)
return@channelFlow
}
if (!LauncherPreferences.instance.searchCalculator) return@channelFlow
val calc = when {
val searchCalculator = dataStore.data.map { it.calculatorSearch.enabled }
searchCalculator.collectLatest {
if (it) {
send(queryCalculator(query))
} else {
send(null)
}
}
}
private suspend fun queryCalculator(query: String): Calculator? {
return when {
query.matches(Regex("0x[0-9a-fA-F]+")) -> {
val solution = query.substring(2).toIntOrNull(16) ?: run {
send(null)
return@channelFlow
return null
}
Calculator(term = query, solution = solution.toDouble())
}
query.matches(Regex("0b[01]+")) -> {
val solution = query.substring(2).toIntOrNull(2) ?: run {
send(null)
return@channelFlow
return null
}
Calculator(term = query, solution = solution.toDouble())
}
query.matches(Regex("0[0-7]+")) -> {
val solution = query.substring(1).toIntOrNull(8) ?: run {
send(null)
return@channelFlow
return null
}
Calculator(term = query, solution = solution.toDouble())
}
else -> {
val exp = Expression(query)
if (exp.checkSyntax()) {
Calculator(term = query, solution = exp.calculate())
} else {
val exp2 = Expression(query.replace(',', '.').replace(';', ','))
if (exp2.checkSyntax()) {
Calculator(term = query, solution = exp2.calculate())
} else null
withContext(Dispatchers.IO) {
val exp = Expression(query)
if (exp.checkSyntax()) {
Calculator(term = query, solution = exp.calculate())
} else {
val exp2 = Expression(query.replace(',', '.').replace(';', ','))
if (exp2.checkSyntax()) {
Calculator(term = query, solution = exp2.calculate())
} else null
}
}
}
}
send(calc)
}
}

View File

@ -1,7 +1,13 @@
package de.mm20.launcher2.calendar
import android.content.ContentUris
import android.content.Context
import android.provider.CalendarContract
import androidx.core.database.getStringOrNull
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.CalendarEvent
import kotlinx.coroutines.Dispatchers
@ -9,6 +15,10 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import java.util.*
interface CalendarRepository {
fun search(query: String): Flow<List<CalendarEvent>>
@ -19,34 +29,131 @@ interface CalendarRepository {
class CalendarRepositoryImpl(
private val context: Context,
hiddenItemsRepository: HiddenItemsRepository
) : CalendarRepository {
) : CalendarRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val permissionsManager: PermissionsManager by inject()
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
override fun search(query: String): Flow<List<CalendarEvent>> = channelFlow {
if (query.isBlank()) {
if (query.isBlank() || query.length < 3) {
send(emptyList())
return@channelFlow
}
val events = withContext(Dispatchers.IO) {
val now = System.currentTimeMillis()
CalendarEvent.search(
context,
query,
intervalStart = now,
intervalEnd = now + 14 * 24 * 60 * 60 * 1000L,
)
}
hiddenItems.collectLatest { hiddenItems ->
val calendarResults = withContext(Dispatchers.IO) {
events.filter { !hiddenItems.contains(it.key) }
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
val searchCalendar = dataStore.data.map { it.calendarSearch.enabled }
combine(hasPermission, searchCalendar) { permission, search ->
permission && search
}.map {
if (it) {
val now = System.currentTimeMillis()
queryCalendarEvents(
query,
intervalStart = now,
intervalEnd = now + 14 * 24 * 60 * 60 * 1000L,
)
} else {
emptyList()
}
send(calendarResults)
}.flatMapLatest { events ->
hiddenItems.map { hidden ->
events.filter { !hidden.contains(it.key) }
}
}.collectLatest {
send(it)
}
}
private suspend fun queryCalendarEvents(
query: String,
intervalStart: Long,
intervalEnd: Long,
limit: Int = 10,
excludeAllDayEvents: Boolean = false,
excludeCalendars: List<Long> = emptyList(),
): List<CalendarEvent> {
val results = withContext(Dispatchers.IO) {
val results = mutableListOf<CalendarEvent>()
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
ContentUris.appendId(builder, intervalStart)
ContentUris.appendId(builder, intervalEnd)
val uri = builder.build()
val projection = arrayOf(
CalendarContract.Instances.EVENT_ID,
CalendarContract.Instances.TITLE,
CalendarContract.Instances.BEGIN,
CalendarContract.Instances.END,
CalendarContract.Instances.ALL_DAY,
CalendarContract.Instances.DISPLAY_COLOR,
CalendarContract.Instances.EVENT_LOCATION,
CalendarContract.Instances.CALENDAR_ID,
CalendarContract.Instances.DESCRIPTION
)
val selection = mutableListOf<String>()
if (query.isNotEmpty()) selection.add("${CalendarContract.Instances.TITLE} LIKE ?")
if (excludeCalendars.isNotEmpty()) selection.add("${CalendarContract.Instances.CALENDAR_ID} NOT IN (${excludeCalendars.joinToString()})")
if (excludeAllDayEvents) selection.add("${CalendarContract.Instances.ALL_DAY} = 0")
val selArgs = if (query.isBlank()) null else arrayOf("%$query%")
val sort =
"${CalendarContract.Instances.BEGIN} ASC" + if (limit > -1) " LIMIT $limit" else ""
val cursor = context.contentResolver.query(
uri,
projection,
selection.joinToString(separator = " AND "),
selArgs,
sort
) ?: return@withContext mutableListOf()
val proj = arrayOf(
CalendarContract.Attendees.EVENT_ID,
CalendarContract.Attendees.ATTENDEE_NAME,
CalendarContract.Attendees.ATTENDEE_EMAIL
)
val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC"
while (cursor.moveToNext()) {
val sel = "${CalendarContract.Attendees.EVENT_ID} = ${cursor.getLong(0)}"
val cur = context.contentResolver.query(
CalendarContract.Attendees.CONTENT_URI,
proj, sel, null, s
) ?: return@withContext mutableListOf()
val attendees = mutableListOf<String>()
while (cur.moveToNext()) {
attendees.add(cur.getString(1).takeUnless { it.isNullOrBlank() }
?: cur.getString(2))
}
cur.close()
val allday = cursor.getInt(4) > 0
val begin = cursor.getLong(2)
val tzOffset = if (allday) {
Calendar.getInstance().timeZone.getOffset(begin)
} else {
0
}
val event = CalendarEvent(
label = cursor.getString(1) ?: "",
id = cursor.getLong(0),
color = cursor.getInt(5),
startTime = begin - tzOffset,
endTime = cursor.getLong(3) - tzOffset - if (allday) 1 else 0,
allDay = allday,
location = cursor.getString(6) ?: "",
attendees = attendees,
description = cursor.getStringOrNull(8)
?: "",
calendar = cursor.getLong(7)
)
results.add(event)
}
cursor.close()
return@withContext results
}
return results
}
override fun getUpcomingEvents(): Flow<List<CalendarEvent>> = channelFlow {
val unselectedCalendars = callbackFlow {
val unregister =
@ -70,23 +177,26 @@ class CalendarRepositoryImpl(
}
}
merge(unselectedCalendars, hideAlldayEvents, hiddenItems).collectLatest {
val now = System.currentTimeMillis()
val end = now + 14 * 24 * 60 * 60 * 1000L
val events = withContext(Dispatchers.IO) {
CalendarEvent.search(
context = context,
query = "",
intervalStart = now,
intervalEnd = end,
limit = 700,
hideAllDayEvents = LauncherPreferences.instance.calendarHideAllday,
unselectedCalendars = LauncherPreferences.instance.unselectedCalendars
).filter {
!hiddenItems.value.contains(it.key)
hideAlldayEvents.collectLatest { hideAllday ->
unselectedCalendars.collectLatest { unselected ->
hiddenItems.collectLatest { hidden ->
val now = System.currentTimeMillis()
val end = now + 14 * 24 * 60 * 60 * 1000L
val events = withContext(Dispatchers.IO) {
queryCalendarEvents(
query = "",
intervalStart = now,
intervalEnd = end,
limit = 700,
excludeAllDayEvents = hideAllday,
excludeCalendars = unselected
).filter {
!hiddenItems.value.contains(it.key)
}
}
send(events)
}
}
send(events)
}
}

View File

@ -80,99 +80,6 @@ class CalendarEvent(
}
companion object: KoinComponent {
fun search(
context: Context,
query: String,
intervalStart: Long,
intervalEnd: Long,
limit: Int = 10,
hideAllDayEvents: Boolean = false,
unselectedCalendars: List<Long> = emptyList(),
hiddenEvents: List<Long> = emptyList()
): List<CalendarEvent> {
val permissionsManager: PermissionsManager = get()
val results = mutableListOf<CalendarEvent>()
if (!query.isEmpty() && query.length < 3) return results
if (!LauncherPreferences.instance.searchCalendars) return listOf()
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Calendar)) {
return emptyList()
}
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
ContentUris.appendId(builder, intervalStart)
ContentUris.appendId(builder, intervalEnd)
val uri = builder.build()
val projection = arrayOf(
CalendarContract.Instances.EVENT_ID,
CalendarContract.Instances.TITLE,
CalendarContract.Instances.BEGIN,
CalendarContract.Instances.END,
CalendarContract.Instances.ALL_DAY,
CalendarContract.Instances.DISPLAY_COLOR,
CalendarContract.Instances.EVENT_LOCATION,
CalendarContract.Instances.CALENDAR_ID,
CalendarContract.Instances.DESCRIPTION
)
val selection = mutableListOf<String>()
if (query.isNotEmpty()) selection.add("${CalendarContract.Instances.TITLE} LIKE ?")
if (hiddenEvents.isNotEmpty()) selection.add("${CalendarContract.Instances.EVENT_ID} NOT IN (${hiddenEvents.joinToString()})")
if (unselectedCalendars.isNotEmpty()) selection.add("${CalendarContract.Instances.CALENDAR_ID} NOT IN (${unselectedCalendars.joinToString()})")
if (hideAllDayEvents) selection.add("${CalendarContract.Instances.ALL_DAY} = 0")
val selArgs = if (query.isBlank()) null else arrayOf("%$query%")
val sort =
"${CalendarContract.Instances.BEGIN} ASC" + if (limit > -1) " LIMIT $limit" else ""
val cursor = context.contentResolver.query(
uri,
projection,
selection.joinToString(separator = " AND "),
selArgs,
sort
)
?: return mutableListOf()
val proj = arrayOf(
CalendarContract.Attendees.EVENT_ID,
CalendarContract.Attendees.ATTENDEE_NAME,
CalendarContract.Attendees.ATTENDEE_EMAIL
)
val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC"
while (cursor.moveToNext()) {
val sel = "${CalendarContract.Attendees.EVENT_ID} = ${cursor.getLong(0)}"
val cur = context.contentResolver.query(
CalendarContract.Attendees.CONTENT_URI,
proj, sel, null, s
) ?: return mutableListOf()
val attendees = mutableListOf<String>()
while (cur.moveToNext()) {
attendees.add(cur.getString(1).takeUnless { it.isNullOrBlank() }
?: cur.getString(2))
}
cur.close()
val allday = cursor.getInt(4) > 0
val begin = cursor.getLong(2)
val tzOffset = if (allday) {
Calendar.getInstance().timeZone.getOffset(begin)
} else {
0
}
val event = CalendarEvent(
label = cursor.getString(1) ?: "",
id = cursor.getLong(0),
color = cursor.getInt(5),
startTime = begin - tzOffset,
endTime = cursor.getLong(3) - tzOffset - if (allday) 1 else 0,
allDay = allday,
location = cursor.getString(6) ?: "",
attendees = attendees,
description = cursor.getStringOrNull(8)
?: "",
calendar = cursor.getLong(7)
)
results.add(event)
}
cursor.close()
return results
}
fun getCalendars(context: Context): List<UserCalendar> {
val calendars = mutableListOf<UserCalendar>()

View File

@ -1,14 +1,17 @@
package de.mm20.launcher2.contacts
import android.content.Context
import android.provider.ContactsContract
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Contact
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
interface ContactRepository {
fun search(query: String): Flow<List<Contact>>
@ -17,19 +20,61 @@ interface ContactRepository {
class ContactRepositoryImpl(
private val context: Context,
hiddenItemsRepository: HiddenItemsRepository
) : ContactRepository {
) : ContactRepository, KoinComponent {
private val permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject()
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
override fun search(query: String): Flow<List<Contact>> = channelFlow {
val contacts = withContext(Dispatchers.IO) {
Contact.search(context, query)
val searchContacts = dataStore.data.map { it.contactsSearch.enabled }
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
if (query.length < 3) {
send(emptyList())
return@channelFlow
}
hiddenItems.collectLatest { hiddenItems ->
val contactResults = withContext(Dispatchers.IO) {
contacts.filter { !hiddenItems.contains(it.key) }
combine(searchContacts, hasPermission) { search, permission ->
search && permission
}.map {
if (it) {
queryContacts(query)
} else {
emptyList()
}
send(contactResults)
}.flatMapLatest { contacts ->
hiddenItems.map { hidden ->
contacts.filter { !hidden.contains(it.key) }
}
}.collectLatest {
send(it)
}
}
private suspend fun queryContacts(query: String): List<Contact> {
val results = withContext(Dispatchers.IO) {
val proj = arrayOf(
ContactsContract.RawContacts.CONTACT_ID,
ContactsContract.RawContacts._ID
)
val sel = "${ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY} LIKE ?"
val selArgs = arrayOf("%$query%")
val cursor = context.contentResolver.query(
ContactsContract.RawContacts.CONTENT_URI, proj, sel, selArgs, null
) ?: return@withContext mutableListOf()
//Maps raw contact ids to contact ids
val contactMap = mutableMapOf<Long, MutableSet<Long>>()
while (cursor.moveToNext()) {
contactMap.getOrPut(cursor.getLong(0)) { mutableSetOf() }.add(cursor.getLong(1))
}
cursor.close()
val results = mutableListOf<Contact>()
for ((id, rawIds) in contactMap) {
Contact.contactById(context, id, rawIds)?.let { results.add(it) }
}
results.sortedBy { it }
}
return results
}
}

View File

@ -74,38 +74,7 @@ class Contact(
return null
}
companion object: KoinComponent {
fun search(context: Context, query: String): List<Contact> {
if (query.length < 3) return mutableListOf()
if (!LauncherPreferences.instance.searchContacts) {
return mutableListOf()
}
val permissionsManager: PermissionsManager = get()
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Contacts)) {
return mutableListOf()
}
val proj = arrayOf(
ContactsContract.RawContacts.CONTACT_ID,
ContactsContract.RawContacts._ID
)
val sel = "${ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY} LIKE ?"
val selArgs = arrayOf("%$query%")
val cursor = context.contentResolver.query(
ContactsContract.RawContacts.CONTENT_URI, proj, sel, selArgs, null
) ?: return mutableListOf()
//Maps raw contact ids to contact ids
val contactMap = mutableMapOf<Long, MutableSet<Long>>()
while (cursor.moveToNext()) {
contactMap.getOrPut(cursor.getLong(0)) { mutableSetOf() }.add(cursor.getLong(1))
}
cursor.close()
val results = mutableListOf<Contact>()
for ((id, rawIds) in contactMap) {
contactById(context, id, rawIds)?.let { results.add(it) }
}
return results.sortedBy { it }
}
companion object {
internal fun contactById(context: Context, id: Long, rawIds: Set<Long>): Contact? {
val s = "(" + rawIds.joinToString(separator = " OR ",
transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" +

View File

@ -106,34 +106,23 @@
<string name="file_meta_app_pkgname">Paketname</string>
<string name="file_meta_app_min_sdk">Min-SDK-Version</string>
<string name="preference_screen_services">Dienste</string>
<string name="preference_screen_search">Suche</string>
<string name="preference_screen_search_summary">Suchkategorien</string>
<string name="preference_category_search_favorites">Favoriten</string>
<string name="preference_search_show_favorites">Favoriten anzeigen</string>
<string name="preference_search_show_favorites_summary">Favoritenliste über allen Apps anzeigen</string>
<string name="preference_search_auto_add_favorites">Häufig genutze Elemente</string>
<string name="preference_search_auto_add_favorites_summary">Häufig genutzte Elemente automatisch in den Favoriten anzeigen</string>
<string name="preference_category_search_files">Dateien</string>
<string name="preference_search_files">Dateisuche</string>
<string name="preference_search_files_summary">Ordner, Dokumente, Fotos und andere Dateitypen auf diesem Gerät durchsuchen</string>
<string name="preference_category_search_wikipedia">Wikipedia</string>
<string name="preference_search_wikipedia">Wikipedia durchsuchen</string>
<string name="preference_search_mobile_data">Mobile Daten verwenden</string>
<string name="preference_search_mobile_data_summary">Zusätzliche Kosten können anfallen</string>
<string name="preference_category_search_websites">Webseiten</string>
<string name="preference_search_websites">Nach Webseiten suchen</string>
<string name="preference_search_websites_summary">Probieren Sie „wikipedia.org“</string>
<string name="preference_search_websites_protocol">Standard Web-Protokoll</string>
<string name="preference_search_websites_protocol_summary">Standard-Protokoll, um Webseiten zu suchen, wenn nicht angegeben. Stellen Sie https:// oder http:// vor Ihre Suchanfrage, um das Protokoll explizit festzulegen</string>
<string name="websites_protocol_http">HTTP (unverschlüsselt)</string>
<string name="websites_protocol_https">HTTPS (verschlüsselt)</string>
<string name="preference_category_search_calculator">Taschenrechner</string>
<string name="preference_search_calculator_summary">Probieren Sie „4*2+9“</string>
<string name="preference_search_calculator">Taschenrechner aktivieren</string>
<string name="preference_search_activities_summary">Probieren Sie „%1$s“</string>
<string name="preference_category_search_websearch">Websuche</string>
<string name="preference_search_websearch_summary">Shortcuts zu verschiedenen Websuch-Engines anzeigen</string>
<string name="preference_search_websearch">Websuchen-Shortcuts</string>
<string name="preference_search_edit_websearch">Websuchen bearbeiten</string>
<string name="file_meta_location">Ort</string>
<string name="websearch_google">Google</string>
@ -192,9 +181,7 @@
<string name="menu_contact_postal">Ort</string>
<string name="contact_multiple_postals">%1$d Postadressen</string>
<string name="preference_category_search_calendar">Kalender</string>
<string name="preference_search_calendar">Kalendersuche aktivieren</string>
<string name="preference_category_search_contacts">Kontakte</string>
<string name="preference_search_contacts">Kontakte durchsuchen</string>
<string name="menu_app_info">App-Info</string>
<string name="preference_screen_services_summary">Verbundene Accounts und Dienste verwalten</string>
<string name="preference_category_services_google">Google</string>
@ -277,8 +264,6 @@
<string name="weather_unknown">Unbekannt</string>
<string name="preference_location_disabled_summary">Von diesem Wetterdienst nicht unterstützt</string>
<string name="preference_category_search_unitconverter">Einheitenrechner</string>
<string name="preference_search_unitconverter_summary">Probieren Sie „23 kg“ oder„5 cm &gt;&gt; in“</string>
<string name="preference_search_unitconverter">Einheitenrechner aktivieren</string>
<string name="preference_legacy_icon_bg">Symbol-Hintergrund</string>
<string name="preference_legacy_icon_bg_none">Keiner</string>
<string name="preference_legacy_icon_bg_color">Dynamisch</string>
@ -418,8 +403,10 @@
<string name="preference_grid_column_count">Spaltenanzahl</string>
<string name="grant_permission">Gewähren</string>
<string name="missing_permission_auto_location">Standortzugriff wird benötigt um den Standort automatisch zu ermitteln</string>
<string name="missing_permission_music_widget">Benachrichtigungszugriff wird benötigt um Medienwiedergabe zu steuern</string>
<string name="missing_permission_auto_location">Standortzugriff wird benötigt, um den Standort automatisch zu ermitteln</string>
<string name="missing_permission_music_widget">Benachrichtigungszugriff wird benötigt, um Medienwiedergabe zu steuern</string>
<string name="missing_permission_contact_search">Kontakteberechtigung ist erforderlich, um Kontakte durchsuchen zu können</string>
<string name="missing_permission_calendar_search">Kalenderberechtigung ist erforderlich, um Kalender durchsuchen zu können</string>
<string name="weather_widget_set_location">Standort festlegen</string>
<string name="preference_screen_debug">Debug</string>
@ -445,6 +432,27 @@
<string name="preference_music_filter_sources">Auf Musik-Apps begrenzen</string>
<string name="preference_music_filter_sources_summary">Mediensitzungen von Apps ignorieren, die keine Musik-Apps sind</string>
<string name="preference_screen_search">Suche</string>
<string name="preference_screen_search_summary">Konfigurieren was durchsucht werden soll</string>
<string name="preference_search_favorites">Favoriten</string>
<string name="preference_search_favorites_summary">Angepinnte und häufig genutzte Elemente über dem App-Raster anzeigen</string>
<string name="preference_search_files">Dateien</string>
<string name="preference_search_files_summary">Lokale Dateien und Cloud-Dateien durchsuchen</string>
<string name="preference_search_contacts">Kontakte</string>
<string name="preference_search_contacts_summary">Kontakte auf diesem Gerät durchsuchen</string>
<string name="preference_search_calendar">Kalender</string>
<string name="preference_search_calendar_summary">Anstehende Termine durchsuchen</string>
<string name="preference_search_calculator">Taschenrechner</string>
<string name="preference_search_calculator_summary">Mathematische Terme auswerten</string>
<string name="preference_search_unitconverter">Einheitenrechner</string>
<string name="preference_search_unitconverter_summary">Benutzung: „1,5 kg“ oder „4 cm >> in“</string>
<string name="preference_search_wikipedia">Wikipedia</string>
<string name="preference_search_wikipedia_summary">Die freie Enzyklopädie durchsuchen</string>
<string name="preference_search_websites">Webseiten</string>
<string name="preference_search_websites_summary">Vorschau einer Webseite anzeigen wenn die Suchanfrage eine URL ist</string>
<string name="preference_search_websearch">Websuchen</string>
<string name="preference_search_websearch_summary">Shortcuts zu verschiedenen Websuch-Engines anzeigen</string>
<string name="music_widget_default_title">%1$s spielt Medien</string>
<string name="music_widget_no_data">Bisher wurden keine Medien abgespielt</string>
</resources>

View File

@ -147,8 +147,6 @@
<string name="file_meta_app_pkgname">Package name</string>
<string name="file_meta_app_min_sdk">Min SDK version</string>
<string name="preference_screen_services">Services</string>
<string name="preference_screen_search">Search</string>
<string name="preference_screen_search_summary">Search categories</string>
<string name="preference_theme_auto">Auto (by time of day)</string>
<string name="preference_theme_system">Follow system</string>
<string name="preference_category_search_favorites">Favorites</string>
@ -157,29 +155,18 @@
<string name="preference_search_auto_add_favorites">Frequently used items</string>
<string name="preference_search_auto_add_favorites_summary">Automatically add frequently used items to favorites</string>
<string name="preference_category_search_files">Files</string>
<string name="preference_search_files">File search</string>
<string name="preference_search_files_summary">Search folders, documents, photos and other kinds of file on this device</string>
<string name="preference_category_search_wikipedia">Wikipedia</string>
<string name="preference_search_wikipedia">Search Wikipedia</string>
<string name="preference_search_mobile_data">Allow mobile data usage</string>
<string name="preference_search_mobile_data_summary">Additional fees may apply</string>
<string name="preference_category_search_websites">Websites</string>
<string name="preference_search_websites">Search for websites</string>
<string name="preference_search_websites_summary">Try \'wikipedia.org\'</string>
<string name="preference_search_websites_protocol">Default web protocol</string>
<string name="preference_search_websites_protocol_summary">Default protocol for websites, if not given. You can explicate protocol by adding https:// or http:// in front of your search term</string>
<string name="websites_protocol_http">HTTP (unencrypted)</string>
<string name="websites_protocol_https">HTTPS (encrypted)</string>
<string name="preference_search_calculator">Enable calculator</string>
<string name="preference_search_calculator_summary">Try \'4*2+9\'</string>
<string name="preference_category_search_unitconverter">Unit converter</string>
<string name="preference_search_unitconverter_summary">Try \'23 kg\' or \'5 cm >> in\'</string>
<string name="preference_search_unitconverter">Enable unit converter</string>
<string name="preference_category_search_calculator">Calculator</string>
<string name="preference_search_activities_summary">Try \'%1$s\'</string>
<string name="preference_category_search_websearch">Web search</string>
<string name="preference_search_websearch_summary">Show shortcuts to several web search engines</string>
<string name="preference_search_websearch">Web search shortcuts</string>
<string name="preference_search_edit_websearch">Edit web searches</string>
<string name="file_meta_location">Location</string>
<string name="websearch_google">Google</string>
@ -237,9 +224,7 @@
<string name="menu_contact_postal">Location</string>
<string name="contact_multiple_postals">%1$d postal addresses</string>
<string name="preference_category_search_calendar">Calendar</string>
<string name="preference_search_calendar">Search calendar events</string>
<string name="preference_category_search_contacts">Contacts</string>
<string name="preference_search_contacts">Search contacts</string>
<string name="menu_app_info">App info</string>
<string name="preference_screen_services_summary">Manage connected accounts and services</string>
<string name="preference_category_services_google">Google</string>
@ -458,6 +443,8 @@
<string name="grant_permission">Grant</string>
<string name="missing_permission_auto_location">Location access is required to determine the location automatically</string>
<string name="missing_permission_music_widget">Notification access is required to control media playback</string>
<string name="missing_permission_contact_search">Contact permission is required to search contacts</string>
<string name="missing_permission_calendar_search">Calendar permission is required to search contacts</string>
<string name="weather_widget_set_location">Set location</string>
<string name="preference_screen_debug">Debug</string>
@ -480,6 +467,27 @@
<string name="preference_crash_reporter_summary">Error and crash reports</string>
<string name="preference_export_log">Export log file</string>
<string name="preference_screen_search">Search</string>
<string name="preference_screen_search_summary">Configure what should be searched</string>
<string name="preference_search_favorites">Favorites</string>
<string name="preference_search_favorites_summary">Show pinned and frequently used items above app grid</string>
<string name="preference_search_files">Files</string>
<string name="preference_search_files_summary">Search local files and cloud files</string>
<string name="preference_search_contacts">Contacts</string>
<string name="preference_search_contacts_summary">Search contacts on this device</string>
<string name="preference_search_calendar">Calendar</string>
<string name="preference_search_calendar_summary">Search upcoming calendar events</string>
<string name="preference_search_calculator">Calculator</string>
<string name="preference_search_calculator_summary">Evaluate mathematical terms</string>
<string name="preference_search_unitconverter">Unit converter</string>
<string name="preference_search_unitconverter_summary">Usage: "1.5 kg" or "4 cm >> in"</string>
<string name="preference_search_wikipedia">Wikipedia</string>
<string name="preference_search_wikipedia_summary">Search the free encyclopedia</string>
<string name="preference_search_websites">Websites</string>
<string name="preference_search_websites_summary">Show a preview of a website if the search query is a URL</string>
<string name="preference_search_websearch">Web search</string>
<string name="preference_search_websearch_summary">Show shortcuts to different search engines</string>
<string name="preference_music_filter_sources">Restrict to music apps</string>
<string name="preference_music_filter_sources_summary">Ignore media sessions of apps that are not music apps</string>

View File

@ -27,5 +27,46 @@ fun createFactorySettings(context: Context): Settings {
.setClockStyle(Settings.ClockWidgetSettings.ClockStyle.DigitalClock1)
.build()
)
.setFavorites(Settings.FavoritesSettings
.newBuilder()
.setEnabled(true)
)
.setFileSearch(Settings.FilesSearchSettings
.newBuilder()
.setLocalFiles(true)
.setNextcloud(false)
.setGdrive(false)
.setOnedrive(false)
.setNextcloud(false)
)
.setContactsSearch(Settings.ContactsSearchSettings
.newBuilder()
.setEnabled(true)
)
.setCalendarSearch(Settings.CalendarSearchSettings
.newBuilder()
.setEnabled(true)
)
.setCalculatorSearch(Settings.CalculatorSearchSettings
.newBuilder()
.setEnabled(true)
)
.setUnitConverterSearch(Settings.UnitConverterSearchSettings
.newBuilder()
.setEnabled(true)
)
.setWikipediaSearch(Settings.WikipediaSearchSettings
.newBuilder()
.setEnabled(false)
.setCustomUrl(null)
)
.setWebsiteSearch(Settings.WebsiteSearchSettings
.newBuilder()
.setEnabled(false)
)
.setWebSearch(Settings.WebSearchSettings
.newBuilder()
.setEnabled(true)
)
.build()
}

View File

@ -52,4 +52,55 @@ message Settings {
}
ClockWidgetSettings clock_widget = 7;
message FavoritesSettings {
bool enabled = 1;
}
FavoritesSettings favorites = 8;
message FilesSearchSettings {
bool local_files = 1;
bool gdrive = 2;
bool onedrive = 3;
bool nextcloud = 4;
bool owncloud = 5;
}
FilesSearchSettings file_search = 9;
message ContactsSearchSettings {
bool enabled = 1;
}
ContactsSearchSettings contacts_search = 10;
message CalendarSearchSettings {
bool enabled = 1;
}
CalendarSearchSettings calendar_search = 11;
message CalculatorSearchSettings {
bool enabled = 1;
}
CalculatorSearchSettings calculator_search = 12;
message UnitConverterSearchSettings {
bool enabled = 1;
}
UnitConverterSearchSettings unit_converter_search = 13;
message WikipediaSearchSettings {
bool enabled = 1;
bool images = 2;
string custom_url = 3;
}
WikipediaSearchSettings wikipedia_search = 14;
message WebsiteSearchSettings {
bool enabled = 1;
}
WebsiteSearchSettings website_search = 15;
message WebSearchSettings {
bool enabled = 1;
}
WebSearchSettings web_search = 16;
}

View File

@ -45,4 +45,5 @@ dependencies {
implementation(project(":base"))
implementation(project(":database"))
implementation(project(":preferences"))
}

View File

@ -1,9 +1,12 @@
package de.mm20.launcher2.search
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Websearch
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
interface WebsearchRepository {
fun search(query: String): Flow<List<Websearch>>
@ -16,7 +19,9 @@ interface WebsearchRepository {
class WebsearchRepositoryImpl(
private val database: AppDatabase
) : WebsearchRepository {
) : WebsearchRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val scope = CoroutineScope(Job() + Dispatchers.Main)
@ -25,12 +30,18 @@ class WebsearchRepositoryImpl(
send(emptyList())
return@channelFlow
}
withContext(Dispatchers.IO) {
database.searchDao().getWebSearches().map {
it.map { Websearch(it, query) }
dataStore.data.map { it.webSearch.enabled }.collectLatest {
if (it) {
withContext(Dispatchers.IO) {
database.searchDao().getWebSearches().map {
it.map { Websearch(it, query) }
}
}.collectLatest {
send(it)
}
} else {
send(emptyList())
}
}.collectLatest {
send(it)
}
}

View File

@ -0,0 +1,57 @@
package de.mm20.launcher2.ui.component.preferences
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Switch
import androidx.compose.material.SwitchDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@Composable
fun PreferenceWithSwitch(
title: String,
summary: String? = null,
icon: ImageVector? = null,
enabled: Boolean = true,
onClick: () -> Unit = {},
switchValue: Boolean,
onSwitchChanged: (Boolean) -> Unit
) {
Row(
verticalAlignment = (Alignment.CenterVertically)
) {
Box(
modifier = Modifier.weight(1f)
) {
Preference(
title = title,
summary = summary,
icon = icon,
enabled = enabled,
onClick = onClick
)
}
Box(
modifier = Modifier
.height(36.dp)
.width(1.dp)
.alpha(0.38f)
.background(LocalContentColor.current)
)
Switch(
modifier = Modifier.padding(horizontal = 16.dp),
checked = switchValue,
enabled = enabled,
onCheckedChange = onSwitchChanged,
colors = SwitchDefaults.colors(
uncheckedThumbColor = MaterialTheme.colorScheme.onSurface
)
)
}
}

View File

@ -763,4 +763,47 @@ val Icons.Rounded.Fdroid
lineTo(6.3535156f, 10.242188f)
close()
}
}
val Icons.Rounded.Wikipedia
get() = materialIcon("Icons.Rounded.Wikipedia") {
materialPath {
moveTo(14.97f, 18.95f)
lineTo(12.41f, 12.92f)
curveTo(11.39f, 14.91f, 10.27f, 17f, 9.31f, 18.95f)
curveTo(9.3f, 18.96f, 8.84f, 18.95f, 8.84f, 18.95f)
curveTo(7.37f, 15.5f, 5.85f, 12.1f, 4.37f, 8.68f)
curveTo(4.03f, 7.84f, 2.83f, 6.5f, 2f, 6.5f)
curveTo(2f, 6.4f, 2f, 6.18f, 2f, 6.05f)
horizontalLineTo(7.06f)
verticalLineTo(6.5f)
curveTo(6.46f, 6.5f, 5.44f, 6.9f, 5.7f, 7.55f)
curveTo(6.42f, 9.09f, 8.94f, 15.06f, 9.63f, 16.58f)
curveTo(10.1f, 15.64f, 11.43f, 13.16f, 12f, 12.11f)
curveTo(11.55f, 11.23f, 10.13f, 7.93f, 9.71f, 7.11f)
curveTo(9.39f, 6.57f, 8.58f, 6.5f, 7.96f, 6.5f)
curveTo(7.96f, 6.35f, 7.97f, 6.25f, 7.96f, 6.06f)
lineTo(12.42f, 6.07f)
verticalLineTo(6.47f)
curveTo(11.81f, 6.5f, 11.24f, 6.71f, 11.5f, 7.29f)
curveTo(12.1f, 8.53f, 12.45f, 9.42f, 13f, 10.57f)
curveTo(13.17f, 10.23f, 14.07f, 8.38f, 14.5f, 7.41f)
curveTo(14.76f, 6.76f, 14.37f, 6.5f, 13.29f, 6.5f)
curveTo(13.3f, 6.38f, 13.3f, 6.17f, 13.3f, 6.07f)
curveTo(14.69f, 6.06f, 16.78f, 6.06f, 17.15f, 6.05f)
verticalLineTo(6.47f)
curveTo(16.44f, 6.5f, 15.71f, 6.88f, 15.33f, 7.46f)
lineTo(13.5f, 11.3f)
curveTo(13.68f, 11.81f, 15.46f, 15.76f, 15.65f, 16.2f)
lineTo(19.5f, 7.37f)
curveTo(19.2f, 6.65f, 18.34f, 6.5f, 18f, 6.5f)
curveTo(18f, 6.37f, 18f, 6.2f, 18f, 6.05f)
lineTo(22f, 6.08f)
verticalLineTo(6.1f)
lineTo(22f, 6.5f)
curveTo(21.12f, 6.5f, 20.57f, 7f, 20.25f, 7.75f)
curveTo(19.45f, 9.54f, 17f, 15.24f, 15.4f, 18.95f)
curveTo(15.4f, 18.95f, 14.97f, 18.95f, 14.97f, 18.95f)
close()
}
}

View File

@ -28,6 +28,7 @@ import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
import de.mm20.launcher2.ui.settings.license.LicenseScreen
import de.mm20.launcher2.ui.settings.main.MainSettingsScreen
import de.mm20.launcher2.ui.settings.musicwidget.MusicWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
import de.mm20.launcher2.ui.settings.weatherwidget.WeatherWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.widgets.WidgetsSettingsScreen
@ -85,6 +86,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/appearance") {
AppearanceSettingsScreen()
}
composable("settings/search") {
SearchSettingsScreen()
}
composable("settings/widgets") {
WidgetsSettingsScreen()
}

View File

@ -33,7 +33,10 @@ fun MainSettingsScreen() {
Preference(
icon = Icons.Rounded.Search,
title = stringResource(id = R.string.preference_screen_search),
summary = stringResource(id = R.string.preference_screen_search_summary)
summary = stringResource(id = R.string.preference_screen_search_summary),
onClick = {
navController?.navigate("settings/search")
}
)
Preference(
icon = Icons.Rounded.Widgets,

View File

@ -0,0 +1,146 @@
package de.mm20.launcher2.ui.settings.search
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.preferences.*
import de.mm20.launcher2.ui.icons.Wikipedia
@Composable
fun SearchSettingsScreen() {
val viewModel: SearchSettingsScreenVM = viewModel()
val context = LocalContext.current
PreferenceScreen(title = stringResource(R.string.preference_screen_search)) {
item {
PreferenceCategory {
val favorites by viewModel.favorites.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_search_favorites),
summary = stringResource(R.string.preference_search_favorites_summary),
icon = Icons.Rounded.Star,
value = favorites == true,
onValueChanged = {
viewModel.setFavorites(it)
}
)
Preference(
title = stringResource(R.string.preference_search_files),
summary = stringResource(R.string.preference_search_files_summary),
icon = Icons.Rounded.Description
)
val hasContactsPermission by viewModel.hasContactsPermission.observeAsState()
AnimatedVisibility(hasContactsPermission == false) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_contact_search),
onClick = {
viewModel.requestContactsPermission(context as AppCompatActivity)
},
modifier = Modifier.padding(16.dp)
)
}
val contacts by viewModel.contacts.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_search_contacts),
summary = stringResource(R.string.preference_search_contacts_summary),
icon = Icons.Rounded.Person,
value = contacts == true,
onValueChanged = {
viewModel.setContacts(it)
}
)
val hasCalendarPermission by viewModel.hasCalendarPermission.observeAsState()
AnimatedVisibility(hasCalendarPermission == false) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_calendar_search),
onClick = {
viewModel.requestCalendarPermission(context as AppCompatActivity)
},
modifier = Modifier.padding(16.dp)
)
}
val calendar by viewModel.calendar.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_search_calendar),
summary = stringResource(R.string.preference_search_calendar_summary),
icon = Icons.Rounded.Today,
value = calendar == true,
onValueChanged = {
viewModel.setCalendar(it)
}
)
val calculator by viewModel.calculator.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_search_calculator),
summary = stringResource(R.string.preference_search_calculator_summary),
icon = Icons.Rounded.Calculate,
value = calculator == true,
onValueChanged = {
viewModel.setCalculator(it)
}
)
val unitConverter by viewModel.unitConverter.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_search_unitconverter),
summary = stringResource(R.string.preference_search_unitconverter_summary),
icon = Icons.Rounded.Loop,
value = unitConverter == true,
onValueChanged = {
viewModel.setUnitConverter(it)
}
)
val wikipedia by viewModel.wikipedia.observeAsState()
PreferenceWithSwitch(
title = stringResource(R.string.preference_search_wikipedia),
summary = stringResource(R.string.preference_search_wikipedia_summary),
icon = Icons.Rounded.Wikipedia,
switchValue = wikipedia == true,
onSwitchChanged = {
viewModel.setWikipedia(it)
}
)
val websites by viewModel.websites.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_search_websites),
summary = stringResource(R.string.preference_search_websites_summary),
icon = Icons.Rounded.Language,
value = websites == true,
onValueChanged = {
viewModel.setWebsites(it)
}
)
val webSearch by viewModel.webSearch.observeAsState()
PreferenceWithSwitch(
title = stringResource(R.string.preference_search_websearch),
summary = stringResource(R.string.preference_search_websearch_summary),
icon = Icons.Rounded.TravelExplore,
switchValue = webSearch == true,
onSwitchChanged = {
viewModel.setWebSearch(it)
}
)
}
}
}
}

View File

@ -0,0 +1,139 @@
package de.mm20.launcher2.ui.settings.search
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class SearchSettingsScreenVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val permissionsManager: PermissionsManager by inject()
val favorites = dataStore.data.map { it.favorites.enabled }.asLiveData()
fun setFavorites(favorites: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setFavorites(
it.favorites.toBuilder()
.setEnabled(favorites)
)
.build()
}
}
}
val hasContactsPermission = permissionsManager.hasPermission(PermissionGroup.Contacts).asLiveData()
val contacts = dataStore.data.map { it.contactsSearch.enabled }.asLiveData()
fun setContacts(contacts: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setContactsSearch(
it.contactsSearch.toBuilder()
.setEnabled(contacts)
)
.build()
}
}
}
fun requestContactsPermission(activity: AppCompatActivity) {
permissionsManager.requestPermission(activity, PermissionGroup.Contacts)
}
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar).asLiveData()
val calendar = dataStore.data.map { it.calendarSearch.enabled }.asLiveData()
fun setCalendar(calendar: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setCalendarSearch(
it.calendarSearch.toBuilder()
.setEnabled(calendar)
)
.build()
}
}
}
fun requestCalendarPermission(activity: AppCompatActivity) {
permissionsManager.requestPermission(activity, PermissionGroup.Calendar)
}
val calculator = dataStore.data.map { it.calculatorSearch.enabled }.asLiveData()
fun setCalculator(calculator: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setCalculatorSearch(
it.calculatorSearch.toBuilder()
.setEnabled(calculator)
)
.build()
}
}
}
val unitConverter = dataStore.data.map { it.unitConverterSearch.enabled }.asLiveData()
fun setUnitConverter(unitConverter: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setUnitConverterSearch(
it.unitConverterSearch.toBuilder()
.setEnabled(unitConverter)
)
.build()
}
}
}
val wikipedia = dataStore.data.map { it.wikipediaSearch.enabled }.asLiveData()
fun setWikipedia(wikipedia: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setWikipediaSearch(
it.wikipediaSearch.toBuilder()
.setEnabled(wikipedia)
)
.build()
}
}
}
val websites = dataStore.data.map { it.websiteSearch.enabled }.asLiveData()
fun setWebsites(websites: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setWebsiteSearch(
it.websiteSearch.toBuilder()
.setEnabled(websites)
)
.build()
}
}
}
val webSearch = dataStore.data.map { it.webSearch.enabled }.asLiveData()
fun setWebSearch(webSearch: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setWebSearch(
it.webSearch.toBuilder()
.setEnabled(webSearch)
)
.build()
}
}
}
}

View File

@ -1,51 +1,11 @@
package de.mm20.launcher2.search.data
import android.content.Context
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.unitconverter.Dimension
import de.mm20.launcher2.unitconverter.UnitValue
import de.mm20.launcher2.unitconverter.converters.*
open class UnitConverter(
val dimension: Dimension,
val inputValue: UnitValue,
val values: List<UnitValue>
) {
companion object {
suspend fun search(context: Context, query: String): UnitConverter? {
if (!LauncherPreferences.instance.searchUnitConverter) return null
if (!query.matches(Regex("[0-9,.:]+ [A-Za-z/²³°.]+")) && !query.matches(Regex("[0-9,.:]+ [A-Za-z/²³°.]+ >> [A-Za-z/²³°]+"))) return null
val valueStr: String
val unitStr: String
val targetUnitStr: String?
query.split(" ").also {
valueStr = it.get(0)
unitStr = it.get(1)
targetUnitStr = it.getOrNull(3)
}
val value = valueStr.toDoubleOrNull() ?: valueStr.replace(',', '.').toDoubleOrNull()
?: return null
val converters = listOf(
lazy { MassConverter(context) },
lazy { LengthConverter(context) },
lazy { CurrencyConverter() },
lazy { DataConverter(context) },
lazy { TimeConverter(context) },
lazy { VelocityConverter(context) },
lazy { AreaConverter(context) },
lazy { TemperatureConverter(context) }
)
for (conv in converters) {
val converter = conv.value
if (!converter.isValidUnit(unitStr)) continue
if (targetUnitStr != null && !converter.isValidUnit(targetUnitStr)) continue
return converter.convert(context, unitStr, value, targetUnitStr)
}
return null
}
}
}
val dimension: Dimension,
val inputValue: UnitValue,
val values: List<UnitValue>
)

View File

@ -2,19 +2,69 @@ package de.mm20.launcher2.unitconverter
import android.content.Context
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.unitconverter.converters.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
interface UnitConverterRepository {
fun search(query:String): Flow<UnitConverter?>
}
class UnitConverterRepositoryImpl(val context: Context) : UnitConverterRepository {
class UnitConverterRepositoryImpl(val context: Context) : UnitConverterRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
val unitConverter = MutableLiveData<UnitConverter?>()
override fun search(query: String): Flow<UnitConverter?> = channelFlow {
send(UnitConverter.search(context, query))
if (query.isBlank()) {
send(null)
return@channelFlow
}
dataStore.data.map { it.unitConverterSearch.enabled }.collectLatest {
if (it) {
send(queryUnitConverter(query))
} else {
send(null)
}
}
}
private suspend fun queryUnitConverter(query: String): UnitConverter? {
if (!query.matches(Regex("[0-9,.:]+ [A-Za-z/²³°.]+")) && !query.matches(Regex("[0-9,.:]+ [A-Za-z/²³°.]+ >> [A-Za-z/²³°]+"))) return null
val valueStr: String
val unitStr: String
val targetUnitStr: String?
query.split(" ").also {
valueStr = it.get(0)
unitStr = it.get(1)
targetUnitStr = it.getOrNull(3)
}
val value = valueStr.toDoubleOrNull() ?: valueStr.replace(',', '.').toDoubleOrNull()
?: return null
val converters = listOf(
lazy { MassConverter(context) },
lazy { LengthConverter(context) },
lazy { CurrencyConverter() },
lazy { DataConverter(context) },
lazy { TimeConverter(context) },
lazy { VelocityConverter(context) },
lazy { AreaConverter(context) },
lazy { TemperatureConverter(context) }
)
for (conv in converters) {
val converter = conv.value
if (!converter.isValidUnit(unitStr)) continue
if (targetUnitStr != null && !converter.isValidUnit(targetUnitStr)) continue
return converter.convert(context, unitStr, value, targetUnitStr)
}
return null
}
}

View File

@ -89,78 +89,4 @@ class Website(
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) {
""
}
}
}
}

View File

@ -1,19 +1,36 @@
package de.mm20.launcher2.websites
import android.content.Context
import android.webkit.URLUtil
import androidx.core.graphics.toColorInt
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Website
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.Jsoup
import org.jsoup.UncheckedIOException
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.IOException
import java.net.MalformedURLException
import java.net.URISyntaxException
import java.net.URL
import java.util.concurrent.TimeUnit
interface WebsiteRepository {
fun search(query: String): Flow<Website?>
}
class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository {
class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val httpClient = OkHttpClient
.Builder()
@ -24,18 +41,86 @@ class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository {
override fun search(query: String): Flow<Website?> = channelFlow {
send(null)
httpClient.dispatcher.run {
runningCalls().forEach {
it.cancel()
}
queuedCalls().forEach {
it.cancel()
}
withContext(Dispatchers.IO) {
httpClient.dispatcher.cancelAll()
}
if (query.isBlank()) return@channelFlow
val website = withContext(Dispatchers.IO) {
Website.search(context, query, httpClient)
dataStore.data.map { it.websiteSearch.enabled }.collectLatest {
if(it) {
val website = queryWebsite(query)
send(website)
} else {
send(null)
}
}
}
private suspend fun queryWebsite(query: String): Website? {
val result = withContext(Dispatchers.IO) {
var url = query
val protocol = "https://"
if (!query.startsWith("https://") && !query.startsWith("http://")) url =
"$protocol$query"
if (!URLUtil.isValidUrl(url)) return@withContext null
try {
val request = Request.Builder()
.url(URL(url))
.get()
.tag("onlinesearch")
.build()
val response = httpClient.newCall(request).execute()
url = response.request.url.toString()
val body = response.body?.string() ?: return@withContext 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 = resolveUrl(response.request.url, favicon)
if (!image.isBlank()) image = resolveUrl(response.request.url, image)
return@withContext 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@withContext null
}
return result
}
private fun resolveUrl(url: HttpUrl, link: String): String {
return try {
URL(url.toUrl(), link).toString()
} catch (e: MalformedURLException) {
""
}
send(website)
}
}

View File

@ -2,11 +2,17 @@ package de.mm20.launcher2.wikipedia
import android.content.Context
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Wikipedia
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
@ -17,7 +23,9 @@ interface WikipediaRepository {
class WikipediaRepositoryImpl(
private val context: Context
): WikipediaRepository {
) : WikipediaRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val httpClient by lazy {
OkHttpClient
@ -36,51 +44,56 @@ class WikipediaRepositoryImpl(
.build()
}
val wikipediaService by lazy {
private val wikipediaService by lazy {
retrofit.create(WikipediaApi::class.java)
}
override fun search(query: String): Flow<Wikipedia?> = channelFlow {
send(null)
httpClient.dispatcher.run {
runningCalls().forEach {
it.cancel()
}
queuedCalls().forEach {
it.cancel()
withContext(Dispatchers.IO) {
httpClient.dispatcher.cancelAll()
}
if (query.isBlank()) return@channelFlow
dataStore.data.map { it.wikipediaSearch }.collectLatest {
if (it.enabled) {
send(queryWikipedia(query, it.images))
} else {
send(null)
}
}
}
private suspend fun queryWikipedia(query: String, loadImages: Boolean): Wikipedia? {
if (query.isBlank()) return@channelFlow
val result = try {
wikipediaService.search(query)
} catch (e: Exception) {
CrashReporter.logException(e)
return@channelFlow
return null
}
val page = result.query?.pages?.values?.toList()?.getOrNull(0) ?: return@channelFlow
val page = result.query?.pages?.values?.toList()?.getOrNull(0) ?: return null
val image = if (LauncherPreferences.instance.searchWikipediaPictures) {
val image = if (loadImages) {
val width = context.resources.displayMetrics.widthPixels / 2
val imageResult = try {
wikipediaService.getPageImage(page.pageid, width)
} catch (e: Exception) {
CrashReporter.logException(e)
return@channelFlow
return null
}
imageResult.query?.pages?.values?.toList()?.getOrNull(0)?.thumbnail?.source
} else null
val wiki = Wikipedia(
return Wikipedia(
label = page.title,
id = page.pageid,
text = page.extract,
image = image
)
send(wiki)
}
}