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

View File

@@ -0,0 +1,84 @@
package de.mm20.launcher2
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Bitmap
import androidx.appcompat.app.AppCompatDelegate
import de.mm20.launcher2.debug.Debug
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.preferences.Themes
import de.mm20.launcher2.ui.legacy.helper.WallpaperBlur
import kotlinx.coroutines.*
import java.text.Collator
import kotlin.coroutines.CoroutineContext
class LauncherApplication : Application(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + SupervisorJob()
var blurredWallpaper: Bitmap? = null
private val appReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
IconRepository.getInstance(this@LauncherApplication).requestIconPackListUpdate()
}
}
override fun onCreate() {
super.onCreate()
Debug()
instance = this
LauncherPreferences.initialize(this)
IconRepository.getInstance(this).requestIconPackListUpdate()
registerReceiver(appReceiver, IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_REPLACED)
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addAction(Intent.ACTION_MY_PACKAGE_REPLACED)
addAction(Intent.ACTION_PACKAGE_CHANGED)
addDataScheme("package")
})
val theme = LauncherPreferences.instance.theme
AppCompatDelegate.setDefaultNightMode(
when (theme) {
Themes.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO // light
Themes.DARK -> AppCompatDelegate.MODE_NIGHT_YES // dark, black
Themes.AUTO -> AppCompatDelegate.MODE_NIGHT_AUTO // auto
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM //system
}
)
WallpaperBlur.requestBlur(this)
@Suppress("DEPRECATION") // We need to access the wallpaper directly to blur it
registerReceiver(WallpaperReceiver(), IntentFilter(Intent.ACTION_WALLPAPER_CHANGED))
}
companion object {
lateinit var instance: LauncherApplication
val collator: Collator by lazy {
Collator.getInstance().apply { strength = Collator.SECONDARY }
}
}
}
object PermissionRequests {
const val CALENDAR = 309
const val LOCATION = 410
const val ALL = 666
}
class WallpaperReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
WallpaperBlur.requestBlur(context)
}
}

View File

@@ -0,0 +1,28 @@
package de.mm20.launcher2.activity;
import android.app.Activity
import android.content.Context
import android.content.pm.LauncherApps
import android.os.Build
import android.os.Bundle
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.search.data.AppShortcut
class AddItemActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val launcherApps = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
val pinRequest = launcherApps.getPinItemRequest(intent) ?: return run { finish() }
val shortcutInfo = pinRequest.shortcutInfo ?: return run { finish() }
val shortcut = AppShortcut(this.applicationContext, shortcutInfo,
packageManager.getApplicationInfo(shortcutInfo.`package`, 0)
.loadLabel(packageManager).toString())
if (pinRequest.accept()) {
FavoritesRepository.getInstance(this).pinItem(shortcut)
}
}
finish()
}
}

View File

@@ -0,0 +1,86 @@
package de.mm20.launcher2.activity
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import de.mm20.launcher2.R
import de.mm20.launcher2.fragment.PreferencesCalendarFragment
import de.mm20.launcher2.fragment.PreferencesMainFragment
import de.mm20.launcher2.fragment.PreferencesServicesFragment
import de.mm20.launcher2.fragment.PreferencesWeatherFragment
import de.mm20.launcher2.ui.legacy.activity.LauncherActivity
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
val fragment = getStartFragment()
setupActionBar()
supportFragmentManager
.beginTransaction()
.add(android.R.id.content, fragment)
.commit()
} else if (!savedInstanceState.getBoolean("theme_change")) {
val fragment = getStartFragment()
setupActionBar()
supportFragmentManager
.beginTransaction()
.replace(android.R.id.content, fragment)
.commit()
}
findViewById<View>(android.R.id.content)?.setBackgroundColor(getColor(R.color.settings_window_background))
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean("theme_change", true)
}
private fun getStartFragment(): Fragment {
return when (intent.extras?.getString(FRAGMENT, "")) {
FRAGMENT_CALENDAR -> PreferencesCalendarFragment()
FRAGMENT_WEATHER -> PreferencesWeatherFragment()
FRAGMENT_SERVICES -> PreferencesServicesFragment()
else -> PreferencesMainFragment()
}
}
private fun setupActionBar() {
actionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == android.R.id.home) {
if (supportFragmentManager.backStackEntryCount == 0) {
finish()
startActivity(Intent(this, LauncherActivity::class.java))
} else {
supportFragmentManager.popBackStack()
}
return true
}
return super.onOptionsItemSelected(item)
}
override fun onBackPressed() {
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack()
} else {
finish()
startActivity(Intent(this, LauncherActivity::class.java))
}
}
companion object {
const val RESULT_NEED_RESTART = 0x09
const val FRAGMENT_WEATHER: String = "weather"
const val FRAGMENT_CALENDAR: String = "calendar"
const val FRAGMENT_SERVICES: String = "services"
const val FRAGMENT: String = "fragment"
}
}

View File

@@ -0,0 +1,5 @@
package de.mm20.launcher2.content
import androidx.core.content.FileProvider
class GenericFileProvider: FileProvider()

View File

@@ -0,0 +1,176 @@
package de.mm20.launcher2.fragment
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import com.afollestad.materialdialogs.MaterialDialog
import de.mm20.launcher2.R
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.helper.DebugInformationDumper
class PreferencesAboutFragment : PreferenceFragmentCompat() {
private var easterEggCounter = 0
@SuppressLint("ResourceType")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_about)
val versionPref = findPreference<Preference>("version")!!
try {
val version = requireContext().packageManager.getPackageInfo(
requireActivity().application.packageName,
0
).versionName
versionPref.summary = version
} catch (e: PackageManager.NameNotFoundException) {
//Should never happen
versionPref.summary = "Ich mag Bockwurst-Bananen"
}
versionPref.setOnPreferenceClickListener {
if (easterEggCounter in arrayOf(3, 4, 7)) Toast.makeText(
context, when (easterEggCounter) {
3 -> R.string.easter_egg_1
4 -> R.string.easter_egg_2
7 -> R.string.easter_egg_3
else -> 0
}, Toast.LENGTH_SHORT
).show()
if (easterEggCounter == 8) {
easterEggCounter = 0
requireFragmentManager().beginTransaction()
.setCustomAnimations(
R.anim.preference_fragment_child_enter,
R.anim.preference_fragment_parent_exit,
R.anim.preference_fragment_parent_enter,
R.anim.preference_fragment_child_exit
)
.replace(
android.R.id.content,
PreferencesEasterEggFragment()
)
.addToBackStack(null)
.commit()
}
easterEggCounter++
false
}
val licenses = findPreference<Preference>("category_licenses") as PreferenceCategory
for (l in LICENSES) {
val license = resources.obtainTypedArray(l)
val preference = Preference(activity, null, 0, R.style.Preference_Material)
preference.title = license.getString(0)
preference.summary = license.getString(1)
preference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
parentFragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.preference_fragment_child_enter,
R.anim.preference_fragment_parent_exit,
R.anim.preference_fragment_parent_enter,
R.anim.preference_fragment_child_exit
)
.replace(android.R.id.content,
PreferencesLicenseFragment().apply { library = l })
.addToBackStack(null)
.commit()
true
}
license.recycle()
licenses.addPreference(preference)
}
findPreference<Preference>("crash_reporter")?.setOnPreferenceClickListener {
startActivity(CrashReporter.getLaunchIntent())
true
}
findPreference<Preference>("export_debug")?.setOnPreferenceClickListener {
Toast.makeText(
activity,
getString(
R.string.debug_export_information_file,
DebugInformationDumper().dump(requireContext())
),
Toast.LENGTH_SHORT
).show()
true
}
findPreference<Preference>("export_databases")?.setOnPreferenceClickListener {
MaterialDialog(requireContext()).show {
message(res = R.string.debug_export_databases_warning)
positiveButton(res = R.string.dialog_continue, click = {
Toast.makeText(
activity,
getString(
R.string.debug_export_information_file,
DebugInformationDumper().exportDatabases(requireContext())
),
Toast.LENGTH_SHORT
).show()
it.dismiss()
})
negativeButton(res = android.R.string.cancel, click = {
it.cancel()
})
}
true
}
findPreference<Preference>("license")?.setOnPreferenceClickListener {
parentFragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.preference_fragment_child_enter,
R.anim.preference_fragment_parent_exit,
R.anim.preference_fragment_parent_enter,
R.anim.preference_fragment_child_exit
)
.replace(android.R.id.content,
PreferencesLicenseFragment().apply { library = R.array.license_mm20launcher2 })
.addToBackStack(null)
.commit()
true
}
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar?.setTitle(R.string.preference_screen_about)
}
companion object {
private val LICENSES = intArrayOf(
R.array.license_accompanist,
R.array.license_android_jetpack,
R.array.license_suncalc,
R.array.license_crashreporter,
R.array.license_draglinearlayout,
R.array.license_glide,
R.array.license_glide_transformations,
R.array.license_google_apiclient,
R.array.license_google_auth,
R.array.license_groupie,
R.array.license_gson,
R.array.license_jsoup,
R.array.license_kotlin_stdlib,
R.array.license_lottie,
R.array.license_mdicons,
R.array.license_material_components,
R.array.license_materialdialogs,
R.array.license_msal,
R.array.license_msgraph,
R.array.license_mxparser,
R.array.license_okhttp,
R.array.license_retrofit,
R.array.license_textdrawable,
R.array.license_viewpropertyobjectanimator
)
}
}

View File

@@ -0,0 +1,182 @@
package de.mm20.launcher2.fragment
import android.Manifest
import android.app.WallpaperManager
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.lifecycle.lifecycleScope
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
import de.mm20.launcher2.R
import de.mm20.launcher2.icons.IconPackManager
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.preferences.Themes
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import kotlinx.coroutines.launch
class PreferencesAppearanceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_appearance)
findPreference<Preference>("theme")?.setOnPreferenceChangeListener { _, newValue ->
val theme = Themes.byValue(newValue as String)
@Suppress("DEPRECATION") // Still using MODE_NIGHT_AUTO
AppCompatDelegate.setDefaultNightMode(when (theme) {
Themes.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
Themes.DARK -> AppCompatDelegate.MODE_NIGHT_YES
Themes.AUTO -> AppCompatDelegate.MODE_NIGHT_AUTO
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
})
requireActivity().recreate()
true
}
if (WallpaperManager.getInstance(activity).wallpaperInfo != null) {
findPreference<Preference>("blur_cards")?.apply {
setSummary(R.string.preference_blur_cards_summary_lwp)
isEnabled = false
}
}
findPreference<Preference>("blur_cards")?.setOnPreferenceChangeListener { _, newValue ->
val newVal = newValue as? Boolean ?: return@setOnPreferenceChangeListener true
if (newVal && requireActivity().checkPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) {
ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 0)
}
true
}
findPreference<Preference>("wallpaper")?.setOnPreferenceClickListener {
requireContext().startActivity(Intent.createChooser(Intent(Intent.ACTION_SET_WALLPAPER), null))
true
}
findPreference<Preference>("cards")?.setOnPreferenceClickListener {
requireFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.preference_fragment_child_enter, R.anim.preference_fragment_parent_exit,
R.anim.preference_fragment_parent_enter, R.anim.preference_fragment_child_exit)
.replace(android.R.id.content, PreferencesCardFragment())
.addToBackStack(null)
.commit()
true
}
val manager = IconPackManager.getInstance(requireContext())
lifecycleScope.launch {
val packs = manager.getInstalledIconPacks()
findPreference<ListPreference>("icon_pack")?.apply {
entries = packs.map { it.name }.toMutableList().apply { add(0, "System") }.toTypedArray()
entryValues = (-1 until packs.size).map { it.toString() }.toTypedArray()
if (packs.isEmpty()) {
isEnabled = false
setSummary(R.string.preference_icon_pack_summary_empty)
} else {
isEnabled = true
summary = "%s"
value = packs.indexOfFirst { it.packageName == manager.selectedIconPack }.toString()
}
setOnPreferenceChangeListener { _, newValue ->
val index = (newValue as String).toInt()
IconRepository.getInstance(requireContext()).clearCache()
if (index == -1) manager.selectIconPack("")
else {
manager.selectIconPack(packs[index].packageName)
}
true
}
}
}
findPreference<Preference>("legacy_icon_bg")?.setOnPreferenceChangeListener { _, _ ->
IconRepository.getInstance(requireContext()).clearCache()
true
}
val shapePreference = findPreference<Preference>("icon_shape")!!
shapePreference.summary = getShapeName()
shapePreference.setOnPreferenceClickListener {
val launcherIcon = LauncherIcon(
foreground = requireContext().getDrawable(R.mipmap.ic_launcher_foreground)!!,
background = requireContext().getDrawable(R.mipmap.ic_launcher_background)
)
val iconShapeList = LinearLayout(requireContext())
iconShapeList.orientation = LinearLayout.VERTICAL
val shapes = arrayOf(
IconShape.PLATFORM_DEFAULT to R.string.preference_icon_shape_platform,
IconShape.CIRCLE to R.string.preference_icon_shape_circle,
IconShape.ROUNDED_SQUARE to R.string.preference_icon_shape_rounded_square,
IconShape.SQUARE to R.string.preference_icon_shape_square,
IconShape.SQUIRCLE to R.string.preference_icon_shape_squircle,
IconShape.HEXAGON to R.string.preference_icon_shape_hexagon,
IconShape.TRIANGLE to R.string.preference_icon_shape_triangle,
IconShape.PENTAGON to R.string.preference_icon_shape_pentagon
)
val layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT)
val dialog = MaterialDialog(requireContext())
shapes.forEachIndexed { i, shape ->
val view = View.inflate(requireContext(), R.layout.preference_icon_shape_row, null)
view.findViewById<LauncherIconView>(R.id.icon).also { iconView ->
iconView.icon = launcherIcon
iconView.shape = shape.first
}
view.findViewById<TextView>(R.id.label).also { labelView ->
labelView.setText(shape.second)
}
view.layoutParams = layoutParams
iconShapeList.addView(view)
view.setOnClickListener {
LauncherPreferences.instance.iconShape = shape.first
shapePreference.summary = getShapeName()
dialog.dismiss()
}
}
dialog.customView(view = iconShapeList, scrollable = true)
.title(R.string.preference_icon_shape)
.negativeButton(android.R.string.cancel) {
dialog.cancel()
}
.show()
true
}
val systemBarsCategory = findPreference<PreferenceCategory>("system_bars")!!
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
systemBarsCategory.removePreference(findPreference("light_nav_bar"))
}
}
private fun getShapeName(): String {
return requireContext().getString(when (LauncherIconView.getDefaultShape(requireContext())) {
IconShape.TRIANGLE -> R.string.preference_icon_shape_triangle
IconShape.HEXAGON -> R.string.preference_icon_shape_hexagon
IconShape.ROUNDED_SQUARE -> R.string.preference_icon_shape_rounded_square
IconShape.SQUIRCLE -> R.string.preference_icon_shape_squircle
IconShape.SQUARE -> R.string.preference_icon_shape_square
IconShape.PENTAGON -> R.string.preference_icon_shape_pentagon
IconShape.PLATFORM_DEFAULT -> R.string.preference_icon_shape_platform
else -> R.string.preference_icon_shape_circle
})
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar
?.setTitle(R.string.preference_screen_appearance)
}
}

View File

@@ -0,0 +1,47 @@
package de.mm20.launcher2.fragment
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import de.mm20.launcher2.R
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.notifications.NotificationService
class PreferencesBadgesFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_badges)
findPreference<Preference>("notification_badges")?.setOnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) {
de.mm20.launcher2.notifications.NotificationService.getInstance()?.generateBadges()
} else {
BadgeProvider.getInstance(requireContext()).removeNotificationBadges()
}
true
}
findPreference<Preference>("suspended_badges")?.setOnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) {
BadgeProvider.getInstance(requireContext()).addSuspendBadges()
} else {
BadgeProvider.getInstance(requireContext()).removeSuspendBadges()
}
true
}
findPreference<Preference>("cloud_badges")?.setOnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) {
BadgeProvider.getInstance(requireContext()).addCloudBadges()
} else {
BadgeProvider.getInstance(requireContext()).removeCloudBadges()
}
true
}
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar
?.setTitle(R.string.preference_screen_badges)
}
}

View File

@@ -0,0 +1,121 @@
package de.mm20.launcher2.fragment
import android.Manifest
import android.content.res.ColorStateList
import android.os.Bundle
import android.widget.CheckBox
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.setPadding
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
import com.afollestad.materialdialogs.customview.customView
import de.mm20.launcher2.R
import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.UserCalendar
class PreferencesCalendarFragment : PreferenceFragmentCompat() {
private var hasCalendarPermission = false
private val calendars = mutableListOf<UserCalendar>()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_calendar)
init()
}
fun init(requestPermission: Boolean = true) {
val context = context ?: return
hasCalendarPermission = context.checkPermission(Manifest.permission.READ_CALENDAR)
if (hasCalendarPermission) {
calendars.clear()
calendars.addAll(CalendarEvent.getCalendars(context))
val unselectedCalendars = LauncherPreferences.instance.unselectedCalendars.toMutableList()
findPreference<Preference>("calendar_calendars")?.apply {
var count = calendars.size - unselectedCalendars.size
summary = resources.getQuantityString(R.plurals.preference_calendar_calendars_summary, count, count)
isEnabled = true
setOnPreferenceClickListener {
val sheetView = LinearLayout(activity)
sheetView.setPadding((8 * context.dp).toInt())
sheetView.orientation = LinearLayout.VERTICAL
sheetView.setBackgroundColor(ContextCompat.getColor(context, R.color.bottom_sheet))
var owner = ""
val padding = (8 * context.dp).toInt()
for (c in calendars) {
if (owner != c.owner) {
owner = c.owner
val text = TextView(activity)
text.setTextColor(ContextCompat.getColor(context, R.color.text_color_secondary_normal))
text.setPadding(padding, 2 * padding, padding, padding)
text.text = owner
sheetView.addView(text)
}
val checkbox = CheckBox(activity)
checkbox.text = c.name
checkbox.buttonTintList = ColorStateList.valueOf(CalendarEvent.getDisplayColor(context, c.color))
checkbox.setPadding(padding)
checkbox.isChecked = !unselectedCalendars.contains(c.id)
checkbox.setOnCheckedChangeListener { _, checked ->
if (checked) {
unselectedCalendars.remove(c.id)
} else {
unselectedCalendars.add(c.id)
}
LauncherPreferences.instance.unselectedCalendars = unselectedCalendars
count = calendars.size - unselectedCalendars.size
summary = resources.getQuantityString(R.plurals.preference_calendar_calendars_summary, count, count)
}
sheetView.addView(checkbox)
}
val scrollView = ScrollView(context)
scrollView.isNestedScrollingEnabled = true
scrollView.addView(sheetView)
MaterialDialog(context, BottomSheet()).show {
customView(view = scrollView)
title(R.string.preference_calendar_calendars)
.negativeButton(R.string.close) {
dismiss()
}
}
true
}
}
} else {
if (requestPermission) {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR),
0)
}
findPreference<Preference>("calendar_calendars")?.apply {
isEnabled = false
setSummary(R.string.preference_permission_denied)
}
}
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar?.setTitle(R.string.preference_screen_calendar)
hasCalendarPermission = requireActivity().checkPermission(Manifest.permission.READ_CALENDAR)
&& requireActivity().checkPermission(Manifest.permission.WRITE_CALENDAR)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
init(false)
}
}

View File

@@ -0,0 +1,199 @@
package de.mm20.launcher2.fragment
import android.animation.Animator
import android.animation.ObjectAnimator
import android.app.WallpaperManager
import android.graphics.*
import android.os.Bundle
import android.view.View
import android.view.ViewOutlineProvider
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.doOnNextLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import de.mm20.launcher2.LauncherApplication
import de.mm20.launcher2.R
import de.mm20.launcher2.ktx.castTo
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.translate
import de.mm20.launcher2.preferences.CardBackground
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.ui.legacy.helper.WallpaperBlur
import kotlinx.android.synthetic.main.fragment_card_settings.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import kotlin.math.roundToInt
class PreferencesCardFragment : Fragment(R.layout.fragment_card_settings) {
val preferences = LauncherPreferences.instance
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
previewCard.strokeOpacity = 0xFF
previewCardBlur.clipToOutline = true
previewCardBlur.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline?) {
val radius = preferences.cardRadius
outline?.setRoundRect(0, 0, view.width, view.height, radius * dp)
}
}
val context = requireContext()
val previewCardBlur = previewCardBlur
val previewCard = previewCard
val prefFragment = PreferenesCardInnerFragment()
prefFragment.onPreferencesReady = {
findPreference<Preference>("card_radius")?.let {
it.summary = preferences.cardRadius.toString()
it.setOnPreferenceChangeListener { pref, newValue ->
val value = newValue as Int
previewCard.radius = value * dp
previewCardBlur.invalidateOutline()
pref.summary = value.toString()
true
}
}
findPreference<Preference>("card_opacity")?.let {
it.summary = preferences.cardOpacity.toString()
it.setOnPreferenceChangeListener { pref, newValue ->
val value = newValue as Int
previewCard.backgroundOpacity = value
previewCard.cardElevation = if (value == 0xFF) resources.getDimension(R.dimen.card_elevation) else 0f
pref.summary = value.toString()
true
}
}
findPreference<Preference>("card_stroke_width")?.let {
it.summary = preferences.cardRadius.toString()
it.setOnPreferenceChangeListener { pref, newValue ->
val value = newValue as Int
previewCard.strokeWidth = (value * dp).roundToInt()
pref.summary = value.toString()
true
}
}
findPreference<Preference>("blur_cards")?.let {
if (WallpaperManager.getInstance(requireContext()).wallpaperInfo != null) {
it.isEnabled = false
it.setSummary(R.string.preference_blur_cards_summary_lwp)
previewCardBlur.visibility = View.INVISIBLE
} else {
previewCardBlur.visibility = if (preferences.blurCards) {
View.VISIBLE
} else {
View.INVISIBLE
}
it.setOnPreferenceChangeListener { pref, newValue ->
previewCardBlur.visibility = if (newValue as Boolean) {
View.VISIBLE
} else {
View.INVISIBLE
}
true
}
}
}
findPreference<Preference>("card_background")?.let {
it.setOnPreferenceChangeListener { preference, newValue ->
val background = CardBackground.byValue(newValue as String)
var color = when (background) {
CardBackground.BLACK -> context.getColor(R.color.cardview_background_black)
else -> context.getColor(R.color.cardview_background)
}
color = color and ((previewCard.backgroundOpacity shl 24) or 0xFFFFFF)
previewCard.setCardBackgroundColor(color)
true
}
}
}
childFragmentManager.beginTransaction()
.replace(R.id.preferencesView, prefFragment)
.commit()
}
private var blurBitmap: Bitmap? = null
private var animator: Animator? = null
override fun onStart() {
super.onStart()
val content = activity?.findViewById<View>(android.R.id.content) ?: return
animator = ObjectAnimator.ofArgb(content, "backgroundColor", ResourcesCompat.getColor(resources, R.color.settings_window_background, null), Color.TRANSPARENT)
.apply {
duration = 200
startDelay = resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
start()
}
if (preferences.blurCards && preferences.cardOpacity < 0xFF) {
lifecycleScope.launch {
val wallpaper = withContext(Dispatchers.IO) {
WallpaperBlur.getCachedBitmap(requireContext())
}
LauncherApplication.instance.blurredWallpaper = wallpaper
}
}
content.doOnNextLayout {
WallpaperManager.getInstance(requireContext()).setWallpaperOffsets(it.windowToken, 0.5f, 0.5f)
}
val activity = requireActivity()
lifecycleScope.launch {
val viewPosition = intArrayOf(0, 0)
val rect = Rect(0, 0, previewCardBlur.width, previewCardBlur.height)
val screen = Point()
activity.windowManager.defaultDisplay.getRealSize(screen)
previewCardBlur.getLocationOnScreen(viewPosition)
val file = File(requireContext().cacheDir, "wallpaper")
if (!file.exists()) return@launch
blurBitmap = withContext(Dispatchers.IO) {
val wallpaperWidth: Int
val wallpaperHeight: Int
val decoder = BitmapRegionDecoder
.newInstance(file.absolutePath, false)
wallpaperHeight = decoder.height
wallpaperWidth = decoder.width
if (wallpaperWidth >= screen.x && wallpaperHeight >= screen.y) {
val translateX = (wallpaperWidth - previewCardBlur.width) / 2f
val translateY = (wallpaperHeight - screen.y) / 2f + viewPosition[1]
rect.translate(translateX.roundToInt(),
translateY.roundToInt())
}
decoder.decodeRegion(rect, null)
}
previewCardBlur.setImageBitmap(blurBitmap)
}
}
override fun onStop() {
super.onStop()
if (animator?.isRunning == true) animator?.end()
activity?.findViewById<View>(android.R.id.content)?.setBackgroundColor(ResourcesCompat.getColor(resources, R.color.settings_window_background, null))
}
}
class PreferenesCardInnerFragment : PreferenceFragmentCompat() {
var onPreferencesReady: (PreferenesCardInnerFragment.() -> Unit)? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_cards)
onPreferencesReady?.invoke(this)
}
}

View File

@@ -0,0 +1,29 @@
package de.mm20.launcher2.fragment
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import android.widget.ToggleButton
import androidx.fragment.app.Fragment
import de.mm20.launcher2.R
import de.mm20.launcher2.preferences.LauncherPreferences
class PreferencesEasterEggFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_easteregg, null, false)
val root = view.findViewById<FrameLayout>(R.id.easterEggRoot)
val toggle = view.findViewById<ToggleButton>(R.id.magicModeToggle)
toggle.isChecked = LauncherPreferences.instance.easterEggEnabled
toggle.setOnCheckedChangeListener { _, isChecked ->
LauncherPreferences.instance.easterEggEnabled = isChecked
Toast.makeText(requireContext(), if (isChecked) R.string.easter_egg_activated else R.string.easter_egg_deactivated, Toast.LENGTH_SHORT).show()
}
return view
}
}

View File

@@ -0,0 +1,60 @@
package de.mm20.launcher2.fragment
import android.annotation.SuppressLint
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.fragment.app.Fragment
import com.bumptech.glide.Glide
import de.mm20.launcher2.R
class PreferencesLicenseFragment : Fragment() {
var library: Int = 0
@SuppressLint("ResourceType")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_license, null, false)
val license = resources.obtainTypedArray(library)
(activity as AppCompatActivity).supportActionBar?.title = license.getString(0)
val icon = view.findViewById<ImageView>(R.id.icon)
val iconUri = license.getString(2)
if (iconUri == null) icon.visibility = View.GONE
else {
Glide
.with(icon)
.load(iconUri)
.into(icon)
icon.visibility = View.VISIBLE
}
val url = license.getString(6)
val website = view.findViewById<TextView>(R.id.website)
website.setOnClickListener {
val intent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(CustomTabColorSchemeParams
.Builder()
.setToolbarColor(-0x9f8275)
.build())
.setShowTitle(true)
.build()
intent.launchUrl(activity as AppCompatActivity, Uri.parse(url))
}
val description = view.findViewById<TextView>(R.id.description)
description.text = license.getString(1)
val licenseTitle = view.findViewById<TextView>(R.id.licenseTitle)
val licenseText = view.findViewById<TextView>(R.id.licenseText)
val licenseCopyright = view.findViewById<TextView>(R.id.licenseCopyright)
licenseTitle.text = license.getString(3)
licenseCopyright.text = license.getString(4)
licenseText.text = resources.openRawResource(license.getResourceId(5, 0)).reader().readText()
license.recycle()
return view
}
}

View File

@@ -0,0 +1,60 @@
package de.mm20.launcher2.fragment
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import de.mm20.launcher2.R
class PreferencesMainFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_main)
findPreference<Preference>("screen_appearance")?.setOnPreferenceClickListener {
setSettingsScreen(PreferencesAppearanceFragment())
true
}
findPreference<Preference>("screen_about")?.setOnPreferenceClickListener {
setSettingsScreen(PreferencesAboutFragment())
true
}
findPreference<Preference>("screen_weather")?.setOnPreferenceClickListener {
setSettingsScreen(PreferencesWeatherFragment())
true
}
findPreference<Preference>("screen_services")?.setOnPreferenceClickListener {
setSettingsScreen(PreferencesServicesFragment())
true
}
findPreference<Preference>("screen_search")?.setOnPreferenceClickListener {
setSettingsScreen(PreferencesSearchFragment())
true
}
findPreference<Preference>("screen_calendar")?.setOnPreferenceClickListener {
setSettingsScreen(PreferencesCalendarFragment())
true
}
findPreference<Preference>("screen_badges")?.setOnPreferenceClickListener {
setSettingsScreen(PreferencesBadgesFragment())
true
}
}
private fun setSettingsScreen(fragment: Fragment) {
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.preference_fragment_child_enter, R.anim.preference_fragment_parent_exit,
R.anim.preference_fragment_parent_enter, R.anim.preference_fragment_child_exit)
.replace(android.R.id.content, fragment)
.addToBackStack(null)
.commit()
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar?.setTitle(R.string.title_activity_settings)
}
}

View File

@@ -0,0 +1,176 @@
package de.mm20.launcher2.fragment
import android.Manifest
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import de.mm20.launcher2.R
import de.mm20.launcher2.gservices.GoogleApiHelper
import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.msservices.MicrosoftGraphApiHelper
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.owncloud.OwncloudClient
import de.mm20.launcher2.preferences.LauncherPreferences
import kotlinx.coroutines.launch
class PreferencesSearchFragment : PreferenceFragmentCompat() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
updateGoogleDrive()
updateOneDrive()
}
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_search)
findPreference<Preference>("search_activities")?.summary =
getString(
R.string.preference_search_activities_summary,
requireActivity().componentName.flattenToShortString()
)
findPreference<Preference>("search_files")?.setOnPreferenceChangeListener { _, newValue ->
if (newValue == true &&
requireContext().checkPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
) {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
0
)
}
true
}
findPreference<Preference>("search_edit_websearch")?.setOnPreferenceClickListener {
setSettingsScreen(PreferencesWebSearchesFragment())
true
}
}
private suspend fun updateGoogleDrive() {
val googleApiHelper = GoogleApiHelper.getInstance(context ?: return)
val account = googleApiHelper.getAccount()
val pref = findPreference<Preference>("search_gdrive")!!
if (account == null) {
pref.apply {
setSummary(R.string.preference_summary_not_logged_in)
}
} else {
pref.apply {
summary = context.getString(R.string.preference_search_gdrive_summary, account.name)
}
}
val isSignedIn = account != null
pref.setOnPreferenceChangeListener { _, value ->
val newVal = value as Boolean
if (newVal && !isSignedIn) {
googleLogin()
}
true
}
}
private suspend fun updateOneDrive() {
val oneDrivePref = findPreference<Preference>("search_onedrive")!!
val user = MicrosoftGraphApiHelper.getInstance(requireContext()).getUser()
if (user == null) {
oneDrivePref.setSummary(R.string.preference_summary_not_logged_in)
oneDrivePref.setOnPreferenceChangeListener { _, value ->
if (value as Boolean) {
lifecycleScope.launch launch2@{
MicrosoftGraphApiHelper.getInstance(requireContext())
.login(requireActivity())
updateOneDrive()
}
}
true
}
} else {
oneDrivePref.summary =
context?.getString(R.string.preference_search_onedrive_summary, user.name)
}
}
private fun updateNextcloud() {
val nextcloudPref = findPreference<Preference>("search_nextcloud")!!
val client = NextcloudApiHelper(context ?: return)
lifecycleScope.launch {
val user = client.getLoggedInUser()
if (user == null) {
nextcloudPref.setSummary(R.string.preference_summary_not_logged_in)
LauncherPreferences.instance.searchNextcloud = false
nextcloudPref.setOnPreferenceChangeListener { _, value ->
if (value as Boolean) {
lifecycleScope.launch launch2@{
updateNextcloud()
}
}
true
}
} else {
nextcloudPref.summary = context?.getString(
R.string.preference_search_nextcloud_summary,
user.displayName
)
}
}
}
private fun updateOwncloud() {
val owncloudPref = findPreference<Preference>("search_owncloud")!!
lifecycleScope.launch {
val client = OwncloudClient(context ?: return@launch)
val user = client.getLoggedInUser()
if (user == null) {
owncloudPref.setSummary(R.string.preference_summary_not_logged_in)
LauncherPreferences.instance.searchOwncloud = false
owncloudPref.setOnPreferenceChangeListener { _, value ->
if (value as Boolean) {
lifecycleScope.launch launch2@{
client.login(requireActivity(), 0)
updateOwncloud()
}
}
true
}
} else {
owncloudPref.summary = context?.getString(
R.string.preference_search_nextcloud_summary,
user.displayName,
)
}
}
}
private fun googleLogin() {
GoogleApiHelper.getInstance(requireContext()).login(requireActivity())
}
private fun setSettingsScreen(fragment: Fragment) {
parentFragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.preference_fragment_child_enter, R.anim.preference_fragment_parent_exit,
R.anim.preference_fragment_parent_enter, R.anim.preference_fragment_child_exit
)
.replace(android.R.id.content, fragment)
.addToBackStack(null)
.commit()
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar?.setTitle(R.string.preference_screen_search)
updateNextcloud()
updateOwncloud()
}
}

View File

@@ -0,0 +1,174 @@
package de.mm20.launcher2.fragment
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import de.mm20.launcher2.R
import de.mm20.launcher2.gservices.GoogleApiHelper
import de.mm20.launcher2.msservices.MicrosoftGraphApiHelper
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.owncloud.OwncloudClient
import kotlinx.coroutines.launch
class PreferencesServicesFragment : PreferenceFragmentCompat() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
updateGooglePreferences()
updateMicrosoftPreferences()
updateNextcloudPreferences()
}
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_services)
}
private suspend fun updateGooglePreferences() {
val pref = findPreference<Preference>("google_signin")!!
val googleApiHelper = GoogleApiHelper.getInstance(requireContext())
if (!googleApiHelper.isAvailable()) {
pref.isEnabled = false
pref.summary = context?.getString(R.string.feature_not_available, context?.getString(R.string.app_name))
return
}
val account = googleApiHelper.getAccount()
if (account == null) {
pref.apply {
setTitle(R.string.preference_google_signin)
setSummary(R.string.preference_google_signin_summary)
setOnPreferenceClickListener {
googleApiHelper.login(requireActivity())
true
}
}
} else {
pref.apply {
title = context.getString(R.string.preference_signin_logout)
summary = context.getString(R.string.preference_signin_user, account.name)
setOnPreferenceClickListener {
googleApiHelper.logout()
lifecycleScope.launch {
updateGooglePreferences()
}
true
}
}
}
}
private suspend fun updateMicrosoftPreferences() {
val pref = findPreference<Preference>("ms_signin")!!
val msApiHelper = MicrosoftGraphApiHelper.getInstance(requireContext())
if (!msApiHelper.isAvailable()) {
pref.isEnabled = false
pref.summary = context?.getString(R.string.feature_not_available, context?.getString(R.string.app_name))
return
}
val user = MicrosoftGraphApiHelper.getInstance(requireContext()).getUser()
if (user == null) {
pref.setTitle(R.string.preference_ms_signin)
pref.setSummary(R.string.preference_ms_signin_summary)
pref.setOnPreferenceClickListener {
lifecycleScope.launch {
msApiHelper.login(requireActivity())
updateMicrosoftPreferences()
}
true
}
} else {
pref.setTitle(R.string.preference_signin_logout)
pref.summary = context?.getString(R.string.preference_signin_user, user.name)
pref.setOnPreferenceClickListener {
lifecycleScope.launch {
msApiHelper.logout()
updateMicrosoftPreferences()
}
true
}
}
}
private suspend fun updateNextcloudPreferences() {
val nextcloud = NextcloudApiHelper(requireContext())
val user = nextcloud.getLoggedInUser()
if (user == null) {
findPreference<Preference>("nextcloud_signin")?.let {
it.setOnPreferenceClickListener {
nextcloud.login(requireActivity())
true
}
it.setTitle(R.string.preference_nextcloud_signin)
it.setSummary(R.string.preference_nextcloud_signin_summary)
}
} else {
findPreference<Preference>("nextcloud_signin")?.let {
it.setOnPreferenceClickListener {
lifecycleScope.launch {
nextcloud.logout()
updateNextcloudPreferences()
}
true
}
it.setTitle(R.string.preference_signin_logout)
it.summary = context?.getString(
R.string.preference_signin_user_nextcloud,
user.displayName
)
}
}
}
private fun updateOwncloudPreferences() {
val client = OwncloudClient(context ?: return)
lifecycleScope.launch {
val user = client.getLoggedInUser()
if (user == null) {
findPreference<Preference>("owncloud_signin")?.let {
it.setOnPreferenceClickListener {
OwncloudClient(requireContext()).login(
requireActivity(),
REQUEST_OWNCLOUD_LOGIN
)
true
}
it.setTitle(R.string.preference_owncloud_signin)
it.setSummary(R.string.preference_owncloud_signin_summary)
}
} else {
findPreference<Preference>("owncloud_signin")?.let {
it.setOnPreferenceClickListener {
OwncloudClient(requireContext()).logout()
updateOwncloudPreferences()
true
}
it.setTitle(R.string.preference_signin_logout)
it.summary = context?.getString(
R.string.preference_signin_user_nextcloud,
user.displayName,
)
}
}
}
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar?.setTitle(R.string.preference_screen_services)
updateOwncloudPreferences()
}
companion object {
const val REQUEST_OWNCLOUD_LOGIN = 581
}
}

View File

@@ -0,0 +1,166 @@
package de.mm20.launcher2.fragment
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems
import com.afollestad.materialdialogs.list.listItemsSingleChoice
import de.mm20.launcher2.R
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.preferences.WeatherProviders
import de.mm20.launcher2.weather.WeatherProvider
import de.mm20.launcher2.weather.WeatherViewModel
import de.mm20.launcher2.weather.here.HereProvider
import de.mm20.launcher2.weather.metno.MetNoProvider
import de.mm20.launcher2.weather.openweathermap.OpenWeatherMapProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class PreferencesWeatherFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_weather)
findPreference<Preference>("location")?.setOnPreferenceChangeListener { _, newValue ->
lifecycleScope.launch {
val locations = withContext(Dispatchers.IO) {
WeatherProvider.getInstance(requireContext())
?.lookupLocation(newValue as String)
} ?: return@launch
onLookupCompleted(locations)
}
false
}
/*findPreference<Preference>("weather_provider")?.setOnPreferenceChangeListener { pref, newValue ->
val newProvider = WeatherProviders.byValue(newValue as String)
LauncherPreferences.instance.weatherProvider = newProvider
WeatherProvider.getInstance(requireContext())?.resetLastUpdate()
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(requireContext())
updateProviderPreferences()
true
}*/
val providerPref = findPreference<Preference>("weather_provider")!!
val context = requireContext()
val providers = mutableListOf<Pair<WeatherProviders, String>>()
OpenWeatherMapProvider(context).takeIf { it.isAvailable() }?.let {
providers.add(WeatherProviders.OPENWEATHERMAP to it.name)
}
HereProvider(context).takeIf { it.isAvailable() }?.let {
providers.add(WeatherProviders.HERE to it.name)
}
MetNoProvider(context).takeIf { it.isAvailable() }?.let {
providers.add(WeatherProviders.MET_NO to it.name)
}
if (providers.isEmpty()) {
providerPref.summary = context.getString(
R.string.feature_not_available,
context.getString(R.string.app_name)
)
providerPref.isEnabled = false
} else {
providerPref.setOnPreferenceClickListener {
MaterialDialog(context).show {
title(R.string.preference_weather_provider)
listItemsSingleChoice(
items = providers.map { it.second },
initialSelection = providers.indexOfFirst { it.first == LauncherPreferences.instance.weatherProvider }
) { dialog, index, text ->
LauncherPreferences.instance.weatherProvider = providers[index].first
WeatherProvider.getInstance(requireContext())?.resetLastUpdate()
ViewModelProvider(this@PreferencesWeatherFragment)
.get(WeatherViewModel::class.java)
.requestUpdate(requireContext())
updateProviderPreferences()
dialog.dismiss()
}
}
true
}
}
findPreference<Preference>("auto_location")?.setOnPreferenceChangeListener { _, newValue ->
val autoLocation = newValue as Boolean
val provider = WeatherProvider.getInstance(requireContext())
provider?.autoLocation = autoLocation
provider?.resetLastUpdate()
provider?.setLocation(null, "")
ViewModelProvider(this).get(WeatherViewModel::class.java)
.requestUpdate(requireContext())
updateProviderPreferences()
true
}
updateProviderPreferences()
}
private fun updateProviderPreferences() {
val provider = WeatherProvider.getInstance(requireContext())
val autoLocationPref = findPreference<SwitchPreference>("auto_location")!!
val locationPref = findPreference<Preference>("location")!!
val unitsPref = findPreference<Preference>("imperial_units")!!
val providerPref = findPreference<Preference>("weather_provider")!!
locationPref.parent?.isVisible = provider != null
unitsPref.isVisible = provider != null
provider ?: return
providerPref.summary = provider.name
if (provider.supportsAutoLocation) {
autoLocationPref.setSummary(R.string.preference_automatic_location_summary)
autoLocationPref.isChecked = provider.autoLocation
if (!provider.supportsManualLocation) {
autoLocationPref.isEnabled = false
autoLocationPref.isChecked = true
locationPref.isEnabled = false
locationPref.setSummary(R.string.preference_location_disabled_summary)
} else {
autoLocationPref.isEnabled = true
autoLocationPref.isChecked = provider.autoLocation
locationPref.isEnabled = true
locationPref.summary = provider.getLastLocation()
}
} else {
autoLocationPref.isEnabled = false
autoLocationPref.setSummary(R.string.preference_automatic_location_disabled_summary)
autoLocationPref.isChecked = false
}
}
private fun onLookupCompleted(results: List<Pair<Any?, String>>) {
MaterialDialog(requireContext())
.listItems(
items = results.map { it.second },
waitForPositiveButton = false
) { dialog, index, _ ->
val provider = WeatherProvider.getInstance(requireContext())
?: return@listItems dialog.dismiss()
provider.resetLastUpdate()
provider.setLocation(results[index].first, results[index].second)
findPreference<Preference>("location")?.summary = results[index].second
ViewModelProvider(this).get(WeatherViewModel::class.java)
.requestUpdate(requireContext())
dialog.dismiss()
}
.negativeButton {
it.cancel()
}
.show()
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar?.setTitle(R.string.preference_screen_weather)
}
}

View File

@@ -0,0 +1,229 @@
package de.mm20.launcher2.fragment
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.scale
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
import com.afollestad.materialdialogs.color.colorChooser
import com.afollestad.materialdialogs.customview.customView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
import de.mm20.launcher2.R
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.search.SearchViewModel
import de.mm20.launcher2.search.WebsearchViewModel
import de.mm20.launcher2.search.data.Websearch
import java.io.File
import java.io.FileOutputStream
import java.lang.ref.WeakReference
class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
private lateinit var rootView: View
private var sheetIcon: WeakReference<ImageView>? = null
private val viewModel by lazy {
ViewModelProvider(context as AppCompatActivity)[WebsearchViewModel::class.java]
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceScreen = preferenceManager.createPreferenceScreen(activity)
val searches = viewModel.allWebsearches
searches.observe(context as AppCompatActivity, Observer {
updatePreferenceScreen(it)
})
}
private fun updatePreferenceScreen(searches: List<Websearch>) {
preferenceScreen.removeAll()
for (search in searches) {
val pref = Preference(context)
pref.title = search.label
if (search.icon == null) {
val drawable = resources.getDrawable(R.drawable.ic_search, requireActivity().theme).mutate()
drawable.setTintMode(PorterDuff.Mode.SRC_ATOP)
drawable.setTint(search.color)
pref.icon = drawable
} else {
Glide.with(requireContext())
.asDrawable()
.load(search.icon)
.into(object : SimpleTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
pref.icon = resource
}
})
}
pref.setOnPreferenceClickListener {
editSearch(search)
true
}
preferenceScreen.addPreference(pref)
}
val newPref = Preference(activity)
newPref.setTitle(R.string.preference_websearch_new)
newPref.setIcon(R.drawable.ic_preference_websearch_new)
newPref.setOnPreferenceClickListener {
editSearch(null)
true
}
preferenceScreen.addPreference(newPref)
}
private fun editSearch(search: Websearch?) {
val websearch = search ?: Websearch("", "", 0xFF555555.toInt(), null, null)
val dialogView = LayoutInflater.from(activity).inflate(R.layout.dialog_websearch, null)
val nameEdit = dialogView.findViewById<EditText>(R.id.websearchName)
nameEdit.setText(websearch.label)
val urlEdit = dialogView.findViewById<EditText>(R.id.websearchUrl)
urlEdit.setText(websearch.urlTemplate)
val iconView = dialogView.findViewById<ImageView>(R.id.websearchIcon)
iconView.apply {
if (websearch.icon == null) {
setImageResource(R.drawable.ic_search)
imageTintList = ColorStateList.valueOf(websearch.color)
} else {
Glide.with(this)
.load(websearch.icon)
.into(this)
}
sheetIcon = WeakReference(this)
}
val sheet = MaterialDialog(requireContext(), BottomSheet())
.cornerRadius(8f)
.customView(view = dialogView)
val radius = 8 * dialogView.dp
dialogView.background = GradientDrawable().apply {
cornerRadii = floatArrayOf(
radius, radius, // top left
radius, radius, // top right
0f, 0f, // bottom left
0f, 0f // bottom right
)
}
var newColor = websearch.color
var newIcon: String? = websearch.icon
sheet.noAutoDismiss()
.positiveButton(android.R.string.ok) {
val newUrl = urlEdit.text.toString()
val newName = nameEdit.text.toString()
if (!newUrl.contains("\${1}")) {
urlEdit.error = getString(R.string.websearch_dialog_url_error)
return@positiveButton
}
File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }?.let {
websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() }
val newFile = File(requireContext().filesDir, "websearch-${System.currentTimeMillis()}")
it.copyTo(newFile, true)
it.delete()
newIcon = newFile.absolutePath
}
if (newIcon == null) {
websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() }
}
websearch.urlTemplate = newUrl
websearch.label = newName
websearch.icon = newIcon
websearch.color = newColor
viewModel.insertWebsearch(websearch)
sheet.dismiss()
}
sheet.negativeButton(android.R.string.cancel) {
sheet.cancel()
}
@Suppress("DEPRECATION")
sheet.neutralButton(R.string.menu_delete) {
sheet.dismiss()
websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() }
viewModel.deleteWebsearch(websearch)
}
sheet.setOnCancelListener {
File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }?.delete()
}
dialogView.findViewById<View>(R.id.websearchIcon).setOnClickListener {
MaterialDialog(requireContext()).show {
@Suppress("DEPRECATION")
neutralButton(R.string.custom_icon) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "image/*"
try {
startActivityForResult(intent, 24)
} catch (e: ActivityNotFoundException) {
}
dismiss()
}
title(R.string.websearch_dialog_choose_icon_color)
colorChooser(
colors = context.resources.getIntArray(R.array.color_chooser_presets),
allowCustomArgb = true,
showAlphaSelector = false
) { _, color ->
iconView.setImageResource(R.drawable.ic_search)
iconView.imageTintList = ColorStateList.valueOf(color)
newColor = color
newIcon = null
File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }?.delete()
dismiss()
}
}
}
sheet.show()
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar
?.setTitle(R.string.preference_search_edit_websearch)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val dataUri = data?.data
if (requestCode == 24 && resultCode == Activity.RESULT_OK && dataUri != null) {
val stream = requireActivity().contentResolver.openInputStream(dataUri)
val icon = BitmapFactory.decodeStream(stream)
val scaledIcon = icon.scale((32 * requireContext().dp).toInt(), (32 * requireContext().dp).toInt())
val out = FileOutputStream(File(requireContext().cacheDir, "websearch-tmp"))
scaledIcon.compress(Bitmap.CompressFormat.PNG, 100, out)
out.close()
sheetIcon?.get()?.apply {
imageTintList = null
setImageBitmap(scaledIcon)
}
}
}
}

View File

@@ -0,0 +1,45 @@
package de.mm20.launcher2.helper
import android.content.Context
import android.os.Build
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
class DebugInformationDumper {
fun dump(context: Context): String {
val df = SimpleDateFormat("yyyy-MM-dd-HHmmss")
val file = File(context.getExternalFilesDir(null), "kvaesitso-log-${df.format(Date(System.currentTimeMillis()))}")
val fos = file.outputStream().writer()
fos.write("Device: ${Build.DEVICE}\n")
fos.write("SDK version: ${Build.VERSION.SDK_INT}\n")
fos.write("====================================\n")
Thread {
val input = Runtime.getRuntime().exec("/system/bin/sh -c logcat").inputStream.bufferedReader()
var line = input.readLine()
while (line != null) {
line = input.readLine()
fos.write("$line\n")
}
fos.close()
}.start()
return file.absolutePath
}
fun exportDatabases(context: Context): String {
val df = SimpleDateFormat("yyyy-MM-dd-HHmmss")
val exportFile = File(context.getExternalFilesDir(null), "room-${df.format(Date(System.currentTimeMillis()))}.db")
GlobalScope.launch {
withContext(Dispatchers.IO) {
context.getDatabasePath("room").copyTo(exportFile)
}
}
return exportFile.absolutePath
}
}

View File

@@ -0,0 +1,99 @@
package de.mm20.launcher2.ui.preferences
import android.animation.Animator
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.postDelayed
import androidx.preference.Preference
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
import com.airbnb.lottie.LottieAnimationView
import de.mm20.launcher2.R
import de.mm20.launcher2.preferences.AppStartAnimation
import de.mm20.launcher2.preferences.LauncherPreferences
class AppStartAnimPreference @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.preferenceStyle) : Preference(context, attrs, defStyleAttr) {
init {
summary = getNameForAnimation(LauncherPreferences.instance.appStartAnim)
setOnPreferenceClickListener {
val anims = mutableListOf(
AppStartAnimation.M to R.raw.app_start_anim_m,
AppStartAnimation.SLIDE_BOTTOM to R.raw.app_start_anim_slide_bottom,
AppStartAnimation.FADE to R.raw.app_start_anim_fade
)
val dialog = MaterialDialog(context)
val layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
val list = LinearLayout(context)
list.orientation = LinearLayout.VERTICAL
anims.forEachIndexed { _, anim ->
val view = View.inflate(context, R.layout.preference_start_anim_item, null)
view.findViewById<LottieAnimationView>(R.id.icon).also { iconView ->
iconView.setAnimation(anim.second)
iconView.addAnimatorListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {
}
override fun onAnimationEnd(animation: Animator) {
iconView.postDelayed(500) {
iconView.frame = 0
}
iconView.postDelayed(1300) {
iconView.playAnimation()
}
}
override fun onAnimationCancel(animation: Animator?) {
}
override fun onAnimationStart(animation: Animator?) {
}
})
iconView.postDelayed(300) {
iconView.playAnimation()
}
}
view.findViewById<TextView>(R.id.label).also { labelView ->
labelView.setText(getNameForAnimation(anim.first))
}
view.layoutParams = layoutParams
list.addView(view)
view.setOnClickListener {
LauncherPreferences.instance.appStartAnim = anim.first
summary = getNameForAnimation(anim.first)
dialog.dismiss()
}
}
dialog.customView(view = list, scrollable = true)
.title(R.string.preference_app_start_animation)
.negativeButton(android.R.string.cancel) {
dialog.cancel()
}
.show()
true
}
}
private fun getNameForAnimation(anim: AppStartAnimation): String {
return when (anim) {
AppStartAnimation.FADE -> context.getString(R.string.preference_app_start_animation_fade)
AppStartAnimation.SLIDE_BOTTOM -> context.getString(R.string.preference_app_start_animation_slide_bottom)
AppStartAnimation.M -> context.getString(R.string.preference_app_start_animation_m)
else -> context.getString(R.string.preference_app_start_animation_default)
}
}
}

View File

@@ -0,0 +1,73 @@
package de.mm20.launcher2.ui.view
import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.XmlRes
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import de.mm20.launcher2.R
import de.mm20.launcher2.ktx.castTo
class PreferencesView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private val fragment = PreferenceViewFragment()
init {
if (id == View.NO_ID) id = View.generateViewId()
context.castTo<AppCompatActivity>().supportFragmentManager.beginTransaction()
.add(id, fragment)
.commit()
attrs?.let {
val ta = context.theme.obtainStyledAttributes(it, R.styleable.PreferencesView, 0, defStyleAttr)
val preferenceScreen = ta.getResourceId(R.styleable.SearchGridView_columnCount, 0)
setPreferenceResource(preferenceScreen)
ta.recycle()
}
}
fun setPreferenceResource(@XmlRes resId: Int) {
if (resId == 0) return
fragment.setPreferenceResource(resId)
}
fun <T : Preference> findPreference(key: String): T? {
return fragment.findPreference<T>(key)
}
var onPreferencesReady: (() -> Unit)? = null
set(value) {
field = value
fragment.onPreferencesReady = value
}
}
class PreferenceViewFragment : PreferenceFragmentCompat() {
private var isInitialized = false
var onPreferencesReady: (() -> Unit)? = null
@XmlRes
private var preferenceResource = 0
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
if (preferenceResource != 0) addPreferencesFromResource(preferenceResource)
isInitialized = true
onPreferencesReady?.invoke()
onPreferencesReady = null
}
internal fun setPreferenceResource(@XmlRes resId: Int) {
preferenceResource = resId
if (isInitialized && resId != 0) {
addPreferencesFromResource(resId)
}
}
}