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
+52
View File
@@ -0,0 +1,52 @@
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.textdrawable)
api(project(":search"))
implementation(project(":preferences"))
implementation(project(":ktx"))
implementation(project(":base"))
implementation(project(":hiddenitems"))
implementation(project(":permissions"))
}
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.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.calendar">
<uses-permission android:name="android.permission.READ_CALENDAR" />
</manifest>
@@ -0,0 +1,81 @@
package de.mm20.launcher2.calendar
import android.content.Context
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.data.CalendarEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class CalendarRepository private constructor(val context: Context) : BaseSearchableRepository() {
val calendarEvents = MediatorLiveData<List<CalendarEvent>?>()
val upcomingCalendarEvents = MutableLiveData<List<CalendarEvent>>(emptyList())
private val allEvents = MutableLiveData<List<CalendarEvent>?>(emptyList())
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys
init {
calendarEvents.addSource(hiddenItemKeys) { keys ->
calendarEvents.value = allEvents.value?.filter { !keys.contains(it.key) }
}
calendarEvents.addSource(allEvents) { e ->
calendarEvents.value = e?.filter { hiddenItemKeys.value?.contains(it.key) != true }
}
hiddenItemKeys.observeForever {
requestCalendarUpdate()
}
}
fun requestCalendarUpdate() {
launch {
val unselectedCalendars = LauncherPreferences.instance.unselectedCalendars
val hideAlldayEvents = LauncherPreferences.instance.calendarHideAllday
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 = hideAlldayEvents,
unselectedCalendars = unselectedCalendars,
hiddenEvents = hiddenItemKeys.value?.mapNotNull {
if (it.startsWith("calendar")) it.substringAfterLast("/").toLong()
else null
} ?: emptyList()
)
}
upcomingCalendarEvents.value = events
}
}
override suspend fun search(query: String) {
if (query.isBlank()) {
allEvents.value = null
return
}
val startTime = System.currentTimeMillis()
val endTime = System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000
val events = withContext(Dispatchers.IO) {
CalendarEvent.search(context, query, startTime, endTime)
}
allEvents.value = events
}
companion object {
private lateinit var instance: CalendarRepository
fun getInstance(context: Context): CalendarRepository {
if (!::instance.isInitialized) instance = CalendarRepository(context.applicationContext)
return instance
}
}
}
@@ -0,0 +1,11 @@
package de.mm20.launcher2.calendar
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import de.mm20.launcher2.search.data.CalendarEvent
class CalendarViewModel(app:Application): AndroidViewModel(app) {
val calendarEvents: LiveData<List<CalendarEvent>?> = CalendarRepository.getInstance(app).calendarEvents
val upcomingCalendarEvents: LiveData<List<CalendarEvent>> = CalendarRepository.getInstance(app).upcomingCalendarEvents
}
@@ -0,0 +1,321 @@
package de.mm20.launcher2.search.data
import android.Manifest
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.LayerDrawable
import android.provider.CalendarContract
import androidx.core.content.ContextCompat
import androidx.core.database.getStringOrNull
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.blue
import androidx.core.graphics.green
import androidx.core.graphics.red
import com.amulyakhare.textdrawable.TextDrawable
import de.mm20.launcher2.calendar.R
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.preferences.LauncherPreferences
import org.json.JSONObject
import java.lang.NullPointerException
import java.text.SimpleDateFormat
import java.util.*
class CalendarEvent(
override val label: String,
val id: Long,
val color: Int,
val startTime: Long,
val endTime: Long,
val allDay: Boolean,
val location: String,
val attendees: List<String>,
val description: String,
val calendar: Long
) : Searchable() {
override fun serialize(): String {
val json = JSONObject()
json.put("id", id)
return json.toString()
}
override val key: String
get() = "calendar://$id"
override fun getPlaceholderIcon(context: Context): LauncherIcon {
val df = SimpleDateFormat("d")
val day = df.format(startTime)
df.applyPattern("MMM")
val month = df.format(startTime)
val fgLayers = arrayOf(
TextDrawable
.builder()
.beginConfig()
.textColor(Color.WHITE)
.useFont(Typeface.DEFAULT_BOLD)
.fontSize((36 * context.dp).toInt())
.endConfig()
.buildRect(day, 0),
TextDrawable
.builder()
.beginConfig()
.textColor(Color.WHITE)
.bold()
.fontSize((26 * context.dp).toInt())
.endConfig()
.buildRect(month, 0)
)
val foreground = LayerDrawable(fgLayers)
foreground.setLayerInset(0, 0, 0, 0, (26 * context.dp).toInt())
foreground.setLayerInset(1, 0, (36 * context.dp).toInt(), 0, 0)
val background = ColorDrawable(getDisplayColor(context, color))
return LauncherIcon(
foreground = foreground,
background = background,
foregroundScale = 0.74f
)
}
override fun getLaunchIntent(context: Context): Intent? {
return null
}
companion object {
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 results = mutableListOf<CalendarEvent>()
if (!query.isEmpty() && query.length < 3) return results
if (!LauncherPreferences.instance.searchCalendars) return listOf()
if (!PermissionsManager.checkPermission(context, PermissionsManager.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 deserialize(context: Context, serialized: String): CalendarEvent? {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) return null
val json = JSONObject(serialized)
val id = json.getLong("id")
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
ContentUris.appendId(builder, System.currentTimeMillis())
ContentUris.appendId(builder, System.currentTimeMillis() + 63072000000L)
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 = CalendarContract.Instances.EVENT_ID + " = ?"
val selArgs = arrayOf(id.toString())
val cursor = context.contentResolver.query(uri, projection, selection, selArgs, null)
?: return null
if (cursor.moveToNext()) {
val title = cursor.getString(1)
val begin = cursor.getLong(2)
val end = cursor.getLong(3)
val allday = cursor.getInt(4) != 0
val color = cursor.getInt(5)
val location = cursor.getString(6)
val calendar = cursor.getLong(7)
val description = cursor.getStringOrNull(8)
?: ""
cursor.close()
val proj = arrayOf(
CalendarContract.Attendees.EVENT_ID,
CalendarContract.Attendees.ATTENDEE_NAME,
CalendarContract.Attendees.ATTENDEE_EMAIL
)
val sel = "${CalendarContract.Attendees.EVENT_ID} = $id"
val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC"
val cur = context.contentResolver.query(
CalendarContract.Attendees.CONTENT_URI,
proj, sel, null, s
) ?: return null
val attendees = mutableListOf<String>()
while (cur.moveToNext()) {
attendees.add(cur.getString(1).takeUnless { it.isNullOrBlank() }
?: cur.getString(2))
}
cur.close()
val tzOffset = if (allday) {
Calendar.getInstance().timeZone.getOffset(begin)
} else {
0
}
return CalendarEvent(
label = title,
id = id,
color = color,
startTime = begin - tzOffset,
endTime = end - tzOffset - if (allday) 1 else 0,
allDay = allday,
location = location ?: "",
attendees = attendees,
description = description,
calendar = calendar
)
}
cursor.close()
return null
}
fun getCalendars(context: Context): List<UserCalendar> {
val calendars = mutableListOf<UserCalendar>()
val uri = CalendarContract.Calendars.CONTENT_URI
val proj = arrayOf(
CalendarContract.Calendars._ID,
CalendarContract.Calendars.NAME,
CalendarContract.Calendars.ACCOUNT_NAME,
CalendarContract.Calendars.CALENDAR_COLOR,
CalendarContract.Calendars.VISIBLE,
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
)
if (!context.checkPermission(Manifest.permission.READ_CALENDAR)) return calendars
val cursor = context.contentResolver.query(uri, proj, null, null, null)
?: return emptyList()
while (cursor.moveToNext()) {
try {
calendars.add(UserCalendar(
id = cursor.getLong(0),
name = cursor.getString(5) ?: cursor.getString(1) ?: "",
owner = cursor.getString(2),
color = cursor.getInt(3)
))
} catch (e: NullPointerException) {
continue
}
}
cursor.close()
calendars.sortBy { it.owner }
return calendars
}
fun getDisplayColor(context: Context, color: Int): Int {
val hsl = FloatArray(3).let {
ColorUtils.RGBToHSL(color.red, color.green, color.blue, it)
it
}
return if (context.resources.getBoolean(R.bool.is_dark_theme)) {
if (ColorUtils.calculateContrast(ContextCompat.getColor(context, R.color.calendar_foreground_color), color) < 2.5 || true) {
if (color.red == color.green && color.red == color.blue) {
val level = 0xFF - ((0xFF - color.red) * 0.7f).toInt()
Color.rgb(level, level, level)
} else {
hsl[2] = hsl[2] + (1 - hsl[2]) * 0.2f
hsl[1] = 1 - (1 - hsl[1]) * 0.9f
ColorUtils.HSLToColor(hsl)
}
} else return color
} else {
if (ColorUtils.calculateContrast(ContextCompat.getColor(context, R.color.calendar_foreground_color), color) < 1.8) {
if (color.red == color.green && color.red == color.blue) {
val level = (color.red * 0.7f).toInt()
Color.rgb(level, level, level)
} else {
hsl[2] = (0.5f - hsl[2]) * 0.8f + hsl[2]
hsl[1] = 1 - (1 - hsl[1]) * 0.8f
ColorUtils.HSLToColor(hsl)
}
} else return color
}
}
}
}
data class UserCalendar(
val id: Long,
val name: String,
val owner: String,
val color: Int
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB