..
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher
|
||||
|
||||
import android.Manifest
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import rasel.lunar.launcher.apps.AppDrawer
|
||||
import rasel.lunar.launcher.databinding.LauncherActivityBinding
|
||||
import rasel.lunar.launcher.feeds.Feeds
|
||||
import rasel.lunar.launcher.feeds.WidgetHost
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APPLICATION_THEME
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_BACK_HOME
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_FIRST_LAUNCH
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_STATUS_BAR
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_WINDOW_BACKGROUND
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_FIRST_LAUNCH
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.widgetHostId
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.getColorResId
|
||||
import rasel.lunar.launcher.helpers.ViewPagerAdapter
|
||||
import rasel.lunar.launcher.home.LauncherHome
|
||||
|
||||
|
||||
internal class LauncherActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: LauncherActivityBinding
|
||||
private lateinit var settingsPrefs: SharedPreferences
|
||||
lateinit var viewPager: ViewPager2
|
||||
|
||||
companion object {
|
||||
@JvmStatic var lActivity: LauncherActivity? = null
|
||||
@JvmStatic var appWidgetManager: AppWidgetManager? = null
|
||||
@JvmStatic var appWidgetHost: WidgetHost? = null
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
DynamicColors.applyToActivityIfAvailable(this)
|
||||
|
||||
settingsPrefs = getSharedPreferences(PREFS_SETTINGS, 0)
|
||||
AppCompatDelegate.setDefaultNightMode(settingsPrefs.getInt(KEY_APPLICATION_THEME, MODE_NIGHT_FOLLOW_SYSTEM))
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
binding = LauncherActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
lActivity = this
|
||||
appWidgetManager = AppWidgetManager.getInstance(applicationContext)
|
||||
appWidgetHost = WidgetHost(applicationContext, widgetHostId)
|
||||
appWidgetHost?.startListening()
|
||||
|
||||
/* if this is the first launch,
|
||||
then remember the event and show the welcome dialog */
|
||||
welcomeDialog()
|
||||
setupView()
|
||||
|
||||
/* handle navigation back events */
|
||||
handleBackPress()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
appWidgetHost?.stopListening()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (settingsPrefs.getBoolean(KEY_BACK_HOME, false)) viewPager.currentItem = 1
|
||||
statusBarView()
|
||||
setBgColor()
|
||||
}
|
||||
|
||||
private fun welcomeDialog() {
|
||||
getSharedPreferences(PREFS_FIRST_LAUNCH, 0).let {
|
||||
if (it.getBoolean(KEY_FIRST_LAUNCH, true)) {
|
||||
it.edit().putBoolean(KEY_FIRST_LAUNCH, false).apply()
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.welcome)
|
||||
.setMessage(R.string.welcome_description)
|
||||
.setPositiveButton(R.string.got_it) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
askPermissions()
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ask for the permissions */
|
||||
private fun askPermissions() {
|
||||
/* phone permission */
|
||||
if (this.checkSelfPermission(Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
|
||||
this.requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), 1)
|
||||
}
|
||||
/* modify system settings */
|
||||
if (!Settings.System.canWrite(this)) {
|
||||
this.startActivity(
|
||||
Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)
|
||||
.setData(Uri.parse("package:" + this.packageName))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/* set up viewpager2 */
|
||||
private fun setupView() {
|
||||
viewPager = binding.viewPager.apply {
|
||||
adapter = ViewPagerAdapter(
|
||||
supportFragmentManager, mutableListOf(Feeds(), LauncherHome(), AppDrawer()), lifecycle)
|
||||
offscreenPageLimit = 1
|
||||
setCurrentItem(1, false)
|
||||
reduceDragSensitivity()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBgColor() {
|
||||
binding.root.setBackgroundColor(Color.parseColor("#${
|
||||
settingsPrefs.getString(KEY_WINDOW_BACKGROUND, getString(getColorResId(this, android.R.attr.colorBackground))
|
||||
.replace("#", ""))}"))
|
||||
}
|
||||
|
||||
private fun statusBarView() {
|
||||
if (settingsPrefs.getBoolean(KEY_STATUS_BAR, false)) {
|
||||
/* hide status bar */
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.insetsController?.hide(WindowInsets.Type.statusBars())
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
}
|
||||
topPadding(false)
|
||||
} else {
|
||||
/* show status bar */
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.insetsController?.show(WindowInsets.Type.statusBars())
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
}
|
||||
topPadding(true)
|
||||
}
|
||||
}
|
||||
|
||||
/* alternative of deprecated onBackPressed method */
|
||||
private fun handleBackPress() {
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
/* while in to-do manager, go back to home screen */
|
||||
if (supportFragmentManager.backStackEntryCount != 0) supportFragmentManager.popBackStack()
|
||||
|
||||
/* while in feeds or app drawer, go back to home screen */
|
||||
if (viewPager.currentItem != 1) viewPager.currentItem = 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun topPadding(topPadding: Boolean) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
|
||||
windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures()).let {
|
||||
val topInset = if (topPadding) {
|
||||
if (it.top == 0) windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).top
|
||||
else it.top
|
||||
} else 0
|
||||
|
||||
view.updatePadding(0, topInset, 0, it.bottom)
|
||||
}
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
}
|
||||
|
||||
private fun ViewPager2.reduceDragSensitivity() {
|
||||
ViewPager2::class.java.getDeclaredField("mRecyclerView").apply {
|
||||
isAccessible = true
|
||||
}.let { recyclerViewField ->
|
||||
(recyclerViewField.get(this) as RecyclerView).let { recyclerView ->
|
||||
RecyclerView::class.java.getDeclaredField("mTouchSlop").apply {
|
||||
isAccessible = true
|
||||
set(recyclerView, this.get(recyclerView) as Int * 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
|
||||
|
||||
internal class LunarLauncher : Application() {
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) SQLiteDatabase.releaseMemory()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.apps
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import rasel.lunar.launcher.apps.AppDrawer.Companion.alphabetList
|
||||
import rasel.lunar.launcher.apps.AppDrawer.Companion.letterPreview
|
||||
import rasel.lunar.launcher.apps.AppDrawer.Companion.listenScroll
|
||||
import rasel.lunar.launcher.apps.AppDrawer.Companion.settingsPrefs
|
||||
import rasel.lunar.launcher.apps.AppsAdapter.Companion.appsSize
|
||||
import rasel.lunar.launcher.helpers.Constants
|
||||
|
||||
|
||||
internal class AlphabetScrollbar : View {
|
||||
|
||||
private var paint: Paint? = null
|
||||
private var selectedIndex = -1
|
||||
private val alphabet get() = alphabetList.distinct()
|
||||
|
||||
constructor(context: Context?) : super(context) {
|
||||
init()
|
||||
}
|
||||
|
||||
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
|
||||
init()
|
||||
}
|
||||
|
||||
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
init()
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
private fun init() {
|
||||
paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = defaultTextColor
|
||||
textSize = 16f
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
val width = width
|
||||
val height = height
|
||||
val letterHeight: Int = height / alphabet.count()
|
||||
alphabet.indices.forEach { i: Int ->
|
||||
val x = width / 2f - paint!!.measureText(alphabet[i]) / 2f
|
||||
val y = i * letterHeight + letterHeight / 2f
|
||||
when (i) {
|
||||
selectedIndex -> paint!!.textSize = 20f
|
||||
else -> paint!!.textSize = 16f
|
||||
}
|
||||
canvas.drawText(alphabet[i], x, y, paint!!)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
|
||||
val y = event.y
|
||||
val index = (y / height * alphabet.count()).toInt()
|
||||
if (index != selectedIndex) {
|
||||
selectedIndex = index
|
||||
invalidate()
|
||||
}
|
||||
|
||||
if (!settingsPrefs!!.getBoolean(Constants.KEY_APPS_COUNT, true)) letterPreview?.visibility = VISIBLE
|
||||
try { letterPreview?.text = alphabet[selectedIndex] }
|
||||
catch (exception: Exception) { exception.printStackTrace() }
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
when {
|
||||
selectedIndex < 0 -> listenScroll(alphabet[0])
|
||||
selectedIndex > alphabet.count() - 1 -> listenScroll(alphabet[alphabet.count() - 1])
|
||||
else -> listenScroll(alphabet[selectedIndex])
|
||||
}
|
||||
|
||||
selectedIndex = -1
|
||||
invalidate()
|
||||
if (settingsPrefs!!.getBoolean(Constants.KEY_APPS_COUNT, true)) letterPreview?.text = appsSize.toString()
|
||||
else letterPreview?.visibility = GONE
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private val defaultTextColor: Int get() {
|
||||
val resolvedAttr = TypedValue()
|
||||
context.theme.resolveAttribute(android.R.attr.textColorPrimary, resolvedAttr, true)
|
||||
val colorRes = resolvedAttr.run { if (resourceId != 0) resourceId else data }
|
||||
return ContextCompat.getColor(context, colorRes)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.apps
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import rasel.lunar.launcher.BuildConfig
|
||||
import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
|
||||
import rasel.lunar.launcher.R
|
||||
import rasel.lunar.launcher.databinding.AppDrawerBinding
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_GRID_COLUMNS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_SCROLLBAR_HEIGHT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APPS_COUNT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APPS_LAYOUT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_DRAW_ALIGN
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_GRID_COLUMNS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_KEYBOARD_SEARCH
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_QUICK_LAUNCH
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_SCROLLBAR_HEIGHT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_STATUS_BAR
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_APP_NAMES
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
|
||||
import java.text.Normalizer
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
|
||||
internal class AppDrawer : Fragment() {
|
||||
|
||||
private lateinit var binding: AppDrawerBinding
|
||||
private var layoutType: Int = 0
|
||||
private var isSearchShown: Boolean = false
|
||||
private var isKeyboardShowing: Boolean = false
|
||||
|
||||
companion object {
|
||||
private var packageManager: PackageManager? = null
|
||||
private var appsAdapter: AppsAdapter? = null
|
||||
private var packageInfoList: MutableList<ResolveInfo> = mutableListOf()
|
||||
private var packageList = mutableListOf<Packages>()
|
||||
private val numberPattern = Pattern.compile("[0-9]")
|
||||
private val alphabetPattern = Pattern.compile("[A-Z]")
|
||||
@JvmStatic var settingsPrefs: SharedPreferences? = null
|
||||
@JvmStatic var appNamesPrefs: SharedPreferences? = null
|
||||
@JvmStatic var alphabetList = mutableListOf<String>()
|
||||
@JvmStatic var letterPreview: MaterialTextView? = null
|
||||
|
||||
private fun appName(resolver: ResolveInfo): String {
|
||||
return appNamesPrefs?.getString(resolver.activityInfo.packageName, resolver.loadLabel(packageManager).toString())!!
|
||||
}
|
||||
|
||||
fun listenScroll(letter: String) {
|
||||
packageList.clear()
|
||||
for (resolver in packageInfoList) {
|
||||
when {
|
||||
letter == "#" -> {
|
||||
if (numberPattern.matcher(appName(resolver).first().uppercase()).matches()) {
|
||||
packageList.add(Packages(resolver.activityInfo.packageName, appName(resolver)))
|
||||
}
|
||||
}
|
||||
alphabetPattern.matcher(letter).matches() -> {
|
||||
if (appName(resolver).first().uppercase() == letter) {
|
||||
packageList.add(Packages(resolver.activityInfo.packageName, appName(resolver)))
|
||||
}
|
||||
}
|
||||
letter == "⠶" -> {
|
||||
if (!numberPattern.matcher(appName(resolver).first().uppercase()).matches() &&
|
||||
!alphabetPattern.matcher(appName(resolver).first().uppercase()).matches()) {
|
||||
packageList.add(Packages(resolver.activityInfo.packageName, appName(resolver)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
appsAdapter?.updateData(packageList.sortedBy { it.appName.lowercase() })
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = AppDrawerBinding.inflate(inflater, container, false)
|
||||
|
||||
settingsPrefs = requireContext().getSharedPreferences(PREFS_SETTINGS, 0)
|
||||
appNamesPrefs = requireContext().getSharedPreferences(PREFS_APP_NAMES, 0)
|
||||
layoutType = settingsPrefs!!.getInt(KEY_APPS_LAYOUT, 0)
|
||||
packageManager = lActivity?.packageManager
|
||||
appsAdapter = AppsAdapter(layoutType, packageManager!!, childFragmentManager, binding.appsCount)
|
||||
letterPreview = binding.appsCount
|
||||
|
||||
binding.appsCount.visibility = if (settingsPrefs!!.getBoolean(KEY_APPS_COUNT, true)) VISIBLE else GONE
|
||||
|
||||
setLayout()
|
||||
fetchApps()
|
||||
getAlphabetItems()
|
||||
setKeyboardPadding()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.reset.setOnClickListener { onResume() }
|
||||
|
||||
binding.moveDown.setOnClickListener {
|
||||
binding.appsList.smoothScrollToPosition(packageList.size - 1)
|
||||
}
|
||||
|
||||
binding.moveUp.setOnClickListener {
|
||||
binding.appsList.smoothScrollToPosition(0)
|
||||
}
|
||||
|
||||
binding.search.setOnClickListener {
|
||||
when (isSearchShown) {
|
||||
true -> closeSearch()
|
||||
false -> openSearch()
|
||||
}
|
||||
}
|
||||
|
||||
binding.searchInput.doOnTextChanged { inputText, _, _, _ ->
|
||||
binding.searchInput.text?.let { binding.searchInput.setSelection(it.length) }
|
||||
filterAppsList(inputText.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
fetchApps()
|
||||
getAlphabetItems()
|
||||
|
||||
binding.appsCount.visibility = if (settingsPrefs!!.getBoolean(KEY_APPS_COUNT, true)) VISIBLE else GONE
|
||||
|
||||
if (settingsPrefs!!.getInt(KEY_APPS_LAYOUT, 0) in 0..1) {
|
||||
appsAdapter?.updateGravity(settingsPrefs!!.getInt(KEY_DRAW_ALIGN, Gravity.CENTER))
|
||||
}
|
||||
|
||||
/* pop up the keyboard */
|
||||
if (settingsPrefs!!.getBoolean(KEY_KEYBOARD_SEARCH, false)) openSearch()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
closeSearch()
|
||||
}
|
||||
|
||||
private fun setLayout() {
|
||||
when (layoutType) {
|
||||
0, 1 -> {
|
||||
binding.appsList.layoutManager = LinearLayoutManager(requireContext())
|
||||
appsAdapter!!.updateGravity(settingsPrefs!!.getInt(KEY_DRAW_ALIGN, Gravity.CENTER))
|
||||
}
|
||||
2 -> binding.appsList.layoutManager = GridLayoutManager(requireContext(), settingsPrefs!!.getInt(KEY_GRID_COLUMNS, DEFAULT_GRID_COLUMNS))
|
||||
}
|
||||
|
||||
/* initialize apps list adapter */
|
||||
binding.appsList.adapter = appsAdapter
|
||||
}
|
||||
|
||||
/* update app list with app and package name */
|
||||
fun fetchApps() {
|
||||
packageInfoList = (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager?.queryIntentActivities(
|
||||
Intent(Intent.ACTION_MAIN, null).addCategory(Intent.CATEGORY_LAUNCHER),
|
||||
PackageManager.ResolveInfoFlags.of(0)
|
||||
)
|
||||
} else {
|
||||
(packageManager?.queryIntentActivities(
|
||||
Intent(Intent.ACTION_MAIN, null).addCategory(Intent.CATEGORY_LAUNCHER), 0))
|
||||
})?.apply {
|
||||
removeIf { it.activityInfo.packageName.equals(BuildConfig.APPLICATION_ID) }
|
||||
sortWith(ResolveInfo.DisplayNameComparator(packageManager))
|
||||
}!!
|
||||
|
||||
/* add package and app names to the list */
|
||||
packageList.clear()
|
||||
for (resolver in packageInfoList) {
|
||||
packageList.add(Packages(resolver.activityInfo.packageName, appName(resolver)))
|
||||
}
|
||||
|
||||
when {
|
||||
packageList.size < 1 -> return
|
||||
else -> appsAdapter?.updateData(packageList.sortedBy { it.appName.lowercase() })
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAlphabetItems() {
|
||||
// settingsPrefs!!.getInt(KEY_SCROLLBAR_HEIGHT, DEFAULT_SCROLLBAR_HEIGHT).let { height: Int ->
|
||||
// if (height == 0) { binding.alphabets.visibility = GONE }
|
||||
// else {
|
||||
// binding.alphabets.apply {
|
||||
// if (visibility == GONE) visibility = VISIBLE
|
||||
// updateLayoutParams { this.height = height }
|
||||
// }
|
||||
// alphabetList.clear()
|
||||
// for (mPackage in packageList) {
|
||||
// mPackage.appName.first().uppercase().let { firstLetter: String ->
|
||||
// when {
|
||||
// numberPattern.matcher(firstLetter).matches() -> alphabetList.add(0, "#")
|
||||
// alphabetPattern.matcher(firstLetter).matches() -> alphabetList.add(firstLetter)
|
||||
// !numberPattern.matcher(firstLetter).matches() &&
|
||||
// !alphabetPattern.matcher(firstLetter).matches() -> alphabetList.add(alphabetList.size,"⠶")
|
||||
// else -> {}
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// binding.alphabets.invalidate()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private fun filterAppsList(searchString: String) {
|
||||
/* check each app name and add if it matches the search string */
|
||||
packageList.clear()
|
||||
for (resolver in packageInfoList) {
|
||||
appName(resolver).let {
|
||||
if (normalize(it).contains(searchString)) {
|
||||
packageList.add(Packages(resolver.activityInfo.packageName, it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (packageList.size == 1 && settingsPrefs!!.getBoolean(KEY_QUICK_LAUNCH, true)) {
|
||||
var dialog = AlertDialog.Builder(requireContext())
|
||||
dialog.setTitle("앱 실행 확인")
|
||||
dialog.setMessage("${searchString} 검색 결과 '${packageList[0].appName}' 준비됨")
|
||||
dialog.setCancelable(false)
|
||||
dialog.setOnCancelListener {
|
||||
binding.searchInput.setText("")
|
||||
it.dismiss()
|
||||
}
|
||||
dialog.setPositiveButton("실행") { s,d ->
|
||||
startActivity(packageManager?.getLaunchIntentForPackage(packageList[0].packageName))
|
||||
s.dismiss()
|
||||
binding.searchInput.setText("")
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
else appsAdapter?.updateData(packageList.sortedBy { it.appName.lowercase() })
|
||||
}
|
||||
|
||||
private fun normalize(str: String): String {
|
||||
val normalizedString =
|
||||
Normalizer.normalize(str.replace("\\W".toRegex(), ""), Normalizer.Form.NFD)
|
||||
val pattern = Pattern.compile("\\p{InCombiningDiacriticalMarks}+")
|
||||
return pattern.matcher(normalizedString).replaceAll("").lowercase()
|
||||
}
|
||||
|
||||
private fun openSearch() {
|
||||
isSearchShown = true
|
||||
binding.search.setImageResource(R.drawable.ic_close)
|
||||
binding.searchInput.apply {
|
||||
visibility = VISIBLE
|
||||
requestFocus()
|
||||
let {
|
||||
(lActivity!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
|
||||
.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* clear search string, hide keyboard and search box */
|
||||
private fun closeSearch() {
|
||||
isSearchShown = false
|
||||
binding.search.setImageResource(R.drawable.ic_search)
|
||||
binding.searchInput.apply {
|
||||
text?.clear()
|
||||
visibility = GONE
|
||||
let {
|
||||
(lActivity!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
|
||||
.hideSoftInputFromWindow(it.windowToken, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setKeyboardPadding() {
|
||||
binding.root.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
val rect = Rect()
|
||||
binding.root.getWindowVisibleDisplayFrame(rect)
|
||||
val screenHeight = binding.root.height
|
||||
val keyboardHeight = screenHeight - (rect.bottom - rect.top)
|
||||
|
||||
when {
|
||||
keyboardHeight > screenHeight * 0.15 -> {
|
||||
if (!isKeyboardShowing &&
|
||||
!settingsPrefs!!.getBoolean(KEY_STATUS_BAR, false)) {
|
||||
isKeyboardShowing = true
|
||||
binding.root.setPadding(0, 0, 0, keyboardHeight)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (isKeyboardShowing) {
|
||||
isKeyboardShowing = false
|
||||
binding.root.setPadding(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.apps
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityOptions
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Rect
|
||||
import android.icu.text.SimpleDateFormat
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.button.MaterialButtonToggleGroup
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
|
||||
import rasel.lunar.launcher.R
|
||||
import rasel.lunar.launcher.apps.AppDrawer.Companion.appNamesPrefs
|
||||
import rasel.lunar.launcher.databinding.ActivityBrowserDialogBinding
|
||||
import rasel.lunar.launcher.databinding.AppInfoDialogBinding
|
||||
import rasel.lunar.launcher.databinding.AppMenuBinding
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APP_NO_
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.MAX_FAVORITE_APPS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_FAVORITE_APPS
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.copyToClipboard
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.screenHeight
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.screenWidth
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
|
||||
internal class AppMenu : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var binding: AppMenuBinding
|
||||
private lateinit var packageName: String
|
||||
private lateinit var packageManager: PackageManager
|
||||
private lateinit var appInfo: ApplicationInfo
|
||||
private lateinit var defAppName: String
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = AppMenuBinding.inflate(inflater, container, false)
|
||||
|
||||
/* get package name from fragment's tag */
|
||||
packageName = tag.toString()
|
||||
packageManager = requireContext().packageManager
|
||||
|
||||
/* get application info */
|
||||
appInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getApplicationInfo(packageName,
|
||||
PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()))
|
||||
} else {
|
||||
packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
|
||||
}
|
||||
|
||||
/* get default app name */
|
||||
defAppName = packageManager.resolveActivity(Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
.setPackage(packageName), 0)?.loadLabel(packageManager).toString()
|
||||
|
||||
/* set application name and package name */
|
||||
binding.appName.apply {
|
||||
setText(appNamesPrefs?.getString(packageName, defAppName))
|
||||
hint = defAppName
|
||||
|
||||
}
|
||||
binding.appPackage.text = packageName
|
||||
/* favorite apps */
|
||||
favoriteApps()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
(requireDialog() as BottomSheetDialog).dismissWithAnimation = true
|
||||
|
||||
/* copy package name */
|
||||
binding.appPackage.setOnClickListener {
|
||||
copyToClipboard(requireContext(), packageName)
|
||||
}
|
||||
|
||||
appName()
|
||||
binding.detailedInfo.setOnClickListener { detailedInfo() }
|
||||
binding.activityBrowser.setOnClickListener { activityBrowser() }
|
||||
binding.appStore.setOnClickListener { appStore() }
|
||||
binding.appFreeform.setOnClickListener { freeform() }
|
||||
binding.appInfo.setOnClickListener { appInfo() }
|
||||
binding.appShare.setOnClickListener { share() }
|
||||
binding.appUninstall.setOnClickListener { uninstall() }
|
||||
}
|
||||
|
||||
/* manage initial preview and clicks for favorite apps */
|
||||
@SuppressLint("PrivateResource")
|
||||
private fun favoriteApps() {
|
||||
val sharedPreferences = requireContext().getSharedPreferences(PREFS_FAVORITE_APPS, 0)
|
||||
val enabledStroke =
|
||||
ColorStateList.valueOf(requireContext().getColor(com.google.android.material.R.color.material_on_surface_stroke))
|
||||
val disabledStroke =
|
||||
ColorStateList.valueOf(requireContext().getColor(com.google.android.material.R.color.m3_chip_stroke_color))
|
||||
|
||||
for (position in 1..MAX_FAVORITE_APPS) {
|
||||
val button = outlinedButton
|
||||
val savedPackageName = sharedPreferences.getString(KEY_APP_NO_ + position, "")
|
||||
|
||||
/* set previews */
|
||||
if (packageName == savedPackageName) button.isChecked = true
|
||||
if (savedPackageName?.isNotEmpty() == true) button.strokeColor = enabledStroke
|
||||
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
packageManager.getPackageInfo(savedPackageName!!, PackageManager.PackageInfoFlags.of(0))
|
||||
else
|
||||
packageManager.getPackageInfo(savedPackageName!!, 0)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
requireContext().getSharedPreferences(PREFS_FAVORITE_APPS, 0)
|
||||
.edit().remove(KEY_APP_NO_ + position).apply()
|
||||
button.strokeColor = disabledStroke
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
/* listen on clicks */
|
||||
binding.favGroup.addOnButtonCheckedListener { _: MaterialButtonToggleGroup?,
|
||||
checkedId: Int, isChecked: Boolean ->
|
||||
try {
|
||||
if (checkedId == button.id) {
|
||||
if (isChecked) {
|
||||
requireContext().getSharedPreferences(PREFS_FAVORITE_APPS, 0)
|
||||
.edit().putString(KEY_APP_NO_ + position, packageName).apply()
|
||||
button.strokeColor = enabledStroke
|
||||
} else {
|
||||
requireContext().getSharedPreferences(PREFS_FAVORITE_APPS, 0)
|
||||
.edit().remove(KEY_APP_NO_ + position).apply()
|
||||
button.strokeColor = disabledStroke
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun appName() {
|
||||
binding.appName.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) binding.appName.minWidth = resources.getDimensionPixelOffset(R.dimen.twoSeventySix)
|
||||
else {
|
||||
(requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
|
||||
.hideSoftInputFromWindow(binding.appName.windowToken, 0)
|
||||
|
||||
binding.appName.apply {
|
||||
minWidth = resources.getDimensionPixelOffset(R.dimen.zero)
|
||||
|
||||
if (text!!.isBlank()) setText(defAppName)
|
||||
else setText(text!!.trim())
|
||||
|
||||
if (text.toString() == defAppName) appNamesPrefs?.edit()!!.remove(packageName).apply()
|
||||
else appNamesPrefs?.edit()!!.putString(packageName, text.toString()).apply()
|
||||
|
||||
(requireParentFragment() as AppDrawer).fetchApps()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.appName.setOnKeyListener { _, keyCode, event ->
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
binding.appName.clearFocus()
|
||||
return@setOnKeyListener true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/* detailed info dialog */
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun detailedInfo() {
|
||||
val dialogBinding = AppInfoDialogBinding.inflate(lActivity!!.layoutInflater)
|
||||
MaterialAlertDialogBuilder(lActivity!!)
|
||||
.setView(dialogBinding.root)
|
||||
.setPositiveButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
|
||||
/* show app name */
|
||||
dialogBinding.appName.text = packageManager.getApplicationLabel(appInfo)
|
||||
|
||||
/* get package info */
|
||||
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
|
||||
} else {
|
||||
packageManager.getPackageInfo(packageName, 0)
|
||||
}
|
||||
|
||||
/* show infos */
|
||||
dialogBinding.mixed.text =
|
||||
"${resources.getString(R.string.version)}: ${packageInfo.versionName} (${PackageInfoCompat.getLongVersionCode(packageInfo).toInt()})\n" +
|
||||
"${resources.getString(R.string.sdk)}: ${appInfo.minSdkVersion} ~ ${appInfo.targetSdkVersion}\n" +
|
||||
"${resources.getString(R.string.uid)}: ${appInfo.uid}\n" +
|
||||
"${resources.getString(R.string.first_install)}: ${dateTimeFormat(packageInfo.firstInstallTime)}\n" +
|
||||
"${resources.getString(R.string.last_update)}: ${dateTimeFormat(packageInfo.lastUpdateTime)}"
|
||||
|
||||
/* show permissions */
|
||||
dialogBinding.permissions.text = permissionsList
|
||||
}
|
||||
|
||||
/* activity browser dialog */
|
||||
private fun activityBrowser() {
|
||||
val dialogBinding = ActivityBrowserDialogBinding.inflate(lActivity!!.layoutInflater)
|
||||
val dialogBuilder = MaterialAlertDialogBuilder(lActivity!!)
|
||||
.setView(dialogBinding.root)
|
||||
.setPositiveButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
|
||||
/* show app name */
|
||||
dialogBinding.appName.text = packageManager.getApplicationLabel(appInfo)
|
||||
|
||||
/* get activity info */
|
||||
val activityInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getPackageInfo(
|
||||
packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_ACTIVITIES.toLong())
|
||||
)
|
||||
} else {
|
||||
packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
|
||||
}
|
||||
|
||||
/* show activity list */
|
||||
val activityAdapter: ArrayAdapter<String> =
|
||||
ArrayAdapter(requireContext(), R.layout.list_item, R.id.itemText, ArrayList())
|
||||
if (activityInfo.activities.isNotEmpty()) {
|
||||
for (activity in activityInfo.activities) {
|
||||
activityAdapter.add(
|
||||
activity.toString().split(" ").toTypedArray()[1].replace("}", "")
|
||||
)
|
||||
}
|
||||
dialogBinding.activityList.adapter = activityAdapter
|
||||
}
|
||||
|
||||
/* listen item clicks */
|
||||
dialogBinding.activityList.onItemClickListener =
|
||||
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, i: Int, _: Long ->
|
||||
try {
|
||||
/* open activity */
|
||||
val intent = Intent()
|
||||
intent.component = ComponentName(packageName, activityAdapter.getItem(i).toString())
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
requireContext().startActivity(intent)
|
||||
} catch (exception: Exception) {
|
||||
/* couldn't open activity */
|
||||
exception.printStackTrace()
|
||||
val exceptionShort = (exception.toString().split(": ").toTypedArray())[0]
|
||||
Toast.makeText(requireContext(),
|
||||
"${resources.getString(R.string.unable_to_launch)} -\n$exceptionShort", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
dialogBuilder.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
/* open app's page in app store/market */
|
||||
private fun appStore() {
|
||||
try {
|
||||
val storeIntent = Intent(Intent.ACTION_VIEW)
|
||||
storeIntent.data = Uri.parse("market://details?id=$packageName")
|
||||
storeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
requireContext().startActivity(storeIntent)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
/* no app store found exception */
|
||||
Toast.makeText(requireContext(), requireContext().getString(R.string.null_app_store_message),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
activityNotFoundException.printStackTrace()
|
||||
}
|
||||
this.dismiss()
|
||||
}
|
||||
|
||||
/* launch app as a freeform window */
|
||||
private fun freeform() {
|
||||
val freeformIntent = requireContext().packageManager.getLaunchIntentForPackage(packageName)
|
||||
freeformIntent!!.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT or
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
val rect = Rect(0, screenHeight / 2, screenWidth, screenHeight)
|
||||
var activityOptions = activityOptions
|
||||
activityOptions = activityOptions.setLaunchBounds(rect)
|
||||
requireContext().startActivity(freeformIntent, activityOptions.toBundle())
|
||||
this.dismiss()
|
||||
}
|
||||
|
||||
/* open android's app info screen */
|
||||
private fun appInfo() {
|
||||
val infoIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
infoIntent.data = Uri.parse("package:$packageName")
|
||||
infoIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
requireContext().startActivity(infoIntent)
|
||||
this.dismiss()
|
||||
}
|
||||
|
||||
private fun share() {
|
||||
try {
|
||||
// Create a temporary file to copy the APK
|
||||
val apkLabel = packageManager.getApplicationLabel(appInfo).toString().lowercase().replace(" ", "_")
|
||||
val tempApkFile = File(requireContext().externalCacheDir, "$apkLabel.apk")
|
||||
|
||||
// Copy the APK file
|
||||
FileInputStream(File(appInfo.sourceDir)).use { `in` ->
|
||||
FileOutputStream(tempApkFile).use { out ->
|
||||
val buffer = ByteArray(1024)
|
||||
var length: Int
|
||||
while (`in`.read(buffer).also { length = it } > 0) {
|
||||
out.write(buffer, 0, length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a content URI using FileProvider
|
||||
val contentUri =
|
||||
FileProvider.getUriForFile(requireContext(), "${requireContext().packageName}.fileprovider", tempApkFile)
|
||||
|
||||
//requireContext().grantUriPermission(receivers.package.name, contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
// Create a Share Intent
|
||||
Intent(Intent.ACTION_SEND).apply {
|
||||
type = "application/vnd.android.package-archive"
|
||||
putExtra(Intent.EXTRA_STREAM, contentUri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}.let {
|
||||
// Start the chooser activity
|
||||
startActivity(Intent.createChooser(it, getString(R.string.share_apk_message)))
|
||||
}
|
||||
}
|
||||
catch (e: PackageManager.NameNotFoundException) { e.printStackTrace() }
|
||||
catch (e: IOException) { e.printStackTrace() }
|
||||
this.dismiss()
|
||||
}
|
||||
|
||||
/* uninstall the app */
|
||||
private fun uninstall() {
|
||||
val uninstallIntent = Intent(Intent.ACTION_DELETE)
|
||||
uninstallIntent.data = Uri.parse("package:$packageName")
|
||||
uninstallIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
requireContext().startActivity(uninstallIntent)
|
||||
this.dismiss()
|
||||
}
|
||||
|
||||
/* create and add an outlined button to the toggle group */
|
||||
private val outlinedButton: MaterialButton get() {
|
||||
val style = com.google.android.material.R.attr.materialButtonOutlinedStyle
|
||||
val button = MaterialButton(requireContext(), null, style)
|
||||
button.layoutParams = LinearLayoutCompat.LayoutParams(
|
||||
LinearLayoutCompat.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayoutCompat.LayoutParams.WRAP_CONTENT, 1F
|
||||
)
|
||||
binding.favGroup.addView(button)
|
||||
return button
|
||||
}
|
||||
|
||||
/* long value to local date-time format */
|
||||
private fun dateTimeFormat(long: Long) : String = SimpleDateFormat.getDateTimeInstance().format(Date(long))
|
||||
|
||||
/* get and arrange all the permissions for an application */
|
||||
private val permissionsList : String get() {
|
||||
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
|
||||
} else {
|
||||
packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS)
|
||||
}
|
||||
|
||||
return if (packageInfo.requestedPermissions.isNotEmpty()) {
|
||||
val stringBuilder = StringBuilder()
|
||||
packageInfo.requestedPermissions.indices.forEach { i: Int ->
|
||||
if (i != packageInfo.requestedPermissions.size - 1)
|
||||
stringBuilder.append("${packageInfo.requestedPermissions[i]}\n\n")
|
||||
/* don't add any new line after the last entry */
|
||||
else
|
||||
stringBuilder.append(packageInfo.requestedPermissions[i])
|
||||
}
|
||||
stringBuilder.toString()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/* get activity options for launching app in freeform mode */
|
||||
private val activityOptions: ActivityOptions get() {
|
||||
val activityOptions = ActivityOptions.makeBasic()
|
||||
try {
|
||||
val method =
|
||||
ActivityOptions::class.java.getMethod("setLaunchWindowingMode", Int::class.javaPrimitiveType)
|
||||
method.invoke(activityOptions, 5)
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
return activityOptions
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.apps
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
|
||||
import rasel.lunar.launcher.R
|
||||
import rasel.lunar.launcher.apps.IconPackManager.Companion.getDrawableIconForPackage
|
||||
import rasel.lunar.launcher.databinding.AppsChildBinding
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.dpToPx
|
||||
|
||||
|
||||
internal class AppsAdapter(
|
||||
private val layoutType: Int,
|
||||
private val packageManager: PackageManager,
|
||||
private val fragmentManager: FragmentManager,
|
||||
private val appsCount: MaterialTextView) : RecyclerView.Adapter<AppsAdapter.AppsViewHolder>() {
|
||||
|
||||
private var oldList = mutableListOf<Packages>()
|
||||
private var appGravity: Int = Gravity.CENTER
|
||||
|
||||
companion object {
|
||||
@JvmStatic var appsSize: Int? = null
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): AppsViewHolder =
|
||||
AppsViewHolder(AppsChildBinding.inflate(LayoutInflater.from(viewGroup.context), viewGroup, false))
|
||||
|
||||
override fun onBindViewHolder(holder: AppsViewHolder, i: Int) {
|
||||
val item = oldList[i]
|
||||
val fourDp = dpToPx(lActivity!!, R.dimen.four)
|
||||
val eightDp = dpToPx(lActivity!!, R.dimen.eight)
|
||||
val twelveDp = dpToPx(lActivity!!, R.dimen.twelve)
|
||||
val sixteenDp = dpToPx(lActivity!!, R.dimen.sixteen)
|
||||
|
||||
holder.view.apply {
|
||||
childTextview.text = item.appName
|
||||
|
||||
when (layoutType) {
|
||||
0 -> {
|
||||
appIcon.visibility = View.GONE
|
||||
appIconTwo.visibility = View.GONE
|
||||
childTextview.apply {
|
||||
gravity = appGravity
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, lActivity!!.resources.getDimension(R.dimen.twentyTwo))
|
||||
}
|
||||
root.setPadding(sixteenDp, fourDp, sixteenDp, fourDp)
|
||||
}
|
||||
1 -> {
|
||||
appIcon.visibility = View.GONE
|
||||
appIconTwo.setImageDrawable(getDrawableIconForPackage(item.packageName, packageManager.getApplicationIcon(item.packageName)))
|
||||
childTextview.apply {
|
||||
gravity = appGravity or Gravity.CENTER_VERTICAL
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, lActivity!!.resources.getDimension(R.dimen.twenty))
|
||||
updatePadding(left = twelveDp)
|
||||
}
|
||||
root.setPadding(sixteenDp, eightDp, sixteenDp, eightDp)
|
||||
}
|
||||
2 -> {
|
||||
appIconTwo.visibility = View.GONE
|
||||
appIcon.setImageDrawable(getDrawableIconForPackage(item.packageName, packageManager.getApplicationIcon(item.packageName)))
|
||||
childTextview.apply {
|
||||
gravity = Gravity.CENTER
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, lActivity!!.resources.getDimension(R.dimen.twelve))
|
||||
}
|
||||
root.setPadding(eightDp, eightDp, eightDp, eightDp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
holder.view.root.apply {
|
||||
/* on click - open app */
|
||||
setOnClickListener {
|
||||
context.startActivity(packageManager.getLaunchIntentForPackage(item.packageName))
|
||||
}
|
||||
|
||||
/* on long click - open app menu */
|
||||
setOnLongClickListener {
|
||||
AppMenu().show(fragmentManager, item.packageName)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = oldList.size
|
||||
|
||||
inner class AppsViewHolder(var view: AppsChildBinding) : RecyclerView.ViewHolder(view.root)
|
||||
|
||||
/* update app list */
|
||||
fun updateData(newList: List<Packages>) {
|
||||
val diffUtilResult = DiffUtil.calculateDiff(AppsDiffUtil(oldList, newList))
|
||||
|
||||
oldList.clear()
|
||||
oldList.addAll(newList)
|
||||
diffUtilResult.dispatchUpdatesTo(this)
|
||||
|
||||
newList.size.let {
|
||||
appsCount.text = it.toString()
|
||||
appsSize = it
|
||||
}
|
||||
}
|
||||
|
||||
/* update text gravity (alignment) */
|
||||
@SuppressLint("RtlHardcoded", "NotifyDataSetChanged")
|
||||
fun updateGravity(gravity: Int){
|
||||
/* the first check is to avoid calling notifyDataSetChanged() everytime */
|
||||
if (gravity != appGravity &&
|
||||
(gravity == Gravity.LEFT || gravity == Gravity.CENTER || gravity == Gravity.RIGHT)) {
|
||||
appGravity = gravity
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal data class Packages (
|
||||
val packageName: String,
|
||||
val appName: String
|
||||
)
|
||||
|
||||
internal class AppsDiffUtil(
|
||||
private val oldList: List<Packages>, private val newList: List<Packages>
|
||||
) : DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize(): Int = oldList.size
|
||||
override fun getNewListSize(): Int = newList.size
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
oldList[oldItemPosition].packageName == newList[newItemPosition].packageName
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
oldList[oldItemPosition] == newList[newItemPosition]
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.apps
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_ICON_PACK
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_ICON_PACK
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
|
||||
import rasel.lunar.launcher.utils.BLog
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
internal class IconPackManager {
|
||||
|
||||
@SuppressLint("DiscouragedApi")
|
||||
companion object {
|
||||
|
||||
private val settingsPrefs = lActivity!!.getSharedPreferences(PREFS_SETTINGS, 0)
|
||||
private val packageName = settingsPrefs.getString(KEY_ICON_PACK, DEFAULT_ICON_PACK)
|
||||
private var loaded = false
|
||||
private val packagesDrawables = HashMap<String?, String?>()
|
||||
private val backImages: MutableList<Bitmap> = ArrayList()
|
||||
private var maskImage: Bitmap? = null
|
||||
private var frontImage: Bitmap? = null
|
||||
private var factor = 1.0f
|
||||
private var totalIcons = 0
|
||||
private var iconPackRes: Resources? = null
|
||||
|
||||
private fun load() {
|
||||
/* load appfilter.xml from the icon pack package */
|
||||
try {
|
||||
var xpp: XmlPullParser? = null
|
||||
iconPackRes = lActivity!!.packageManager.getResourcesForApplication(packageName!!)
|
||||
val appFilterId = iconPackRes!!.getIdentifier("appfilter", "xml", packageName)
|
||||
if (appFilterId > 0) {
|
||||
xpp = iconPackRes!!.getXml(appFilterId)
|
||||
} else {
|
||||
/* no resource found, try to open it from assets folder */
|
||||
try {
|
||||
xpp = XmlPullParserFactory.newInstance().apply { isNamespaceAware = true }
|
||||
.newPullParser().apply { setInput(iconPackRes!!.assets.open("appfilter.xml"), "utf-8") }
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
BLog.w("", "Couldn't find the appfilter.xml file")
|
||||
}
|
||||
}
|
||||
if (xpp != null) {
|
||||
var eventType = xpp.eventType
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
when (xpp.name) {
|
||||
"iconback" -> {
|
||||
for (i in 0 until xpp.attributeCount) {
|
||||
if (xpp.getAttributeName(i).startsWith("img")) {
|
||||
loadBitmap(xpp.getAttributeValue(i))?.let { backImages.add(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
"iconmask" -> {
|
||||
if (xpp.attributeCount > 0 && xpp.getAttributeName(0) == "img1") {
|
||||
maskImage = loadBitmap(xpp.getAttributeValue(0))
|
||||
}
|
||||
}
|
||||
"iconupon" -> {
|
||||
if (xpp.attributeCount > 0 && xpp.getAttributeName(0) == "img1") {
|
||||
frontImage = loadBitmap(xpp.getAttributeValue(0))
|
||||
}
|
||||
}
|
||||
"scale" -> {
|
||||
if (xpp.attributeCount > 0 && xpp.getAttributeName(0) == "factor") {
|
||||
factor = java.lang.Float.valueOf(xpp.getAttributeValue(0))
|
||||
}
|
||||
}
|
||||
"item" -> {
|
||||
var componentName: String? = null
|
||||
var drawableName: String? = null
|
||||
for (i in 0 until xpp.attributeCount) {
|
||||
when (xpp.getAttributeName(i)) {
|
||||
"component" -> componentName = xpp.getAttributeValue(i)
|
||||
"drawable" -> drawableName = xpp.getAttributeValue(i)
|
||||
}
|
||||
}
|
||||
if (!packagesDrawables.containsKey(componentName)) {
|
||||
packagesDrawables[componentName] = drawableName
|
||||
totalIcons += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
eventType = xpp.next()
|
||||
}
|
||||
}
|
||||
loaded = true
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
BLog.w("", "Failed to load the icon pack")
|
||||
} catch (e: XmlPullParserException) {
|
||||
BLog.w("", "Failed to parse the appfilter.xml file")
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadBitmap(drawableName: String): Bitmap? {
|
||||
iconPackRes!!.getIdentifier(drawableName, "drawable", packageName).let { id ->
|
||||
if (id > 0) {
|
||||
ResourcesCompat.getDrawable(iconPackRes!!, id, null).let {
|
||||
if (it is BitmapDrawable) return it.bitmap
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun loadDrawable(drawableName: String): Drawable? {
|
||||
iconPackRes!!.getIdentifier(drawableName, "drawable", packageName).let {
|
||||
return if (it > 0) ResourcesCompat.getDrawable(iconPackRes!!, it, null)
|
||||
else null
|
||||
}
|
||||
}
|
||||
|
||||
fun getDrawableIconForPackage(appPackageName: String?, defaultDrawable: Drawable?): Drawable? {
|
||||
when (packageName) {
|
||||
DEFAULT_ICON_PACK -> return defaultDrawable
|
||||
else -> {
|
||||
if (!loaded) load()
|
||||
var componentName: String? = null
|
||||
if (lActivity!!.packageManager.getLaunchIntentForPackage(appPackageName!!) != null) {
|
||||
componentName = lActivity!!.packageManager.getLaunchIntentForPackage(appPackageName)!!.component.toString()
|
||||
}
|
||||
var drawable = packagesDrawables[componentName]
|
||||
if (!drawable.isNullOrEmpty()) return loadDrawable(drawable)
|
||||
else {
|
||||
/* try to get a resource with the component filename */
|
||||
if (!componentName.isNullOrEmpty()) {
|
||||
val start = componentName.indexOf("{") + 1
|
||||
val end = componentName.indexOf("}", start)
|
||||
if (end > start) {
|
||||
drawable = componentName.substring(start, end).lowercase(Locale.getDefault()).replace(".", "_").replace("/", "_")
|
||||
try {
|
||||
if (iconPackRes!!.getIdentifier(drawable, "drawable", packageName) > 0) return loadDrawable(drawable)
|
||||
} catch (e: NullPointerException) {
|
||||
settingsPrefs.edit().putString(KEY_ICON_PACK, DEFAULT_ICON_PACK).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultDrawable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,705 @@
|
||||
package rasel.lunar.launcher.apps
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.View.OnTouchListener
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class GestureAnalyser @JvmOverloads constructor(
|
||||
swipeSlopeIntolerance: Int = 3,
|
||||
doubleTapMaxDelayMillis: Int = 500,
|
||||
doubleTapMaxDownMillis: Int = 100
|
||||
) {
|
||||
private val initialX = DoubleArray(5)
|
||||
private val initialY = DoubleArray(5)
|
||||
private val finalX = DoubleArray(5)
|
||||
private val finalY = DoubleArray(5)
|
||||
private val currentX = DoubleArray(5)
|
||||
private val currentY = DoubleArray(5)
|
||||
private val delX = DoubleArray(5)
|
||||
private val delY = DoubleArray(5)
|
||||
|
||||
private var numFingers = 0
|
||||
private var initialT: Long = 0
|
||||
private var finalT: Long = 0
|
||||
private var currentT: Long = 0
|
||||
|
||||
private var prevInitialT: Long = 0
|
||||
private var prevFinalT: Long = 0
|
||||
|
||||
private var swipeSlopeIntolerance = 3
|
||||
|
||||
private val doubleTapMaxDelayMillis: Long
|
||||
private val doubleTapMaxDownMillis: Long
|
||||
|
||||
init {
|
||||
this.swipeSlopeIntolerance = swipeSlopeIntolerance
|
||||
this.doubleTapMaxDownMillis = doubleTapMaxDownMillis.toLong()
|
||||
this.doubleTapMaxDelayMillis = doubleTapMaxDelayMillis.toLong()
|
||||
}
|
||||
|
||||
fun trackGesture(ev: MotionEvent) {
|
||||
val n = ev.pointerCount
|
||||
for (i in 0 until n) {
|
||||
initialX[i] = ev.getX(i).toDouble()
|
||||
initialY[i] = ev.getY(i).toDouble()
|
||||
}
|
||||
numFingers = n
|
||||
initialT = SystemClock.uptimeMillis()
|
||||
}
|
||||
|
||||
fun untrackGesture() {
|
||||
numFingers = 0
|
||||
prevFinalT = SystemClock.uptimeMillis()
|
||||
prevInitialT = initialT
|
||||
}
|
||||
|
||||
fun getGesture(ev: MotionEvent): GestureType {
|
||||
var averageDistance = 0.0
|
||||
for (i in 0 until numFingers) {
|
||||
finalX[i] = ev.getX(i).toDouble()
|
||||
finalY[i] = ev.getY(i).toDouble()
|
||||
delX[i] = finalX[i] - initialX[i]
|
||||
delY[i] = finalY[i] - initialY[i]
|
||||
|
||||
averageDistance += sqrt(
|
||||
(finalX[i] - initialX[i]).pow(2.0) + (finalY[i] - initialY[i]).pow(
|
||||
2.0
|
||||
)
|
||||
)
|
||||
}
|
||||
averageDistance /= numFingers.toDouble()
|
||||
|
||||
finalT = SystemClock.uptimeMillis()
|
||||
val gt = GestureType()
|
||||
gt.gestureFlag = calcGesture()
|
||||
gt.gestureDuration = finalT - initialT
|
||||
gt.gestureDistance = averageDistance
|
||||
return gt
|
||||
}
|
||||
|
||||
fun getOngoingGesture(ev: MotionEvent): Int {
|
||||
for (i in 0 until numFingers) {
|
||||
currentX[i] = ev.getX(i).toDouble()
|
||||
currentY[i] = ev.getY(i).toDouble()
|
||||
delX[i] = finalX[i] - initialX[i]
|
||||
delY[i] = finalY[i] - initialY[i]
|
||||
}
|
||||
currentT = SystemClock.uptimeMillis()
|
||||
return calcGesture()
|
||||
}
|
||||
|
||||
private fun calcGesture(): Int {
|
||||
if (isDoubleTap) {
|
||||
return DOUBLE_TAP_1
|
||||
}
|
||||
|
||||
if (numFingers == 1) {
|
||||
if ((-(delY[0])) > (swipeSlopeIntolerance * (abs(
|
||||
delX[0]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_1_UP
|
||||
}
|
||||
|
||||
if (((delY[0])) > (swipeSlopeIntolerance * (abs(
|
||||
delX[0]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_1_DOWN
|
||||
}
|
||||
|
||||
if ((-(delX[0])) > (swipeSlopeIntolerance * (abs(
|
||||
delY[0]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_1_LEFT
|
||||
}
|
||||
|
||||
if (((delX[0])) > (swipeSlopeIntolerance * (abs(
|
||||
delY[0]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_1_RIGHT
|
||||
}
|
||||
}
|
||||
if (numFingers == 2) {
|
||||
if (((-delY[0]) > (swipeSlopeIntolerance * abs(
|
||||
delX[0]
|
||||
))) && ((-delY[1]) > (swipeSlopeIntolerance * abs(
|
||||
delX[1]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_2_UP
|
||||
}
|
||||
if (((delY[0]) > (swipeSlopeIntolerance * abs(
|
||||
delX[0]
|
||||
))) && ((delY[1]) > (swipeSlopeIntolerance * abs(
|
||||
delX[1]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_2_DOWN
|
||||
}
|
||||
if (((-delX[0]) > (swipeSlopeIntolerance * abs(
|
||||
delY[0]
|
||||
))) && ((-delX[1]) > (swipeSlopeIntolerance * abs(
|
||||
delY[1]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_2_LEFT
|
||||
}
|
||||
if (((delX[0]) > (swipeSlopeIntolerance * abs(
|
||||
delY[0]
|
||||
))) && ((delX[1]) > (swipeSlopeIntolerance * abs(
|
||||
delY[1]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_2_RIGHT
|
||||
}
|
||||
if (finalFingDist(0, 1) > 2 * (initialFingDist(0, 1))) {
|
||||
return UNPINCH_2
|
||||
}
|
||||
if (finalFingDist(0, 1) < 0.5 * (initialFingDist(0, 1))) {
|
||||
return PINCH_2
|
||||
}
|
||||
}
|
||||
if (numFingers == 3) {
|
||||
if (((-delY[0]) > (swipeSlopeIntolerance * abs(
|
||||
delX[0]
|
||||
)))
|
||||
&& ((-delY[1]) > (swipeSlopeIntolerance * abs(
|
||||
delX[1]
|
||||
)))
|
||||
&& ((-delY[2]) > (swipeSlopeIntolerance * abs(
|
||||
delX[2]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_3_UP
|
||||
}
|
||||
if (((delY[0]) > (swipeSlopeIntolerance * abs(
|
||||
delX[0]
|
||||
)))
|
||||
&& ((delY[1]) > (swipeSlopeIntolerance * abs(
|
||||
delX[1]
|
||||
)))
|
||||
&& ((delY[2]) > (swipeSlopeIntolerance * abs(
|
||||
delX[2]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_3_DOWN
|
||||
}
|
||||
if (((-delX[0]) > (swipeSlopeIntolerance * abs(
|
||||
delY[0]
|
||||
)))
|
||||
&& ((-delX[1]) > (swipeSlopeIntolerance * abs(
|
||||
delY[1]
|
||||
)))
|
||||
&& ((-delX[2]) > (swipeSlopeIntolerance * abs(
|
||||
delY[2]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_3_LEFT
|
||||
}
|
||||
if (((delX[0]) > (swipeSlopeIntolerance * abs(
|
||||
delY[0]
|
||||
)))
|
||||
&& ((delX[1]) > (swipeSlopeIntolerance * abs(
|
||||
delY[1]
|
||||
)))
|
||||
&& ((delX[2]) > (swipeSlopeIntolerance * abs(
|
||||
delY[2]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_3_RIGHT
|
||||
}
|
||||
|
||||
if ((finalFingDist(0, 1) > 1.75 * (initialFingDist(0, 1)))
|
||||
&& (finalFingDist(1, 2) > 1.75 * (initialFingDist(1, 2)))
|
||||
&& (finalFingDist(2, 0) > 1.75 * (initialFingDist(2, 0)))
|
||||
) {
|
||||
return UNPINCH_3
|
||||
}
|
||||
if ((finalFingDist(0, 1) < 0.66 * (initialFingDist(0, 1)))
|
||||
&& (finalFingDist(1, 2) < 0.66 * (initialFingDist(1, 2)))
|
||||
&& (finalFingDist(2, 0) < 0.66 * (initialFingDist(2, 0)))
|
||||
) {
|
||||
return PINCH_3
|
||||
}
|
||||
}
|
||||
if (numFingers == 4) {
|
||||
if (((-delY[0]) > (swipeSlopeIntolerance * abs(
|
||||
delX[0]
|
||||
)))
|
||||
&& ((-delY[1]) > (swipeSlopeIntolerance * abs(
|
||||
delX[1]
|
||||
)))
|
||||
&& ((-delY[2]) > (swipeSlopeIntolerance * abs(
|
||||
delX[2]
|
||||
)))
|
||||
&& ((-delY[3]) > (swipeSlopeIntolerance * abs(
|
||||
delX[3]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_4_UP
|
||||
}
|
||||
if (((delY[0]) > (swipeSlopeIntolerance * abs(
|
||||
delX[0]
|
||||
)))
|
||||
&& ((delY[1]) > (swipeSlopeIntolerance * abs(
|
||||
delX[1]
|
||||
)))
|
||||
&& ((delY[2]) > (swipeSlopeIntolerance * abs(
|
||||
delX[2]
|
||||
)))
|
||||
&& ((delY[3]) > (swipeSlopeIntolerance * abs(
|
||||
delX[3]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_4_DOWN
|
||||
}
|
||||
if (((-delX[0]) > (swipeSlopeIntolerance * abs(
|
||||
delY[0]
|
||||
)))
|
||||
&& ((-delX[1]) > (swipeSlopeIntolerance * abs(
|
||||
delY[1]
|
||||
)))
|
||||
&& ((-delX[2]) > (swipeSlopeIntolerance * abs(
|
||||
delY[2]
|
||||
)))
|
||||
&& ((-delX[3]) > (swipeSlopeIntolerance * abs(
|
||||
delY[3]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_4_LEFT
|
||||
}
|
||||
if (((delX[0]) > (swipeSlopeIntolerance * abs(
|
||||
delY[0]
|
||||
)))
|
||||
&& ((delX[1]) > (swipeSlopeIntolerance * abs(
|
||||
delY[1]
|
||||
)))
|
||||
&& ((delX[2]) > (swipeSlopeIntolerance * abs(
|
||||
delY[2]
|
||||
)))
|
||||
&& ((delX[3]) > (swipeSlopeIntolerance * abs(
|
||||
delY[3]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_4_RIGHT
|
||||
}
|
||||
if ((finalFingDist(0, 1) > 1.5 * (initialFingDist(0, 1)))
|
||||
&& (finalFingDist(1, 2) > 1.5 * (initialFingDist(1, 2)))
|
||||
&& (finalFingDist(2, 3) > 1.5 * (initialFingDist(2, 3)))
|
||||
&& (finalFingDist(3, 0) > 1.5 * (initialFingDist(3, 0)))
|
||||
) {
|
||||
return UNPINCH_4
|
||||
}
|
||||
if ((finalFingDist(0, 1) < 0.8 * (initialFingDist(0, 1)))
|
||||
&& (finalFingDist(1, 2) < 0.8 * (initialFingDist(1, 2)))
|
||||
&& (finalFingDist(2, 3) < 0.8 * (initialFingDist(2, 3)))
|
||||
&& (finalFingDist(3, 0) < 0.8 * (initialFingDist(3, 0)))
|
||||
) {
|
||||
return PINCH_4
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun initialFingDist(fingNum1: Int, fingNum2: Int): Double {
|
||||
return sqrt(
|
||||
(initialX[fingNum1] - initialX[fingNum2]).pow(2.0) + (initialY[fingNum1] - initialY[fingNum2]).pow(
|
||||
2.0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun finalFingDist(fingNum1: Int, fingNum2: Int): Double {
|
||||
return sqrt(
|
||||
(finalX[fingNum1] - finalX[fingNum2]).pow(2.0) + (finalY[fingNum1] - finalY[fingNum2]).pow(
|
||||
2.0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val isDoubleTap: Boolean
|
||||
get() = if (initialT - prevFinalT < doubleTapMaxDelayMillis && finalT - initialT < doubleTapMaxDownMillis && prevFinalT - prevInitialT < doubleTapMaxDownMillis) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
inner class GestureType {
|
||||
var gestureFlag: Int = 0
|
||||
var gestureDuration: Long = 0
|
||||
|
||||
var gestureDistance: Double = 0.0
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
const val DEBUG: Boolean = true
|
||||
|
||||
// Finished gestures flags
|
||||
const val SWIPE_1_UP: Int = 11
|
||||
const val SWIPE_1_DOWN: Int = 12
|
||||
const val SWIPE_1_LEFT: Int = 13
|
||||
const val SWIPE_1_RIGHT: Int = 14
|
||||
const val SWIPE_2_UP: Int = 21
|
||||
const val SWIPE_2_DOWN: Int = 22
|
||||
const val SWIPE_2_LEFT: Int = 23
|
||||
const val SWIPE_2_RIGHT: Int = 24
|
||||
const val SWIPE_3_UP: Int = 31
|
||||
const val SWIPE_3_DOWN: Int = 32
|
||||
const val SWIPE_3_LEFT: Int = 33
|
||||
const val SWIPE_3_RIGHT: Int = 34
|
||||
const val SWIPE_4_UP: Int = 41
|
||||
const val SWIPE_4_DOWN: Int = 42
|
||||
const val SWIPE_4_LEFT: Int = 43
|
||||
const val SWIPE_4_RIGHT: Int = 44
|
||||
const val PINCH_2: Int = 25
|
||||
const val UNPINCH_2: Int = 26
|
||||
const val PINCH_3: Int = 35
|
||||
const val UNPINCH_3: Int = 36
|
||||
const val PINCH_4: Int = 45
|
||||
const val UNPINCH_4: Int = 46
|
||||
|
||||
const val DOUBLE_TAP_1: Int = 107
|
||||
|
||||
//Ongoing gesture flags
|
||||
const val SWIPING_1_UP: Int = 101
|
||||
const val SWIPING_1_DOWN: Int = 102
|
||||
const val SWIPING_1_LEFT: Int = 103
|
||||
const val SWIPING_1_RIGHT: Int = 104
|
||||
const val SWIPING_2_UP: Int = 201
|
||||
const val SWIPING_2_DOWN: Int = 202
|
||||
const val SWIPING_2_LEFT: Int = 203
|
||||
const val SWIPING_2_RIGHT: Int = 204
|
||||
const val PINCHING: Int = 205
|
||||
const val UNPINCHING: Int = 206
|
||||
private const val TAG = "GestureAnalyser"
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleFingerGestures : OnTouchListener {
|
||||
private var debug = true
|
||||
var consumeTouchEvents: Boolean = false
|
||||
|
||||
protected var tracking: BooleanArray = booleanArrayOf(false, false, false, false, false)
|
||||
private var ga: GestureAnalyser
|
||||
private var onFingerGestureListener: OnFingerGestureListener? = null
|
||||
|
||||
|
||||
/**
|
||||
* Constructor that creates an internal [in.championswimmer.sfg.lib.GestureAnalyser] object as well
|
||||
*/
|
||||
constructor() {
|
||||
ga = GestureAnalyser()
|
||||
}
|
||||
|
||||
constructor(
|
||||
swipeSlopeIntolerance: Int,
|
||||
doubleTapMaxDelayMillis: Int,
|
||||
doubleTapMaxDownMillis: Int
|
||||
) {
|
||||
ga = GestureAnalyser(swipeSlopeIntolerance, doubleTapMaxDelayMillis, doubleTapMaxDownMillis)
|
||||
}
|
||||
|
||||
fun setDebug(debug: Boolean) {
|
||||
this.debug = debug
|
||||
}
|
||||
|
||||
constructor(omfgl: OnFingerGestureListener?) {
|
||||
ga = GestureAnalyser()
|
||||
setOnFingerGestureListener(omfgl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback to be invoked when multi-finger gestures take place
|
||||
*
|
||||
*
|
||||
* <br></br>
|
||||
*
|
||||
*
|
||||
* For the callbacks implemented via this, check the interface [in.championswimmer.sfg.lib.SimpleFingerGestures.OnFingerGestureListener]
|
||||
*
|
||||
*
|
||||
* @param omfgl The callback that will run
|
||||
*/
|
||||
fun setOnFingerGestureListener(omfgl: OnFingerGestureListener?) {
|
||||
onFingerGestureListener = omfgl
|
||||
}
|
||||
|
||||
|
||||
override fun onTouch(view: View, ev: MotionEvent): Boolean {
|
||||
if (debug) Log.d(TAG, "onTouch")
|
||||
when (ev.action and MotionEvent.ACTION_MASK) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (debug) Log.d(TAG, "ACTION_DOWN")
|
||||
startTracking(0)
|
||||
ga.trackGesture(ev)
|
||||
return consumeTouchEvents
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (debug) Log.d(TAG, "ACTION_UP")
|
||||
if (tracking[0]) {
|
||||
doCallBack(ga.getGesture(ev))
|
||||
}
|
||||
stopTracking(0)
|
||||
ga.untrackGesture()
|
||||
return consumeTouchEvents
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
if (debug) Log.d(TAG, "ACTION_POINTER_DOWN" + " " + "num" + ev.pointerCount)
|
||||
startTracking(ev.pointerCount - 1)
|
||||
ga.trackGesture(ev)
|
||||
return consumeTouchEvents
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_POINTER_UP -> {
|
||||
if (debug) Log.d(TAG, "ACTION_POINTER_UP" + " " + "num" + ev.pointerCount)
|
||||
if (tracking[1]) {
|
||||
doCallBack(ga.getGesture(ev))
|
||||
}
|
||||
stopTracking(ev.pointerCount - 1)
|
||||
ga.untrackGesture()
|
||||
return consumeTouchEvents
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
if (debug) Log.d(TAG, "ACTION_CANCEL")
|
||||
return true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (debug) Log.d(TAG, "ACTION_MOVE")
|
||||
return consumeTouchEvents
|
||||
}
|
||||
}
|
||||
return consumeTouchEvents
|
||||
}
|
||||
|
||||
private fun doCallBack(mGt: GestureAnalyser.GestureType) {
|
||||
when (mGt.gestureFlag) {
|
||||
GestureAnalyser.SWIPE_1_UP -> onFingerGestureListener!!.onSwipeUp(
|
||||
1,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_1_DOWN -> onFingerGestureListener!!.onSwipeDown(
|
||||
1,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_1_LEFT -> onFingerGestureListener!!.onSwipeLeft(
|
||||
1,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_1_RIGHT -> onFingerGestureListener!!.onSwipeRight(
|
||||
1,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_2_UP -> onFingerGestureListener!!.onSwipeUp(
|
||||
2,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_2_DOWN -> onFingerGestureListener!!.onSwipeDown(
|
||||
2,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_2_LEFT -> onFingerGestureListener!!.onSwipeLeft(
|
||||
2,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_2_RIGHT -> onFingerGestureListener!!.onSwipeRight(
|
||||
2,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.PINCH_2 -> onFingerGestureListener!!.onPinch(
|
||||
2,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.UNPINCH_2 -> onFingerGestureListener!!.onUnpinch(
|
||||
2,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_3_UP -> onFingerGestureListener!!.onSwipeUp(
|
||||
3,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_3_DOWN -> onFingerGestureListener!!.onSwipeDown(
|
||||
3,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_3_LEFT -> onFingerGestureListener!!.onSwipeLeft(
|
||||
3,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_3_RIGHT -> onFingerGestureListener!!.onSwipeRight(
|
||||
3,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.PINCH_3 -> onFingerGestureListener!!.onPinch(
|
||||
3,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.UNPINCH_3 -> onFingerGestureListener!!.onUnpinch(
|
||||
3,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_4_UP -> onFingerGestureListener!!.onSwipeUp(
|
||||
4,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_4_DOWN -> onFingerGestureListener!!.onSwipeDown(
|
||||
4,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_4_LEFT -> onFingerGestureListener!!.onSwipeLeft(
|
||||
4,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_4_RIGHT -> onFingerGestureListener!!.onSwipeRight(
|
||||
4,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.PINCH_4 -> onFingerGestureListener!!.onPinch(
|
||||
4,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.UNPINCH_4 -> {
|
||||
onFingerGestureListener!!.onUnpinch(4, mGt.gestureDuration, mGt.gestureDistance)
|
||||
onFingerGestureListener!!.onDoubleTap(1)
|
||||
}
|
||||
|
||||
GestureAnalyser.DOUBLE_TAP_1 -> onFingerGestureListener!!.onDoubleTap(1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startTracking(nthPointer: Int) {
|
||||
for (i in 0..nthPointer) {
|
||||
tracking[i] = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopTracking(nthPointer: Int) {
|
||||
for (i in nthPointer until tracking.size) {
|
||||
tracking[i] = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Interface definition for the callback to be invoked when 2-finger gestures are performed
|
||||
*/
|
||||
interface OnFingerGestureListener {
|
||||
/**
|
||||
* Called when user swipes **up** with two fingers
|
||||
*
|
||||
* @param fingers number of fingers involved in this gesture
|
||||
* @param gestureDuration duration in milliSeconds
|
||||
* @return
|
||||
*/
|
||||
fun onSwipeUp(fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
|
||||
|
||||
/**
|
||||
* Called when user swipes **down** with two fingers
|
||||
*
|
||||
* @param fingers number of fingers involved in this gesture
|
||||
* @param gestureDuration duration in milliSeconds
|
||||
* @return
|
||||
*/
|
||||
fun onSwipeDown(fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
|
||||
|
||||
/**
|
||||
* Called when user swipes **left** with two fingers
|
||||
*
|
||||
* @param fingers number of fingers involved in this gesture
|
||||
* @param gestureDuration duration in milliSeconds
|
||||
* @return
|
||||
*/
|
||||
fun onSwipeLeft(fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
|
||||
|
||||
/**
|
||||
* Called when user swipes **right** with two fingers
|
||||
*
|
||||
* @param fingers number of fingers involved in this gesture
|
||||
* @param gestureDuration duration in milliSeconds
|
||||
* @return
|
||||
*/
|
||||
fun onSwipeRight(fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
|
||||
|
||||
/**
|
||||
* Called when user **pinches** with two fingers (bring together)
|
||||
*
|
||||
* @param fingers number of fingers involved in this gesture
|
||||
* @param gestureDuration duration in milliSeconds
|
||||
* @return
|
||||
*/
|
||||
fun onPinch(fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
|
||||
|
||||
/**
|
||||
* Called when user **un-pinches** with two fingers (take apart)
|
||||
*
|
||||
* @param fingers number of fingers involved in this gesture
|
||||
* @param gestureDuration duration in milliSeconds
|
||||
* @return
|
||||
*/
|
||||
fun onUnpinch(fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
|
||||
|
||||
fun onDoubleTap(fingers: Int): Boolean
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Will see if these need to be used. For now just returning duration in milliS
|
||||
const val GESTURE_SPEED_SLOW: Long = 1500
|
||||
const val GESTURE_SPEED_MEDIUM: Long = 1000
|
||||
const val GESTURE_SPEED_FAST: Long = 500
|
||||
private const val TAG = "SimpleFingerGestures"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.feeds
|
||||
|
||||
import android.R.attr.*
|
||||
import android.app.Activity.RESULT_CANCELED
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.*
|
||||
import android.view.*
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.widget.LinearLayoutCompat.LayoutParams
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.app.JobIntentService.enqueueWork
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.material.button.MaterialButtonToggleGroup
|
||||
import kotlinx.coroutines.*
|
||||
import rasel.lunar.launcher.LauncherActivity.Companion.appWidgetHost
|
||||
import rasel.lunar.launcher.LauncherActivity.Companion.appWidgetManager
|
||||
import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
|
||||
import rasel.lunar.launcher.R
|
||||
import rasel.lunar.launcher.databinding.FeedsBinding
|
||||
import rasel.lunar.launcher.feeds.rss.Rss
|
||||
import rasel.lunar.launcher.feeds.rss.RssAdapter
|
||||
import rasel.lunar.launcher.feeds.rss.RssService
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_RSS_URL
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_WIDGET_HEIGHTS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_WIDGET_IDS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_WIDGETS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.RSS_ITEMS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.RSS_RECEIVER
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.SEPARATOR
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.requestCreateWidget
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.requestPickWidget
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.rssJobId
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.isNetworkAvailable
|
||||
import java.util.*
|
||||
|
||||
|
||||
internal class Feeds : Fragment() {
|
||||
|
||||
private lateinit var binding: FeedsBinding
|
||||
private val requestCodeString = "requestCode"
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FeedsBinding.inflate(inflater, container, false)
|
||||
|
||||
updateWidgets()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
expandCollapse()
|
||||
systemInfo()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
registerForContextMenu(binding.widgetContainer)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
unregisterForContextMenu(binding.widgetContainer)
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo)
|
||||
menu.clearHeader()
|
||||
lActivity!!.menuInflater.inflate(R.menu.add_widget, menu)
|
||||
}
|
||||
|
||||
override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.add_widget) selectWidget()
|
||||
return super.onContextItemSelected(item)
|
||||
}
|
||||
|
||||
/* control view's expand-collapse actions */
|
||||
private fun expandCollapse() {
|
||||
binding.expandableButtons.addOnButtonCheckedListener { _: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean ->
|
||||
if (isChecked) {
|
||||
when (checkedId) {
|
||||
binding.expandRss.id -> {
|
||||
binding.feedsSysInfos.expandableSystemInfo.collapse()
|
||||
binding.feedsRss.expandableRss.expand()
|
||||
startService()
|
||||
}
|
||||
binding.expandSystemInfo.id -> {
|
||||
binding.feedsRss.expandableRss.collapse()
|
||||
binding.feedsSysInfos.expandableSystemInfo.expand()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when (checkedId) {
|
||||
binding.expandRss.id -> binding.feedsRss.expandableRss.collapse()
|
||||
binding.expandSystemInfo.id -> binding.feedsSysInfos.expandableSystemInfo.collapse()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* start rss service if network is active and rss url is not empty */
|
||||
private fun startService() {
|
||||
val rssUrl = lActivity!!.getSharedPreferences(PREFS_SETTINGS, 0)
|
||||
.getString(KEY_RSS_URL, "")
|
||||
when {
|
||||
isNetworkAvailable && !rssUrl.isNullOrEmpty() -> {
|
||||
Intent(lActivity!!, RssService::class.java)
|
||||
.putExtra(RSS_RECEIVER, resultReceiver).let {
|
||||
enqueueWork(lActivity!!, RssService::class.java, rssJobId, it)
|
||||
}
|
||||
}
|
||||
else -> resumeService()
|
||||
}
|
||||
}
|
||||
|
||||
/* retry to start rss service */
|
||||
private fun resumeService() {
|
||||
binding.feedsRss.apply {
|
||||
rss.visibility = View.GONE
|
||||
loading.visibility = View.GONE
|
||||
refresh.visibility = View.VISIBLE
|
||||
refresh.setOnClickListener { startService() }
|
||||
}
|
||||
}
|
||||
|
||||
/* rss service's result receiver */
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private val resultReceiver: ResultReceiver = object : ResultReceiver(Handler(Looper.getMainLooper())) {
|
||||
override fun onReceiveResult(resultCode: Int, resultData: Bundle) {
|
||||
when (val items = resultData.getSerializable(RSS_ITEMS) as List<Rss>?) {
|
||||
null -> resumeService()
|
||||
else -> {
|
||||
binding.feedsRss.apply {
|
||||
rss.adapter = RssAdapter(items, requireContext())
|
||||
refresh.visibility = View.GONE
|
||||
loading.visibility = View.GONE
|
||||
rss.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun systemInfo() {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
SystemStats().apply {
|
||||
intStorage(binding.feedsSysInfos.intParent)
|
||||
extStorage(binding.feedsSysInfos.extParent)
|
||||
while (isActive) {
|
||||
ram(binding.feedsSysInfos.ramParent)
|
||||
cpu(binding.feedsSysInfos.cpuParent)
|
||||
misc(binding.feedsSysInfos.misc)
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectWidget() {
|
||||
Intent(AppWidgetManager.ACTION_APPWIDGET_PICK).apply {
|
||||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetHost?.allocateAppWidgetId())
|
||||
putParcelableArrayListExtra(AppWidgetManager.EXTRA_CUSTOM_INFO, ArrayList())
|
||||
putParcelableArrayListExtra(AppWidgetManager.EXTRA_CUSTOM_EXTRAS, ArrayList())
|
||||
putExtra(requestCodeString, requestPickWidget)
|
||||
}.let { widgetPicker.launch(it) }
|
||||
}
|
||||
|
||||
private val widgetPicker =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val data = result.data
|
||||
val appWidgetId = data?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
when (data?.getIntExtra(requestCodeString, requestPickWidget)) {
|
||||
requestPickWidget -> configureWidget(appWidgetId!!)
|
||||
requestCreateWidget -> createWidget(appWidgetId!!, null)
|
||||
}
|
||||
} else if (result.resultCode == RESULT_CANCELED && data != null) {
|
||||
if (appWidgetId != -1) appWidgetHost?.deleteAppWidgetId(appWidgetId!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureWidget(appWidgetId: Int) {
|
||||
when (val appWidgetConfig = appWidgetManager!!.getAppWidgetInfo(appWidgetId).configure) {
|
||||
null -> createWidget(appWidgetId, null)
|
||||
else -> {
|
||||
Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE).apply {
|
||||
component = appWidgetConfig
|
||||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
putExtra(requestCodeString, requestCreateWidget)
|
||||
}.let {
|
||||
try { widgetPicker.launch(it) }
|
||||
catch (e: Exception) { e.printStackTrace() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createWidget(appWidgetId: Int, height: Int?) {
|
||||
if (appWidgetId == -1) return
|
||||
|
||||
val appWidgetInfo = appWidgetManager!!.getAppWidgetInfo(appWidgetId)
|
||||
val params: LayoutParams?
|
||||
|
||||
when (height) {
|
||||
null -> {
|
||||
params = LayoutParams(LayoutParams.MATCH_PARENT, appWidgetInfo.minHeight)
|
||||
val updatedIds = splitWidgetIds.plus("$appWidgetId")
|
||||
val updatedHeights = splitWidgetHeights.plus("${appWidgetInfo.minHeight}")
|
||||
saveWidgetData(updatedIds, updatedHeights)
|
||||
}
|
||||
else -> params = LayoutParams(LayoutParams.MATCH_PARENT, height)
|
||||
}
|
||||
|
||||
(appWidgetHost?.createView(lActivity!!.applicationContext, appWidgetId, appWidgetInfo) as WidgetHostView)
|
||||
.apply {
|
||||
setAppWidget(appWidgetId, appWidgetInfo)
|
||||
}.let {
|
||||
binding.widgetContainer.addView(it, params)
|
||||
widgetMenu(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWidgets() {
|
||||
if (splitWidgetIds.size > 0) {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
binding.widgetContainer.removeAllViews()
|
||||
splitWidgetIds.indices.forEach { i: Int ->
|
||||
createWidget(splitWidgetIds[i]!!.int(), splitWidgetHeights[i]!!.int())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun widgetMenu(hostView: WidgetHostView) {
|
||||
val appWidgetId = hostView.appWidgetId
|
||||
hostView.setOnLongClickListener {
|
||||
PopupMenu(requireContext(), it, Gravity.END).apply {
|
||||
menuInflater.inflate(R.menu.widget_menu, this.menu)
|
||||
show()
|
||||
setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.move_up -> moveWidget(appWidgetId, true)
|
||||
R.id.move_down -> moveWidget(appWidgetId, false)
|
||||
R.id.increase_height -> resizeWidget(appWidgetId, true)
|
||||
R.id.decrease_height -> resizeWidget(appWidgetId, false)
|
||||
R.id.delete_widget -> removeWidget(it as WidgetHostView)
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun moveWidget(widgetId: Int, moveUp: Boolean) {
|
||||
val tempIds = splitWidgetIds
|
||||
val tempHeights = splitWidgetHeights
|
||||
|
||||
splitWidgetIds.indexOf(widgetId.toString()).let { i ->
|
||||
when {
|
||||
moveUp && i > 0 -> {
|
||||
tempIds.swap(i-1, i)
|
||||
tempHeights.swap(i-1, i)
|
||||
}
|
||||
!moveUp && i < splitWidgetIds.size - 1 -> {
|
||||
tempIds.swap(i, i+1)
|
||||
tempHeights.swap(i, i+1)
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
|
||||
saveWidgetData(tempIds, tempHeights)
|
||||
updateWidgets()
|
||||
}
|
||||
|
||||
private fun resizeWidget(widgetId: Int, shouldAdd: Boolean) {
|
||||
val tempList = splitWidgetHeights
|
||||
|
||||
splitWidgetIds.indexOf(widgetId.toString()).let { i ->
|
||||
tempList[i] = when (shouldAdd) {
|
||||
true -> (splitWidgetHeights[i]!!.int().plus(50)).toString()
|
||||
false -> (splitWidgetHeights[i]!!.int().minus(50)).toString()
|
||||
}
|
||||
}
|
||||
|
||||
widgetPref.edit().putString(KEY_WIDGET_HEIGHTS, tempList.joinToString(separator = SEPARATOR)).apply()
|
||||
updateWidgets()
|
||||
}
|
||||
|
||||
private fun removeWidget(hostView: WidgetHostView) {
|
||||
hostView.let { v ->
|
||||
appWidgetHost?.deleteAppWidgetId(v.appWidgetId)
|
||||
binding.widgetContainer.removeView(v)
|
||||
|
||||
splitWidgetIds.indexOf(v.appWidgetId.toString()).let { i ->
|
||||
saveWidgetData(splitWidgetIds.minus(splitWidgetIds[i]), splitWidgetHeights.minus(splitWidgetHeights[i]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveWidgetData(idList: List<String?>, heightList: List<String?>) {
|
||||
widgetPref.edit()
|
||||
.putString(KEY_WIDGET_IDS, idList.joinToString(separator = SEPARATOR))
|
||||
.putString(KEY_WIDGET_HEIGHTS, heightList.joinToString(separator = SEPARATOR))
|
||||
.apply()
|
||||
}
|
||||
|
||||
private val widgetPref: SharedPreferences get() = lActivity!!.getSharedPreferences(PREFS_WIDGETS, 0)
|
||||
private val widgetIds: String? get() = widgetPref.getString(KEY_WIDGET_IDS, "")
|
||||
private val widgetHeights: String? get() = widgetPref.getString(KEY_WIDGET_HEIGHTS, "")
|
||||
private val splitWidgetIds: MutableList<String?> get() = widgetIds!!.split(SEPARATOR).toMutableList()
|
||||
private val splitWidgetHeights: MutableList<String?> get() = widgetHeights!!.split(SEPARATOR).toMutableList()
|
||||
|
||||
private fun <T> MutableList<T>.swap(index1: Int, index2: Int){
|
||||
val temp = this[index1]
|
||||
this[index1] = this[index2]
|
||||
this[index2] = temp
|
||||
}
|
||||
|
||||
private fun String.int() : Int {
|
||||
return try {
|
||||
this.toInt()
|
||||
} catch (e: Exception) {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.feeds
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.*
|
||||
import android.text.Html
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
|
||||
import rasel.lunar.launcher.R
|
||||
import rasel.lunar.launcher.databinding.ChildSysInfoBinding
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TEMP_UNIT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.isNetworkAvailable
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
import java.io.RandomAccessFile
|
||||
import java.net.NetworkInterface
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
internal class SystemStats {
|
||||
|
||||
private val toGb = 1.07374182E9f
|
||||
private fun string(id: Int) : String { return lActivity!!.getString(id) }
|
||||
private val inflater : LayoutInflater get() { return lActivity!!.layoutInflater }
|
||||
|
||||
/* ram info */
|
||||
fun ram(ramParent: LinearLayoutCompat) {
|
||||
val parent = ramParent.findViewById<View>(R.id.childSysInfo)
|
||||
val indicator = parent.findViewById<LinearProgressIndicator>(R.id.indicator)
|
||||
val textView = parent.findViewById<MaterialTextView>(R.id.textView)
|
||||
|
||||
val totalMem = memoryInfo.totalMem / toGb
|
||||
val availMem = memoryInfo.availMem / toGb
|
||||
val usedMem = totalMem - availMem
|
||||
|
||||
indicator.progress = (usedMem * 100 / totalMem).toInt()
|
||||
textView.text = Html.fromHtml(
|
||||
"<b>${string(R.string.ram)}</b><br>" +
|
||||
"${string(R.string.total)}: ${String.format("%.03f", totalMem)} GB | " +
|
||||
"${string(R.string.used)}: ${String.format("%.03f", usedMem)} GB | " +
|
||||
"${string(R.string.free)}: ${String.format("%.03f", availMem)} GB",
|
||||
Html.FROM_HTML_MODE_COMPACT)
|
||||
}
|
||||
|
||||
|
||||
/* cpu and battery info */
|
||||
fun cpu(cpuParent: LinearLayoutCompat) {
|
||||
val parent = cpuParent.findViewById<View>(R.id.childSysInfo)
|
||||
val indicator = parent.findViewById<LinearProgressIndicator>(R.id.indicator)
|
||||
val textView = parent.findViewById<MaterialTextView>(R.id.textView)
|
||||
|
||||
var cpuTemp = 0.0f
|
||||
try {
|
||||
val cpuTempProcess = Runtime.getRuntime().exec("cat sys/class/thermal/thermal_zone0/temp")
|
||||
cpuTempProcess.waitFor()
|
||||
val cpuTempReader = BufferedReader(InputStreamReader(cpuTempProcess.inputStream))
|
||||
cpuTemp = cpuTempReader.readLine().toFloat() / 1000.0f
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
|
||||
val finalCpuTemp = when (tempUnit) {
|
||||
0 -> "$cpuTemp ºC"
|
||||
1 -> "${String.format("%.01f", cpuTemp * 1.8 + 32)} ºF"
|
||||
else -> "$cpuTemp ºC"
|
||||
}
|
||||
|
||||
val cpuFreq = "${String.format("%.02f", minCpuFrequency.toFloat() / 1000)} - " +
|
||||
"${String.format("%.02f", maxCpuFrequency.toFloat() / 1000)} GHz"
|
||||
|
||||
indicator.progress = when (maxCpuFrequency) {
|
||||
0 -> 30
|
||||
else -> frequencyOfCore * 100 / maxCpuFrequency
|
||||
}
|
||||
textView.text = Html.fromHtml(
|
||||
"<b>${string(R.string.cpu)}</b><br>" +
|
||||
"${string(R.string.temperature)}: $finalCpuTemp | " +
|
||||
"${string(R.string.frequency)}: $cpuFreq",
|
||||
Html.FROM_HTML_MODE_COMPACT)
|
||||
}
|
||||
|
||||
|
||||
/* internal storage */
|
||||
fun intStorage(intParent: LinearLayoutCompat) {
|
||||
val parent = intParent.findViewById<View>(R.id.childSysInfo)
|
||||
val indicator = parent.findViewById<LinearProgressIndicator>(R.id.indicator)
|
||||
val textView = parent.findViewById<MaterialTextView>(R.id.textView)
|
||||
|
||||
val intPath = Environment.getExternalStorageDirectory().absolutePath
|
||||
val statFs = StatFs(intPath)
|
||||
val totalStorage = statFs.blockCountLong * statFs.blockSizeLong / toGb
|
||||
val availStorage = statFs.availableBlocksLong * statFs.blockSizeLong / toGb
|
||||
val usedStorage = totalStorage - availStorage
|
||||
|
||||
indicator.progress = (usedStorage * 100 / totalStorage).toInt()
|
||||
textView.text = Html.fromHtml(
|
||||
"<b>${intPath + File.separator}</b><br>" +
|
||||
"${string(R.string.total)}: ${String.format("%.03f", totalStorage)} GB | " +
|
||||
"${string(R.string.used)}: ${String.format("%.03f", usedStorage)} GB | " +
|
||||
"${string(R.string.free)}: ${String.format("%.03f", availStorage)} GB",
|
||||
Html.FROM_HTML_MODE_COMPACT)
|
||||
}
|
||||
|
||||
|
||||
/* external storage */
|
||||
fun extStorage(extParent: LinearLayoutCompat) {
|
||||
val extStorages = ContextCompat.getExternalFilesDirs(lActivity!!, null)
|
||||
/* sd card is available */
|
||||
if (extStorages.size > 1) {
|
||||
extParent.removeAllViews()
|
||||
for (extStorage in extStorages) {
|
||||
if (extStorage != null) {
|
||||
val binding = ChildSysInfoBinding.inflate(inflater)
|
||||
extParent.addView(binding.root)
|
||||
|
||||
val statFs = StatFs(extStorage.path)
|
||||
val blockSize = statFs.blockSizeLong
|
||||
val totalStorage = statFs.blockCountLong * blockSize / toGb
|
||||
val availStorage = statFs.availableBlocksLong * blockSize / toGb
|
||||
val usedStorage = totalStorage - availStorage
|
||||
|
||||
val sdcardPaths = extStorage.path.split(File.separator).toTypedArray()
|
||||
val sdPath = File.separator + sdcardPaths[1] + File.separator + sdcardPaths[2] + File.separator
|
||||
|
||||
binding.indicator.progress = (usedStorage * 100 / totalStorage).toInt()
|
||||
binding.textView.text = Html.fromHtml(
|
||||
"<b>$sdPath</b><br>" +
|
||||
"${string(R.string.total)}: ${String.format("%.03f", totalStorage)} GB | " +
|
||||
"${string(R.string.used)}: ${String.format("%.03f", usedStorage)} GB | " +
|
||||
"${string(R.string.free)}: ${String.format("%.03f", availStorage)} GB",
|
||||
Html.FROM_HTML_MODE_COMPACT)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
extParent.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun misc(misc: MaterialTextView) {
|
||||
val totalRootStorage = StatFs(Environment.getRootDirectory().path).blockCountLong *
|
||||
StatFs(Environment.getRootDirectory().path).blockSizeLong / toGb
|
||||
|
||||
val batteryIntent = lActivity!!.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||
val batteryTemp = batteryIntent!!.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0).toFloat() / 10
|
||||
val voltage = batteryIntent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0).toFloat() / 1000
|
||||
|
||||
val finalBatteryTemp = when (tempUnit) {
|
||||
0 -> "$batteryTemp ºC"
|
||||
1 -> "${String.format("%.01f", batteryTemp * 1.8 + 32)} ºF"
|
||||
else -> "$batteryTemp ºC"
|
||||
}
|
||||
|
||||
misc.text =
|
||||
"${longToString(SystemClock.elapsedRealtime())}\n" +
|
||||
"${longToString(SystemClock.uptimeMillis())}\n" +
|
||||
"${String.format("%.02f", memoryInfo.threshold / 1048576f)} MB\n" +
|
||||
"$finalBatteryTemp\n" +
|
||||
"$voltage V\n" +
|
||||
"${String.format("%.03f", totalRootStorage)} GB\n" +
|
||||
"${getIpAddress(true)}\n" +
|
||||
getIpAddress(false)
|
||||
}
|
||||
|
||||
|
||||
private val memoryInfo: ActivityManager.MemoryInfo get() {
|
||||
val memoryInfo = ActivityManager.MemoryInfo()
|
||||
val activityManager = lActivity!!.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
activityManager.getMemoryInfo(memoryInfo)
|
||||
return memoryInfo
|
||||
}
|
||||
|
||||
private val tempUnit: Int get() =
|
||||
lActivity!!.getSharedPreferences(PREFS_SETTINGS, 0).getInt(KEY_TEMP_UNIT, 0)
|
||||
|
||||
private fun longToString(long: Long) : String {
|
||||
var seconds = (long.toDouble() / 1000).roundToInt()
|
||||
val hours = TimeUnit.SECONDS.toHours(seconds.toLong())
|
||||
if (hours > 0) seconds -= TimeUnit.HOURS.toSeconds(hours).toInt()
|
||||
val minutes = if (seconds > 0) TimeUnit.SECONDS.toMinutes(seconds.toLong()) else 0
|
||||
if (minutes > 0) seconds -= TimeUnit.MINUTES.toSeconds(minutes).toInt()
|
||||
return if (hours > 0) String.format("%02d:%02d:%02d", hours, minutes, seconds)
|
||||
else String.format("%02d:%02d", minutes, seconds)
|
||||
}
|
||||
|
||||
/* frequency of core */
|
||||
private val frequencyOfCore: Int get() {
|
||||
var currentFReq = 0
|
||||
try {
|
||||
val currentFreq: Double
|
||||
val readerCurFreq =
|
||||
RandomAccessFile("/sys/devices/system/cpu/cpu" + 0 + "/cpufreq/scaling_cur_freq", "r")
|
||||
val curFreq = readerCurFreq.readLine()
|
||||
currentFreq = curFreq.toDouble() / 1000
|
||||
readerCurFreq.close()
|
||||
currentFReq = currentFreq.toInt()
|
||||
println("$currentFReq----------------------------------------------------")
|
||||
} catch (ex: java.lang.Exception) {
|
||||
ex.printStackTrace()
|
||||
}
|
||||
return currentFReq
|
||||
}
|
||||
|
||||
/* minimum cpu frequency */
|
||||
private val minCpuFrequency: Int get() {
|
||||
var minFreq = -1
|
||||
try {
|
||||
val randomAccessFile =
|
||||
RandomAccessFile("/sys/devices/system/cpu/cpu" + 0 + "/cpufreq/cpuinfo_min_freq", "r")
|
||||
while (true) {
|
||||
val line = randomAccessFile.readLine() ?: break
|
||||
val timeInState = line.toInt()
|
||||
if (timeInState > 0) {
|
||||
val freq = timeInState / 1000
|
||||
if (freq > minFreq) {
|
||||
minFreq = freq
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (exception: java.lang.Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
return minFreq
|
||||
}
|
||||
|
||||
/* maximum cpu frequency */
|
||||
private val maxCpuFrequency: Int get() {
|
||||
var currentFReq = 0
|
||||
try {
|
||||
val currentFreq: Double
|
||||
val readerCurFreq =
|
||||
RandomAccessFile("/sys/devices/system/cpu/cpu" + 0 + "/cpufreq/cpuinfo_max_freq", "r")
|
||||
val curFreq = readerCurFreq.readLine()
|
||||
currentFreq = curFreq.toDouble() / 1000
|
||||
readerCurFreq.close()
|
||||
currentFReq = currentFreq.toInt()
|
||||
} catch (exception: java.lang.Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
return currentFReq
|
||||
}
|
||||
|
||||
private fun getIpAddress(getIPv4: Boolean): String {
|
||||
try {
|
||||
for (interFace in Collections.list(NetworkInterface.getNetworkInterfaces())) {
|
||||
for (address in Collections.list(interFace.inetAddresses)) {
|
||||
if (!address.isLoopbackAddress) {
|
||||
val addressStr = address.hostAddress
|
||||
val isIPv4 = addressStr!!.indexOf(':') < 0
|
||||
if (getIPv4) {
|
||||
if (isIPv4) return addressStr
|
||||
} else {
|
||||
if (!isIPv4 && isNetworkAvailable) {
|
||||
val endIndex = addressStr.indexOf('%')
|
||||
return if (endIndex < 0) addressStr
|
||||
else addressStr.substring(0, endIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: java.lang.Exception) { e.printStackTrace() }
|
||||
return string(R.string.na)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2009 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.feeds
|
||||
|
||||
import android.appwidget.AppWidgetHost
|
||||
import android.appwidget.AppWidgetHostView
|
||||
import android.appwidget.AppWidgetProviderInfo
|
||||
import android.content.Context
|
||||
|
||||
|
||||
internal class WidgetHost(context: Context, hostId: Int) : AppWidgetHost(context, hostId) {
|
||||
|
||||
override fun onCreateView(context: Context?, appWidgetId: Int, appWidget: AppWidgetProviderInfo?
|
||||
): AppWidgetHostView = WidgetHostView(context!!)
|
||||
|
||||
override fun stopListening() {
|
||||
super.stopListening()
|
||||
clearViews()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2009 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.feeds
|
||||
|
||||
import android.appwidget.AppWidgetHostView
|
||||
import android.content.Context
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewConfiguration
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
internal class WidgetHostView(context: Context) : AppWidgetHostView(context) {
|
||||
|
||||
private var hasPerformedLongPress = false
|
||||
private var pendingCheckForLongPress: CheckForLongPress? = null
|
||||
private var xPos = 0f
|
||||
private var yPos = 0f
|
||||
|
||||
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
||||
// Consume any touch events for ourselves after longpress is triggered
|
||||
if (hasPerformedLongPress) {
|
||||
hasPerformedLongPress = false
|
||||
return true
|
||||
}
|
||||
|
||||
// Watch for long press events at this level to make sure
|
||||
// users can always pick up this widget
|
||||
when (ev.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
postCheckForLongClick()
|
||||
xPos = ev.x
|
||||
yPos = ev.y
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (abs(ev.x - xPos) > 5 || abs(ev.y - yPos) > 5) {
|
||||
hasPerformedLongPress = false
|
||||
if (pendingCheckForLongPress != null) removeCallbacks(pendingCheckForLongPress)
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
hasPerformedLongPress = false
|
||||
if (pendingCheckForLongPress != null) removeCallbacks(pendingCheckForLongPress)
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise continue letting touch events fall through to children
|
||||
return false
|
||||
}
|
||||
|
||||
internal inner class CheckForLongPress : Runnable {
|
||||
private var originalWindowAttachCount = 0
|
||||
override fun run() {
|
||||
if (parent != null && hasWindowFocus()
|
||||
&& originalWindowAttachCount == windowAttachCount && !hasPerformedLongPress
|
||||
) {
|
||||
if (performLongClick()) hasPerformedLongPress = true
|
||||
}
|
||||
}
|
||||
|
||||
fun rememberWindowAttachCount() { originalWindowAttachCount = windowAttachCount }
|
||||
}
|
||||
|
||||
private fun postCheckForLongClick() {
|
||||
hasPerformedLongPress = false
|
||||
if (pendingCheckForLongPress == null) pendingCheckForLongPress = CheckForLongPress()
|
||||
pendingCheckForLongPress!!.rememberWindowAttachCount()
|
||||
postDelayed(pendingCheckForLongPress, ViewConfiguration.getLongPressTimeout().toLong())
|
||||
}
|
||||
|
||||
override fun cancelLongPress() {
|
||||
super.cancelLongPress()
|
||||
hasPerformedLongPress = false
|
||||
if (pendingCheckForLongPress != null) removeCallbacks(pendingCheckForLongPress)
|
||||
}
|
||||
|
||||
override fun getDescendantFocusability(): Int = FOCUS_BLOCK_DESCENDANTS
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.feeds.rss
|
||||
|
||||
|
||||
internal class Rss(
|
||||
val title: String,
|
||||
val link: String
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.feeds.rss
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.ViewGroup
|
||||
import android.view.LayoutInflater
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.Gravity
|
||||
import androidx.core.content.ContextCompat
|
||||
import android.graphics.Typeface
|
||||
import android.util.TypedValue
|
||||
import android.content.res.ColorStateList
|
||||
import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import rasel.lunar.launcher.databinding.ListItemBinding
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.getColorResId
|
||||
|
||||
|
||||
internal class RssAdapter(private val items: List<Rss>, private val context: Context) :
|
||||
RecyclerView.Adapter<RssAdapter.RssViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RssViewHolder {
|
||||
val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return RssViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: RssViewHolder, position: Int) {
|
||||
/* customize the first item */
|
||||
if (position == 0) {
|
||||
holder.view.itemText.apply {
|
||||
text = "\u22B6 " + items[position].title + " \u22B7"
|
||||
gravity = Gravity.CENTER
|
||||
setTextColor(ContextCompat.getColor(context,
|
||||
getColorResId(context, com.google.android.material.R.attr.colorPrimary)))
|
||||
setTypeface(null, Typeface.BOLD)
|
||||
textSize = 18f
|
||||
}
|
||||
/* reset customization for rest */
|
||||
} else {
|
||||
holder.view.itemText.apply {
|
||||
text = items[position].title
|
||||
gravity = holder.gravity
|
||||
setTextColor(holder.color)
|
||||
typeface = holder.typeface
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, holder.size)
|
||||
}
|
||||
}
|
||||
|
||||
/* on click - open in browser */
|
||||
holder.view.itemText.setOnClickListener {
|
||||
val customTabsIntent = CustomTabsIntent.Builder().setUrlBarHidingEnabled(true).build()
|
||||
customTabsIntent.launchUrl(context, Uri.parse(items[position].link))
|
||||
}
|
||||
}
|
||||
|
||||
inner class RssViewHolder(var view: ListItemBinding) : RecyclerView.ViewHolder(view.root) {
|
||||
/* store previous styles for resetting */
|
||||
var gravity: Int = view.itemText.gravity
|
||||
var color: ColorStateList = view.itemText.textColors
|
||||
var typeface: Typeface = view.itemText.typeface
|
||||
var size: Float = view.itemText.textSize
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.feeds.rss
|
||||
|
||||
import kotlin.Throws
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import android.util.Xml
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.ArrayList
|
||||
|
||||
|
||||
internal class RssParser {
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class)
|
||||
fun parse(inputStream: InputStream): List<Rss> {
|
||||
return inputStream.use { stream ->
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||
parser.setInput(stream, null)
|
||||
parser.nextTag()
|
||||
readFeed(parser)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class)
|
||||
private fun readFeed(parser: XmlPullParser): List<Rss> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "rss")
|
||||
var title: String? = null
|
||||
var link: String? = null
|
||||
val items: MutableList<Rss> = ArrayList()
|
||||
|
||||
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG) {
|
||||
continue
|
||||
}
|
||||
|
||||
val name = parser.name
|
||||
if (name == "title") {
|
||||
title = readTitle(parser)
|
||||
} else if (name == "link") {
|
||||
link = readLink(parser)
|
||||
}
|
||||
|
||||
if (title != null && link != null) {
|
||||
val item = Rss(title, link)
|
||||
items.add(item)
|
||||
title = null
|
||||
link = null
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class)
|
||||
private fun readLink(parser: XmlPullParser): String {
|
||||
parser.require(XmlPullParser.START_TAG, null, "link")
|
||||
val link = readText(parser)
|
||||
parser.require(XmlPullParser.END_TAG, null, "link")
|
||||
return link
|
||||
}
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class)
|
||||
private fun readTitle(parser: XmlPullParser): String {
|
||||
parser.require(XmlPullParser.START_TAG, null, "title")
|
||||
val title = readText(parser)
|
||||
parser.require(XmlPullParser.END_TAG, null, "title")
|
||||
return title
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun readText(parser: XmlPullParser): String {
|
||||
var result = ""
|
||||
if (parser.next() == XmlPullParser.TEXT) {
|
||||
result = parser.text
|
||||
parser.nextTag()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.feeds.rss
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.ResultReceiver
|
||||
import androidx.core.app.JobIntentService
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_RSS_URL
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.RSS_ITEMS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.RSS_RECEIVER
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.Serializable
|
||||
import java.net.URL
|
||||
|
||||
|
||||
internal class RssService : JobIntentService() {
|
||||
|
||||
override fun onHandleWork(intent: Intent) {
|
||||
val settingsPrefs = getSharedPreferences(PREFS_SETTINGS, 0)
|
||||
val rssUrl = settingsPrefs.getString(KEY_RSS_URL, "")
|
||||
var rssItems: List<Rss?>? = null
|
||||
|
||||
try {
|
||||
val parser = RssParser()
|
||||
rssItems = getInputStream(rssUrl)?.let { parser.parse(it) }
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
|
||||
val bundle = Bundle()
|
||||
bundle.putSerializable(RSS_ITEMS, rssItems as? Serializable)
|
||||
|
||||
val receiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(RSS_RECEIVER, ResultReceiver::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION") intent.getParcelableExtra(RSS_RECEIVER)
|
||||
}
|
||||
|
||||
receiver?.send(0, bundle)
|
||||
}
|
||||
|
||||
private fun getInputStream(link: String?): InputStream? {
|
||||
return try {
|
||||
val url = URL(link)
|
||||
url.openConnection().getInputStream()
|
||||
} catch (ioException: IOException) {
|
||||
ioException.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.helpers
|
||||
|
||||
import android.app.admin.DeviceAdminReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
|
||||
|
||||
internal class AdminReceiver : DeviceAdminReceiver() {
|
||||
|
||||
override fun onDisabled(context: Context, intent: Intent) {
|
||||
super.onDisabled(context, intent)
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(
|
||||
Intent("device_admin_action_disabled")
|
||||
)
|
||||
}
|
||||
|
||||
override fun onEnabled(context: Context, intent: Intent) {
|
||||
super.onEnabled(context, intent)
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(
|
||||
Intent("device_admin_action_enabled")
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.helpers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Color
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
|
||||
|
||||
internal class ColorPicker(private val initialColor: String,
|
||||
private val editText: TextInputEditText, private val sliderA: Slider, private val sliderR: Slider,
|
||||
private val sliderG: Slider, private val sliderB: Slider, private val colorPreview: ConstraintLayout) {
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun pickColor() {
|
||||
editText.setText(initialColor)
|
||||
stringToSlider(initialColor)
|
||||
colorPreview.setBackgroundColor(Color.parseColor("#$initialColor"))
|
||||
|
||||
editText.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
if (s.length == 6){
|
||||
sliderA.value = 255F
|
||||
sliderR.value = Integer.parseInt(s.substring(0..1), 16).toFloat()
|
||||
sliderG.value = Integer.parseInt(s.substring(2..3), 16).toFloat()
|
||||
sliderB.value = Integer.parseInt(s.substring(4..5), 16).toFloat()
|
||||
} else if (s.length == 8){
|
||||
stringToSlider(s.toString())
|
||||
} else if (s.isEmpty()) {
|
||||
sliderA.value = 0F
|
||||
sliderR.value = 0F
|
||||
sliderG.value = 0F
|
||||
sliderB.value = 0F
|
||||
}
|
||||
}
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
|
||||
})
|
||||
|
||||
sliderA.addOnChangeListener(Slider.OnChangeListener { _: Slider?, _: Float, _: Boolean ->
|
||||
editText.setText(colorString.uppercase())
|
||||
colorPreview.setBackgroundColor(Color.parseColor("#$colorString"))
|
||||
})
|
||||
|
||||
sliderR.addOnChangeListener(Slider.OnChangeListener { _: Slider?, _: Float, _: Boolean ->
|
||||
editText.setText(colorString.uppercase())
|
||||
colorPreview.setBackgroundColor(Color.parseColor("#$colorString"))
|
||||
})
|
||||
|
||||
sliderG.addOnChangeListener(Slider.OnChangeListener { _: Slider?, _: Float, _: Boolean ->
|
||||
editText.setText(colorString.uppercase())
|
||||
colorPreview.setBackgroundColor(Color.parseColor("#$colorString"))
|
||||
})
|
||||
|
||||
sliderB.addOnChangeListener(Slider.OnChangeListener { _: Slider?, _: Float, _: Boolean ->
|
||||
editText.setText(colorString.uppercase())
|
||||
colorPreview.setBackgroundColor(Color.parseColor("#$colorString"))
|
||||
})
|
||||
}
|
||||
|
||||
private fun stringToSlider(s: String) {
|
||||
sliderA.value = Integer.parseInt(s.substring(0..1), 16).toFloat()
|
||||
sliderR.value = Integer.parseInt(s.substring(2..3), 16).toFloat()
|
||||
sliderG.value = Integer.parseInt(s.substring(4..5), 16).toFloat()
|
||||
sliderB.value = Integer.parseInt(s.substring(6..7), 16).toFloat()
|
||||
}
|
||||
|
||||
private val colorString: String get() {
|
||||
var a = Integer.toHexString((((255*sliderA.value)/sliderA.valueTo).toInt()))
|
||||
if(a.length==1) a = "0$a"
|
||||
var r = Integer.toHexString((((255*sliderR.value)/sliderR.valueTo).toInt()))
|
||||
if(r.length==1) r = "0$r"
|
||||
var g = Integer.toHexString((((255*sliderG.value)/sliderG.valueTo).toInt()))
|
||||
if(g.length==1) g = "0$g"
|
||||
var b = Integer.toHexString((((255*sliderB.value)/sliderB.valueTo).toInt()))
|
||||
if(b.length==1) b = "0$b"
|
||||
return "$a$r$g$b"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.helpers
|
||||
|
||||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
|
||||
|
||||
internal class Constants {
|
||||
|
||||
companion object {
|
||||
|
||||
/* first launch */
|
||||
const val PREFS_FIRST_LAUNCH = "rasel.lunar.launcher.FIRST_LAUNCH"
|
||||
const val KEY_FIRST_LAUNCH = "first_launch"
|
||||
|
||||
/* widgets */
|
||||
const val PREFS_WIDGETS = "rasel.lunar.launcher.WIDGETS"
|
||||
const val KEY_WIDGET_IDS = "widget_ids"
|
||||
const val KEY_WIDGET_HEIGHTS = "widget_heights"
|
||||
|
||||
/* settings */
|
||||
const val PREFS_SETTINGS = "rasel.lunar.launcher.SETTINGS"
|
||||
const val KEY_TIME_FORMAT = "time_format"
|
||||
const val KEY_DATE_FORMAT = "date_format"
|
||||
const val KEY_CITY_NAME = "city_name"
|
||||
const val KEY_OWM_API = "owm_api"
|
||||
const val KEY_TEMP_UNIT = "temp_unit"
|
||||
const val KEY_SHOW_CITY = "show_city"
|
||||
const val KEY_TODO_COUNTS = "todo_count"
|
||||
const val KEY_TODO_LOCK = "todo_lock"
|
||||
const val KEY_KEYBOARD_SEARCH = "keyboard_search"
|
||||
const val KEY_QUICK_LAUNCH = "quick_launch"
|
||||
const val KEY_APPS_LAYOUT = "apps_layout"
|
||||
const val KEY_APPS_COUNT = "apps_count"
|
||||
const val KEY_DRAW_ALIGN = "drawer_alignment"
|
||||
const val KEY_ICON_PACK = "icon_pack"
|
||||
const val KEY_GRID_COLUMNS = "grid_columns"
|
||||
const val KEY_SCROLLBAR_HEIGHT = "scrollbar_height"
|
||||
const val KEY_WINDOW_BACKGROUND = "window_background"
|
||||
const val KEY_APPLICATION_THEME = "application_theme"
|
||||
const val KEY_STATUS_BAR = "status_bar"
|
||||
const val KEY_BACK_HOME = "back_home"
|
||||
const val KEY_SHORTCUT_COUNT = "shortcut_count"
|
||||
const val KEY_ICON_SIZE = "icon_size"
|
||||
const val KEY_RSS_URL = "rss_url"
|
||||
const val KEY_LOCK_METHOD = "lock_method"
|
||||
|
||||
/* --- */
|
||||
const val DEFAULT_DATE_FORMAT = "EEE dx MMM, yyyy"
|
||||
const val DEFAULT_ICON_SIZE = 44
|
||||
const val DEFAULT_ICON_PACK = "default_icon_pack"
|
||||
const val DEFAULT_GRID_COLUMNS = 4
|
||||
const val DEFAULT_SCROLLBAR_HEIGHT = 400
|
||||
const val MAX_SHORTCUTS = 6
|
||||
const val MAX_FAVORITE_APPS = 6
|
||||
|
||||
const val BOTTOM_SHEET_TAG = "rasel.lunar.launcher.TAG"
|
||||
const val SEPARATOR = "||"
|
||||
const val ACCESSIBILITY_SERVICE_LOCK_SCREEN = "rasel.lunar.launcher.LOCK_SCREEN_SERVICE"
|
||||
const val AUTHENTICATOR_TYPE = BIOMETRIC_WEAK or DEVICE_CREDENTIAL
|
||||
|
||||
const val rssJobId = 101
|
||||
const val widgetHostId = 102
|
||||
const val requestPickWidget = 103
|
||||
const val requestCreateWidget = 104
|
||||
|
||||
/* app names */
|
||||
const val PREFS_APP_NAMES = "rasel.lunar.launcher.APP_NAMES"
|
||||
|
||||
/* favorite apps */
|
||||
const val PREFS_FAVORITE_APPS = "rasel.lunar.launcher.FAVORITE_APPS"
|
||||
const val KEY_APP_NO_ = "app_no_"
|
||||
|
||||
/* phone and url shortcuts */
|
||||
const val PREFS_SHORTCUTS = "rasel.lunar.launcher.SHORTCUTS"
|
||||
const val KEY_SHORTCUT_NO_ = "shortcut_no_"
|
||||
const val SHORTCUT_TYPE_URL = "shortcut_type_url"
|
||||
const val SHORTCUT_TYPE_PHONE = "shortcut_type_phone"
|
||||
|
||||
/* to-do database */
|
||||
const val TODO_DATABASE_NAME = "rasel.lunar.launcher.TODOS"
|
||||
const val TODO_DATABASE_VERSION = 1
|
||||
const val TODO_TABLE_NAME = "todo_table"
|
||||
const val TODO_COLUMN_ID = "todo_column_id"
|
||||
const val TODO_COLUMN_NAME = "todo_column_name"
|
||||
const val TODO_COLUMN_CREATED = "todo_column_created"
|
||||
|
||||
/* rss feed */
|
||||
const val RSS_ITEMS = "rss_items"
|
||||
const val RSS_RECEIVER = "rss_receiver"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.helpers
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.AccessibilityServiceInfo
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
|
||||
|
||||
internal class LockService : AccessibilityService() {
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
|
||||
override fun onInterrupt() {}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN)
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
/* check whether accessibility service is enabled */
|
||||
fun isAccessibilityServiceEnabled(context: Context): Boolean {
|
||||
val accessibilityManager =
|
||||
context.getSystemService(ACCESSIBILITY_SERVICE) as AccessibilityManager
|
||||
val accessibilityServiceInfoList: List<AccessibilityServiceInfo> =
|
||||
accessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK)
|
||||
|
||||
for (enabledService in accessibilityServiceInfoList) {
|
||||
val enabledServiceInfo = enabledService.resolveInfo.serviceInfo
|
||||
if (enabledServiceInfo.packageName == context.packageName && enabledServiceInfo.name == LockService::class.java.name) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.helpers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.GestureDetector
|
||||
import android.view.GestureDetector.SimpleOnGestureListener
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.View.OnTouchListener
|
||||
import android.view.ViewParent
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
internal open class SwipeTouchListener(c: Context?) : OnTouchListener {
|
||||
|
||||
private val gestureDetector: GestureDetector = GestureDetector(c, GestureListener())
|
||||
private lateinit var viewParent: ViewParent
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
|
||||
viewParent = view.parent
|
||||
return gestureDetector.onTouchEvent(motionEvent)
|
||||
}
|
||||
|
||||
private inner class GestureListener : SimpleOnGestureListener() {
|
||||
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
onClick()
|
||||
return super.onSingleTapUp(e)
|
||||
}
|
||||
|
||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||
onDoubleClick()
|
||||
return super.onDoubleTap(e)
|
||||
}
|
||||
|
||||
override fun onLongPress(e: MotionEvent) {
|
||||
onLongClick()
|
||||
viewParent.requestDisallowInterceptTouchEvent(true)
|
||||
super.onLongPress(e)
|
||||
}
|
||||
|
||||
override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
|
||||
try {
|
||||
val diffY = e2.y - e1!!.y
|
||||
val diffX = e2.x - e1.x
|
||||
val swipeThreshold = 15
|
||||
val swipeVelocityThreshold = 90
|
||||
if (abs(diffX) > abs(diffY)) {
|
||||
if (abs(diffX) > swipeThreshold && abs(velocityX) > swipeVelocityThreshold) {
|
||||
when {
|
||||
diffX > 0 -> onSwipeRight()
|
||||
else -> onSwipeLeft()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (abs(diffY) > swipeThreshold && abs(velocityY) > swipeVelocityThreshold) {
|
||||
when {
|
||||
diffY > 0 -> onSwipeDown()
|
||||
else -> onSwipeUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun onSwipeRight() {}
|
||||
fun onSwipeLeft() {}
|
||||
open fun onSwipeUp() {}
|
||||
open fun onSwipeDown() {}
|
||||
open fun onClick() {}
|
||||
open fun onDoubleClick() {}
|
||||
open fun onLongClick() {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.helpers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
|
||||
import rasel.lunar.launcher.R
|
||||
import rasel.lunar.launcher.apps.IconPackManager.Companion.getDrawableIconForPackage
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.ACCESSIBILITY_SERVICE_LOCK_SCREEN
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.AUTHENTICATOR_TYPE
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_ICON_SIZE
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APPS_LAYOUT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APP_NO_
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_ICON_SIZE
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.MAX_FAVORITE_APPS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_FAVORITE_APPS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
|
||||
import java.io.DataOutputStream
|
||||
|
||||
|
||||
internal class UniUtils {
|
||||
|
||||
companion object {
|
||||
|
||||
/* get display width */
|
||||
val screenWidth: Int get() {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val windowMetrics = lActivity!!.windowManager.currentWindowMetrics
|
||||
val insets = windowMetrics.windowInsets
|
||||
.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
|
||||
windowMetrics.bounds.width() - insets.left - insets.right
|
||||
} else {
|
||||
val displayMetrics = DisplayMetrics()
|
||||
@Suppress("DEPRECATION") lActivity!!.windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
displayMetrics.widthPixels
|
||||
}
|
||||
}
|
||||
|
||||
/* get display height */
|
||||
val screenHeight: Int get() {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val windowMetrics = lActivity!!.windowManager.currentWindowMetrics
|
||||
val insets = windowMetrics.windowInsets
|
||||
.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
|
||||
windowMetrics.bounds.height() - insets.top - insets.bottom
|
||||
} else {
|
||||
val displayMetrics = DisplayMetrics()
|
||||
@Suppress("DEPRECATION") lActivity!!.windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
displayMetrics.heightPixels
|
||||
}
|
||||
}
|
||||
|
||||
/* copy texts to clipboard */
|
||||
fun copyToClipboard(context: Context, copiedString: String?) {
|
||||
val clipBoard =
|
||||
lActivity!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipBoard.setPrimaryClip(ClipData.newPlainText("", copiedString))
|
||||
Toast.makeText(context, context.getString(R.string.copied_message), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
/* expand notification panel */
|
||||
@SuppressLint("WrongConstant")
|
||||
fun expandNotificationPanel(context: Context) {
|
||||
try {
|
||||
Class.forName("android.app.StatusBarManager")
|
||||
.getMethod("expandNotificationsPanel")
|
||||
.invoke(context.getSystemService("statusbar"))
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
/* lock using preferred method */
|
||||
fun lockMethod(lockMethodValue: Int, context: Context, linearLayoutCompat: LinearLayoutCompat) {
|
||||
when (lockMethodValue) {
|
||||
0 -> populateFavApps(context, linearLayoutCompat)
|
||||
1 -> lockAccessibility()
|
||||
2 -> lockDeviceAdmin(context)
|
||||
3 -> lockRoot()
|
||||
}
|
||||
}
|
||||
|
||||
/* check if the device is rooted */
|
||||
val isRooted: Boolean get() {
|
||||
var process: Process? = null
|
||||
return try {
|
||||
process = Runtime.getRuntime().exec("su")
|
||||
true
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
false
|
||||
} finally {
|
||||
if (process != null) {
|
||||
try {
|
||||
process.destroy()
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* check if the device is connected to the internet */
|
||||
val isNetworkAvailable: Boolean get() {
|
||||
val connectivityManager =
|
||||
lActivity!!.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
@Suppress("DEPRECATION") val activeNetworkInfo = connectivityManager.activeNetworkInfo
|
||||
@Suppress("DEPRECATION") return activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting
|
||||
}
|
||||
|
||||
/* check if authenticator available */
|
||||
fun canAuthenticate(context: Context): Boolean {
|
||||
val biometricManager = BiometricManager.from(context)
|
||||
return biometricManager.canAuthenticate(AUTHENTICATOR_TYPE) == BIOMETRIC_SUCCESS
|
||||
}
|
||||
|
||||
/* show device authenticator */
|
||||
fun biometricPromptInfo(title: String): BiometricPrompt.PromptInfo {
|
||||
return BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(title)
|
||||
.setSubtitle(lActivity!!.getString(R.string.authentication_subtitle))
|
||||
.setConfirmationRequired(true)
|
||||
.setAllowedAuthenticators(AUTHENTICATOR_TYPE)
|
||||
.build()
|
||||
}
|
||||
|
||||
/* get color red id from attribute */
|
||||
fun getColorResId(context: Context, colorAttr: Int) : Int {
|
||||
val typedValue = TypedValue()
|
||||
context.theme.resolveAttribute(colorAttr, typedValue, true)
|
||||
return typedValue.resourceId
|
||||
}
|
||||
|
||||
/* convert dp value to px */
|
||||
fun dpToPx(context: Context, id: Int) : Int = (context.resources.getDimension(id) * context.resources.displayMetrics.density).toInt()
|
||||
|
||||
/* lock screen using device admin */
|
||||
private fun lockDeviceAdmin(context: Context) {
|
||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
if (powerManager.isInteractive) {
|
||||
val policy =
|
||||
context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
|
||||
try {
|
||||
policy.lockNow()
|
||||
} catch (exception: SecurityException) {
|
||||
/* open device admin manager screen */
|
||||
lActivity!!.startActivity(
|
||||
Intent().setComponent(
|
||||
ComponentName(
|
||||
"com.android.settings",
|
||||
"com.android.settings.DeviceAdminSettings"
|
||||
)
|
||||
)
|
||||
)
|
||||
exception.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* favorite apps */
|
||||
private fun populateFavApps(context: Context, linearLayoutCompat: LinearLayoutCompat) {
|
||||
val prefsFavApps = context.getSharedPreferences(PREFS_FAVORITE_APPS, 0)
|
||||
if (linearLayoutCompat.isVisible || prefsFavApps.all.toString().length < 3) {
|
||||
linearLayoutCompat.visibility = View.GONE
|
||||
} else {
|
||||
linearLayoutCompat.removeAllViews()
|
||||
linearLayoutCompat.visibility = View.VISIBLE
|
||||
val iconSize = context.getSharedPreferences(PREFS_SETTINGS, 0).getInt(KEY_ICON_SIZE, DEFAULT_ICON_SIZE)
|
||||
|
||||
for (position in 1..MAX_FAVORITE_APPS) {
|
||||
val packageName = prefsFavApps.getString(KEY_APP_NO_ + position.toString(), "").toString()
|
||||
/* package name is not empty for a specific position */
|
||||
if (packageName.isNotEmpty()) {
|
||||
try {
|
||||
ShapeableImageView(context).apply {
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(
|
||||
(iconSize * resources.displayMetrics.density).toInt(),
|
||||
(iconSize * resources.displayMetrics.density).toInt(), 1F)
|
||||
}.let { sImageView ->
|
||||
context.packageManager.getApplicationIcon(packageName).let { defaultIcon ->
|
||||
sImageView.setImageDrawable(
|
||||
if (context.getSharedPreferences(PREFS_SETTINGS, 0).getInt(KEY_APPS_LAYOUT, 0) != 0)
|
||||
getDrawableIconForPackage(packageName, defaultIcon)
|
||||
else defaultIcon
|
||||
)
|
||||
}
|
||||
sImageView.setOnClickListener {
|
||||
context.startActivity(context.packageManager.getLaunchIntentForPackage(packageName))
|
||||
}
|
||||
linearLayoutCompat.addView(sImageView)
|
||||
}
|
||||
} catch (nameNotFoundException: PackageManager.NameNotFoundException) {
|
||||
context.getSharedPreferences(PREFS_FAVORITE_APPS, 0)
|
||||
.edit().remove(KEY_APP_NO_ + position).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* lock screen using accessibility service */
|
||||
private fun lockAccessibility() {
|
||||
if (LockService().isAccessibilityServiceEnabled(lActivity!!.applicationContext)) {
|
||||
try {
|
||||
lActivity!!.startService(
|
||||
Intent(lActivity!!.applicationContext, LockService::class.java)
|
||||
.setAction(ACCESSIBILITY_SERVICE_LOCK_SCREEN)
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
} else {
|
||||
/* open accessibility service screen */
|
||||
lActivity!!.startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
|
||||
}
|
||||
}
|
||||
|
||||
/* lock screen using root */
|
||||
private fun lockRoot() {
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("su")
|
||||
val dataOutputStream = DataOutputStream(process.outputStream)
|
||||
dataOutputStream.writeBytes("input keyevent \${KeyEvent.KEYCODE_POWER}\n")
|
||||
dataOutputStream.writeBytes("exit\n")
|
||||
dataOutputStream.flush()
|
||||
dataOutputStream.close()
|
||||
process.waitFor()
|
||||
process.destroy()
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.helpers
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
|
||||
|
||||
internal class ViewPagerAdapter(
|
||||
fragmentManager: FragmentManager, private val fragments: MutableList<Fragment>, lifecycle: Lifecycle) :
|
||||
FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||
|
||||
override fun getItemCount(): Int = fragments.size
|
||||
override fun createFragment(position: Int): Fragment = fragments[position]
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.home
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.BatteryManager
|
||||
import android.provider.Settings
|
||||
import android.view.animation.AnimationUtils
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
import rasel.lunar.launcher.R
|
||||
|
||||
|
||||
internal class BatteryReceiver(private val progressBar: CircularProgressIndicator) : BroadcastReceiver() {
|
||||
|
||||
/* get current battery percentage */
|
||||
private fun batteryPercentage(intent: Intent): Int {
|
||||
val percentage = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)) /
|
||||
(intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)).toFloat()
|
||||
return (percentage * 100).toInt()
|
||||
}
|
||||
|
||||
/* get current charging status */
|
||||
private fun chargingStatus(intent: Intent): Int = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val animationDuration = try {
|
||||
Settings.Global.getFloat(context?.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE)
|
||||
} catch (e: Settings.SettingNotFoundException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
/* set battery percentage value to the circular progress bar */
|
||||
progressBar.progress = batteryPercentage(intent!!)
|
||||
|
||||
/* progress bar animation */
|
||||
if (chargingStatus(intent) == BatteryManager.BATTERY_STATUS_CHARGING ||
|
||||
chargingStatus(intent) == BatteryManager.BATTERY_STATUS_FULL) {
|
||||
if (progressBar.animation == null && animationDuration != 0f) {
|
||||
progressBar.startAnimation(
|
||||
AnimationUtils.loadAnimation(context, R.anim.rotate_clockwise)
|
||||
)
|
||||
}
|
||||
} else if (chargingStatus(intent) == BatteryManager.BATTERY_STATUS_DISCHARGING ||
|
||||
chargingStatus(intent) == BatteryManager.BATTERY_STATUS_NOT_CHARGING) {
|
||||
progressBar.clearAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.home
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.provider.AlarmClock
|
||||
import android.text.format.DateFormat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
|
||||
import rasel.lunar.launcher.R
|
||||
import rasel.lunar.launcher.databinding.LauncherHomeBinding
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.BOTTOM_SHEET_TAG
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_DATE_FORMAT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_DATE_FORMAT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_LOCK_METHOD
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TIME_FORMAT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TODO_LOCK
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
|
||||
import rasel.lunar.launcher.helpers.SwipeTouchListener
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.biometricPromptInfo
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.canAuthenticate
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.expandNotificationPanel
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.lockMethod
|
||||
import rasel.lunar.launcher.home.weather.WeatherExecutor
|
||||
import rasel.lunar.launcher.qaccess.QuickAccess
|
||||
import rasel.lunar.launcher.settings.SettingsActivity
|
||||
import rasel.lunar.launcher.todos.TodoAdapter
|
||||
import rasel.lunar.launcher.todos.TodoManager
|
||||
import rasel.lunar.launcher.utils.SimpleFingerGestures
|
||||
import java.util.*
|
||||
|
||||
|
||||
internal class LauncherHome : Fragment() {
|
||||
|
||||
private lateinit var binding: LauncherHomeBinding
|
||||
private lateinit var fragManager: FragmentManager
|
||||
private lateinit var settingsPrefs: SharedPreferences
|
||||
private lateinit var batteryReceiver: BatteryReceiver
|
||||
private var shouldResume = true
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
|
||||
binding = LauncherHomeBinding.inflate(inflater, container, false)
|
||||
fragManager = lActivity!!.supportFragmentManager
|
||||
settingsPrefs = requireContext().getSharedPreferences(PREFS_SETTINGS, 0)
|
||||
batteryReceiver = BatteryReceiver(binding.batteryProgress)
|
||||
|
||||
binding.favAppsGroup.visibility = View.GONE
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
/* handle gesture events */
|
||||
rootViewGestures()
|
||||
batteryProgressGestures()
|
||||
todosGestures()
|
||||
|
||||
/* refresh the to-do list after getting back from TodoManager */
|
||||
fragManager.addOnBackStackChangedListener {
|
||||
shouldResume = if (fragManager.backStackEntryCount == 0) {
|
||||
binding.root.visibility = View.VISIBLE
|
||||
showTodoList()
|
||||
true
|
||||
} else {
|
||||
binding.root.visibility = View.GONE
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (shouldResume) {
|
||||
/* register battery changes */
|
||||
requireContext().registerReceiver(batteryReceiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||
|
||||
/* time and date */
|
||||
if (DateFormat.is24HourFormat(requireContext())) {
|
||||
binding.time.format24Hour = timeFormat
|
||||
binding.date.format24Hour = dateFormat
|
||||
} else {
|
||||
binding.time.format12Hour = timeFormat
|
||||
binding.date.format12Hour = dateFormat
|
||||
}
|
||||
|
||||
/* show weather */
|
||||
WeatherExecutor(settingsPrefs).generateWeatherString(binding.weather)
|
||||
/* show to-do list */
|
||||
showTodoList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
/* unregister battery changes */
|
||||
if (shouldResume) requireContext().unregisterReceiver(batteryReceiver)
|
||||
}
|
||||
|
||||
|
||||
var mFingerGestureListener = object : SimpleFingerGestures.OnFingerGestureListener {
|
||||
override fun onSwipeUp(
|
||||
targetView: View,
|
||||
fingers: Int,
|
||||
gestureDuration: Long,
|
||||
gestureDistance: Double
|
||||
): Boolean {
|
||||
when(fingers) {
|
||||
1 ->
|
||||
if (targetView?.equals(binding.batteryProgress) ?: false) {
|
||||
QuickAccess().show(fragManager, BOTTOM_SHEET_TAG)
|
||||
} else {
|
||||
QuickAccess().show(fragManager, BOTTOM_SHEET_TAG)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onSwipeDown(
|
||||
targetView: View,
|
||||
fingers: Int,
|
||||
gestureDuration: Long,
|
||||
gestureDistance: Double
|
||||
): Boolean {
|
||||
when(fingers) {
|
||||
1 ->
|
||||
if (targetView?.equals(binding.batteryProgress) ?: false) {
|
||||
expandNotificationPanel(requireContext())
|
||||
} else {
|
||||
expandNotificationPanel(requireContext())
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onSwipeLeft(
|
||||
targetView: View,
|
||||
fingers: Int,
|
||||
gestureDuration: Long,
|
||||
gestureDistance: Double
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onSwipeRight(
|
||||
targetView: View,
|
||||
fingers: Int,
|
||||
gestureDuration: Long,
|
||||
gestureDistance: Double
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onPinch(
|
||||
targetView: View,
|
||||
fingers: Int,
|
||||
gestureDuration: Long,
|
||||
gestureDistance: Double
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onUnpinch(
|
||||
targetView: View,
|
||||
fingers: Int,
|
||||
gestureDuration: Long,
|
||||
gestureDistance: Double
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDoubleTap(targetView: View,fingers: Int): Boolean {
|
||||
|
||||
when(fingers) {
|
||||
1 -> if (targetView?.equals(binding.batteryProgress) ?: false) {
|
||||
lockMethod(settingsPrefs.getInt(KEY_LOCK_METHOD, 0), requireContext(), binding.favAppsGroup)
|
||||
} else {
|
||||
lockMethod(settingsPrefs.getInt(KEY_LOCK_METHOD, 0), requireContext(), binding.favAppsGroup)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onLongPress(targetView: View): Boolean {
|
||||
if (view?.equals(binding.batteryProgress) ?: false) {
|
||||
lActivity!!.startActivity(Intent(requireContext(), SettingsActivity::class.java))
|
||||
} else if (view?.equals(binding.notes) ?: false) {
|
||||
when (settingsPrefs.getBoolean(KEY_TODO_LOCK, false)) {
|
||||
false -> launchTodoManager()
|
||||
/* show authentication screen if lock is on */
|
||||
true -> {
|
||||
if (canAuthenticate(requireContext())) {
|
||||
val biometricPrompt = BiometricPrompt(lActivity!!, authenticationCallback)
|
||||
try {
|
||||
biometricPrompt.authenticate(biometricPromptInfo(lActivity!!.getString(R.string.todo_manager)))
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onClick(targetView: View): Boolean {
|
||||
if (view?.equals(binding.batteryProgress) ?: false) {
|
||||
requireContext().startActivity(
|
||||
Intent(AlarmClock.ACTION_SHOW_ALARMS).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
} else if (view?.equals(binding.batteryProgress) ?: false) {
|
||||
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* gestures on root view */
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun rootViewGestures() {
|
||||
|
||||
binding.root.setOnTouchListener(SimpleFingerGestures(context = requireContext(), binding.root , mFingerGestureListener))
|
||||
// {
|
||||
|
||||
/* open quick access panel on swipe up */
|
||||
// override fun onSwipeUp() {
|
||||
// super.onSwipeUp()
|
||||
// QuickAccess().show(fragManager, BOTTOM_SHEET_TAG)
|
||||
// }
|
||||
// /* expand notification panel on swipe down */
|
||||
// override fun onSwipeDown() {
|
||||
// super.onSwipeDown()
|
||||
// expandNotificationPanel(requireContext())
|
||||
// }
|
||||
// /* lock the screen on double tap (optional) */
|
||||
// override fun onDoubleClick() {
|
||||
// super.onDoubleClick()
|
||||
// lockMethod(settingsPrefs.getInt(KEY_LOCK_METHOD, 0), requireContext(), binding.favAppsGroup)
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* gestures on battery progress indicator area */
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun batteryProgressGestures() {
|
||||
binding.batteryProgress.setOnTouchListener(SimpleFingerGestures(context = requireContext(), binding.batteryProgress , mFingerGestureListener))
|
||||
// binding.batteryProgress.setOnTouchListener(object : SwipeTouchListener(requireContext()) {
|
||||
// /* open alarms list with default clock app */
|
||||
// override fun onClick() {
|
||||
// super.onClick()
|
||||
// requireContext().startActivity(
|
||||
// Intent(AlarmClock.ACTION_SHOW_ALARMS).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
// )
|
||||
// }
|
||||
// /* open settings activity on long click */
|
||||
// override fun onLongClick() {
|
||||
// super.onLongClick()
|
||||
// lActivity!!.startActivity(Intent(requireContext(), SettingsActivity::class.java))
|
||||
// }
|
||||
// /* expand notification panel on swipe down */
|
||||
// override fun onSwipeDown() {
|
||||
// super.onSwipeDown()
|
||||
// expandNotificationPanel(requireContext())
|
||||
// }
|
||||
// /* lock the screen on double tap (optional) */
|
||||
// override fun onDoubleClick() {
|
||||
// super.onDoubleClick()
|
||||
// lockMethod(settingsPrefs.getInt(KEY_LOCK_METHOD, 0), requireContext(), binding.favAppsGroup)
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
/* gestures on to-do area */
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun todosGestures() {
|
||||
binding.notes.setOnTouchListener(SimpleFingerGestures(context = requireContext(), binding.notes , mFingerGestureListener))
|
||||
// binding.notes.setOnTouchListener(object : SwipeTouchListener(requireContext()) {
|
||||
// /* open TodoManager on long click */
|
||||
// override fun onLongClick() {
|
||||
// super.onLongClick()
|
||||
// when (settingsPrefs.getBoolean(KEY_TODO_LOCK, false)) {
|
||||
// false -> launchTodoManager()
|
||||
// /* show authentication screen if lock is on */
|
||||
// true -> {
|
||||
// if (canAuthenticate(requireContext())) {
|
||||
// val biometricPrompt = BiometricPrompt(lActivity!!, authenticationCallback)
|
||||
// try {
|
||||
// biometricPrompt.authenticate(biometricPromptInfo(lActivity!!.getString(R.string.todo_manager)))
|
||||
// } catch (exception: Exception) {
|
||||
// exception.printStackTrace()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// /* open quick access panel on swipe up */
|
||||
// override fun onSwipeUp() {
|
||||
// super.onSwipeUp()
|
||||
// QuickAccess().show(fragManager, BOTTOM_SHEET_TAG)
|
||||
// }
|
||||
// /* expand notification panel on swipe down */
|
||||
// override fun onSwipeDown() {
|
||||
// super.onSwipeDown()
|
||||
// expandNotificationPanel(requireContext())
|
||||
// }
|
||||
// /* lock the screen on double tap (optional) */
|
||||
// override fun onDoubleClick() {
|
||||
// super.onDoubleClick()
|
||||
// lockMethod(settingsPrefs.getInt(KEY_LOCK_METHOD, 0), requireContext(), binding.favAppsGroup)
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
/* authentication callback for TodoManager lock */
|
||||
private val authenticationCallback = object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
launchTodoManager()
|
||||
}
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
Toast.makeText(requireContext(), lActivity!!.getString(R.string.authentication_error), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
override fun onAuthenticationFailed() {
|
||||
Toast.makeText(requireContext(), lActivity!!.getString(R.string.authentication_failed), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/* launch TodoManager fragment */
|
||||
private fun launchTodoManager() {
|
||||
fragManager.beginTransaction().replace(R.id.mainFragmentsContainer, TodoManager())
|
||||
.addToBackStack("").commit()
|
||||
}
|
||||
|
||||
/* to-do list */
|
||||
private fun showTodoList() {
|
||||
binding.notes.adapter = TodoAdapter(null, requireContext())
|
||||
}
|
||||
|
||||
/* get time format string */
|
||||
private val timeFormat: String? get() {
|
||||
when (settingsPrefs.getInt(KEY_TIME_FORMAT, 0)) {
|
||||
0 -> return if (DateFormat.is24HourFormat(requireContext())) {
|
||||
"kk:mm"
|
||||
} else {
|
||||
"h:mm a"
|
||||
}
|
||||
1 -> return "h:mm a"
|
||||
2 -> return "kk:mm"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/* get date number suffix */
|
||||
private val dateNumberSuffix: String get() {
|
||||
return when (Calendar.getInstance()[Calendar.DAY_OF_MONTH]) {
|
||||
1, 21, 31 -> "ˢᵗ"
|
||||
2, 22 -> "ⁿᵈ"
|
||||
3, 23 -> "ʳᵈ"
|
||||
else -> "ᵗʰ"
|
||||
}
|
||||
}
|
||||
|
||||
/* get date format string */
|
||||
private val dateFormat: String get() {
|
||||
settingsPrefs.getString(KEY_DATE_FORMAT, DEFAULT_DATE_FORMAT).let {
|
||||
return if (it!!.contains("x")) {
|
||||
it.replace("x", dateNumberSuffix)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.home.weather
|
||||
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
|
||||
internal class JsonParser {
|
||||
|
||||
fun getMyWeather(jsonStr: String): Weather {
|
||||
val weather = Weather()
|
||||
|
||||
try {
|
||||
val jsonObject = JSONObject(jsonStr)
|
||||
|
||||
/* Get weather condition */
|
||||
val weatherArray = jsonObject.getJSONArray("weather")
|
||||
val weatherObject = weatherArray.getJSONObject(0)
|
||||
weather.weatherCondition = weatherObject.getString("main")
|
||||
weather.weatherDescription = weatherObject.getString("description")
|
||||
weather.weatherIconId = weatherObject.getString("icon")
|
||||
|
||||
/* Get temperature */
|
||||
val mainObject = jsonObject.getJSONObject("main")
|
||||
weather.temperature = mainObject.getDouble("temp").toFloat()
|
||||
|
||||
weather.cityName = jsonObject.getString("name")
|
||||
} catch (jsonException: JSONException) {
|
||||
jsonException.printStackTrace()
|
||||
}
|
||||
|
||||
return weather
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.home.weather
|
||||
|
||||
|
||||
internal class Weather {
|
||||
var weatherCondition: String? = null
|
||||
var weatherDescription: String? = null
|
||||
var weatherIconId: String? = null
|
||||
var temperature = 0f
|
||||
var cityName: String? = null
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.home.weather
|
||||
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.lang.Exception
|
||||
import java.lang.StringBuilder
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
|
||||
internal class WeatherClient {
|
||||
|
||||
fun fetchWeather(wUrl: String?): String? {
|
||||
var httpURLConnection: HttpURLConnection? = null
|
||||
var bufferedReader: BufferedReader? = null
|
||||
|
||||
try {
|
||||
httpURLConnection = URL(wUrl).openConnection() as HttpURLConnection
|
||||
httpURLConnection.connect()
|
||||
bufferedReader = BufferedReader(InputStreamReader(httpURLConnection.inputStream))
|
||||
|
||||
val stringBuilder = StringBuilder()
|
||||
var line: String?
|
||||
while (bufferedReader.readLine().also { line = it } != null) {
|
||||
stringBuilder.append("$line\n")
|
||||
}
|
||||
|
||||
if (stringBuilder.isNotEmpty()) return stringBuilder.toString()
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
} finally {
|
||||
httpURLConnection?.disconnect()
|
||||
if (bufferedReader != null) {
|
||||
try {
|
||||
bufferedReader.close()
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.home.weather
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_CITY_NAME
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_OWM_API
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_SHOW_CITY
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TEMP_UNIT
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.isNetworkAvailable
|
||||
import java.net.URLEncoder
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
internal class WeatherExecutor(sharedPreferences: SharedPreferences) {
|
||||
|
||||
private val cityName: String
|
||||
private val owmApi: String
|
||||
private val weatherUrl: String
|
||||
private val tempUnit: Int
|
||||
private val showCity: Boolean
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun generateWeatherString(materialTextView: MaterialTextView) {
|
||||
materialTextView.visibility = View.GONE
|
||||
|
||||
/* run the executor if network is available,
|
||||
and city name and owm api values are not empty */
|
||||
if (isNetworkAvailable && cityName.isNotEmpty() && owmApi.isNotEmpty()) {
|
||||
try {
|
||||
Executors.newSingleThreadExecutor().execute {
|
||||
var weather: Weather? = null
|
||||
WeatherClient().fetchWeather(weatherUrl).let {
|
||||
if (!it.isNullOrEmpty()) weather = JsonParser().getMyWeather(it)
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
if (weather != null) {
|
||||
materialTextView.apply {
|
||||
visibility = View.VISIBLE
|
||||
text = weather!!.temperature.toString().substringBefore(".") +
|
||||
(if (tempUnit == 0) "ºC" else "ºF") +
|
||||
(if (showCity) " at ${weather!!.cityName}" else "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
cityName = sharedPreferences.getString(KEY_CITY_NAME, "").toString()
|
||||
owmApi = sharedPreferences.getString(KEY_OWM_API, "").toString()
|
||||
tempUnit = sharedPreferences.getInt(KEY_TEMP_UNIT, 0)
|
||||
showCity = sharedPreferences.getBoolean(KEY_SHOW_CITY, false)
|
||||
weatherUrl = URLEncoder.encode("https://api.openweathermap.org/data/2.5/weather?q=$cityName&APPID=$owmApi&units=" + if (tempUnit == 0) "metric" else "imperial","utf-8")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.qaccess
|
||||
|
||||
import android.Manifest
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.*
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.text.InputType
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButtonToggleGroup
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
|
||||
import rasel.lunar.launcher.R
|
||||
import rasel.lunar.launcher.databinding.QuickAccessBinding
|
||||
import rasel.lunar.launcher.databinding.ShortcutMakerBinding
|
||||
import rasel.lunar.launcher.helpers.ColorPicker
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_ICON_SIZE
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_ICON_SIZE
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_SHORTCUT_COUNT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_SHORTCUT_NO_
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.MAX_SHORTCUTS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SHORTCUTS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.SEPARATOR
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.SHORTCUT_TYPE_PHONE
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.SHORTCUT_TYPE_URL
|
||||
import java.util.*
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
|
||||
internal class QuickAccess : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var binding: QuickAccessBinding
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
private var iconSize by Delegates.notNull<Int>()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = QuickAccessBinding.inflate(inflater, container, false)
|
||||
|
||||
sharedPreferences = requireContext().getSharedPreferences(PREFS_SHORTCUTS, 0)
|
||||
iconSize = requireContext().getSharedPreferences(PREFS_SETTINGS, 0).getInt(KEY_ICON_SIZE, DEFAULT_ICON_SIZE)
|
||||
|
||||
/* set up volume sliders, brightness slider and favorite apps */
|
||||
volumeControllers()
|
||||
controlBrightness()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
/* enable dismiss animation */
|
||||
(requireDialog() as BottomSheetDialog).dismissWithAnimation = true
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
/* repopulate shortcuts and apps */
|
||||
shortcuts()
|
||||
}
|
||||
|
||||
/* control the volumes */
|
||||
private fun volumeControllers() {
|
||||
val audioManager = lActivity!!.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
/* max value */
|
||||
binding.notification.valueTo = audioManager.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION).toFloat()
|
||||
binding.alarm.valueTo = audioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM).toFloat()
|
||||
binding.media.valueTo = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC).toFloat()
|
||||
binding.voice.valueTo = audioManager.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL).toFloat()
|
||||
binding.ring.valueTo = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING).toFloat()
|
||||
/* current value */
|
||||
binding.notification.value = audioManager.getStreamVolume(AudioManager.STREAM_NOTIFICATION).toFloat()
|
||||
binding.alarm.value = audioManager.getStreamVolume(AudioManager.STREAM_ALARM).toFloat()
|
||||
binding.media.value = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat()
|
||||
binding.voice.value = audioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL).toFloat()
|
||||
binding.ring.value = audioManager.getStreamVolume(AudioManager.STREAM_RING).toFloat()
|
||||
|
||||
/* slider change listener for alarm volume */
|
||||
binding.alarm.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_ALARM, value.toInt(), 0)
|
||||
})
|
||||
|
||||
/* slider change listener for media volume */
|
||||
binding.media.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, value.toInt(), 0)
|
||||
})
|
||||
|
||||
/* slider change listener for voice call volume */
|
||||
binding.voice.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL, value.toInt(), 0)
|
||||
})
|
||||
|
||||
/* notify and ring volume sliders will work only if
|
||||
the device isn't in dnd or silent mode */
|
||||
if (Settings.Global.getInt(lActivity!!.contentResolver, "zen_mode") == 0 &&
|
||||
audioManager.ringerMode != AudioManager.RINGER_MODE_SILENT) {
|
||||
/* slider change listener for notify volume */
|
||||
binding.notification.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_NOTIFICATION, value.toInt(), 0)
|
||||
})
|
||||
/* slider change listener for ring volume */
|
||||
binding.ring.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, value.toInt(), 0)
|
||||
})
|
||||
} else {
|
||||
binding.notification.isEnabled = false
|
||||
binding.ring.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
/* set up contact and url shortcuts */
|
||||
private fun shortcuts() {
|
||||
binding.shortcutsGroup.removeAllViews()
|
||||
val shortcutCount =
|
||||
requireContext().getSharedPreferences(PREFS_SETTINGS, 0).getInt(KEY_SHORTCUT_COUNT, MAX_SHORTCUTS)
|
||||
if (shortcutCount == 0) binding.shortcutsGroup.visibility = View.GONE
|
||||
|
||||
for (position in 1..shortcutCount) {
|
||||
val shortcutValue = sharedPreferences.getString(KEY_SHORTCUT_NO_ + position.toString(), "").toString()
|
||||
val splitShortcutValue = shortcutValue.split(SEPARATOR).toTypedArray()
|
||||
|
||||
var shortcutType = ""
|
||||
var intentString = ""
|
||||
var thumbLetter = ""
|
||||
var color = ""
|
||||
|
||||
try {
|
||||
if (splitShortcutValue.size >= 4) {
|
||||
shortcutType = splitShortcutValue[0]
|
||||
intentString = splitShortcutValue[1]
|
||||
thumbLetter = splitShortcutValue[2]
|
||||
color = splitShortcutValue[3]
|
||||
}
|
||||
} catch (exception : Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
|
||||
shortcutsUtil(textView, shortcutType, intentString, thumbLetter, color, position)
|
||||
}
|
||||
}
|
||||
|
||||
/* control the brightness */
|
||||
private fun controlBrightness() {
|
||||
val resolver = lActivity!!.contentResolver
|
||||
/* set max value */
|
||||
binding.brightness.valueTo = maxBrightness
|
||||
|
||||
/* set slider value to current brightness value */
|
||||
try {
|
||||
binding.brightness.value = Settings.System.getInt(resolver, Settings.System.SCREEN_BRIGHTNESS).toFloat()
|
||||
} catch (settingNotFoundException: Settings.SettingNotFoundException) {
|
||||
settingNotFoundException.printStackTrace()
|
||||
}
|
||||
|
||||
/* listen slider value changes */
|
||||
binding.brightness.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
|
||||
/* if write settings permission is not allowed already,
|
||||
again ask for it to be granted */
|
||||
if (!Settings.System.canWrite(lActivity!!)) {
|
||||
lActivity!!.startActivity(
|
||||
Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)
|
||||
.setData(Uri.parse("package:" + lActivity!!.packageName))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
/* set the brightness according to the slider value */
|
||||
} else {
|
||||
Settings.System.putInt(
|
||||
resolver,
|
||||
Settings.System.SCREEN_BRIGHTNESS_MODE,
|
||||
Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL
|
||||
)
|
||||
Settings.System.putInt(resolver, Settings.System.SCREEN_BRIGHTNESS, value.toInt())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* contact/url shortcuts */
|
||||
private fun shortcutsUtil(textView: MaterialTextView, shortcutType: String, intentString: String,
|
||||
thumbLetter: String, color: String, position: Int) {
|
||||
/* show plus sign for empty positions and set click listener */
|
||||
if (intentString.isEmpty()) {
|
||||
textView.text = "+"
|
||||
textView.setOnClickListener {
|
||||
shortcutsSaverDialog(position, "00000000", "", "", "")
|
||||
}
|
||||
} else {
|
||||
/* show thumbnail letter */
|
||||
textView.text = thumbLetter
|
||||
/* set background color */
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
textView.background.colorFilter =
|
||||
BlendModeColorFilter(Color.parseColor("#$color"), BlendMode.MULTIPLY)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
textView.background.setColorFilter(Color.parseColor("#$color"), PorterDuff.Mode.MULTIPLY)
|
||||
}
|
||||
|
||||
/* on normal click */
|
||||
textView.setOnClickListener {
|
||||
/* type is url */
|
||||
if (shortcutType == SHORTCUT_TYPE_URL) {
|
||||
var url = intentString
|
||||
/* add http before the url if it doesn't have http/https prefix */
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
url = "http://$intentString"
|
||||
}
|
||||
/* open the url */
|
||||
lActivity!!.startActivity(
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse(url)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
/* type is contact */
|
||||
} else if (shortcutType == SHORTCUT_TYPE_PHONE) {
|
||||
/* if the necessary permission is not granted already,
|
||||
ask for it again */
|
||||
if (lActivity!!.checkSelfPermission(Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
|
||||
lActivity!!.requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), 1)
|
||||
} else {
|
||||
/* make phone call */
|
||||
lActivity!!.startActivity(
|
||||
Intent(Intent.ACTION_CALL, Uri.parse("tel:$intentString"))
|
||||
)
|
||||
}
|
||||
}
|
||||
this.dismiss()
|
||||
}
|
||||
|
||||
/* reset the shortcut on long click */
|
||||
textView.setOnLongClickListener {
|
||||
shortcutsSaverDialog(position, color, thumbLetter, shortcutType, intentString)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* dialog for creating shortcuts */
|
||||
private fun shortcutsSaverDialog(
|
||||
position: Int, color: String, thumbLetter: String, shortcutType: String, intentString: String) {
|
||||
val dialogBinding = ShortcutMakerBinding.inflate(lActivity!!.layoutInflater)
|
||||
val dialogBuilder = MaterialAlertDialogBuilder(lActivity!!)
|
||||
.setView(dialogBinding.root)
|
||||
.setNeutralButton(R.string.delete, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
|
||||
dialogBinding.thumbField.setText(thumbLetter)
|
||||
dialogBinding.inputField.setText(intentString)
|
||||
when (shortcutType) {
|
||||
SHORTCUT_TYPE_PHONE -> dialogBinding.shortcutType.check(dialogBinding.contact.id)
|
||||
SHORTCUT_TYPE_URL -> dialogBinding.shortcutType.check(dialogBinding.url.id)
|
||||
}
|
||||
|
||||
/* set up color picker section */
|
||||
ColorPicker(color, dialogBinding.colorPicker.colorInput, dialogBinding.colorPicker.colorA,
|
||||
dialogBinding.colorPicker.colorR, dialogBinding.colorPicker.colorG,
|
||||
dialogBinding.colorPicker.colorB, dialogBinding.root).pickColor()
|
||||
|
||||
/* shortcut type chooser - contact/url */
|
||||
var updatedShortcutType = shortcutType
|
||||
dialogBinding.shortcutType.addOnButtonCheckedListener {
|
||||
_: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean ->
|
||||
if (isChecked) {
|
||||
when (checkedId) {
|
||||
dialogBinding.contact.id -> {
|
||||
updatedShortcutType = SHORTCUT_TYPE_PHONE
|
||||
dialogBinding.inputField.inputType = InputType.TYPE_CLASS_PHONE
|
||||
}
|
||||
dialogBinding.url.id -> {
|
||||
updatedShortcutType = SHORTCUT_TYPE_URL
|
||||
dialogBinding.inputField.inputType = InputType.TYPE_TEXT_VARIATION_URI
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialogBuilder.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
|
||||
sharedPreferences.edit().remove(KEY_SHORTCUT_NO_ + position).apply()
|
||||
dialogBuilder.dismiss()
|
||||
this.onResume()
|
||||
}
|
||||
|
||||
/* save the shortcut values */
|
||||
dialogBuilder.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
/* get shortcut value */
|
||||
val updatedIntentString =
|
||||
Objects.requireNonNull(dialogBinding.inputField.text).toString().trim { it <= ' ' }
|
||||
/* get thumbnail letter */
|
||||
val updatedThumbLetter =
|
||||
Objects.requireNonNull(dialogBinding.thumbField.text).toString().trim { it <= ' ' }.uppercase()
|
||||
/* get color value */
|
||||
val updatedColor =
|
||||
Objects.requireNonNull(dialogBinding.colorPicker.colorInput.text).toString().trim { it <= ' ' }
|
||||
|
||||
/* save the values if every field is filled */
|
||||
if (updatedShortcutType.isNotEmpty() && updatedIntentString.isNotEmpty() &&
|
||||
updatedThumbLetter.isNotEmpty() && updatedColor.isNotEmpty()) {
|
||||
sharedPreferences.edit().putString(KEY_SHORTCUT_NO_ + position,
|
||||
"$updatedShortcutType$SEPARATOR$updatedIntentString$SEPARATOR" +
|
||||
"$updatedThumbLetter$SEPARATOR$updatedColor").apply()
|
||||
dialogBuilder.dismiss()
|
||||
this.onResume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* create text view for shortcut thumbnails */
|
||||
private val textView: MaterialTextView get() {
|
||||
val relativeLayout = RelativeLayout(lActivity!!)
|
||||
relativeLayout.apply {
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(
|
||||
LinearLayoutCompat.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayoutCompat.LayoutParams.WRAP_CONTENT, 1F)
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
binding.shortcutsGroup.addView(relativeLayout)
|
||||
|
||||
MaterialTextView(requireContext()).apply {
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(
|
||||
(iconSize * resources.displayMetrics.density).toInt(),
|
||||
(iconSize * resources.displayMetrics.density).toInt())
|
||||
gravity = Gravity.CENTER
|
||||
textSize = (iconSize / 4) * resources.displayMetrics.density
|
||||
setTypeface(null, Typeface.BOLD)
|
||||
background = ContextCompat.getDrawable(requireContext(), R.drawable.rounded_bg)
|
||||
}.let {
|
||||
relativeLayout.addView(it)
|
||||
return it
|
||||
}
|
||||
}
|
||||
|
||||
/* returns maximum brightness value of the device */
|
||||
private val maxBrightness: Float get() {
|
||||
val powerManager = requireContext().getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
var value = 255f
|
||||
for (f in powerManager.javaClass.declaredFields) {
|
||||
if (f.name.equals("BRIGHTNESS_ON")) {
|
||||
f.isAccessible = true
|
||||
value = try {
|
||||
f.getInt(powerManager).toFloat()
|
||||
} catch (e: IllegalAccessException) {
|
||||
255f
|
||||
}
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.settings
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import rasel.lunar.launcher.BuildConfig
|
||||
import rasel.lunar.launcher.R
|
||||
import rasel.lunar.launcher.databinding.AboutBinding
|
||||
import rasel.lunar.launcher.databinding.SettingsActivityBinding
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.BOTTOM_SHEET_TAG
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
|
||||
import rasel.lunar.launcher.settings.childs.*
|
||||
|
||||
|
||||
internal class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: SettingsActivityBinding
|
||||
private val sourceCode = "https://github.com/iamrasel/lunar-launcher"
|
||||
|
||||
companion object {
|
||||
@JvmStatic var settingsPrefs: SharedPreferences? = null
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
DynamicColors.applyToActivityIfAvailable(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
/* set up view */
|
||||
binding = SettingsActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
settingsPrefs = this.getSharedPreferences(PREFS_SETTINGS, 0)
|
||||
|
||||
/* launch child settings dialogs on button clicks */
|
||||
binding.timeDate.setOnClickListener {
|
||||
TimeDate().show(supportFragmentManager, BOTTOM_SHEET_TAG)
|
||||
}
|
||||
|
||||
binding.weather.setOnClickListener {
|
||||
WeatherSettings().show(supportFragmentManager, BOTTOM_SHEET_TAG)
|
||||
}
|
||||
|
||||
binding.todo.setOnClickListener {
|
||||
TodoSettings().show(supportFragmentManager, BOTTOM_SHEET_TAG)
|
||||
}
|
||||
|
||||
binding.apps.setOnClickListener {
|
||||
Apps().show(supportFragmentManager, BOTTOM_SHEET_TAG)
|
||||
}
|
||||
|
||||
binding.appearances.setOnClickListener {
|
||||
Appearances().show(supportFragmentManager, BOTTOM_SHEET_TAG)
|
||||
}
|
||||
|
||||
binding.misc.setOnClickListener {
|
||||
Misc().show(supportFragmentManager, BOTTOM_SHEET_TAG)
|
||||
}
|
||||
|
||||
binding.advance.setOnClickListener {
|
||||
Advance().show(supportFragmentManager, BOTTOM_SHEET_TAG)
|
||||
}
|
||||
|
||||
/* about and support dialogs */
|
||||
binding.about.setOnClickListener { aboutDialog() }
|
||||
binding.support.setOnClickListener { supportDialog() }
|
||||
|
||||
/* show app version name */
|
||||
binding.version.text = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
|
||||
}
|
||||
|
||||
override fun getTheme(): Resources.Theme {
|
||||
val theme = super.getTheme()
|
||||
theme.applyStyle(R.style.SettingsNavBar, true)
|
||||
return theme
|
||||
}
|
||||
|
||||
/* about dialog */
|
||||
private fun aboutDialog() {
|
||||
val bottomSheetDialog = BottomSheetDialog(this)
|
||||
val aboutBinding = AboutBinding.inflate(this.layoutInflater)
|
||||
bottomSheetDialog.setContentView(aboutBinding.root)
|
||||
bottomSheetDialog.show()
|
||||
bottomSheetDialog.dismissWithAnimation = true
|
||||
|
||||
/* source code at github */
|
||||
aboutBinding.sourceCode.setOnClickListener {
|
||||
bottomSheetDialog.dismiss()
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(sourceCode)))
|
||||
}
|
||||
/* wiki at github */
|
||||
aboutBinding.wiki.setOnClickListener {
|
||||
bottomSheetDialog.dismiss()
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("$sourceCode/wiki")))
|
||||
}
|
||||
/* telegram community */
|
||||
aboutBinding.telegramGroup.setOnClickListener {
|
||||
bottomSheetDialog.dismiss()
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://t.me/LunarLauncher_chats")))
|
||||
}
|
||||
}
|
||||
|
||||
/* support dialog */
|
||||
private fun supportDialog() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.support)
|
||||
.setMessage(R.string.support_message)
|
||||
/* star button */
|
||||
.setNeutralButton(R.string.star) { _, _ ->
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(sourceCode)))
|
||||
}
|
||||
/* affiliate button */
|
||||
.setNegativeButton(R.string.amazon) { _, _ ->
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://amzn.to/44krAw9")))
|
||||
}
|
||||
/* donate button */
|
||||
.setPositiveButton(R.string.donate) { _, _ ->
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://iamrasel.github.io/donate")))
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.settings.childs
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import rasel.lunar.launcher.R
|
||||
import rasel.lunar.launcher.databinding.SettingsAdvanceBinding
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
|
||||
internal class Advance : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var binding : SettingsAdvanceBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = SettingsAdvanceBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
(requireDialog() as BottomSheetDialog).dismissWithAnimation = true
|
||||
|
||||
/* open Default Home App screen from device settings */
|
||||
binding.chooseLauncher.setOnClickListener {
|
||||
requireContext().startActivity(Intent(Settings.ACTION_HOME_SETTINGS))
|
||||
this.dismiss()
|
||||
}
|
||||
|
||||
/* reset and restart button click listeners */
|
||||
binding.reset.setOnClickListener { reset() }
|
||||
binding.restart.setOnClickListener { exitProcess(0) }
|
||||
}
|
||||
|
||||
/* reset app data */
|
||||
private fun reset() {
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.reset)
|
||||
.setMessage(R.string.reset_message)
|
||||
.setPositiveButton(R.string.proceed) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
Runtime.getRuntime().exec("pm clear " + requireContext().packageName)
|
||||
}
|
||||
.setNeutralButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
|
||||
.show()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.settings.childs
|
||||
|
||||
import android.Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
import android.Manifest.permission.READ_MEDIA_IMAGES
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.app.AlertDialog
|
||||
import android.app.WallpaperManager
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import android.graphics.Matrix
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
|
||||
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import rasel.lunar.launcher.R
|
||||
import rasel.lunar.launcher.databinding.ColorPickerBinding
|
||||
import rasel.lunar.launcher.databinding.SettingsAppearancesBinding
|
||||
import rasel.lunar.launcher.helpers.ColorPicker
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APPLICATION_THEME
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_STATUS_BAR
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_WINDOW_BACKGROUND
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.getColorResId
|
||||
import rasel.lunar.launcher.settings.SettingsActivity.Companion.settingsPrefs
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
|
||||
internal class Appearances : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var binding : SettingsAppearancesBinding
|
||||
private lateinit var windowBackground : String
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = SettingsAppearancesBinding.inflate(inflater, container, false)
|
||||
|
||||
/* initialize views according to the saved values */
|
||||
when (settingsPrefs!!.getInt(KEY_APPLICATION_THEME, MODE_NIGHT_FOLLOW_SYSTEM)) {
|
||||
MODE_NIGHT_FOLLOW_SYSTEM -> binding.followSystemTheme.isChecked = true
|
||||
MODE_NIGHT_YES -> binding.selectDarkTheme.isChecked = true
|
||||
MODE_NIGHT_NO -> binding.selectLightTheme.isChecked = true
|
||||
}
|
||||
|
||||
when (settingsPrefs!!.getBoolean(KEY_STATUS_BAR, false)) {
|
||||
false -> binding.hideStatusNegative.isChecked = true
|
||||
true -> binding.hideStatusPositive.isChecked = true
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
(requireDialog() as BottomSheetDialog).dismissWithAnimation = true
|
||||
|
||||
/* change theme */
|
||||
binding.themeGroup.setOnCheckedStateChangeListener { group, _ ->
|
||||
when (group.checkedChipId) {
|
||||
binding.followSystemTheme.id -> {
|
||||
settingsPrefs!!.edit().putInt(KEY_APPLICATION_THEME, MODE_NIGHT_FOLLOW_SYSTEM).apply()
|
||||
AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
binding.selectDarkTheme.id -> {
|
||||
settingsPrefs!!.edit().putInt(KEY_APPLICATION_THEME, MODE_NIGHT_YES).apply()
|
||||
AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_YES)
|
||||
}
|
||||
binding.selectLightTheme.id -> {
|
||||
settingsPrefs!!.edit().putInt(KEY_APPLICATION_THEME, MODE_NIGHT_NO).apply()
|
||||
AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.background.setOnClickListener { selectBackground() }
|
||||
binding.changeWallpaper.setOnClickListener { selectWallpaper() }
|
||||
|
||||
binding.hideStatusGroup.setOnCheckedStateChangeListener { group, _ ->
|
||||
when (group.checkedChipId) {
|
||||
binding.hideStatusNegative.id -> settingsPrefs!!.edit().putBoolean(KEY_STATUS_BAR, false).apply()
|
||||
binding.hideStatusPositive.id -> settingsPrefs!!.edit().putBoolean(KEY_STATUS_BAR, true).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
windowBackground = settingsPrefs!!.getString(KEY_WINDOW_BACKGROUND, defaultColorString).toString()
|
||||
binding.background.iconTint = ColorStateList.valueOf(Color.parseColor("#$windowBackground"))
|
||||
}
|
||||
|
||||
private fun selectBackground() {
|
||||
val colorPickerBinding = ColorPickerBinding.inflate(requireActivity().layoutInflater)
|
||||
val dialogBuilder = MaterialAlertDialogBuilder(requireActivity())
|
||||
.setView(colorPickerBinding.root)
|
||||
.setNeutralButton(R.string.default_, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
settingsPrefs!!.edit().putString(KEY_WINDOW_BACKGROUND,
|
||||
Objects.requireNonNull(colorPickerBinding.colorInput.text).toString().trim { it <= ' ' }).apply()
|
||||
this.onResume()
|
||||
}
|
||||
.show()
|
||||
|
||||
/* set up color picker section */
|
||||
ColorPicker(windowBackground, colorPickerBinding.colorInput,
|
||||
colorPickerBinding.colorA, colorPickerBinding.colorR, colorPickerBinding.colorG,
|
||||
colorPickerBinding.colorB, colorPickerBinding.root).pickColor()
|
||||
|
||||
dialogBuilder.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
|
||||
colorPickerBinding.colorInput.text =
|
||||
SpannableStringBuilder(defaultColorString)
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectWallpaper() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// only for TIRAMISU and newer versions
|
||||
if (requireActivity().checkSelfPermission(READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) {
|
||||
requireActivity().requestPermissions(arrayOf(READ_MEDIA_IMAGES), 1)
|
||||
} else {
|
||||
wallpaperChangeLauncher.launch(Intent(Intent.ACTION_PICK).setType("image/*"))
|
||||
}
|
||||
} else {
|
||||
if (requireActivity().checkSelfPermission(READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
requireActivity().requestPermissions(arrayOf(READ_EXTERNAL_STORAGE), 1)
|
||||
} else {
|
||||
wallpaperChangeLauncher.launch(Intent(Intent.ACTION_PICK).setType("image/*"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val defaultColorString: String get() =
|
||||
requireActivity().getString(getColorResId(requireContext(), android.R.attr.colorBackground))
|
||||
.replace("#", "")
|
||||
|
||||
private var wallpaperChangeLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
try {
|
||||
val uri = result.data?.data
|
||||
val projection = arrayOf(MediaStore.Images.Media.DATA)
|
||||
val cursor = requireContext().contentResolver.query(
|
||||
uri!!, projection, null, null, null
|
||||
)
|
||||
cursor?.moveToFirst()
|
||||
val index = cursor!!.getColumnIndex(projection[0])
|
||||
val filePath = cursor.getString(index)
|
||||
cursor.close()
|
||||
val bitmap = BitmapFactory.decodeFile(filePath)
|
||||
val matrix = Matrix()
|
||||
matrix.postRotate(0F)
|
||||
try {
|
||||
if (bitmap != null) {
|
||||
WallpaperManager.getInstance(requireContext()).setBitmap(bitmap)
|
||||
Toast.makeText(requireContext(),
|
||||
requireActivity().getString(R.string.wallpaper_change_success), Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(requireContext(),
|
||||
requireActivity().getString(R.string.image_pick_failed), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(requireContext(),
|
||||
requireActivity().getString(R.string.something_went_wrong), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.settings.childs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.core.view.children
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipDrawable
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.slider.Slider
|
||||
import rasel.lunar.launcher.R
|
||||
import rasel.lunar.launcher.databinding.SettingsAppsBinding
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_GRID_COLUMNS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_ICON_PACK
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_SCROLLBAR_HEIGHT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APPS_COUNT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APPS_LAYOUT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_DRAW_ALIGN
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_GRID_COLUMNS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_ICON_PACK
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_KEYBOARD_SEARCH
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_QUICK_LAUNCH
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_SCROLLBAR_HEIGHT
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.dpToPx
|
||||
import rasel.lunar.launcher.settings.SettingsActivity.Companion.settingsPrefs
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
|
||||
internal class Apps : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var binding: SettingsAppsBinding
|
||||
private var settingsChanged: Boolean = false
|
||||
private var packageManager: PackageManager? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val bottomSheetView = View.inflate(context, R.layout.settings_apps, null)
|
||||
val dialog = super.onCreateDialog(savedInstanceState).apply {
|
||||
setContentView(bottomSheetView)
|
||||
}
|
||||
|
||||
// Set the height to wrap_content
|
||||
BottomSheetBehavior.from(bottomSheetView.parent as View)
|
||||
.peekHeight = resources.displayMetrics.heightPixels // Adjust this if needed
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = SettingsAppsBinding.inflate(inflater, container, false)
|
||||
packageManager = requireActivity().packageManager
|
||||
|
||||
/* initialize views according to the saved values */
|
||||
when (settingsPrefs!!.getBoolean(KEY_KEYBOARD_SEARCH, false)) {
|
||||
false -> binding.keyboardAutoNegative.isChecked = true
|
||||
true -> binding.keyboardAutoPositive.isChecked = true
|
||||
}
|
||||
|
||||
when (settingsPrefs!!.getBoolean(KEY_QUICK_LAUNCH, true)) {
|
||||
true -> binding.quickLaunchPositive.isChecked = true
|
||||
false -> binding.quickLaunchNegative.isChecked = true
|
||||
}
|
||||
|
||||
when (settingsPrefs!!.getBoolean(KEY_APPS_COUNT, true)) {
|
||||
true -> binding.appsCountPositive.isChecked = true
|
||||
false -> binding.appsCountNegative.isChecked = true
|
||||
}
|
||||
|
||||
when (settingsPrefs!!.getInt(KEY_APPS_LAYOUT, 0)) {
|
||||
0 -> {
|
||||
binding.drawerLayoutList.isChecked = true
|
||||
binding.appAlignmentGroup.children.forEach { it.isEnabled = true }
|
||||
binding.iconPackChooser.isEnabled = false
|
||||
binding.columnsCount.isEnabled = false
|
||||
}
|
||||
1 -> {
|
||||
binding.drawerLayoutListIcon.isChecked = true
|
||||
binding.appAlignmentGroup.children.forEach { it.isEnabled = true }
|
||||
binding.iconPackChooser.isEnabled = true
|
||||
binding.columnsCount.isEnabled = false
|
||||
}
|
||||
2 -> {
|
||||
binding.drawerLayoutGrid.isChecked = true
|
||||
binding.appAlignmentGroup.children.forEach { it.isEnabled = false }
|
||||
binding.iconPackChooser.isEnabled = true
|
||||
binding.columnsCount.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
when (settingsPrefs!!.getInt(KEY_DRAW_ALIGN, Gravity.CENTER)) {
|
||||
Gravity.CENTER -> binding.appAlignmentCenter.isChecked = true
|
||||
Gravity.LEFT -> binding.appAlignmentLeft.isChecked = true
|
||||
Gravity.RIGHT -> binding.appAlignmentRight.isChecked = true
|
||||
}
|
||||
|
||||
binding.columnsCount.value = settingsPrefs!!.getInt(KEY_GRID_COLUMNS, DEFAULT_GRID_COLUMNS).toFloat()
|
||||
binding.scrollbarHeight.value = settingsPrefs!!.getInt(KEY_SCROLLBAR_HEIGHT, DEFAULT_SCROLLBAR_HEIGHT).toFloat()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
(requireDialog() as BottomSheetDialog).dismissWithAnimation = true
|
||||
|
||||
/* change search with keyboard value */
|
||||
binding.keyboardAutoGroup.setOnCheckedStateChangeListener { group, _ ->
|
||||
when (group.checkedChipId) {
|
||||
binding.keyboardAutoPositive.id -> settingsPrefs!!.edit().putBoolean(KEY_KEYBOARD_SEARCH, true).apply()
|
||||
binding.keyboardAutoNegative.id -> settingsPrefs!!.edit().putBoolean(KEY_KEYBOARD_SEARCH, false).apply()
|
||||
}
|
||||
}
|
||||
|
||||
/* change settings for quick launch */
|
||||
binding.quickLaunchGroup.setOnCheckedStateChangeListener { group, _ ->
|
||||
when (group.checkedChipId) {
|
||||
binding.quickLaunchPositive.id -> settingsPrefs!!.edit().putBoolean(KEY_QUICK_LAUNCH, true).apply()
|
||||
binding.quickLaunchNegative.id -> settingsPrefs!!.edit().putBoolean(KEY_QUICK_LAUNCH, false).apply()
|
||||
}
|
||||
}
|
||||
|
||||
binding.appsCountGroup.setOnCheckedStateChangeListener { group, _ ->
|
||||
when (group.checkedChipId) {
|
||||
binding.appsCountPositive.id -> settingsPrefs!!.edit().putBoolean(KEY_APPS_COUNT, true).apply()
|
||||
binding.appsCountNegative.id -> settingsPrefs!!.edit().putBoolean(KEY_APPS_COUNT, false).apply()
|
||||
}
|
||||
}
|
||||
|
||||
binding.drawerLayoutGroup.setOnCheckedStateChangeListener { group, _ ->
|
||||
settingsChanged = true
|
||||
when (group.checkedChipId) {
|
||||
binding.drawerLayoutList.id -> {
|
||||
settingsPrefs!!.edit().putInt(KEY_APPS_LAYOUT, 0).apply()
|
||||
binding.appAlignmentGroup.children.forEach { if (!it.isEnabled) it.isEnabled = true }
|
||||
binding.iconPackChooser.let { if (it.isEnabled) it.isEnabled = false }
|
||||
binding.columnsCount.let { if (it.isEnabled) it.isEnabled = false }
|
||||
}
|
||||
binding.drawerLayoutListIcon.id -> {
|
||||
settingsPrefs!!.edit().putInt(KEY_APPS_LAYOUT, 1).apply()
|
||||
binding.appAlignmentGroup.children.forEach { if (!it.isEnabled) it.isEnabled = true }
|
||||
binding.iconPackChooser.let { if (!it.isEnabled) it.isEnabled = true }
|
||||
binding.columnsCount.let { if (it.isEnabled) it.isEnabled = false }
|
||||
}
|
||||
binding.drawerLayoutGrid.id -> {
|
||||
settingsPrefs!!.edit().putInt(KEY_APPS_LAYOUT, 2).apply()
|
||||
binding.appAlignmentGroup.children.forEach { if (it.isEnabled) it.isEnabled = false }
|
||||
binding.iconPackChooser.let { if (!it.isEnabled) it.isEnabled = true }
|
||||
binding.columnsCount.let { if (!it.isEnabled) it.isEnabled = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.appAlignmentGroup.setOnCheckedStateChangeListener { group, _ ->
|
||||
when (group.checkedChipId) {
|
||||
binding.appAlignmentLeft.id -> settingsPrefs!!.edit().putInt(KEY_DRAW_ALIGN, Gravity.LEFT).apply()
|
||||
binding.appAlignmentCenter.id -> settingsPrefs!!.edit().putInt(KEY_DRAW_ALIGN, Gravity.CENTER).apply()
|
||||
binding.appAlignmentRight.id -> settingsPrefs!!.edit().putInt(KEY_DRAW_ALIGN, Gravity.RIGHT).apply()
|
||||
}
|
||||
}
|
||||
|
||||
binding.iconPackChooser.setOnClickListener { iconPackChooser() }
|
||||
|
||||
binding.columnsCount.addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
|
||||
settingsChanged = true
|
||||
settingsPrefs!!.edit().putInt(KEY_GRID_COLUMNS, value.toInt()).apply()
|
||||
})
|
||||
|
||||
binding.scrollbarHeight.addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
|
||||
settingsPrefs!!.edit().putInt(KEY_SCROLLBAR_HEIGHT, value.toInt()).apply()
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
if (settingsChanged) {
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.restart_now)
|
||||
.setMessage(R.string.restart_message)
|
||||
.setPositiveButton(R.string.restart) { _, _ ->
|
||||
exitProcess(0)
|
||||
}
|
||||
.setNeutralButton(R.string.later, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun iconPackChooser() {
|
||||
if (installedIconPacks.isNotEmpty()) {
|
||||
var selectedIconPack: String? = null
|
||||
|
||||
val chipGroup = ChipGroup(requireContext()).apply {
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
|
||||
isSingleSelection = true
|
||||
isSelectionRequired = true
|
||||
setOnCheckedStateChangeListener { group, _ ->
|
||||
selectedIconPack = group.findViewById<Chip>(group.checkedChipId).tag as String
|
||||
}
|
||||
}
|
||||
|
||||
installedIconPacks.indices.forEach { i ->
|
||||
Chip(requireContext()).apply {
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
|
||||
setChipDrawable(ChipDrawable.createFromAttributes(requireContext(), null, 0,
|
||||
com.google.android.material.R.style.Widget_Material3_Chip_Filter_Elevated))
|
||||
|
||||
text = packageManager?.getApplicationLabel(appInfo(installedIconPacks[i])!!)
|
||||
tag = installedIconPacks[i]
|
||||
|
||||
if (settingsPrefs!!.getString(KEY_ICON_PACK, DEFAULT_ICON_PACK).equals(tag as String)) {
|
||||
isChecked = true
|
||||
}
|
||||
}.let { chipGroup.addView(it) }
|
||||
}
|
||||
|
||||
val eightDp = dpToPx(requireContext(), R.dimen.eight)
|
||||
val linearLayoutCompat = LinearLayoutCompat(requireContext()).apply {
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
gravity = Gravity.CENTER
|
||||
setPadding(eightDp, eightDp, eightDp, eightDp)
|
||||
addView(chipGroup)
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(requireActivity()).apply {
|
||||
setTitle(R.string.choose_icon_pack)
|
||||
setView(linearLayoutCompat)
|
||||
setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
when (selectedIconPack) {
|
||||
null -> dialog.dismiss()
|
||||
else -> {
|
||||
if (!selectedIconPack.equals(settingsPrefs!!.getString(KEY_ICON_PACK, DEFAULT_ICON_PACK))) {
|
||||
settingsChanged = true
|
||||
settingsPrefs!!.edit().putString(KEY_ICON_PACK, selectedIconPack).apply()
|
||||
} else { dialog.dismiss() }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
setNeutralButton(R.string.default_) { dialog, _ ->
|
||||
if (DEFAULT_ICON_PACK != settingsPrefs!!.getString(KEY_ICON_PACK, DEFAULT_ICON_PACK)) {
|
||||
settingsChanged = true
|
||||
settingsPrefs!!.edit().putString(KEY_ICON_PACK, DEFAULT_ICON_PACK).apply()
|
||||
} else { dialog.dismiss() }
|
||||
}
|
||||
show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(requireContext(), R.string.icon_pack_not_found, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private val installedIconPacks: ArrayList<String> get() {
|
||||
val iconPacks = ArrayList<String>()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager?.queryIntentActivities(
|
||||
Intent("org.adw.launcher.THEMES"),
|
||||
PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA.toLong())
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
(packageManager?.queryIntentActivities(
|
||||
Intent("org.adw.launcher.THEMES"), PackageManager.GET_META_DATA))
|
||||
}.let {
|
||||
it?.indices?.forEach { i ->
|
||||
it[i].activityInfo.packageName.let { packageName: String? ->
|
||||
iconPacks.add(packageName!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return iconPacks
|
||||
}
|
||||
|
||||
private fun appInfo(packageName: String) : ApplicationInfo? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager?.getApplicationInfo(packageName,
|
||||
PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
packageManager?.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.settings.childs
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.slider.Slider
|
||||
import rasel.lunar.launcher.databinding.SettingsMiscBinding
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_ICON_SIZE
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_BACK_HOME
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_ICON_SIZE
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_LOCK_METHOD
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_RSS_URL
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_SHORTCUT_COUNT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.MAX_SHORTCUTS
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.isRooted
|
||||
import rasel.lunar.launcher.settings.SettingsActivity.Companion.settingsPrefs
|
||||
import java.util.*
|
||||
|
||||
|
||||
internal class Misc : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var binding : SettingsMiscBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = SettingsMiscBinding.inflate(inflater, container, false)
|
||||
|
||||
/* initialize views according to the saved values */
|
||||
when (settingsPrefs!!.getBoolean(KEY_BACK_HOME, false)) {
|
||||
true -> binding.backHomePositive.isChecked = true
|
||||
false -> binding.backHomeNegative.isChecked = true
|
||||
}
|
||||
|
||||
binding.shortcutCount.valueTo = MAX_SHORTCUTS.toFloat()
|
||||
binding.shortcutCount.value = settingsPrefs!!.getInt(KEY_SHORTCUT_COUNT, MAX_SHORTCUTS).toFloat()
|
||||
binding.iconSize.value = settingsPrefs!!.getInt(KEY_ICON_SIZE, DEFAULT_ICON_SIZE).toFloat()
|
||||
binding.inputFeedUrl.text = SpannableStringBuilder(settingsPrefs!!.getString(KEY_RSS_URL, ""))
|
||||
|
||||
when (settingsPrefs!!.getInt(KEY_LOCK_METHOD, 0)) {
|
||||
0 -> binding.selectLockNegative.isChecked = true
|
||||
1 -> binding.selectLockAccessibility.isChecked = true
|
||||
2 -> binding.selectLockAdmin.isChecked = true
|
||||
3 -> binding.selectLockRoot.isChecked = true
|
||||
}
|
||||
|
||||
/* disable accessibility button for devices below android 9 */
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
binding.selectLockAccessibility.isEnabled = false
|
||||
}
|
||||
|
||||
/* disable root button for non-rooted devices */
|
||||
if (!isRooted) {
|
||||
binding.selectLockRoot.isEnabled = false
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
(requireDialog() as BottomSheetDialog).dismissWithAnimation = true
|
||||
|
||||
binding.backHomeGroup.setOnCheckedStateChangeListener { group, _ ->
|
||||
when (group.checkedChipId) {
|
||||
binding.backHomePositive.id -> settingsPrefs!!.edit().putBoolean(KEY_BACK_HOME, true).apply()
|
||||
binding.backHomeNegative.id -> settingsPrefs!!.edit().putBoolean(KEY_BACK_HOME, false).apply()
|
||||
}
|
||||
}
|
||||
|
||||
/* change shortcut count value */
|
||||
binding.shortcutCount.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
|
||||
settingsPrefs!!.edit().putInt(KEY_SHORTCUT_COUNT, value.toInt()).apply()
|
||||
})
|
||||
|
||||
binding.iconSize.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
|
||||
settingsPrefs!!.edit().putInt(KEY_ICON_SIZE, value.toInt()).apply()
|
||||
})
|
||||
|
||||
/* change lock method value */
|
||||
binding.lockGroup.setOnCheckedStateChangeListener { group, _ ->
|
||||
when (group.checkedChipId) {
|
||||
binding.selectLockNegative.id -> settingsPrefs!!.edit().putInt(KEY_LOCK_METHOD, 0).apply()
|
||||
binding.selectLockAccessibility.id -> settingsPrefs!!.edit().putInt(KEY_LOCK_METHOD, 1).apply()
|
||||
binding.selectLockAdmin.id -> settingsPrefs!!.edit().putInt(KEY_LOCK_METHOD, 2).apply()
|
||||
binding.selectLockRoot.id -> settingsPrefs!!.edit().putInt(KEY_LOCK_METHOD, 3).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* save input field value while closing the dialog */
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
settingsPrefs!!.edit().putString(KEY_RSS_URL,
|
||||
Objects.requireNonNull(binding.inputFeedUrl.text).toString().trim { it <= ' ' }).apply()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.settings.childs
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import rasel.lunar.launcher.databinding.SettingsTimeDateBinding
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_DATE_FORMAT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_DATE_FORMAT
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TIME_FORMAT
|
||||
import rasel.lunar.launcher.settings.SettingsActivity.Companion.settingsPrefs
|
||||
import java.util.*
|
||||
|
||||
|
||||
internal class TimeDate : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var binding : SettingsTimeDateBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = SettingsTimeDateBinding.inflate(inflater, container, false)
|
||||
|
||||
/* initialize views according to the saved values */
|
||||
when (settingsPrefs!!.getInt(KEY_TIME_FORMAT, 0)) {
|
||||
0 -> binding.followSystemTime.isChecked = true
|
||||
1 -> binding.selectTwelve.isChecked = true
|
||||
2 -> binding.selectTwentyFour.isChecked = true
|
||||
}
|
||||
|
||||
binding.dateFormat
|
||||
.setText(settingsPrefs!!.getString(KEY_DATE_FORMAT, DEFAULT_DATE_FORMAT).toString())
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
(requireDialog() as BottomSheetDialog).dismissWithAnimation = true
|
||||
|
||||
/* change time format value */
|
||||
binding.timeGroup.setOnCheckedStateChangeListener { group, _ ->
|
||||
when (group.checkedChipId) {
|
||||
binding.followSystemTime.id -> settingsPrefs!!.edit().putInt(KEY_TIME_FORMAT, 0).apply()
|
||||
binding.selectTwelve.id -> settingsPrefs!!.edit().putInt(KEY_TIME_FORMAT, 1).apply()
|
||||
binding.selectTwentyFour.id -> settingsPrefs!!.edit().putInt(KEY_TIME_FORMAT, 2).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* if the input field is empty, then save the default value.
|
||||
else save the value from input field while closing the dialog */
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
val dateFormat = Objects.requireNonNull(binding.dateFormat.text).toString().trim { it <= ' ' }
|
||||
if (dateFormat.isEmpty()) settingsPrefs!!.edit().putString(KEY_DATE_FORMAT, DEFAULT_DATE_FORMAT).apply()
|
||||
else settingsPrefs!!.edit().putString(KEY_DATE_FORMAT, dateFormat).apply()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.settings.childs
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.slider.Slider
|
||||
import rasel.lunar.launcher.databinding.SettingsTodoBinding
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TODO_COUNTS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TODO_LOCK
|
||||
import rasel.lunar.launcher.settings.SettingsActivity.Companion.settingsPrefs
|
||||
|
||||
|
||||
internal class TodoSettings : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var binding : SettingsTodoBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = SettingsTodoBinding.inflate(inflater, container, false)
|
||||
|
||||
/* initialize views according to the saved values */
|
||||
binding.showTodos.value = settingsPrefs!!.getInt(KEY_TODO_COUNTS, 3).toFloat()
|
||||
|
||||
when (settingsPrefs!!.getBoolean(KEY_TODO_LOCK, false)) {
|
||||
false -> binding.todoLockNegative.isChecked = true
|
||||
true -> binding.todoLockPositive.isChecked = true
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
(requireDialog() as BottomSheetDialog).dismissWithAnimation = true
|
||||
|
||||
/* change to-do count value */
|
||||
binding.showTodos.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
|
||||
settingsPrefs!!.edit().putInt(KEY_TODO_COUNTS, value.toInt()).apply()
|
||||
})
|
||||
|
||||
/* change to-do lock state value */
|
||||
binding.todoLockGroup.setOnCheckedStateChangeListener { group, _ ->
|
||||
when (group.checkedChipId) {
|
||||
binding.todoLockPositive.id -> settingsPrefs!!.edit().putBoolean(KEY_TODO_LOCK, true).apply()
|
||||
binding.todoLockNegative.id -> settingsPrefs!!.edit().putBoolean(KEY_TODO_LOCK, false).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.settings.childs
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import rasel.lunar.launcher.databinding.SettingsWeatherBinding
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_CITY_NAME
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_OWM_API
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_SHOW_CITY
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TEMP_UNIT
|
||||
import rasel.lunar.launcher.settings.SettingsActivity.Companion.settingsPrefs
|
||||
import java.util.*
|
||||
|
||||
|
||||
internal class WeatherSettings : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var binding : SettingsWeatherBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = SettingsWeatherBinding.inflate(inflater, container, false)
|
||||
|
||||
/* initialize views according to the saved values */
|
||||
binding.inputCity.setText(settingsPrefs!!.getString(KEY_CITY_NAME, "").toString())
|
||||
binding.inputOwm.setText(settingsPrefs!!.getString(KEY_OWM_API, "").toString())
|
||||
|
||||
when (settingsPrefs!!.getInt(KEY_TEMP_UNIT, 0)) {
|
||||
0 -> binding.selectCelsius.isChecked = true
|
||||
1 -> binding.selectFahrenheit.isChecked = true
|
||||
}
|
||||
|
||||
when (settingsPrefs!!.getBoolean(KEY_SHOW_CITY, false)) {
|
||||
false -> binding.showCityNegative.isChecked = true
|
||||
true -> binding.showCityPositive.isChecked = true
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
(requireDialog() as BottomSheetDialog).dismissWithAnimation = true
|
||||
|
||||
/* change temperature unit value */
|
||||
binding.tempGroup.setOnCheckedStateChangeListener { group, _ ->
|
||||
when (group.checkedChipId) {
|
||||
binding.selectCelsius.id -> settingsPrefs!!.edit().putInt(KEY_TEMP_UNIT, 0).apply()
|
||||
binding.selectFahrenheit.id -> settingsPrefs!!.edit().putInt(KEY_TEMP_UNIT, 1).apply()
|
||||
}
|
||||
}
|
||||
|
||||
/* change show city value */
|
||||
binding.cityGroup.setOnCheckedStateChangeListener { group, _ ->
|
||||
when (group.checkedChipId) {
|
||||
binding.showCityNegative.id -> settingsPrefs!!.edit().putBoolean(KEY_SHOW_CITY, false).apply()
|
||||
binding.showCityPositive.id -> settingsPrefs!!.edit().putBoolean(KEY_SHOW_CITY, true).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* save input field values while closing the dialog */
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
settingsPrefs!!.edit().putString(KEY_CITY_NAME,
|
||||
Objects.requireNonNull(binding.inputCity.text).toString().trim { it <= ' ' }).apply()
|
||||
settingsPrefs!!.edit().putString(KEY_OWM_API,
|
||||
Objects.requireNonNull(binding.inputOwm.text).toString().trim { it <= ' ' }).apply()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.todos
|
||||
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.content.ContentValues
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.database.DatabaseUtils
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.TODO_COLUMN_CREATED
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.TODO_COLUMN_ID
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.TODO_COLUMN_NAME
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.TODO_DATABASE_NAME
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.TODO_DATABASE_VERSION
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.TODO_TABLE_NAME
|
||||
import java.util.ArrayList
|
||||
|
||||
|
||||
internal class DatabaseHandler(context: Context?) :
|
||||
SQLiteOpenHelper(context, TODO_DATABASE_NAME, null, TODO_DATABASE_VERSION) {
|
||||
|
||||
/* create database */
|
||||
override fun onCreate(database: SQLiteDatabase) {
|
||||
val createTodoTable = "CREATE TABLE " + TODO_TABLE_NAME + " (" +
|
||||
TODO_COLUMN_ID + " integer PRIMARY KEY AUTOINCREMENT," +
|
||||
TODO_COLUMN_CREATED + " datetime DEFAULT CURRENT_TIMESTAMP," +
|
||||
TODO_COLUMN_NAME + " varchar)"
|
||||
database.execSQL(createTodoTable)
|
||||
}
|
||||
|
||||
override fun onUpgrade(sqLiteDatabase: SQLiteDatabase, i: Int, i1: Int) {}
|
||||
|
||||
/* add new to-do entry */
|
||||
fun addTodo(todo: Todo) {
|
||||
val database = writableDatabase
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(TODO_COLUMN_NAME, todo.name)
|
||||
database.insert(TODO_TABLE_NAME, null, contentValues)
|
||||
}
|
||||
|
||||
/* update or edit existing to-do */
|
||||
fun updateTodo(todo: Todo) {
|
||||
val database = writableDatabase
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(TODO_COLUMN_NAME, todo.name)
|
||||
database.update(
|
||||
TODO_TABLE_NAME,
|
||||
contentValues,
|
||||
"$TODO_COLUMN_ID=?",
|
||||
arrayOf(todo.id.toString())
|
||||
)
|
||||
}
|
||||
|
||||
/* delete a single to-do */
|
||||
fun deleteTodo(todoId: Long) {
|
||||
writableDatabase.delete(TODO_TABLE_NAME,
|
||||
"$TODO_COLUMN_ID=?", arrayOf(todoId.toString()))
|
||||
}
|
||||
|
||||
/* delete all existing todos at once */
|
||||
fun deleteAll() {
|
||||
writableDatabase.delete(TODO_TABLE_NAME, null, null)
|
||||
}
|
||||
|
||||
@get:SuppressLint("Range")
|
||||
val todos: ArrayList<Todo>
|
||||
get() {
|
||||
val todoList = ArrayList<Todo>()
|
||||
val queryResult =
|
||||
readableDatabase.rawQuery("SELECT * from $TODO_TABLE_NAME", null)
|
||||
|
||||
if (queryResult.moveToFirst()) {
|
||||
do {
|
||||
val todo = Todo()
|
||||
todo.id = queryResult.getLong(queryResult.getColumnIndex(TODO_COLUMN_ID))
|
||||
todo.name = queryResult.getString(queryResult.getColumnIndex(TODO_COLUMN_NAME))
|
||||
todoList.add(todo)
|
||||
} while (queryResult.moveToNext())
|
||||
}
|
||||
|
||||
queryResult.close()
|
||||
return todoList
|
||||
}
|
||||
|
||||
/* check if any item exists in the database */
|
||||
val isTodoExists: Boolean get() {
|
||||
return DatabaseUtils.queryNumEntries(readableDatabase, TODO_TABLE_NAME, 1.toString()) > 0
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.todos
|
||||
|
||||
|
||||
internal class Todo {
|
||||
var id: Long = -1
|
||||
var name = ""
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.todos
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
|
||||
import rasel.lunar.launcher.R
|
||||
import rasel.lunar.launcher.databinding.ListItemBinding
|
||||
import rasel.lunar.launcher.databinding.TodoDialogBinding
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TODO_COUNTS
|
||||
import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
|
||||
import rasel.lunar.launcher.helpers.UniUtils.Companion.copyToClipboard
|
||||
import java.util.*
|
||||
|
||||
|
||||
internal class TodoAdapter(
|
||||
private val todoManager: TodoManager?,
|
||||
private val context: Context) : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {
|
||||
|
||||
private val currentFragment = lActivity!!.supportFragmentManager.findFragmentById(R.id.mainFragmentsContainer)
|
||||
private val todoList = DatabaseHandler(context).todos
|
||||
|
||||
override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): TodoViewHolder {
|
||||
val binding = ListItemBinding.inflate(LayoutInflater.from(viewGroup.context), viewGroup, false)
|
||||
return TodoViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
/* if current fragment is LauncherHome,
|
||||
then return size following the settings value */
|
||||
val sharedPreferences = context.getSharedPreferences(PREFS_SETTINGS, 0)
|
||||
val numberOfTodos = sharedPreferences.getInt(KEY_TODO_COUNTS, 3)
|
||||
return if (currentFragment !is TodoManager) {
|
||||
todoList.size.coerceAtMost(numberOfTodos)
|
||||
} else {
|
||||
todoList.size
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
|
||||
val todo = todoList[position]
|
||||
|
||||
holder.view.itemText.text = "\u25CF ${todo.name}"
|
||||
|
||||
if (currentFragment is TodoManager) {
|
||||
/* multiline texts are enabled for TodoManager */
|
||||
holder.view.itemText.isSingleLine = false
|
||||
/* launch edit or update dialog on item click */
|
||||
holder.view.itemText.setOnClickListener { updateDialog(position) }
|
||||
/* copy texts on long click */
|
||||
holder.view.itemText.setOnLongClickListener {
|
||||
copyToClipboard(context, todo.name)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
/* single line text for home screen */
|
||||
holder.view.itemText.isSingleLine = true
|
||||
}
|
||||
}
|
||||
|
||||
inner class TodoViewHolder(var view: ListItemBinding) : RecyclerView.ViewHolder(view.root)
|
||||
|
||||
/* update dialog */
|
||||
private fun updateDialog(position: Int) {
|
||||
val bottomSheetDialog = BottomSheetDialog(lActivity!!, R.style.BottomSheetDialog)
|
||||
val dialogBinding = TodoDialogBinding.inflate(LayoutInflater.from(context))
|
||||
bottomSheetDialog.setContentView(dialogBinding.root)
|
||||
bottomSheetDialog.show()
|
||||
bottomSheetDialog.dismissWithAnimation = true
|
||||
|
||||
val databaseHandler = DatabaseHandler(context)
|
||||
val todo = databaseHandler.todos[position]
|
||||
|
||||
dialogBinding.apply {
|
||||
deleteAllConfirmation.visibility = View.GONE
|
||||
todoInput.setText(todo.name)
|
||||
todoCancel.text = context.getString(R.string.delete)
|
||||
todoCancel.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_light))
|
||||
todoOk.text = context.getString(R.string.update)
|
||||
}
|
||||
|
||||
/* delete the item */
|
||||
dialogBinding.todoCancel.setOnClickListener {
|
||||
databaseHandler.deleteTodo(todo.id)
|
||||
bottomSheetDialog.dismiss()
|
||||
todoManager?.refreshList()
|
||||
}
|
||||
|
||||
/* update the item */
|
||||
dialogBinding.todoOk.setOnClickListener {
|
||||
val updatedTodoString = Objects.requireNonNull(dialogBinding.todoInput.text).toString().trim { it <= ' ' }
|
||||
if (updatedTodoString.isNotEmpty()) {
|
||||
todo.name = updatedTodoString
|
||||
databaseHandler.updateTodo(todo)
|
||||
bottomSheetDialog.dismiss()
|
||||
todoManager?.refreshList()
|
||||
} else {
|
||||
dialogBinding.todoInput.error = context.getString(R.string.empty_text_field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Lunar Launcher
|
||||
* Copyright (C) 2022 Md Rasel Hossain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rasel.lunar.launcher.todos
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
|
||||
import rasel.lunar.launcher.R
|
||||
import rasel.lunar.launcher.databinding.TodoDialogBinding
|
||||
import rasel.lunar.launcher.databinding.TodoManagerBinding
|
||||
import java.util.*
|
||||
|
||||
|
||||
internal class TodoManager : Fragment() {
|
||||
|
||||
private lateinit var binding: TodoManagerBinding
|
||||
private lateinit var databaseHandler: DatabaseHandler
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
|
||||
binding = TodoManagerBinding.inflate(inflater, container, false)
|
||||
databaseHandler = DatabaseHandler(requireContext())
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
/* click listeners for add new and delete all buttons */
|
||||
binding.addNew.setOnClickListener { addNewDialog() }
|
||||
binding.deleteAll.setOnClickListener { deleteAllDialog() }
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
refreshList()
|
||||
lActivity!!.viewPager.isUserInputEnabled = false
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
lActivity!!.viewPager.isUserInputEnabled = true
|
||||
}
|
||||
|
||||
fun refreshList() {
|
||||
binding.todos.adapter = TodoAdapter(this, requireContext())
|
||||
}
|
||||
|
||||
/* add new dialog */
|
||||
private fun addNewDialog() {
|
||||
val bottomSheetDialog = BottomSheetDialog(lActivity!!, R.style.BottomSheetDialog)
|
||||
val dialogBinding = TodoDialogBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
bottomSheetDialog.setContentView(dialogBinding.root)
|
||||
bottomSheetDialog.show()
|
||||
bottomSheetDialog.dismissWithAnimation = true
|
||||
|
||||
dialogBinding.deleteAllConfirmation.visibility = View.GONE
|
||||
/* automatic keyboard popup */
|
||||
dialogBinding.todoInput.requestFocus()
|
||||
val inputMethodManager = lActivity!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.showSoftInput(dialogBinding.todoInput, InputMethodManager.SHOW_IMPLICIT)
|
||||
|
||||
/* dismiss the dialog on cancel button click */
|
||||
dialogBinding.todoCancel.setOnClickListener { bottomSheetDialog.dismiss() }
|
||||
/* add new item to the database */
|
||||
dialogBinding.todoOk.setOnClickListener {
|
||||
val todo = Todo()
|
||||
val todoString = Objects.requireNonNull(dialogBinding.todoInput.text).toString().trim { it <= ' ' }
|
||||
if (todoString.isNotEmpty()) {
|
||||
todo.name = todoString
|
||||
databaseHandler.addTodo(todo)
|
||||
bottomSheetDialog.dismiss()
|
||||
refreshList()
|
||||
} else {
|
||||
dialogBinding.todoInput.error = getString(R.string.empty_text_field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* delete all dialog */
|
||||
private fun deleteAllDialog() {
|
||||
val bottomSheetDialog = BottomSheetDialog(lActivity!!)
|
||||
val dialogBinding = TodoDialogBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
bottomSheetDialog.setContentView(dialogBinding.root)
|
||||
bottomSheetDialog.show()
|
||||
bottomSheetDialog.dismissWithAnimation = true
|
||||
|
||||
/* if any item does not exist, then disable the ok button */
|
||||
if (!databaseHandler.isTodoExists) {
|
||||
dialogBinding.todoOk.isEnabled = false
|
||||
}
|
||||
|
||||
dialogBinding.todoInput.visibility = View.GONE
|
||||
dialogBinding.todoOk.setTextColor(ContextCompat.getColor(requireContext(), android.R.color.holo_red_light))
|
||||
|
||||
/* dismiss the dialog on cancel button click */
|
||||
dialogBinding.todoCancel.setOnClickListener { bottomSheetDialog.dismiss() }
|
||||
/* delete all the existing items from the database */
|
||||
dialogBinding.todoOk.setOnClickListener {
|
||||
databaseHandler.deleteAll()
|
||||
bottomSheetDialog.dismiss()
|
||||
refreshList()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package rasel.lunar.launcher.utils
|
||||
|
||||
import android.util.Log
|
||||
import rasel.lunar.launcher.BuildConfig
|
||||
import java.lang.Exception
|
||||
|
||||
object BLog {
|
||||
val DEFAULT_TAG = "MyEBook_TAG"
|
||||
enum class BLogType {
|
||||
D,I,E
|
||||
}
|
||||
fun w(tag : String = DEFAULT_TAG, log: String){
|
||||
LOG(BLogType.D,tag,log)
|
||||
}
|
||||
|
||||
fun LOGD(tag : String = DEFAULT_TAG, log: String){
|
||||
LOG(BLogType.D,tag,log)
|
||||
}
|
||||
|
||||
fun LOGI(tag : String = DEFAULT_TAG, log: String){
|
||||
LOG(BLogType.I,tag,log)
|
||||
}
|
||||
|
||||
fun LOGE(tag : String = DEFAULT_TAG, log: Throwable){
|
||||
LOG(BLogType.E,tag,log.toString())
|
||||
}
|
||||
|
||||
fun LOGE(tag : String = DEFAULT_TAG, log: Exception){
|
||||
LOG(BLogType.E,tag,log.toString())
|
||||
}
|
||||
|
||||
fun LOGE(tag : String = DEFAULT_TAG, log: String){
|
||||
LOG(BLogType.E,tag,log)
|
||||
}
|
||||
|
||||
fun LOGE(log: String){
|
||||
LOG(BLogType.E,DEFAULT_TAG,log)
|
||||
}
|
||||
|
||||
private fun LOG(type : BLogType, tag : String, log : String) {
|
||||
if (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.contains("debug")) {
|
||||
when(type) {
|
||||
BLogType.D -> {
|
||||
Log.d(tag,log)
|
||||
}
|
||||
BLogType.I -> {
|
||||
Log.i(tag,log)
|
||||
}
|
||||
BLogType.E -> {
|
||||
Log.e(tag,log)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,781 @@
|
||||
package rasel.lunar.launcher.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.View.OnTouchListener
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
|
||||
class GestureAnalyser @JvmOverloads constructor(
|
||||
swipeSlopeIntolerance: Int = 2,
|
||||
doubleTapMaxDelayMillis: Int = 500,
|
||||
doubleTapMaxDownMillis: Int = 100
|
||||
) {
|
||||
var minValue = 100
|
||||
private val initialX = DoubleArray(5)
|
||||
private val initialY = DoubleArray(5)
|
||||
private val finalX = DoubleArray(5)
|
||||
private val finalY = DoubleArray(5)
|
||||
private val currentX = DoubleArray(5)
|
||||
private val currentY = DoubleArray(5)
|
||||
private val delX = DoubleArray(5)
|
||||
private val delY = DoubleArray(5)
|
||||
|
||||
private var numFingers = 0
|
||||
private var initialT: Long = 0
|
||||
private var finalT: Long = 0
|
||||
private var currentT: Long = 0
|
||||
|
||||
private var prevInitialT: Long = 0
|
||||
private var prevFinalT: Long = 0
|
||||
|
||||
private var swipeSlopeIntolerance = 2
|
||||
|
||||
private val doubleTapMaxDelayMillis: Long
|
||||
private val doubleTapMaxDownMillis: Long
|
||||
|
||||
init {
|
||||
this.swipeSlopeIntolerance = swipeSlopeIntolerance
|
||||
this.doubleTapMaxDownMillis = doubleTapMaxDownMillis.toLong()
|
||||
this.doubleTapMaxDelayMillis = doubleTapMaxDelayMillis.toLong()
|
||||
}
|
||||
|
||||
fun trackGesture(ev: MotionEvent) {
|
||||
val n = ev.pointerCount
|
||||
for (i in 0 until n) {
|
||||
initialX[i] = ev.getX(i).toDouble()
|
||||
initialY[i] = ev.getY(i).toDouble()
|
||||
}
|
||||
numFingers = n
|
||||
initialT = SystemClock.uptimeMillis()
|
||||
}
|
||||
|
||||
fun untrackGesture() {
|
||||
numFingers = 0
|
||||
prevFinalT = SystemClock.uptimeMillis()
|
||||
prevInitialT = initialT
|
||||
}
|
||||
|
||||
fun getGesture(ev: MotionEvent): GestureType {
|
||||
var averageDistance = 0.0
|
||||
for (i in 0 until numFingers) {
|
||||
finalX[i] = ev.getX(i).toDouble()
|
||||
finalY[i] = ev.getY(i).toDouble()
|
||||
delX[i] = finalX[i] - initialX[i]
|
||||
delY[i] = finalY[i] - initialY[i]
|
||||
|
||||
averageDistance += sqrt(
|
||||
(finalX[i] - initialX[i]).pow(2.0) + (finalY[i] - initialY[i]).pow(
|
||||
2.0
|
||||
)
|
||||
)
|
||||
}
|
||||
averageDistance /= numFingers.toDouble()
|
||||
|
||||
finalT = SystemClock.uptimeMillis()
|
||||
val gt = GestureType()
|
||||
gt.gestureFlag = calcGesture()
|
||||
gt.gestureDuration = finalT - initialT
|
||||
gt.gestureDistance = averageDistance
|
||||
return gt
|
||||
}
|
||||
|
||||
fun getOngoingGesture(ev: MotionEvent): Int {
|
||||
for (i in 0 until numFingers) {
|
||||
currentX[i] = ev.getX(i).toDouble()
|
||||
currentY[i] = ev.getY(i).toDouble()
|
||||
delX[i] = finalX[i] - initialX[i]
|
||||
delY[i] = finalY[i] - initialY[i]
|
||||
}
|
||||
currentT = SystemClock.uptimeMillis()
|
||||
return calcGesture()
|
||||
}
|
||||
|
||||
private fun calcGesture(): Int {
|
||||
if (isDoubleTap) {
|
||||
return DOUBLE_TAP_1
|
||||
}
|
||||
|
||||
if (numFingers == 1) {
|
||||
if ((-(delY[0])) > (swipeSlopeIntolerance * (abs(
|
||||
delX[0]
|
||||
))) && abs(delY[0]) > minValue
|
||||
) {
|
||||
return SWIPE_1_UP
|
||||
}
|
||||
|
||||
if (((delY[0])) > (swipeSlopeIntolerance * (abs(
|
||||
delX[0]
|
||||
))) && abs(delY[0]) > minValue
|
||||
) {
|
||||
return SWIPE_1_DOWN
|
||||
}
|
||||
|
||||
if ((-(delX[0])) > (swipeSlopeIntolerance * (abs(
|
||||
delY[0]
|
||||
))) && abs(delX[0]) > minValue
|
||||
) {
|
||||
return SWIPE_1_LEFT
|
||||
}
|
||||
|
||||
if (((delX[0])) > (swipeSlopeIntolerance * (abs(
|
||||
delY[0]
|
||||
))) && abs(delX[0]) > minValue
|
||||
) {
|
||||
return SWIPE_1_RIGHT
|
||||
}
|
||||
BLog.LOGE("initialT = ${initialT} , finalT = ${finalT} , result = ${finalT - initialT}")
|
||||
if (finalT - initialT < 300) {
|
||||
return CLICK_1
|
||||
} else if(finalT - initialT > 600) {
|
||||
return LONG_CLICK_1
|
||||
}
|
||||
}
|
||||
if (numFingers == 2) {
|
||||
if (((-delY[0]) > (swipeSlopeIntolerance * abs(
|
||||
delX[0]
|
||||
))) && ((-delY[1]) > (swipeSlopeIntolerance * abs(
|
||||
delX[1]
|
||||
))) && (abs(delY[0]) > minValue || abs(delY[1]) > minValue)
|
||||
) {
|
||||
return SWIPE_2_UP
|
||||
}
|
||||
if (((delY[0]) > (swipeSlopeIntolerance * abs(
|
||||
delX[0]
|
||||
))) && ((delY[1]) > (swipeSlopeIntolerance * abs(
|
||||
delX[1]
|
||||
))) && (abs(delY[0]) > minValue || abs(delY[1]) > minValue)
|
||||
) {
|
||||
return SWIPE_2_DOWN
|
||||
}
|
||||
if (((-delX[0]) > (swipeSlopeIntolerance * abs(
|
||||
delY[0]
|
||||
))) && ((-delX[1]) > (swipeSlopeIntolerance * abs(
|
||||
delY[1]
|
||||
))) && (abs(delX[0]) > minValue || abs(delX[1]) > minValue)
|
||||
) {
|
||||
return SWIPE_2_LEFT
|
||||
}
|
||||
if (((delX[0]) > (swipeSlopeIntolerance * abs(
|
||||
delY[0]
|
||||
))) && ((delX[1]) > (swipeSlopeIntolerance * abs(
|
||||
delY[1]
|
||||
))) && (abs(delX[0]) > minValue || abs(delX[1]) > minValue)
|
||||
) {
|
||||
return SWIPE_2_RIGHT
|
||||
}
|
||||
if (finalFingDist(0, 1) > 2 * (initialFingDist(0, 1))) {
|
||||
return UNPINCH_2
|
||||
}
|
||||
if (finalFingDist(0, 1) < 0.5 * (initialFingDist(0, 1))) {
|
||||
return PINCH_2
|
||||
}
|
||||
}
|
||||
if (numFingers == 3) {
|
||||
if (((-delY[0]) > (swipeSlopeIntolerance * abs(
|
||||
delX[0]
|
||||
)))
|
||||
&& ((-delY[1]) > (swipeSlopeIntolerance * abs(
|
||||
delX[1]
|
||||
)))
|
||||
&& ((-delY[2]) > (swipeSlopeIntolerance * abs(
|
||||
delX[2]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_3_UP
|
||||
}
|
||||
if (((delY[0]) > (swipeSlopeIntolerance * abs(
|
||||
delX[0]
|
||||
)))
|
||||
&& ((delY[1]) > (swipeSlopeIntolerance * abs(
|
||||
delX[1]
|
||||
)))
|
||||
&& ((delY[2]) > (swipeSlopeIntolerance * abs(
|
||||
delX[2]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_3_DOWN
|
||||
}
|
||||
if (((-delX[0]) > (swipeSlopeIntolerance * abs(
|
||||
delY[0]
|
||||
)))
|
||||
&& ((-delX[1]) > (swipeSlopeIntolerance * abs(
|
||||
delY[1]
|
||||
)))
|
||||
&& ((-delX[2]) > (swipeSlopeIntolerance * abs(
|
||||
delY[2]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_3_LEFT
|
||||
}
|
||||
if (((delX[0]) > (swipeSlopeIntolerance * abs(
|
||||
delY[0]
|
||||
)))
|
||||
&& ((delX[1]) > (swipeSlopeIntolerance * abs(
|
||||
delY[1]
|
||||
)))
|
||||
&& ((delX[2]) > (swipeSlopeIntolerance * abs(
|
||||
delY[2]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_3_RIGHT
|
||||
}
|
||||
|
||||
if ((finalFingDist(0, 1) > 1.75 * (initialFingDist(0, 1)))
|
||||
&& (finalFingDist(1, 2) > 1.75 * (initialFingDist(1, 2)))
|
||||
&& (finalFingDist(2, 0) > 1.75 * (initialFingDist(2, 0)))
|
||||
) {
|
||||
return UNPINCH_3
|
||||
}
|
||||
if ((finalFingDist(0, 1) < 0.66 * (initialFingDist(0, 1)))
|
||||
&& (finalFingDist(1, 2) < 0.66 * (initialFingDist(1, 2)))
|
||||
&& (finalFingDist(2, 0) < 0.66 * (initialFingDist(2, 0)))
|
||||
) {
|
||||
return PINCH_3
|
||||
}
|
||||
}
|
||||
if (numFingers == 4) {
|
||||
if (((-delY[0]) > (swipeSlopeIntolerance * abs(
|
||||
delX[0]
|
||||
)))
|
||||
&& ((-delY[1]) > (swipeSlopeIntolerance * abs(
|
||||
delX[1]
|
||||
)))
|
||||
&& ((-delY[2]) > (swipeSlopeIntolerance * abs(
|
||||
delX[2]
|
||||
)))
|
||||
&& ((-delY[3]) > (swipeSlopeIntolerance * abs(
|
||||
delX[3]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_4_UP
|
||||
}
|
||||
if (((delY[0]) > (swipeSlopeIntolerance * abs(
|
||||
delX[0]
|
||||
)))
|
||||
&& ((delY[1]) > (swipeSlopeIntolerance * abs(
|
||||
delX[1]
|
||||
)))
|
||||
&& ((delY[2]) > (swipeSlopeIntolerance * abs(
|
||||
delX[2]
|
||||
)))
|
||||
&& ((delY[3]) > (swipeSlopeIntolerance * abs(
|
||||
delX[3]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_4_DOWN
|
||||
}
|
||||
if (((-delX[0]) > (swipeSlopeIntolerance * abs(
|
||||
delY[0]
|
||||
)))
|
||||
&& ((-delX[1]) > (swipeSlopeIntolerance * abs(
|
||||
delY[1]
|
||||
)))
|
||||
&& ((-delX[2]) > (swipeSlopeIntolerance * abs(
|
||||
delY[2]
|
||||
)))
|
||||
&& ((-delX[3]) > (swipeSlopeIntolerance * abs(
|
||||
delY[3]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_4_LEFT
|
||||
}
|
||||
if (((delX[0]) > (swipeSlopeIntolerance * abs(
|
||||
delY[0]
|
||||
)))
|
||||
&& ((delX[1]) > (swipeSlopeIntolerance * abs(
|
||||
delY[1]
|
||||
)))
|
||||
&& ((delX[2]) > (swipeSlopeIntolerance * abs(
|
||||
delY[2]
|
||||
)))
|
||||
&& ((delX[3]) > (swipeSlopeIntolerance * abs(
|
||||
delY[3]
|
||||
)))
|
||||
) {
|
||||
return SWIPE_4_RIGHT
|
||||
}
|
||||
if ((finalFingDist(0, 1) > 1.5 * (initialFingDist(0, 1)))
|
||||
&& (finalFingDist(1, 2) > 1.5 * (initialFingDist(1, 2)))
|
||||
&& (finalFingDist(2, 3) > 1.5 * (initialFingDist(2, 3)))
|
||||
&& (finalFingDist(3, 0) > 1.5 * (initialFingDist(3, 0)))
|
||||
) {
|
||||
return UNPINCH_4
|
||||
}
|
||||
if ((finalFingDist(0, 1) < 0.8 * (initialFingDist(0, 1)))
|
||||
&& (finalFingDist(1, 2) < 0.8 * (initialFingDist(1, 2)))
|
||||
&& (finalFingDist(2, 3) < 0.8 * (initialFingDist(2, 3)))
|
||||
&& (finalFingDist(3, 0) < 0.8 * (initialFingDist(3, 0)))
|
||||
) {
|
||||
return PINCH_4
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun initialFingDist(fingNum1: Int, fingNum2: Int): Double {
|
||||
return sqrt(
|
||||
(initialX[fingNum1] - initialX[fingNum2]).pow(2.0) + (initialY[fingNum1] - initialY[fingNum2]).pow(
|
||||
2.0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun finalFingDist(fingNum1: Int, fingNum2: Int): Double {
|
||||
return sqrt(
|
||||
(finalX[fingNum1] - finalX[fingNum2]).pow(2.0) + (finalY[fingNum1] - finalY[fingNum2]).pow(
|
||||
2.0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val isDoubleTap: Boolean
|
||||
get() = if (initialT - prevFinalT < doubleTapMaxDelayMillis && finalT - initialT < doubleTapMaxDownMillis && prevFinalT - prevInitialT < doubleTapMaxDownMillis) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
inner class GestureType {
|
||||
var gestureFlag: Int = 0
|
||||
var gestureDuration: Long = 0
|
||||
|
||||
var gestureDistance: Double = 0.0
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
const val DEBUG: Boolean = true
|
||||
|
||||
// Finished gestures flags
|
||||
const val SWIPE_1_UP: Int = 11
|
||||
const val SWIPE_1_DOWN: Int = 12
|
||||
const val SWIPE_1_LEFT: Int = 13
|
||||
const val SWIPE_1_RIGHT: Int = 14
|
||||
const val SWIPE_2_UP: Int = 21
|
||||
const val SWIPE_2_DOWN: Int = 22
|
||||
const val SWIPE_2_LEFT: Int = 23
|
||||
const val SWIPE_2_RIGHT: Int = 24
|
||||
const val SWIPE_3_UP: Int = 31
|
||||
const val SWIPE_3_DOWN: Int = 32
|
||||
const val SWIPE_3_LEFT: Int = 33
|
||||
const val SWIPE_3_RIGHT: Int = 34
|
||||
const val SWIPE_4_UP: Int = 41
|
||||
const val SWIPE_4_DOWN: Int = 42
|
||||
const val SWIPE_4_LEFT: Int = 43
|
||||
const val SWIPE_4_RIGHT: Int = 44
|
||||
const val PINCH_2: Int = 25
|
||||
const val UNPINCH_2: Int = 26
|
||||
const val PINCH_3: Int = 35
|
||||
const val UNPINCH_3: Int = 36
|
||||
const val PINCH_4: Int = 45
|
||||
const val UNPINCH_4: Int = 46
|
||||
|
||||
const val DOUBLE_TAP_1: Int = 107
|
||||
|
||||
const val CLICK_1 = 19001
|
||||
const val CLICK_2 = 19002
|
||||
const val CLICK_3 = 19003
|
||||
|
||||
|
||||
|
||||
const val LONG_CLICK_1 = 29001
|
||||
const val LONG_CLICK_2 = 29002
|
||||
const val LONG_CLICK_3 = 29003
|
||||
|
||||
//Ongoing gesture flags
|
||||
const val SWIPING_1_UP: Int = 101
|
||||
const val SWIPING_1_DOWN: Int = 102
|
||||
const val SWIPING_1_LEFT: Int = 103
|
||||
const val SWIPING_1_RIGHT: Int = 104
|
||||
const val SWIPING_2_UP: Int = 201
|
||||
const val SWIPING_2_DOWN: Int = 202
|
||||
const val SWIPING_2_LEFT: Int = 203
|
||||
const val SWIPING_2_RIGHT: Int = 204
|
||||
const val PINCHING: Int = 205
|
||||
const val UNPINCHING: Int = 206
|
||||
private const val TAG = "GestureAnalyser"
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleFingerGestures : OnTouchListener {
|
||||
private var debug = true
|
||||
var consumeTouchEvents: Boolean = false
|
||||
var screenHeight : Int = 100
|
||||
protected var tracking: BooleanArray = booleanArrayOf(false, false, false, false, false)
|
||||
private var ga: GestureAnalyser
|
||||
private var onFingerGestureListener: OnFingerGestureListener? = null
|
||||
var targetView : View? = null
|
||||
var mContext : Context? = null
|
||||
constructor(context : Context,targetView : View, onFingerGestureListener: OnFingerGestureListener) {
|
||||
this.mContext = context
|
||||
|
||||
|
||||
this.targetView = targetView
|
||||
ga = GestureAnalyser()
|
||||
this.mContext?.resources?.displayMetrics?.let {
|
||||
screenHeight = (it.heightPixels * 0.18).toInt()
|
||||
ga.minValue = Math.max(screenHeight, 100)
|
||||
}
|
||||
this.onFingerGestureListener = onFingerGestureListener
|
||||
this.targetView?.setOnClickListener { onFingerGestureListener.onClick(it) }
|
||||
this.targetView?.setOnLongClickListener { onFingerGestureListener.onLongPress(it) }
|
||||
}
|
||||
/**
|
||||
* Constructor that creates an internal [in.championswimmer.sfg.lib.GestureAnalyser] object as well
|
||||
*/
|
||||
constructor() {
|
||||
ga = GestureAnalyser()
|
||||
}
|
||||
|
||||
constructor(
|
||||
swipeSlopeIntolerance: Int,
|
||||
doubleTapMaxDelayMillis: Int,
|
||||
doubleTapMaxDownMillis: Int
|
||||
) {
|
||||
ga = GestureAnalyser(swipeSlopeIntolerance, doubleTapMaxDelayMillis, doubleTapMaxDownMillis)
|
||||
}
|
||||
|
||||
fun setDebug(debug: Boolean) {
|
||||
this.debug = debug
|
||||
}
|
||||
|
||||
constructor(omfgl: OnFingerGestureListener?) {
|
||||
ga = GestureAnalyser()
|
||||
setOnFingerGestureListener(omfgl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback to be invoked when multi-finger gestures take place
|
||||
*
|
||||
*
|
||||
* <br></br>
|
||||
*
|
||||
*
|
||||
* For the callbacks implemented via this, check the interface [in.championswimmer.sfg.lib.SimpleFingerGestures.OnFingerGestureListener]
|
||||
*
|
||||
*
|
||||
* @param omfgl The callback that will run
|
||||
*/
|
||||
fun setOnFingerGestureListener(omfgl: OnFingerGestureListener?) {
|
||||
onFingerGestureListener = omfgl
|
||||
}
|
||||
|
||||
|
||||
override fun onTouch(view: View, ev: MotionEvent): Boolean {
|
||||
if (debug) Log.d(TAG, "onTouch")
|
||||
when (ev.action and MotionEvent.ACTION_MASK) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (debug) Log.d(TAG, "ACTION_DOWN")
|
||||
startTracking(0)
|
||||
ga.trackGesture(ev)
|
||||
return consumeTouchEvents
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (debug) Log.d(TAG, "ACTION_UP")
|
||||
if (tracking[0]) {
|
||||
doCallBack(view,ga.getGesture(ev))
|
||||
}
|
||||
stopTracking(0)
|
||||
ga.untrackGesture()
|
||||
return consumeTouchEvents
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
if (debug) Log.d(TAG, "ACTION_POINTER_DOWN" + " " + "num" + ev.pointerCount)
|
||||
startTracking(ev.pointerCount - 1)
|
||||
ga.trackGesture(ev)
|
||||
return consumeTouchEvents
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_POINTER_UP -> {
|
||||
if (debug) Log.d(TAG, "ACTION_POINTER_UP" + " " + "num" + ev.pointerCount)
|
||||
if (tracking[1]) {
|
||||
doCallBack(view,ga.getGesture(ev))
|
||||
}
|
||||
stopTracking(ev.pointerCount - 1)
|
||||
ga.untrackGesture()
|
||||
return consumeTouchEvents
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
if (debug) Log.d(TAG, "ACTION_CANCEL")
|
||||
return true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (debug) Log.d(TAG, "ACTION_MOVE")
|
||||
return consumeTouchEvents
|
||||
}
|
||||
}
|
||||
return consumeTouchEvents
|
||||
}
|
||||
|
||||
private fun doCallBack(targetView : View, mGt: GestureAnalyser.GestureType) {
|
||||
when (mGt.gestureFlag) {
|
||||
GestureAnalyser.SWIPE_1_UP -> onFingerGestureListener!!.onSwipeUp(
|
||||
targetView,
|
||||
1,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_1_DOWN -> onFingerGestureListener!!.onSwipeDown(
|
||||
targetView,
|
||||
1,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_1_LEFT -> onFingerGestureListener!!.onSwipeLeft(
|
||||
targetView,
|
||||
1,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_1_RIGHT -> onFingerGestureListener!!.onSwipeRight(
|
||||
targetView,
|
||||
1,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_2_UP -> onFingerGestureListener!!.onSwipeUp(
|
||||
targetView,
|
||||
2,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_2_DOWN -> onFingerGestureListener!!.onSwipeDown(
|
||||
targetView,
|
||||
2,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_2_LEFT -> onFingerGestureListener!!.onSwipeLeft(
|
||||
targetView,
|
||||
2,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_2_RIGHT -> onFingerGestureListener!!.onSwipeRight(
|
||||
targetView,
|
||||
2,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.PINCH_2 -> onFingerGestureListener!!.onPinch(
|
||||
targetView,
|
||||
2,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.UNPINCH_2 -> onFingerGestureListener!!.onUnpinch(
|
||||
targetView,
|
||||
2,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_3_UP -> onFingerGestureListener!!.onSwipeUp(
|
||||
targetView,
|
||||
3,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_3_DOWN -> onFingerGestureListener!!.onSwipeDown(
|
||||
targetView,
|
||||
3,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_3_LEFT -> onFingerGestureListener!!.onSwipeLeft(
|
||||
targetView,
|
||||
3,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_3_RIGHT -> onFingerGestureListener!!.onSwipeRight(
|
||||
targetView,
|
||||
3,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.PINCH_3 -> onFingerGestureListener!!.onPinch(
|
||||
targetView,
|
||||
3,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.UNPINCH_3 -> onFingerGestureListener!!.onUnpinch(
|
||||
targetView,
|
||||
3,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_4_UP -> onFingerGestureListener!!.onSwipeUp(
|
||||
targetView,
|
||||
4,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_4_DOWN -> onFingerGestureListener!!.onSwipeDown(
|
||||
targetView,
|
||||
4,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_4_LEFT -> onFingerGestureListener!!.onSwipeLeft(
|
||||
targetView,
|
||||
4,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.SWIPE_4_RIGHT -> onFingerGestureListener!!.onSwipeRight(
|
||||
targetView,
|
||||
4,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.PINCH_4 -> onFingerGestureListener!!.onPinch(
|
||||
targetView,
|
||||
4,
|
||||
mGt.gestureDuration,
|
||||
mGt.gestureDistance
|
||||
)
|
||||
|
||||
GestureAnalyser.UNPINCH_4 -> {
|
||||
onFingerGestureListener!!.onUnpinch(targetView,4, mGt.gestureDuration, mGt.gestureDistance)
|
||||
// onFingerGestureListener!!.onDoubleTap(1)
|
||||
}
|
||||
GestureAnalyser.CLICK_1 -> {
|
||||
BLog.LOGE("GestureAnalyser.CLICK_1")
|
||||
onFingerGestureListener!!.onClick(targetView)
|
||||
// onFingerGestureListener!!.onDoubleTap(1)
|
||||
}
|
||||
// GestureAnalyser.CLICK_2 -> {
|
||||
// onFingerGestureListener!!.onUnpinch(targetView,4, mGt.gestureDuration, mGt.gestureDistance)
|
||||
//// onFingerGestureListener!!.onDoubleTap(1)
|
||||
// }
|
||||
// GestureAnalyser.CLICK_3 -> {
|
||||
// onFingerGestureListener!!.onUnpinch(targetView,4, mGt.gestureDuration, mGt.gestureDistance)
|
||||
//// onFingerGestureListener!!.onDoubleTap(1)
|
||||
// }
|
||||
|
||||
GestureAnalyser.LONG_CLICK_1 -> {
|
||||
BLog.LOGE("GestureAnalyser.LONG_CLICK_1")
|
||||
onFingerGestureListener!!.onLongPress(targetView)
|
||||
}
|
||||
|
||||
|
||||
GestureAnalyser.DOUBLE_TAP_1 -> onFingerGestureListener!!.onDoubleTap(targetView,1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startTracking(nthPointer: Int) {
|
||||
for (i in 0..nthPointer) {
|
||||
tracking[i] = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopTracking(nthPointer: Int) {
|
||||
for (i in nthPointer until tracking.size) {
|
||||
tracking[i] = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Interface definition for the callback to be invoked when 2-finger gestures are performed
|
||||
*/
|
||||
interface OnFingerGestureListener {
|
||||
/**
|
||||
* Called when user swipes **up** with two fingers
|
||||
*
|
||||
* @param fingers number of fingers involved in this gesture
|
||||
* @param gestureDuration duration in milliSeconds
|
||||
* @return
|
||||
*/
|
||||
fun onSwipeUp(targetView : View, fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
|
||||
|
||||
/**
|
||||
* Called when user swipes **down** with two fingers
|
||||
*
|
||||
* @param fingers number of fingers involved in this gesture
|
||||
* @param gestureDuration duration in milliSeconds
|
||||
* @return
|
||||
*/
|
||||
fun onSwipeDown(targetView : View,fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
|
||||
|
||||
/**
|
||||
* Called when user swipes **left** with two fingers
|
||||
*
|
||||
* @param fingers number of fingers involved in this gesture
|
||||
* @param gestureDuration duration in milliSeconds
|
||||
* @return
|
||||
*/
|
||||
fun onSwipeLeft(targetView : View,fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
|
||||
|
||||
/**
|
||||
* Called when user swipes **right** with two fingers
|
||||
*
|
||||
* @param fingers number of fingers involved in this gesture
|
||||
* @param gestureDuration duration in milliSeconds
|
||||
* @return
|
||||
*/
|
||||
fun onSwipeRight(targetView : View,fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
|
||||
|
||||
/**
|
||||
* Called when user **pinches** with two fingers (bring together)
|
||||
*
|
||||
* @param fingers number of fingers involved in this gesture
|
||||
* @param gestureDuration duration in milliSeconds
|
||||
* @return
|
||||
*/
|
||||
fun onPinch(targetView : View,fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
|
||||
|
||||
/**
|
||||
* Called when user **un-pinches** with two fingers (take apart)
|
||||
*
|
||||
* @param fingers number of fingers involved in this gesture
|
||||
* @param gestureDuration duration in milliSeconds
|
||||
* @return
|
||||
*/
|
||||
fun onUnpinch(targetView : View,fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
|
||||
|
||||
fun onDoubleTap(targetView : View,fingers: Int): Boolean
|
||||
fun onLongPress(targetView : View): Boolean
|
||||
fun onClick(targetView : View): Boolean
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Will see if these need to be used. For now just returning duration in milliS
|
||||
const val GESTURE_SPEED_SLOW: Long = 1500
|
||||
const val GESTURE_SPEED_MEDIUM: Long = 1000
|
||||
const val GESTURE_SPEED_FAST: Long = 500
|
||||
private const val TAG = "SimpleFingerGestures"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user