From d30cc4a2a309bcde74cc49892e9fc7bf5b73c36d Mon Sep 17 00:00:00 2001 From: Dirk Sierd de Vries Date: Sun, 9 Aug 2026 20:21:40 +0200 Subject: Rename Android package to app.sidephonelauncher - change the applicationId and namespace to app.sidephonelauncher - move Kotlin sources from app.olauncher to app.sidephonelauncher - update XML and navigation references to the new package - rename the shared preferences file key so the fork has its own app identity --- app/build.gradle | 4 +- app/src/main/java/app/olauncher/MainActivity.kt | 418 ---------- app/src/main/java/app/olauncher/MainViewModel.kt | 521 ------------ app/src/main/java/app/olauncher/data/AppModel.kt | 45 - app/src/main/java/app/olauncher/data/Constants.kt | 120 --- .../app/olauncher/data/DrawerCharacterModel.kt | 6 - app/src/main/java/app/olauncher/data/Prefs.kt | 647 --------------- .../java/app/olauncher/helper/AppFilterHelper.kt | 7 - .../java/app/olauncher/helper/AppUsageStats.kt | 21 - .../main/java/app/olauncher/helper/Extensions.kt | 192 ----- .../java/app/olauncher/helper/FakeHomeActivity.kt | 12 - .../helper/HomeNotificationListenerService.kt | 27 - .../app/olauncher/helper/MyAccessibilityService.kt | 45 - .../olauncher/helper/NotificationDotRepository.kt | 58 -- .../java/app/olauncher/helper/PinItemActivity.kt | 51 -- .../java/app/olauncher/helper/SingleLiveEvent.kt | 36 - app/src/main/java/app/olauncher/helper/Utils.kt | 672 --------------- .../java/app/olauncher/helper/WallpaperWorker.kt | 47 -- .../helper/usageStats/ComponentForegroundStat.kt | 28 - .../olauncher/helper/usageStats/EventLogWrapper.kt | 391 --------- .../olauncher/helper/usageStats/SimpleUsageStat.kt | 48 -- .../usageStats/UnmatchedCloseEventGuardian.kt | 65 -- .../java/app/olauncher/listener/DeviceAdmin.kt | 19 - .../app/olauncher/listener/OnSwipeTouchListener.kt | 113 --- .../olauncher/listener/ViewSwipeTouchListener.kt | 96 --- .../main/java/app/olauncher/ui/AppDrawerAdapter.kt | 378 --------- .../java/app/olauncher/ui/AppDrawerFragment.kt | 461 ----------- .../main/java/app/olauncher/ui/HomeAppTextView.kt | 51 -- app/src/main/java/app/olauncher/ui/HomeFragment.kt | 905 --------------------- .../main/java/app/olauncher/ui/SettingsFragment.kt | 695 ---------------- .../java/app/sidephonelauncher/MainActivity.kt | 418 ++++++++++ .../java/app/sidephonelauncher/MainViewModel.kt | 521 ++++++++++++ .../java/app/sidephonelauncher/data/AppModel.kt | 45 + .../java/app/sidephonelauncher/data/Constants.kt | 120 +++ .../sidephonelauncher/data/DrawerCharacterModel.kt | 6 + .../main/java/app/sidephonelauncher/data/Prefs.kt | 647 +++++++++++++++ .../sidephonelauncher/helper/AppFilterHelper.kt | 7 + .../app/sidephonelauncher/helper/AppUsageStats.kt | 21 + .../app/sidephonelauncher/helper/Extensions.kt | 192 +++++ .../sidephonelauncher/helper/FakeHomeActivity.kt | 12 + .../helper/HomeNotificationListenerService.kt | 27 + .../helper/MyAccessibilityService.kt | 45 + .../helper/NotificationDotRepository.kt | 58 ++ .../sidephonelauncher/helper/PinItemActivity.kt | 51 ++ .../sidephonelauncher/helper/SingleLiveEvent.kt | 36 + .../java/app/sidephonelauncher/helper/Utils.kt | 672 +++++++++++++++ .../sidephonelauncher/helper/WallpaperWorker.kt | 47 ++ .../helper/usageStats/ComponentForegroundStat.kt | 28 + .../helper/usageStats/EventLogWrapper.kt | 391 +++++++++ .../helper/usageStats/SimpleUsageStat.kt | 48 ++ .../usageStats/UnmatchedCloseEventGuardian.kt | 65 ++ .../app/sidephonelauncher/listener/DeviceAdmin.kt | 19 + .../listener/OnSwipeTouchListener.kt | 113 +++ .../listener/ViewSwipeTouchListener.kt | 96 +++ .../app/sidephonelauncher/ui/AppDrawerAdapter.kt | 378 +++++++++ .../app/sidephonelauncher/ui/AppDrawerFragment.kt | 461 +++++++++++ .../app/sidephonelauncher/ui/HomeAppTextView.kt | 51 ++ .../java/app/sidephonelauncher/ui/HomeFragment.kt | 905 +++++++++++++++++++++ .../app/sidephonelauncher/ui/SettingsFragment.kt | 695 ++++++++++++++++ app/src/main/res/layout/fragment_home.xml | 16 +- app/src/main/res/navigation/nav_graph.xml | 6 +- 61 files changed, 6188 insertions(+), 6188 deletions(-) delete mode 100644 app/src/main/java/app/olauncher/MainActivity.kt delete mode 100644 app/src/main/java/app/olauncher/MainViewModel.kt delete mode 100644 app/src/main/java/app/olauncher/data/AppModel.kt delete mode 100644 app/src/main/java/app/olauncher/data/Constants.kt delete mode 100644 app/src/main/java/app/olauncher/data/DrawerCharacterModel.kt delete mode 100644 app/src/main/java/app/olauncher/data/Prefs.kt delete mode 100644 app/src/main/java/app/olauncher/helper/AppFilterHelper.kt delete mode 100644 app/src/main/java/app/olauncher/helper/AppUsageStats.kt delete mode 100644 app/src/main/java/app/olauncher/helper/Extensions.kt delete mode 100644 app/src/main/java/app/olauncher/helper/FakeHomeActivity.kt delete mode 100644 app/src/main/java/app/olauncher/helper/HomeNotificationListenerService.kt delete mode 100644 app/src/main/java/app/olauncher/helper/MyAccessibilityService.kt delete mode 100644 app/src/main/java/app/olauncher/helper/NotificationDotRepository.kt delete mode 100644 app/src/main/java/app/olauncher/helper/PinItemActivity.kt delete mode 100644 app/src/main/java/app/olauncher/helper/SingleLiveEvent.kt delete mode 100644 app/src/main/java/app/olauncher/helper/Utils.kt delete mode 100644 app/src/main/java/app/olauncher/helper/WallpaperWorker.kt delete mode 100644 app/src/main/java/app/olauncher/helper/usageStats/ComponentForegroundStat.kt delete mode 100644 app/src/main/java/app/olauncher/helper/usageStats/EventLogWrapper.kt delete mode 100644 app/src/main/java/app/olauncher/helper/usageStats/SimpleUsageStat.kt delete mode 100644 app/src/main/java/app/olauncher/helper/usageStats/UnmatchedCloseEventGuardian.kt delete mode 100644 app/src/main/java/app/olauncher/listener/DeviceAdmin.kt delete mode 100644 app/src/main/java/app/olauncher/listener/OnSwipeTouchListener.kt delete mode 100644 app/src/main/java/app/olauncher/listener/ViewSwipeTouchListener.kt delete mode 100644 app/src/main/java/app/olauncher/ui/AppDrawerAdapter.kt delete mode 100644 app/src/main/java/app/olauncher/ui/AppDrawerFragment.kt delete mode 100644 app/src/main/java/app/olauncher/ui/HomeAppTextView.kt delete mode 100644 app/src/main/java/app/olauncher/ui/HomeFragment.kt delete mode 100644 app/src/main/java/app/olauncher/ui/SettingsFragment.kt create mode 100644 app/src/main/java/app/sidephonelauncher/MainActivity.kt create mode 100644 app/src/main/java/app/sidephonelauncher/MainViewModel.kt create mode 100644 app/src/main/java/app/sidephonelauncher/data/AppModel.kt create mode 100644 app/src/main/java/app/sidephonelauncher/data/Constants.kt create mode 100644 app/src/main/java/app/sidephonelauncher/data/DrawerCharacterModel.kt create mode 100644 app/src/main/java/app/sidephonelauncher/data/Prefs.kt create mode 100644 app/src/main/java/app/sidephonelauncher/helper/AppFilterHelper.kt create mode 100644 app/src/main/java/app/sidephonelauncher/helper/AppUsageStats.kt create mode 100644 app/src/main/java/app/sidephonelauncher/helper/Extensions.kt create mode 100644 app/src/main/java/app/sidephonelauncher/helper/FakeHomeActivity.kt create mode 100644 app/src/main/java/app/sidephonelauncher/helper/HomeNotificationListenerService.kt create mode 100644 app/src/main/java/app/sidephonelauncher/helper/MyAccessibilityService.kt create mode 100644 app/src/main/java/app/sidephonelauncher/helper/NotificationDotRepository.kt create mode 100644 app/src/main/java/app/sidephonelauncher/helper/PinItemActivity.kt create mode 100644 app/src/main/java/app/sidephonelauncher/helper/SingleLiveEvent.kt create mode 100644 app/src/main/java/app/sidephonelauncher/helper/Utils.kt create mode 100644 app/src/main/java/app/sidephonelauncher/helper/WallpaperWorker.kt create mode 100644 app/src/main/java/app/sidephonelauncher/helper/usageStats/ComponentForegroundStat.kt create mode 100644 app/src/main/java/app/sidephonelauncher/helper/usageStats/EventLogWrapper.kt create mode 100644 app/src/main/java/app/sidephonelauncher/helper/usageStats/SimpleUsageStat.kt create mode 100644 app/src/main/java/app/sidephonelauncher/helper/usageStats/UnmatchedCloseEventGuardian.kt create mode 100644 app/src/main/java/app/sidephonelauncher/listener/DeviceAdmin.kt create mode 100644 app/src/main/java/app/sidephonelauncher/listener/OnSwipeTouchListener.kt create mode 100644 app/src/main/java/app/sidephonelauncher/listener/ViewSwipeTouchListener.kt create mode 100644 app/src/main/java/app/sidephonelauncher/ui/AppDrawerAdapter.kt create mode 100644 app/src/main/java/app/sidephonelauncher/ui/AppDrawerFragment.kt create mode 100644 app/src/main/java/app/sidephonelauncher/ui/HomeAppTextView.kt create mode 100644 app/src/main/java/app/sidephonelauncher/ui/HomeFragment.kt create mode 100644 app/src/main/java/app/sidephonelauncher/ui/SettingsFragment.kt (limited to 'app') diff --git a/app/build.gradle b/app/build.gradle index 90b0fc5..56af582 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,7 +28,7 @@ android { } defaultConfig { - applicationId "app.olauncher" + applicationId "app.sidephonelauncher" minSdkVersion 24 targetSdkVersion 35 versionCode ciVersionCode @@ -69,7 +69,7 @@ android { viewBinding = true buildConfig = true } - namespace 'app.olauncher' + namespace 'app.sidephonelauncher' } dependencies { diff --git a/app/src/main/java/app/olauncher/MainActivity.kt b/app/src/main/java/app/olauncher/MainActivity.kt deleted file mode 100644 index ebd05d5..0000000 --- a/app/src/main/java/app/olauncher/MainActivity.kt +++ /dev/null @@ -1,418 +0,0 @@ -package app.olauncher - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.content.pm.ActivityInfo -import android.content.res.Configuration -import android.os.Build -import android.os.Bundle -import android.provider.Settings -import android.view.KeyEvent -import android.view.View -import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS -import androidx.activity.OnBackPressedCallback -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.app.AppCompatDelegate -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.navigation.NavController -import androidx.navigation.findNavController -import app.olauncher.data.Constants -import app.olauncher.data.Prefs -import app.olauncher.databinding.ActivityMainBinding -import app.olauncher.helper.getColorFromAttr -import app.olauncher.helper.hasBeenDays -import app.olauncher.helper.hasBeenHours -import app.olauncher.helper.hasBeenMinutes -import app.olauncher.helper.isDarkThemeOn -import app.olauncher.helper.isDaySince -import app.olauncher.helper.isDefaultLauncher -import app.olauncher.helper.isEinkDisplay -import app.olauncher.helper.isOlauncherDefault -import app.olauncher.helper.isTablet -import app.olauncher.helper.openUrl -import app.olauncher.helper.rateApp -import app.olauncher.helper.resetLauncherViaFakeActivity -import app.olauncher.helper.setPlainWallpaper -import app.olauncher.helper.shareApp -import app.olauncher.helper.showLauncherSelector -import app.olauncher.helper.showToast -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import java.util.Calendar - -class MainActivity : AppCompatActivity() { - - private lateinit var prefs: Prefs - private lateinit var navController: NavController - private lateinit var viewModel: MainViewModel - private lateinit var binding: ActivityMainBinding - private var timerJob: Job? = null - private var isResumed = false - private var profileReceiver: BroadcastReceiver? = null - -// override fun onBackPressed() { -// if (navController.currentDestination?.id != R.id.mainFragment) -// super.onBackPressed() -// } - - override fun attachBaseContext(context: Context) { - val newConfig = Configuration(context.resources.configuration) - newConfig.fontScale = Prefs(context).textSizeScale - applyOverrideConfiguration(newConfig) - super.attachBaseContext(context) - } - - override fun onCreate(savedInstanceState: Bundle?) { - prefs = Prefs(this) - if (isEinkDisplay()) prefs.appTheme = AppCompatDelegate.MODE_NIGHT_NO - AppCompatDelegate.setDefaultNightMode(prefs.appTheme) - super.onCreate(savedInstanceState) - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - setDefaultKeyMode(Activity.DEFAULT_KEYS_DISABLE) - - navController = this.findNavController(R.id.nav_host_fragment) - viewModel = ViewModelProvider(this)[MainViewModel::class.java] - - val onBackPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - if (navController.currentDestination?.id != R.id.mainFragment) { - // then we might want to finish the activity or disable this callback. - if (navController.popBackStack()) { - // Successfully popped back - } else { - // if you want other system/activity level handling - } - } else { - binding.messageLayout.visibility = View.GONE - } - } - } - onBackPressedDispatcher.addCallback(this, onBackPressedCallback) - - if (prefs.firstOpen) { - viewModel.firstOpen(true) - prefs.firstOpen = false - prefs.firstOpenTime = System.currentTimeMillis() - viewModel.setDefaultClockApp() - viewModel.resetLauncherLiveData.call() - } - - initClickListeners() - initObservers(viewModel) - viewModel.getAppList() - setupOrientation() - - window.addFlags(FLAG_LAYOUT_NO_LIMITS) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { - profileReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - viewModel.isPrivateSpaceToggling = false - viewModel.getPrivateSpaceAppList() - } - } - val filter = IntentFilter().apply { - addAction(Intent.ACTION_PROFILE_AVAILABLE) - addAction(Intent.ACTION_PROFILE_UNAVAILABLE) - } - registerReceiver(profileReceiver, filter) - } - } - - override fun dispatchKeyEvent(event: KeyEvent): Boolean { - val dpadKey = when (event.keyCode) { - KeyEvent.KEYCODE_DPAD_UP, - KeyEvent.KEYCODE_DPAD_DOWN, - KeyEvent.KEYCODE_DPAD_LEFT, - KeyEvent.KEYCODE_DPAD_RIGHT, - KeyEvent.KEYCODE_DPAD_CENTER, - KeyEvent.KEYCODE_ENTER -> true - else -> false - } - if (dpadKey) { - val focusedView = currentFocus - if (focusedView == null || focusedView == window.decorView) { - val navHost = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) - val currentFragment = navHost?.childFragmentManager?.fragments?.firstOrNull() - val fragmentView = currentFragment?.view - if (fragmentView != null && fragmentView.dispatchKeyEvent(event)) return true - } - } - return super.dispatchKeyEvent(event) - } - - override fun onStart() { - super.onStart() - restartLauncherOrCheckTheme() - } - - override fun onResume() { - super.onResume() - isResumed = true - viewModel.isPrivateSpaceToggling = false - } - - override fun onStop() { - isResumed = false - backToHomeScreen() - super.onStop() - } - - override fun onUserLeaveHint() { - backToHomeScreen() - super.onUserLeaveHint() - } - - override fun onNewIntent(intent: Intent?) { - // Home button for recents feature disabled - // val alreadyHome = navController.currentDestination?.id == R.id.mainFragment - backToHomeScreen() - // if (alreadyHome && isResumed && prefs.homeButtonShowRecents) - // viewModel.showRecentApps.call() - super.onNewIntent(intent) - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - AppCompatDelegate.setDefaultNightMode(prefs.appTheme) - if (prefs.dailyWallpaper && AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) { - setPlainWallpaper() - viewModel.setWallpaperWorker() - recreate() - } - } - - private fun initClickListeners() { - binding.ivClose.setOnClickListener { - binding.messageLayout.visibility = View.GONE - } - } - - private fun initObservers(viewModel: MainViewModel) { - viewModel.launcherResetFailed.observe(this) { - openLauncherChooser(it) - } - viewModel.resetLauncherLiveData.observe(this) { - if (isDefaultLauncher() || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) - resetLauncherViaFakeActivity() - else - showLauncherSelector(Constants.REQUEST_CODE_LAUNCHER_SELECTOR) - } - viewModel.checkForMessages.observe(this) { - checkForMessages() - } - viewModel.showDialog.observe(this) { - when (it) { - Constants.Dialog.ABOUT -> { - showMessageDialog(R.string.app_name, R.string.welcome_to_olauncher_settings, R.string.okay) { - binding.messageLayout.visibility = View.GONE - } - } - - Constants.Dialog.WALLPAPER -> { - prefs.wallpaperMsgShown = true - prefs.userState = Constants.UserState.REVIEW - showMessageDialog(R.string.did_you_know, R.string.wallpaper_message, R.string.enable) { - prefs.dailyWallpaper = true - viewModel.setWallpaperWorker() - showToast(getString(R.string.your_wallpaper_will_update_shortly)) - } - } - - Constants.Dialog.REVIEW -> { - prefs.userState = Constants.UserState.RATE - showMessageDialog(R.string.hey, R.string.review_message, R.string.leave_a_review) { - prefs.rateClicked = true - showToast("😇❤️") - rateApp() - } - } - - Constants.Dialog.RATE -> { - prefs.userState = Constants.UserState.SHARE - showMessageDialog(R.string.app_name, R.string.rate_us_message, R.string.rate_now) { - prefs.rateClicked = true - showToast("🤩❤️") - rateApp() - } - } - - Constants.Dialog.SHARE -> { - prefs.shareShownTime = System.currentTimeMillis() - showMessageDialog(R.string.hey, R.string.share_message, R.string.share_now) { - showToast("😊❤️") - shareApp() - } - } - - Constants.Dialog.HIDDEN -> { - showMessageDialog(R.string.hidden_apps, R.string.hidden_apps_message, R.string.okay) { - } - } - - Constants.Dialog.KEYBOARD -> { - showMessageDialog(R.string.app_name, R.string.keyboard_message, R.string.okay) { - } - } - - Constants.Dialog.DIGITAL_WELLBEING -> { - showMessageDialog(R.string.screen_time, R.string.app_usage_message, R.string.permission) { - startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)) - } - } - - Constants.Dialog.PRO_MESSAGE -> { - showMessageDialog(R.string.hey, R.string.pro_message, R.string.olauncher_pro) { - openUrl(Constants.URL_OLAUNCHER_PRO) - } - } - } - } - } - - private fun showMessageDialog(title: Int, message: Int, action: Int, clickListener: () -> Unit) { - binding.tvTitle.text = getString(title) - binding.tvMessage.text = getString(message) - binding.tvAction.text = getString(action) - binding.tvAction.setOnClickListener { - clickListener() - binding.messageLayout.visibility = View.GONE - } - binding.messageLayout.visibility = View.VISIBLE - } - - private fun checkForMessages() { - if (prefs.firstOpenTime == 0L) - prefs.firstOpenTime = System.currentTimeMillis() - - val calendar = Calendar.getInstance() - val dayOfYear = calendar.get(Calendar.DAY_OF_YEAR) - if (dayOfYear == 1 && dayOfYear != prefs.shownOnDayOfYear) { - prefs.shownOnDayOfYear = dayOfYear - showMessageDialog(R.string.hey, R.string.new_year_wish, R.string.cheers) {} - return - } else if (dayOfYear == 32 && dayOfYear != prefs.shownOnDayOfYear) { - prefs.shownOnDayOfYear = dayOfYear - showMessageDialog(R.string.hey, R.string.new_year_wish_1, R.string.cheers) {} - return - } - - when (prefs.userState) { - Constants.UserState.START -> { - if (prefs.firstOpenTime.hasBeenMinutes(10)) - prefs.userState = Constants.UserState.WALLPAPER - } - - Constants.UserState.WALLPAPER -> { - if (prefs.wallpaperMsgShown || prefs.dailyWallpaper) - prefs.userState = Constants.UserState.REVIEW - else if (isOlauncherDefault(this)) - viewModel.showDialog.postValue(Constants.Dialog.WALLPAPER) - } - - Constants.UserState.REVIEW -> { - if (prefs.rateClicked) - prefs.userState = Constants.UserState.SHARE - else if (isOlauncherDefault(this) && prefs.firstOpenTime.hasBeenHours(1)) - viewModel.showDialog.postValue(Constants.Dialog.REVIEW) - } - - Constants.UserState.RATE -> { - if (prefs.rateClicked) - prefs.userState = Constants.UserState.SHARE - else if (isOlauncherDefault(this) - && prefs.firstOpenTime.isDaySince() >= 7 - && calendar.get(Calendar.HOUR_OF_DAY) >= 16 - ) viewModel.showDialog.postValue(Constants.Dialog.RATE) - } - - Constants.UserState.SHARE -> { - if (isOlauncherDefault(this) && prefs.firstOpenTime.hasBeenDays(14) - && prefs.shareShownTime.isDaySince() >= 70 - && calendar.get(Calendar.HOUR_OF_DAY) >= 16 - ) viewModel.showDialog.postValue(Constants.Dialog.SHARE) - } - } - } - - @SuppressLint("SourceLockedOrientationActivity") - private fun setupOrientation() { - if (isTablet(this) || Build.VERSION.SDK_INT == Build.VERSION_CODES.O) - return - // In Android 8.0, windowIsTranslucent cannot be used with screenOrientation=portrait - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } - - private fun backToHomeScreen() { - if (viewModel.isPrivateSpaceToggling) return - binding.messageLayout.visibility = View.GONE - if (navController.currentDestination?.id != R.id.mainFragment) - navController.popBackStack(R.id.mainFragment, false) - } - - private fun setPlainWallpaper() { - if (this.isDarkThemeOn()) - setPlainWallpaper(this, android.R.color.black) - else setPlainWallpaper(this, android.R.color.white) - } - - private fun openLauncherChooser(resetFailed: Boolean) { - if (resetFailed) { - val intent = Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS) - startActivity(intent) - } - } - - private fun restartLauncherOrCheckTheme(forceRestart: Boolean = false) { - if (forceRestart || prefs.launcherRestartTimestamp.hasBeenHours(4)) { - prefs.launcherRestartTimestamp = System.currentTimeMillis() - cacheDir.deleteRecursively() - recreate() - } else - checkTheme() - } - - private fun checkTheme() { - timerJob?.cancel() - timerJob = lifecycleScope.launch { - delay(200) - if ((prefs.appTheme == AppCompatDelegate.MODE_NIGHT_YES && getColorFromAttr(R.attr.primaryColor) != getColor(R.color.white)) - || (prefs.appTheme == AppCompatDelegate.MODE_NIGHT_NO && getColorFromAttr(R.attr.primaryColor) != getColor(R.color.black)) - ) - restartLauncherOrCheckTheme(true) - } - } - - override fun onDestroy() { - profileReceiver?.let { - try { - unregisterReceiver(it) - } catch (_: Exception) { - } - } - super.onDestroy() - } - - @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - Constants.REQUEST_CODE_ENABLE_ADMIN -> { - if (resultCode == Activity.RESULT_OK) - prefs.lockModeOn = true - } - - Constants.REQUEST_CODE_LAUNCHER_SELECTOR -> { - if (resultCode == Activity.RESULT_OK) - resetLauncherViaFakeActivity() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/MainViewModel.kt b/app/src/main/java/app/olauncher/MainViewModel.kt deleted file mode 100644 index 13f706b..0000000 --- a/app/src/main/java/app/olauncher/MainViewModel.kt +++ /dev/null @@ -1,521 +0,0 @@ -package app.olauncher - -import android.app.Application -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.pm.LauncherApps -import android.os.Build -import android.os.UserHandle -import android.os.UserManager -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.NetworkType -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkManager -import app.olauncher.data.AppModel -import app.olauncher.data.Constants -import app.olauncher.data.Prefs -import app.olauncher.helper.SingleLiveEvent -import app.olauncher.helper.WallpaperWorker -import app.olauncher.helper.formattedTimeSpent -import app.olauncher.helper.getAppsList -import app.olauncher.helper.getPrivateSpaceApps -import app.olauncher.helper.getPrivateSpaceUserHandle -import app.olauncher.helper.hasBeenMinutes -import app.olauncher.helper.isOlauncherDefault -import app.olauncher.helper.isPackageInstalled -import app.olauncher.helper.isPrivateSpaceLocked -import app.olauncher.helper.showToast -import app.olauncher.helper.usageStats.EventLogWrapper -import kotlinx.coroutines.launch -import java.util.Calendar -import java.util.concurrent.TimeUnit - - -class MainViewModel(application: Application) : AndroidViewModel(application) { - private val appContext by lazy { application.applicationContext } - private val prefs = Prefs(appContext) - - val firstOpen = MutableLiveData() - val refreshHome = MutableLiveData() - val toggleDateTime = MutableLiveData() - val updateSwipeApps = MutableLiveData() - val appList = MutableLiveData?>() - val hiddenApps = MutableLiveData?>() - val isOlauncherDefault = MutableLiveData() - val launcherResetFailed = MutableLiveData() - val homeAppAlignment = MutableLiveData() - val screenTimeValue = MutableLiveData() - - val privateSpaceApps = MutableLiveData?>() - val privateSpaceLocked = MutableLiveData() - val privateSpaceAvailable = MutableLiveData() - - // Suppress backToHomeScreen during Private Space lock/unlock auth - var isPrivateSpaceToggling = false - - val showDialog = SingleLiveEvent() - val checkForMessages = SingleLiveEvent() - val resetLauncherLiveData = SingleLiveEvent() - // Home button for recents feature disabled - // val showRecentApps = SingleLiveEvent() - - fun selectedApp(appModel: AppModel, flag: Int) { - if (appModel is AppModel.PrivateSpaceHeader) return - when (flag) { - Constants.FLAG_LAUNCH_APP -> { - when (appModel) { - is AppModel.PinnedShortcut -> launchShortcut(appModel) - is AppModel.App -> - launchApp(appModel.appPackage, appModel.activityClassName, appModel.user) - - else -> {} - } - } - - Constants.FLAG_HIDDEN_APPS -> { - if (appModel is AppModel.App) { - launchApp(appModel.appPackage, appModel.activityClassName, appModel.user) - } - } - - Constants.FLAG_SET_HOME_APP_1 -> saveHomeApp(appModel, 1) - Constants.FLAG_SET_HOME_APP_2 -> saveHomeApp(appModel, 2) - Constants.FLAG_SET_HOME_APP_3 -> saveHomeApp(appModel, 3) - Constants.FLAG_SET_HOME_APP_4 -> saveHomeApp(appModel, 4) - Constants.FLAG_SET_HOME_APP_5 -> saveHomeApp(appModel, 5) - Constants.FLAG_SET_HOME_APP_6 -> saveHomeApp(appModel, 6) - Constants.FLAG_SET_HOME_APP_7 -> saveHomeApp(appModel, 7) - Constants.FLAG_SET_HOME_APP_8 -> saveHomeApp(appModel, 8) - - Constants.FLAG_SET_SWIPE_LEFT_APP -> saveSwipeApp(appModel, isLeft = true) - Constants.FLAG_SET_SWIPE_RIGHT_APP -> saveSwipeApp(appModel, isLeft = false) - Constants.FLAG_SET_CLOCK_APP -> saveClockApp(appModel) - Constants.FLAG_SET_CALENDAR_APP -> saveCalendarApp(appModel) - Constants.FLAG_SET_SCREEN_TIME_APP -> saveScreenTimeApp(appModel) - } - } - - private fun launchShortcut(appModel: AppModel.PinnedShortcut) { - val launcher = appContext.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val query = LauncherApps.ShortcutQuery().apply { - setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED) - } - launcher.getShortcuts(query, appModel.user)?.find { it.id == appModel.shortcutId } - ?.let { shortcut -> - launcher.startShortcut(shortcut, null, null) - } - } - - private fun saveHomeApp(appModel: AppModel, position: Int) { - when (appModel) { - is AppModel.PrivateSpaceHeader -> return - is AppModel.App -> { - when (position) { - 1 -> { - prefs.appName1 = appModel.appLabel - prefs.appPackage1 = appModel.appPackage - prefs.appUser1 = appModel.user.toString() - prefs.appActivityClassName1 = appModel.activityClassName - prefs.isShortcut1 = false - prefs.shortcutId1 = "" - } - - 2 -> { - prefs.appName2 = appModel.appLabel - prefs.appPackage2 = appModel.appPackage - prefs.appUser2 = appModel.user.toString() - prefs.appActivityClassName2 = appModel.activityClassName - prefs.isShortcut2 = false - prefs.shortcutId2 = "" - } - - 3 -> { - prefs.appName3 = appModel.appLabel - prefs.appPackage3 = appModel.appPackage - prefs.appUser3 = appModel.user.toString() - prefs.appActivityClassName3 = appModel.activityClassName - prefs.isShortcut3 = false - prefs.shortcutId3 = "" - } - - 4 -> { - prefs.appName4 = appModel.appLabel - prefs.appPackage4 = appModel.appPackage - prefs.appUser4 = appModel.user.toString() - prefs.appActivityClassName4 = appModel.activityClassName - prefs.isShortcut4 = false - prefs.shortcutId4 = "" - } - - 5 -> { - prefs.appName5 = appModel.appLabel - prefs.appPackage5 = appModel.appPackage - prefs.appUser5 = appModel.user.toString() - prefs.appActivityClassName5 = appModel.activityClassName - prefs.isShortcut5 = false - prefs.shortcutId5 = "" - } - - 6 -> { - prefs.appName6 = appModel.appLabel - prefs.appPackage6 = appModel.appPackage - prefs.appUser6 = appModel.user.toString() - prefs.appActivityClassName6 = appModel.activityClassName - prefs.isShortcut6 = false - prefs.shortcutId6 = "" - } - - 7 -> { - prefs.appName7 = appModel.appLabel - prefs.appPackage7 = appModel.appPackage - prefs.appUser7 = appModel.user.toString() - prefs.appActivityClassName7 = appModel.activityClassName - prefs.isShortcut7 = false - prefs.shortcutId7 = "" - } - - 8 -> { - prefs.appName8 = appModel.appLabel - prefs.appPackage8 = appModel.appPackage - prefs.appUser8 = appModel.user.toString() - prefs.appActivityClassName8 = appModel.activityClassName - prefs.isShortcut8 = false - prefs.shortcutId8 = "" - } - } - } - - is AppModel.PinnedShortcut -> { - when (position) { - 1 -> { - prefs.appName1 = appModel.appLabel - prefs.appPackage1 = appModel.appPackage - prefs.appUser1 = appModel.user.toString() - prefs.appActivityClassName1 = null - prefs.isShortcut1 = true - prefs.shortcutId1 = appModel.shortcutId - } - - 2 -> { - prefs.appName2 = appModel.appLabel - prefs.appPackage2 = appModel.appPackage - prefs.appUser2 = appModel.user.toString() - prefs.appActivityClassName2 = null - prefs.isShortcut2 = true - prefs.shortcutId2 = appModel.shortcutId - } - - 3 -> { - prefs.appName3 = appModel.appLabel - prefs.appPackage3 = appModel.appPackage - prefs.appUser3 = appModel.user.toString() - prefs.appActivityClassName3 = null - prefs.isShortcut3 = true - prefs.shortcutId3 = appModel.shortcutId - } - - 4 -> { - prefs.appName4 = appModel.appLabel - prefs.appPackage4 = appModel.appPackage - prefs.appUser4 = appModel.user.toString() - prefs.appActivityClassName4 = null - prefs.isShortcut4 = true - prefs.shortcutId4 = appModel.shortcutId - } - - 5 -> { - prefs.appName5 = appModel.appLabel - prefs.appPackage5 = appModel.appPackage - prefs.appUser5 = appModel.user.toString() - prefs.appActivityClassName5 = null - prefs.isShortcut5 = true - prefs.shortcutId5 = appModel.shortcutId - } - - 6 -> { - prefs.appName6 = appModel.appLabel - prefs.appPackage6 = appModel.appPackage - prefs.appUser6 = appModel.user.toString() - prefs.appActivityClassName6 = null - prefs.isShortcut6 = true - prefs.shortcutId6 = appModel.shortcutId - } - - 7 -> { - prefs.appName7 = appModel.appLabel - prefs.appPackage7 = appModel.appPackage - prefs.appUser7 = appModel.user.toString() - prefs.appActivityClassName7 = null - prefs.isShortcut7 = true - prefs.shortcutId7 = appModel.shortcutId - } - - 8 -> { - prefs.appName8 = appModel.appLabel - prefs.appPackage8 = appModel.appPackage - prefs.appUser8 = appModel.user.toString() - prefs.appActivityClassName8 = null - prefs.isShortcut8 = true - prefs.shortcutId8 = appModel.shortcutId - } - } - } - } - refreshHome(false) - } - - private fun saveSwipeApp(appModel: AppModel, isLeft: Boolean) { - when (appModel) { - is AppModel.PrivateSpaceHeader -> return - is AppModel.App -> { - if (isLeft) { - prefs.appNameSwipeLeft = appModel.appLabel - prefs.appPackageSwipeLeft = appModel.appPackage - prefs.appUserSwipeLeft = appModel.user.toString() - prefs.appActivityClassNameSwipeLeft = appModel.activityClassName - prefs.isShortcutSwipeLeft = false - prefs.shortcutIdSwipeLeft = "" - } else { - prefs.appNameSwipeRight = appModel.appLabel - prefs.appPackageSwipeRight = appModel.appPackage - prefs.appUserSwipeRight = appModel.user.toString() - prefs.appActivityClassNameRight = appModel.activityClassName - prefs.isShortcutSwipeRight = false - prefs.shortcutIdSwipeRight = "" - } - } - - is AppModel.PinnedShortcut -> { - if (isLeft) { - prefs.appNameSwipeLeft = appModel.appLabel - prefs.appPackageSwipeLeft = appModel.appPackage - prefs.appUserSwipeLeft = appModel.user.toString() - prefs.appActivityClassNameSwipeLeft = null - prefs.isShortcutSwipeLeft = true - prefs.shortcutIdSwipeLeft = appModel.shortcutId - } else { - prefs.appNameSwipeRight = appModel.appLabel - prefs.appPackageSwipeRight = appModel.appPackage - prefs.appUserSwipeRight = appModel.user.toString() - prefs.appActivityClassNameRight = null - prefs.isShortcutSwipeRight = true - prefs.shortcutIdSwipeRight = appModel.shortcutId - } - } - } - updateSwipeApps() - } - - private fun saveClockApp(appModel: AppModel) { - if (appModel is AppModel.App) { - prefs.clockAppPackage = appModel.appPackage - prefs.clockAppUser = appModel.user.toString() - prefs.clockAppClassName = appModel.activityClassName - } - } - - private fun saveCalendarApp(appModel: AppModel) { - if (appModel is AppModel.App) { - prefs.calendarAppPackage = appModel.appPackage - prefs.calendarAppUser = appModel.user.toString() - prefs.calendarAppClassName = appModel.activityClassName - } - } - - private fun saveScreenTimeApp(appModel: AppModel) { - if (appModel is AppModel.App) { - prefs.screenTimeAppPackage = appModel.appPackage - prefs.screenTimeAppUser = appModel.user.toString() - prefs.screenTimeAppClassName = appModel.activityClassName - } - } - - fun firstOpen(value: Boolean) { - firstOpen.postValue(value) - } - - fun refreshHome(appCountUpdated: Boolean) { - refreshHome.value = appCountUpdated - } - - fun toggleDateTime() { - toggleDateTime.postValue(Unit) - } - - private fun updateSwipeApps() { - updateSwipeApps.postValue(Unit) - } - - private fun launchApp(packageName: String, activityClassName: String?, userHandle: UserHandle) { - val launcher = appContext.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val activityInfo = launcher.getActivityList(packageName, userHandle) - - val isActivityValid = activityClassName.isNullOrBlank().not() - && activityInfo.any { it.componentName.className == activityClassName } - - val component = if (isActivityValid) - ComponentName(packageName, activityClassName) - else { - when (activityInfo.size) { - 0 -> { - appContext.showToast(appContext.getString(R.string.app_not_found)) - return - } - - 1 -> ComponentName(packageName, activityInfo[0].name) - else -> ComponentName(packageName, activityInfo[activityInfo.size - 1].name) - }.also { prefs.updateAppActivityClassName(packageName, it.className) } - } - - try { - launcher.startMainActivity(component, userHandle, null, null) - } catch (e: SecurityException) { - try { - launcher.startMainActivity(component, android.os.Process.myUserHandle(), null, null) - } catch (e: Exception) { - appContext.showToast(appContext.getString(R.string.unable_to_open_app)) - } - } catch (e: Exception) { - appContext.showToast(appContext.getString(R.string.unable_to_open_app)) - } - } - - fun getAppList(includeHiddenApps: Boolean = false) { - viewModelScope.launch { - val apps = getAppsList(appContext, prefs, includeRegularApps = true, includeHiddenApps) - appList.value = apps - } - getPrivateSpaceAppList() - } - - fun getHiddenApps() { - viewModelScope.launch { - hiddenApps.value = - getAppsList(appContext, prefs, includeRegularApps = false, includeHiddenApps = true) - } - } - - fun isOlauncherDefault() { - isOlauncherDefault.value = isOlauncherDefault(appContext) - } - - fun setWallpaperWorker() { - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - val uploadWorkRequest = PeriodicWorkRequestBuilder(4, TimeUnit.HOURS) - .setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.HOURS) - .setConstraints(constraints) - .build() - WorkManager - .getInstance(appContext) - .enqueueUniquePeriodicWork( - Constants.WALLPAPER_WORKER_NAME, - ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, - uploadWorkRequest - ) - } - - fun cancelWallpaperWorker() { - WorkManager.getInstance(appContext).cancelUniqueWork(Constants.WALLPAPER_WORKER_NAME) - prefs.dailyWallpaperUrl = "" - prefs.dailyWallpaper = false - } - - fun updateHomeAlignment(gravity: Int) { - prefs.homeAlignment = gravity - homeAppAlignment.value = prefs.homeAlignment - } - - fun getTodaysScreenTime() { - if (prefs.screenTimeLastUpdated.hasBeenMinutes(1).not()) return - - val eventLogWrapper = EventLogWrapper( - appContext - ) - // Start of today in millis - val calendar = Calendar.getInstance().apply { - set(Calendar.HOUR_OF_DAY, 0) - set(Calendar.MINUTE, 0) - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - } - val startTime = calendar.timeInMillis - val endTime = System.currentTimeMillis() - - val timeSpent = eventLogWrapper.aggregateSimpleUsageStats( - eventLogWrapper.aggregateForegroundStats( - eventLogWrapper.getForegroundStatsByTimestamps(startTime, endTime) - ) - ) - val viewTimeSpent = appContext.formattedTimeSpent(timeSpent) - screenTimeValue.postValue(viewTimeSpent) - prefs.screenTimeLastUpdated = endTime - } - - fun getPrivateSpaceAppList() { - viewModelScope.launch { - val handle = getPrivateSpaceUserHandle(appContext) - privateSpaceAvailable.value = handle != null - if (handle != null) { - privateSpaceLocked.value = isPrivateSpaceLocked(appContext, handle) - privateSpaceApps.value = getPrivateSpaceApps(appContext, prefs) - } else { - privateSpaceLocked.value = true - privateSpaceApps.value = emptyList() - } - } - } - - fun openPrivateSpaceSettings() { - try { - val intent = Intent("android.settings.PRIVATE_SPACE_SETTINGS") - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - appContext.startActivity(intent) - } catch (_: Exception) { - try { - val intent = Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - appContext.startActivity(intent) - } catch (_: Exception) { - appContext.showToast(appContext.getString(R.string.unable_to_open_app)) - } - } - } - - fun togglePrivateSpaceLock() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) return - val handle = getPrivateSpaceUserHandle(appContext) ?: return - try { - isPrivateSpaceToggling = true - val userManager = appContext.getSystemService(Context.USER_SERVICE) as UserManager - val currentlyLocked = userManager.isQuietModeEnabled(handle) - userManager.requestQuietModeEnabled(!currentlyLocked, handle) - } catch (e: Exception) { - isPrivateSpaceToggling = false - e.printStackTrace() - } - } - - fun setDefaultClockApp() { - viewModelScope.launch { - try { - Constants.CLOCK_APP_PACKAGES.firstOrNull { appContext.isPackageInstalled(it) }?.let { packageName -> - appContext.packageManager.getLaunchIntentForPackage(packageName)?.component?.className?.let { - prefs.clockAppPackage = packageName - prefs.clockAppClassName = it - prefs.clockAppUser = android.os.Process.myUserHandle().toString() - } - } - } catch (e: Exception) { - e.printStackTrace() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/data/AppModel.kt b/app/src/main/java/app/olauncher/data/AppModel.kt deleted file mode 100644 index 6df3083..0000000 --- a/app/src/main/java/app/olauncher/data/AppModel.kt +++ /dev/null @@ -1,45 +0,0 @@ -package app.olauncher.data - -import android.os.UserHandle -import java.text.CollationKey - -sealed class AppModel : Comparable { - abstract val appLabel: String - abstract val key: CollationKey? - abstract val appPackage: String - abstract val user: UserHandle - abstract val isNew: Boolean - - data class App( - override val appLabel: String, - override val key: CollationKey?, - override val appPackage: String, - val activityClassName: String?, - override val isNew: Boolean = false, - override val user: UserHandle, - ) : AppModel() - - data class PinnedShortcut( - override val appLabel: String, - override val key: CollationKey?, - override val appPackage: String, - val shortcutId: String, - override val isNew: Boolean = false, - override val user: UserHandle, - ) : AppModel() - - data class PrivateSpaceHeader( - val isLocked: Boolean = true, - override val user: UserHandle = android.os.Process.myUserHandle(), - ) : AppModel() { - override val appLabel: String = "" - override val key: CollationKey? = null - override val appPackage: String = "" - override val isNew: Boolean = false - } - - override fun compareTo(other: AppModel): Int = when { - key != null && other.key != null -> key!!.compareTo(other.key) - else -> appLabel.compareTo(other.appLabel, true) - } -} \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/data/Constants.kt b/app/src/main/java/app/olauncher/data/Constants.kt deleted file mode 100644 index cabfa45..0000000 --- a/app/src/main/java/app/olauncher/data/Constants.kt +++ /dev/null @@ -1,120 +0,0 @@ -package app.olauncher.data - -object Constants { - - object Key { - const val FLAG = "flag" - const val RENAME = "rename" - } - - object Dialog { - const val ABOUT = "ABOUT" - const val WALLPAPER = "WALLPAPER" - const val REVIEW = "REVIEW" - const val RATE = "RATE" - const val SHARE = "SHARE" - const val HIDDEN = "HIDDEN" - const val KEYBOARD = "KEYBOARD" - const val DIGITAL_WELLBEING = "DIGITAL_WELLBEING" - const val PRO_MESSAGE = "PRO_MESSAGE" - } - - object UserState { - const val START = "START" - const val WALLPAPER = "WALLPAPER" - const val REVIEW = "REVIEW" - const val RATE = "RATE" - const val SHARE = "SHARE" - } - - object DateTime { - const val OFF = 0 - const val ON = 1 - const val DATE_ONLY = 2 - - fun isTimeVisible(dateTimeVisibility: Int): Boolean { - return dateTimeVisibility == ON - } - - fun isDateVisible(dateTimeVisibility: Int): Boolean { - return dateTimeVisibility == ON || dateTimeVisibility == DATE_ONLY - } - } - - object SwipeDownAction { - const val SEARCH = 1 - const val NOTIFICATIONS = 2 - } - - object CharacterIndicator { - const val SHOW = 102 - const val HIDE = 101 - } - - val CLOCK_APP_PACKAGES = arrayOf( - "com.google.android.deskclock", //Google Clock - "com.sec.android.app.clockpackage", //Samsung Clock - "com.oneplus.deskclock", //OnePlus Clock - "com.miui.clock", //Xiaomi Clock - ) - - const val WALL_TYPE_LIGHT = "light" - const val WALL_TYPE_DARK = "dark" - -// const val THEME_MODE_DARK = 0 -// const val THEME_MODE_LIGHT = 1 -// const val THEME_MODE_SYSTEM = 2 - - const val FLAG_LAUNCH_APP = 100 - const val FLAG_HIDDEN_APPS = 101 - - const val FLAG_SET_HOME_APP_1 = 1 - const val FLAG_SET_HOME_APP_2 = 2 - const val FLAG_SET_HOME_APP_3 = 3 - const val FLAG_SET_HOME_APP_4 = 4 - const val FLAG_SET_HOME_APP_5 = 5 - const val FLAG_SET_HOME_APP_6 = 6 - const val FLAG_SET_HOME_APP_7 = 7 - const val FLAG_SET_HOME_APP_8 = 8 - - const val FLAG_SET_SWIPE_LEFT_APP = 11 - const val FLAG_SET_SWIPE_RIGHT_APP = 12 - const val FLAG_SET_CLOCK_APP = 13 - const val FLAG_SET_CALENDAR_APP = 14 - const val FLAG_SET_SCREEN_TIME_APP = 15 - - const val REQUEST_CODE_ENABLE_ADMIN = 666 - const val REQUEST_CODE_LAUNCHER_SELECTOR = 678 - - const val HINT_RATE_US = 15 - - const val LONG_PRESS_DELAY_MS = 500L - const val ONE_DAY_IN_MILLIS = 86400000L - const val ONE_HOUR_IN_MILLIS = 3600000L - const val ONE_MINUTE_IN_MILLIS = 60000L - - const val MIN_ANIM_REFRESH_RATE = 30f - - const val URL_ABOUT_OLAUNCHER = "https://tanujnotes.substack.com/p/olauncher-minimal-af-launcher?utm_source=olauncher" - const val URL_OLAUNCHER_PRIVACY = "https://tanujnotes.notion.site/Olauncher-Privacy-Policy-dd6ac5101ddd4b3da9d27057889d44ab" - const val URL_DOUBLE_TAP = "https://tanujnotes.notion.site/Double-tap-to-lock-Olauncher-0f7fb103ec1f47d7a90cdfdcd7fb86ef" - const val URL_OLAUNCHER_GITHUB = "https://www.github.com/tanujnotes/Olauncher" - const val URL_OLAUNCHER_PLAY_STORE = "https://play.google.com/store/apps/details?id=app.olauncher" - const val URL_OLAUNCHER_PRO = "https://play.google.com/store/apps/details?id=app.prolauncher" - const val URL_PLAY_STORE_DEV = "https://play.google.com/store/apps/dev?id=7198807840081074933" - const val URL_TWITTER_TANUJ = "https://x.com/tanujnotes" - const val URL_INSTA_TANUJ = "https://instagram.com/tanuj_notes" - const val URL_WALLPAPERS = "https://gist.githubusercontent.com/tanujnotes/85e2d0343ace71e76615ac346fbff82b/raw" - const val URL_NTS = "https://play.google.com/store/apps/details?id=com.makenotetoself" - const val URL_PENTASTIC = "https://play.google.com/store/apps/details?id=app.pentastic" - const val URL_DEFAULT_DARK_WALLPAPER = "https://images.unsplash.com/photo-1512551980832-13df02babc9e" - const val URL_DEFAULT_LIGHT_WALLPAPER = "https://images.unsplash.com/photo-1515549832467-8783363e19b6" - const val URL_DUCK_SEARCH = "https://duck.co/?q=" - const val URL_DIGITAL_WELLBEING_LEARN_MORE = "https://tanujnotes.substack.com/p/digital-wellbeing-app-on-android?utm_source=olauncher" - - const val DIGITAL_WELLBEING_PACKAGE_NAME = "com.google.android.apps.wellbeing" - const val DIGITAL_WELLBEING_ACTIVITY = "com.google.android.apps.wellbeing.settings.TopLevelSettingsActivity" - const val DIGITAL_WELLBEING_SAMSUNG_PACKAGE_NAME = "com.samsung.android.forest" - const val DIGITAL_WELLBEING_SAMSUNG_ACTIVITY = "com.samsung.android.forest.launcher.LauncherActivity" - const val WALLPAPER_WORKER_NAME = "WALLPAPER_WORKER_NAME" -} \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/data/DrawerCharacterModel.kt b/app/src/main/java/app/olauncher/data/DrawerCharacterModel.kt deleted file mode 100644 index 565020c..0000000 --- a/app/src/main/java/app/olauncher/data/DrawerCharacterModel.kt +++ /dev/null @@ -1,6 +0,0 @@ -package app.olauncher.data - -data class DrawerCharacterModel( - val character: String = "", - val inRange: Boolean = false -) \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/data/Prefs.kt b/app/src/main/java/app/olauncher/data/Prefs.kt deleted file mode 100644 index efe93bb..0000000 --- a/app/src/main/java/app/olauncher/data/Prefs.kt +++ /dev/null @@ -1,647 +0,0 @@ -package app.olauncher.data - -import android.content.Context -import android.content.SharedPreferences -import android.view.Gravity -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.content.edit - -class Prefs(context: Context) { - private val PREFS_FILENAME = "app.olauncher" - - private val FIRST_OPEN = "FIRST_OPEN" - private val FIRST_OPEN_TIME = "FIRST_OPEN_TIME" - private val FIRST_SETTINGS_OPEN = "FIRST_SETTINGS_OPEN" - private val FIRST_HIDE = "FIRST_HIDE" - private val USER_STATE = "USER_STATE" - private val LOCK_MODE = "LOCK_MODE" - private val HOME_APPS_NUM = "HOME_APPS_NUM" - private val AUTO_SHOW_KEYBOARD = "AUTO_SHOW_KEYBOARD" - private val KEYBOARD_MESSAGE = "KEYBOARD_MESSAGE" - private val DAILY_WALLPAPER = "DAILY_WALLPAPER" - private val DAILY_WALLPAPER_URL = "DAILY_WALLPAPER_URL" - private val HOME_ALIGNMENT = "HOME_ALIGNMENT" - private val HOME_BOTTOM_ALIGNMENT = "HOME_BOTTOM_ALIGNMENT" - private val APP_LABEL_ALIGNMENT = "APP_LABEL_ALIGNMENT" - private val STATUS_BAR = "STATUS_BAR" - private val DATE_TIME_VISIBILITY = "DATE_TIME_VISIBILITY" - private val SWIPE_LEFT_ENABLED = "SWIPE_LEFT_ENABLED" - private val SWIPE_RIGHT_ENABLED = "SWIPE_RIGHT_ENABLED" - private val HIDDEN_APPS = "HIDDEN_APPS" - private val HIDDEN_APPS_UPDATED = "HIDDEN_APPS_UPDATED" - private val SHOW_HINT_COUNTER = "SHOW_HINT_COUNTER" - private val APP_THEME = "APP_THEME" - private val ABOUT_CLICKED = "ABOUT_CLICKED" - private val RATE_CLICKED = "RATE_CLICKED" - private val WALLPAPER_MSG_SHOWN = "WALLPAPER_MSG_SHOWN" - private val SHARE_SHOWN_TIME = "SHARE_SHOWN_TIME" - private val SWIPE_DOWN_ACTION = "SWIPE_DOWN_ACTION" - private val TEXT_SIZE_SCALE = "TEXT_SIZE_SCALE" - private val PRO_MESSAGE_SHOWN = "PRO_MESSAGE_SHOWN" - private val HIDE_SET_DEFAULT_LAUNCHER = "HIDE_SET_DEFAULT_LAUNCHER" - private val SCREEN_TIME_LAST_UPDATED = "SCREEN_TIME_LAST_UPDATED" - private val LAUNCHER_RESTART_TIMESTAMP = "LAUNCHER_RECREATE_TIMESTAMP" - private val SHOWN_ON_DAY_OF_YEAR = "SHOWN_ON_DAY_OF_YEAR" - // Home button for recents feature disabled - // private val HOME_BUTTON_SHOW_RECENTS = "HOME_BUTTON_SHOW_RECENTS" - - private val APP_NAME_1 = "APP_NAME_1" - private val APP_NAME_2 = "APP_NAME_2" - private val APP_NAME_3 = "APP_NAME_3" - private val APP_NAME_4 = "APP_NAME_4" - private val APP_NAME_5 = "APP_NAME_5" - private val APP_NAME_6 = "APP_NAME_6" - private val APP_NAME_7 = "APP_NAME_7" - private val APP_NAME_8 = "APP_NAME_8" - private val APP_PACKAGE_1 = "APP_PACKAGE_1" - private val APP_PACKAGE_2 = "APP_PACKAGE_2" - private val APP_PACKAGE_3 = "APP_PACKAGE_3" - private val APP_PACKAGE_4 = "APP_PACKAGE_4" - private val APP_PACKAGE_5 = "APP_PACKAGE_5" - private val APP_PACKAGE_6 = "APP_PACKAGE_6" - private val APP_PACKAGE_7 = "APP_PACKAGE_7" - private val APP_PACKAGE_8 = "APP_PACKAGE_8" - private val APP_ACTIVITY_CLASS_NAME_1 = "APP_ACTIVITY_CLASS_NAME_1" - private val APP_ACTIVITY_CLASS_NAME_2 = "APP_ACTIVITY_CLASS_NAME_2" - private val APP_ACTIVITY_CLASS_NAME_3 = "APP_ACTIVITY_CLASS_NAME_3" - private val APP_ACTIVITY_CLASS_NAME_4 = "APP_ACTIVITY_CLASS_NAME_4" - private val APP_ACTIVITY_CLASS_NAME_5 = "APP_ACTIVITY_CLASS_NAME_5" - private val APP_ACTIVITY_CLASS_NAME_6 = "APP_ACTIVITY_CLASS_NAME_6" - private val APP_ACTIVITY_CLASS_NAME_7 = "APP_ACTIVITY_CLASS_NAME_7" - private val APP_ACTIVITY_CLASS_NAME_8 = "APP_ACTIVITY_CLASS_NAME_8" - private val APP_USER_1 = "APP_USER_1" - private val APP_USER_2 = "APP_USER_2" - private val APP_USER_3 = "APP_USER_3" - private val APP_USER_4 = "APP_USER_4" - private val APP_USER_5 = "APP_USER_5" - private val APP_USER_6 = "APP_USER_6" - private val APP_USER_7 = "APP_USER_7" - private val APP_USER_8 = "APP_USER_8" - - private val APP_NAME_SWIPE_LEFT = "APP_NAME_SWIPE_LEFT" - private val APP_NAME_SWIPE_RIGHT = "APP_NAME_SWIPE_RIGHT" - private val APP_PACKAGE_SWIPE_LEFT = "APP_PACKAGE_SWIPE_LEFT" - private val APP_PACKAGE_SWIPE_RIGHT = "APP_PACKAGE_SWIPE_RIGHT" - private val APP_ACTIVITY_CLASS_NAME_SWIPE_LEFT = "APP_ACTIVITY_CLASS_NAME_SWIPE_LEFT" - private val APP_ACTIVITY_CLASS_NAME_SWIPE_RIGHT = "APP_ACTIVITY_CLASS_NAME_SWIPE_RIGHT" - private val APP_USER_SWIPE_LEFT = "APP_USER_SWIPE_LEFT" - private val APP_USER_SWIPE_RIGHT = "APP_USER_SWIPE_RIGHT" - private val CLOCK_APP_PACKAGE = "CLOCK_APP_PACKAGE" - private val CLOCK_APP_USER = "CLOCK_APP_USER" - private val CLOCK_APP_CLASS_NAME = "CLOCK_APP_CLASS_NAME" - private val CALENDAR_APP_PACKAGE = "CALENDAR_APP_PACKAGE" - private val CALENDAR_APP_USER = "CALENDAR_APP_USER" - private val CALENDAR_APP_CLASS_NAME = "CALENDAR_APP_CLASS_NAME" - private val SCREEN_TIME_APP_PACKAGE = "SCREEN_TIME_APP_PACKAGE" - private val SCREEN_TIME_APP_USER = "SCREEN_TIME_APP_USER" - private val SCREEN_TIME_APP_CLASS_NAME = "SCREEN_TIME_APP_CLASS_NAME" - - private val IS_SHORTCUT_1 = "IS_SHORTCUT_1" - private val SHORTCUT_ID_1 = "SHORTCUT_ID_1" - private val IS_SHORTCUT_2 = "IS_SHORTCUT_2" - private val SHORTCUT_ID_2 = "SHORTCUT_ID_2" - private val IS_SHORTCUT_3 = "IS_SHORTCUT_3" - private val SHORTCUT_ID_3 = "SHORTCUT_ID_3" - private val IS_SHORTCUT_4 = "IS_SHORTCUT_4" - private val SHORTCUT_ID_4 = "SHORTCUT_ID_4" - private val IS_SHORTCUT_5 = "IS_SHORTCUT_5" - private val SHORTCUT_ID_5 = "SHORTCUT_ID_5" - private val IS_SHORTCUT_6 = "IS_SHORTCUT_6" - private val SHORTCUT_ID_6 = "SHORTCUT_ID_6" - private val IS_SHORTCUT_7 = "IS_SHORTCUT_7" - private val SHORTCUT_ID_7 = "SHORTCUT_ID_7" - private val IS_SHORTCUT_8 = "IS_SHORTCUT_8" - private val SHORTCUT_ID_8 = "SHORTCUT_ID_8" - - private val SHORTCUT_ID_SWIPE_LEFT = "SHORTCUT_ID_SWIPE_LEFT" - private val IS_SHORTCUT_SWIPE_LEFT = "IS_SHORTCUT_SWIPE_LEFT" - private val SHORTCUT_ID_SWIPE_RIGHT = "SHORTCUT_ID_SWIPE_RIGHT" - private val IS_SHORTCUT_SWIPE_RIGHT = "IS_SHORTCUT_SWIPE_RIGHT" - - private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_FILENAME, 0) - - var firstOpen: Boolean - get() = prefs.getBoolean(FIRST_OPEN, true) - set(value) = prefs.edit { putBoolean(FIRST_OPEN, value).apply() } - - var firstOpenTime: Long - get() = prefs.getLong(FIRST_OPEN_TIME, 0L) - set(value) = prefs.edit { putLong(FIRST_OPEN_TIME, value).apply() } - - var firstSettingsOpen: Boolean - get() = prefs.getBoolean(FIRST_SETTINGS_OPEN, true) - set(value) = prefs.edit { putBoolean(FIRST_SETTINGS_OPEN, value).apply() } - - var firstHide: Boolean - get() = prefs.getBoolean(FIRST_HIDE, true) - set(value) = prefs.edit { putBoolean(FIRST_HIDE, value).apply() } - - var userState: String - get() = prefs.getString(USER_STATE, Constants.UserState.START).toString() - set(value) = prefs.edit { putString(USER_STATE, value).apply() } - - var lockModeOn: Boolean - get() = prefs.getBoolean(LOCK_MODE, false) - set(value) = prefs.edit { putBoolean(LOCK_MODE, value).apply() } - - var autoShowKeyboard: Boolean - get() = prefs.getBoolean(AUTO_SHOW_KEYBOARD, true) - set(value) = prefs.edit { putBoolean(AUTO_SHOW_KEYBOARD, value).apply() } - - var keyboardMessageShown: Boolean - get() = prefs.getBoolean(KEYBOARD_MESSAGE, false) - set(value) = prefs.edit { putBoolean(KEYBOARD_MESSAGE, value).apply() } - - var dailyWallpaper: Boolean - get() = prefs.getBoolean(DAILY_WALLPAPER, false) - set(value) = prefs.edit { putBoolean(DAILY_WALLPAPER, value).apply() } - - var dailyWallpaperUrl: String - get() = prefs.getString(DAILY_WALLPAPER_URL, "").toString() - set(value) = prefs.edit { putString(DAILY_WALLPAPER_URL, value).apply() } - - var homeAppsNum: Int - get() = prefs.getInt(HOME_APPS_NUM, 4) - set(value) = prefs.edit { putInt(HOME_APPS_NUM, value).apply() } - - var homeAlignment: Int - get() = prefs.getInt(HOME_ALIGNMENT, Gravity.START) - set(value) = prefs.edit { putInt(HOME_ALIGNMENT, value).apply() } - - var homeBottomAlignment: Boolean - get() = prefs.getBoolean(HOME_BOTTOM_ALIGNMENT, false) - set(value) = prefs.edit { putBoolean(HOME_BOTTOM_ALIGNMENT, value).apply() } - - var appLabelAlignment: Int - get() = prefs.getInt(APP_LABEL_ALIGNMENT, Gravity.START) - set(value) = prefs.edit { putInt(APP_LABEL_ALIGNMENT, value).apply() } - - var showStatusBar: Boolean - get() = prefs.getBoolean(STATUS_BAR, false) - set(value) = prefs.edit { putBoolean(STATUS_BAR, value).apply() } - - var dateTimeVisibility: Int - get() = prefs.getInt(DATE_TIME_VISIBILITY, Constants.DateTime.ON) - set(value) = prefs.edit { putInt(DATE_TIME_VISIBILITY, value).apply() } - - var swipeLeftEnabled: Boolean - get() = prefs.getBoolean(SWIPE_LEFT_ENABLED, true) - set(value) = prefs.edit { putBoolean(SWIPE_LEFT_ENABLED, value).apply() } - - var swipeRightEnabled: Boolean - get() = prefs.getBoolean(SWIPE_RIGHT_ENABLED, true) - set(value) = prefs.edit { putBoolean(SWIPE_RIGHT_ENABLED, value).apply() } - - var appTheme: Int - get() = prefs.getInt(APP_THEME, AppCompatDelegate.MODE_NIGHT_YES) - set(value) = prefs.edit { putInt(APP_THEME, value).apply() } - - var textSizeScale: Float - get() = prefs.getFloat(TEXT_SIZE_SCALE, 1.0f) - set(value) = prefs.edit { putFloat(TEXT_SIZE_SCALE, value).apply() } - - var proMessageShown: Boolean - get() = prefs.getBoolean(PRO_MESSAGE_SHOWN, false) - set(value) = prefs.edit { putBoolean(PRO_MESSAGE_SHOWN, value).apply() } - - var hideSetDefaultLauncher: Boolean - get() = prefs.getBoolean(HIDE_SET_DEFAULT_LAUNCHER, false) - set(value) = prefs.edit { putBoolean(HIDE_SET_DEFAULT_LAUNCHER, value).apply() } - - var screenTimeLastUpdated: Long - get() = prefs.getLong(SCREEN_TIME_LAST_UPDATED, 0L) - set(value) = prefs.edit { putLong(SCREEN_TIME_LAST_UPDATED, value).apply() } - - var launcherRestartTimestamp: Long - get() = prefs.getLong(LAUNCHER_RESTART_TIMESTAMP, 0L) - set(value) = prefs.edit { putLong(LAUNCHER_RESTART_TIMESTAMP, value).apply() } - - var shownOnDayOfYear: Int - get() = prefs.getInt(SHOWN_ON_DAY_OF_YEAR, 0) - set(value) = prefs.edit { putInt(SHOWN_ON_DAY_OF_YEAR, value).apply() } - - // Home button for recents feature disabled - // var homeButtonShowRecents: Boolean - // get() = prefs.getBoolean(HOME_BUTTON_SHOW_RECENTS, false) - // set(value) = prefs.edit { putBoolean(HOME_BUTTON_SHOW_RECENTS, value).apply() } - - var hiddenApps: MutableSet - get() = prefs.getStringSet(HIDDEN_APPS, mutableSetOf()) as MutableSet - set(value) = prefs.edit { putStringSet(HIDDEN_APPS, value).apply() } - - var hiddenAppsUpdated: Boolean - get() = prefs.getBoolean(HIDDEN_APPS_UPDATED, false) - set(value) = prefs.edit { putBoolean(HIDDEN_APPS_UPDATED, value).apply() } - - var toShowHintCounter: Int - get() = prefs.getInt(SHOW_HINT_COUNTER, 1) - set(value) = prefs.edit { putInt(SHOW_HINT_COUNTER, value).apply() } - - var aboutClicked: Boolean - get() = prefs.getBoolean(ABOUT_CLICKED, false) - set(value) = prefs.edit { putBoolean(ABOUT_CLICKED, value).apply() } - - var rateClicked: Boolean - get() = prefs.getBoolean(RATE_CLICKED, false) - set(value) = prefs.edit { putBoolean(RATE_CLICKED, value).apply() } - - var wallpaperMsgShown: Boolean - get() = prefs.getBoolean(WALLPAPER_MSG_SHOWN, false) - set(value) = prefs.edit { putBoolean(WALLPAPER_MSG_SHOWN, value).apply() } - - var shareShownTime: Long - get() = prefs.getLong(SHARE_SHOWN_TIME, 0L) - set(value) = prefs.edit { putLong(SHARE_SHOWN_TIME, value).apply() } - - var swipeDownAction: Int - get() = prefs.getInt(SWIPE_DOWN_ACTION, Constants.SwipeDownAction.NOTIFICATIONS) - set(value) = prefs.edit { putInt(SWIPE_DOWN_ACTION, value).apply() } - - var appName1: String - get() = prefs.getString(APP_NAME_1, "").toString() - set(value) = prefs.edit { putString(APP_NAME_1, value).apply() } - - var appName2: String - get() = prefs.getString(APP_NAME_2, "").toString() - set(value) = prefs.edit { putString(APP_NAME_2, value).apply() } - - var appName3: String - get() = prefs.getString(APP_NAME_3, "").toString() - set(value) = prefs.edit { putString(APP_NAME_3, value).apply() } - - var appName4: String - get() = prefs.getString(APP_NAME_4, "").toString() - set(value) = prefs.edit { putString(APP_NAME_4, value).apply() } - - var appName5: String - get() = prefs.getString(APP_NAME_5, "").toString() - set(value) = prefs.edit { putString(APP_NAME_5, value).apply() } - - var appName6: String - get() = prefs.getString(APP_NAME_6, "").toString() - set(value) = prefs.edit { putString(APP_NAME_6, value).apply() } - - var appName7: String - get() = prefs.getString(APP_NAME_7, "").toString() - set(value) = prefs.edit { putString(APP_NAME_7, value).apply() } - - var appName8: String - get() = prefs.getString(APP_NAME_8, "").toString() - set(value) = prefs.edit { putString(APP_NAME_8, value).apply() } - - var appPackage1: String - get() = prefs.getString(APP_PACKAGE_1, "").toString() - set(value) = prefs.edit { putString(APP_PACKAGE_1, value).apply() } - - var appPackage2: String - get() = prefs.getString(APP_PACKAGE_2, "").toString() - set(value) = prefs.edit { putString(APP_PACKAGE_2, value).apply() } - - var appPackage3: String - get() = prefs.getString(APP_PACKAGE_3, "").toString() - set(value) = prefs.edit { putString(APP_PACKAGE_3, value).apply() } - - var appPackage4: String - get() = prefs.getString(APP_PACKAGE_4, "").toString() - set(value) = prefs.edit { putString(APP_PACKAGE_4, value).apply() } - - var appPackage5: String - get() = prefs.getString(APP_PACKAGE_5, "").toString() - set(value) = prefs.edit { putString(APP_PACKAGE_5, value).apply() } - - var appPackage6: String - get() = prefs.getString(APP_PACKAGE_6, "").toString() - set(value) = prefs.edit { putString(APP_PACKAGE_6, value).apply() } - - var appPackage7: String - get() = prefs.getString(APP_PACKAGE_7, "").toString() - set(value) = prefs.edit { putString(APP_PACKAGE_7, value).apply() } - - var appPackage8: String - get() = prefs.getString(APP_PACKAGE_8, "").toString() - set(value) = prefs.edit { putString(APP_PACKAGE_8, value).apply() } - - var appActivityClassName1: String? - get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_1, "").toString() - set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_1, value).apply() } - - var appActivityClassName2: String? - get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_2, "").toString() - set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_2, value).apply() } - - var appActivityClassName3: String? - get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_3, "").toString() - set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_3, value).apply() } - - var appActivityClassName4: String? - get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_4, "").toString() - set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_4, value).apply() } - - var appActivityClassName5: String? - get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_5, "").toString() - set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_5, value).apply() } - - var appActivityClassName6: String? - get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_6, "").toString() - set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_6, value).apply() } - - var appActivityClassName7: String? - get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_7, "").toString() - set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_7, value).apply() } - - var appActivityClassName8: String? - get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_8, "").toString() - set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_8, value).apply() } - - var appUser1: String - get() = prefs.getString(APP_USER_1, "").toString() - set(value) = prefs.edit { putString(APP_USER_1, value).apply() } - - var appUser2: String - get() = prefs.getString(APP_USER_2, "").toString() - set(value) = prefs.edit { putString(APP_USER_2, value).apply() } - - var appUser3: String - get() = prefs.getString(APP_USER_3, "").toString() - set(value) = prefs.edit { putString(APP_USER_3, value).apply() } - - var appUser4: String - get() = prefs.getString(APP_USER_4, "").toString() - set(value) = prefs.edit { putString(APP_USER_4, value).apply() } - - var appUser5: String - get() = prefs.getString(APP_USER_5, "").toString() - set(value) = prefs.edit { putString(APP_USER_5, value).apply() } - - var appUser6: String - get() = prefs.getString(APP_USER_6, "").toString() - set(value) = prefs.edit { putString(APP_USER_6, value).apply() } - - var appUser7: String - get() = prefs.getString(APP_USER_7, "").toString() - set(value) = prefs.edit { putString(APP_USER_7, value).apply() } - - var appUser8: String - get() = prefs.getString(APP_USER_8, "").toString() - set(value) = prefs.edit { putString(APP_USER_8, value).apply() } - - var appNameSwipeLeft: String - get() = prefs.getString(APP_NAME_SWIPE_LEFT, "Camera").toString() - set(value) = prefs.edit { putString(APP_NAME_SWIPE_LEFT, value).apply() } - - var appNameSwipeRight: String - get() = prefs.getString(APP_NAME_SWIPE_RIGHT, "Phone").toString() - set(value) = prefs.edit { putString(APP_NAME_SWIPE_RIGHT, value).apply() } - - var appPackageSwipeLeft: String - get() = prefs.getString(APP_PACKAGE_SWIPE_LEFT, "").toString() - set(value) = prefs.edit { putString(APP_PACKAGE_SWIPE_LEFT, value).apply() } - - var appActivityClassNameSwipeLeft: String? - get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_SWIPE_LEFT, "").toString() - set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_SWIPE_LEFT, value).apply() } - - var appPackageSwipeRight: String - get() = prefs.getString(APP_PACKAGE_SWIPE_RIGHT, "").toString() - set(value) = prefs.edit { putString(APP_PACKAGE_SWIPE_RIGHT, value).apply() } - - var appActivityClassNameRight: String? - get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_SWIPE_RIGHT, "").toString() - set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_SWIPE_RIGHT, value).apply() } - - var appUserSwipeLeft: String - get() = prefs.getString(APP_USER_SWIPE_LEFT, "").toString() - set(value) = prefs.edit { putString(APP_USER_SWIPE_LEFT, value).apply() } - - var appUserSwipeRight: String - get() = prefs.getString(APP_USER_SWIPE_RIGHT, "").toString() - set(value) = prefs.edit { putString(APP_USER_SWIPE_RIGHT, value).apply() } - - var clockAppPackage: String - get() = prefs.getString(CLOCK_APP_PACKAGE, "").toString() - set(value) = prefs.edit { putString(CLOCK_APP_PACKAGE, value).apply() } - - var clockAppUser: String - get() = prefs.getString(CLOCK_APP_USER, "").toString() - set(value) = prefs.edit { putString(CLOCK_APP_USER, value).apply() } - - var clockAppClassName: String? - get() = prefs.getString(CLOCK_APP_CLASS_NAME, "").toString() - set(value) = prefs.edit { putString(CLOCK_APP_CLASS_NAME, value).apply() } - - var calendarAppPackage: String - get() = prefs.getString(CALENDAR_APP_PACKAGE, "").toString() - set(value) = prefs.edit { putString(CALENDAR_APP_PACKAGE, value).apply() } - - var calendarAppUser: String - get() = prefs.getString(CALENDAR_APP_USER, "").toString() - set(value) = prefs.edit { putString(CALENDAR_APP_USER, value).apply() } - - var calendarAppClassName: String? - get() = prefs.getString(CALENDAR_APP_CLASS_NAME, "").toString() - set(value) = prefs.edit { putString(CALENDAR_APP_CLASS_NAME, value).apply() } - - var screenTimeAppPackage: String - get() = prefs.getString(SCREEN_TIME_APP_PACKAGE, "").toString() - set(value) = prefs.edit { putString(SCREEN_TIME_APP_PACKAGE, value).apply() } - - var screenTimeAppUser: String - get() = prefs.getString(SCREEN_TIME_APP_USER, "").toString() - set(value) = prefs.edit { putString(SCREEN_TIME_APP_USER, value).apply() } - - var screenTimeAppClassName: String? - get() = prefs.getString(SCREEN_TIME_APP_CLASS_NAME, "").toString() - set(value) = prefs.edit { putString(SCREEN_TIME_APP_CLASS_NAME, value).apply() } - - var isShortcut1: Boolean - get() = prefs.getBoolean(IS_SHORTCUT_1, false) - set(value) = prefs.edit { putBoolean(IS_SHORTCUT_1, value) } - - var shortcutId1: String - get() = prefs.getString(SHORTCUT_ID_1, "").toString() - set(value) = prefs.edit { putString(SHORTCUT_ID_1, value) } - - var isShortcut2: Boolean - get() = prefs.getBoolean(IS_SHORTCUT_2, false) - set(value) = prefs.edit { putBoolean(IS_SHORTCUT_2, value) } - - var shortcutId2: String - get() = prefs.getString(SHORTCUT_ID_2, "").toString() - set(value) = prefs.edit { putString(SHORTCUT_ID_2, value) } - - var isShortcut3: Boolean - get() = prefs.getBoolean(IS_SHORTCUT_3, false) - set(value) = prefs.edit { putBoolean(IS_SHORTCUT_3, value) } - - var shortcutId3: String - get() = prefs.getString(SHORTCUT_ID_3, "").toString() - set(value) = prefs.edit { putString(SHORTCUT_ID_3, value) } - - var isShortcut4: Boolean - get() = prefs.getBoolean(IS_SHORTCUT_4, false) - set(value) = prefs.edit { putBoolean(IS_SHORTCUT_4, value) } - - var shortcutId4: String - get() = prefs.getString(SHORTCUT_ID_4, "").toString() - set(value) = prefs.edit { putString(SHORTCUT_ID_4, value) } - - var isShortcut5: Boolean - get() = prefs.getBoolean(IS_SHORTCUT_5, false) - set(value) = prefs.edit { putBoolean(IS_SHORTCUT_5, value) } - - var shortcutId5: String - get() = prefs.getString(SHORTCUT_ID_5, "").toString() - set(value) = prefs.edit { putString(SHORTCUT_ID_5, value) } - - var isShortcut6: Boolean - get() = prefs.getBoolean(IS_SHORTCUT_6, false) - set(value) = prefs.edit { putBoolean(IS_SHORTCUT_6, value) } - - var shortcutId6: String - get() = prefs.getString(SHORTCUT_ID_6, "").toString() - set(value) = prefs.edit { putString(SHORTCUT_ID_6, value) } - - var isShortcut7: Boolean - get() = prefs.getBoolean(IS_SHORTCUT_7, false) - set(value) = prefs.edit { putBoolean(IS_SHORTCUT_7, value) } - - var shortcutId7: String - get() = prefs.getString(SHORTCUT_ID_7, "").toString() - set(value) = prefs.edit { putString(SHORTCUT_ID_7, value) } - - var isShortcut8: Boolean - get() = prefs.getBoolean(IS_SHORTCUT_8, false) - set(value) = prefs.edit { putBoolean(IS_SHORTCUT_8, value) } - - var shortcutId8: String - get() = prefs.getString(SHORTCUT_ID_8, "").toString() - set(value) = prefs.edit { putString(SHORTCUT_ID_8, value) } - - var shortcutIdSwipeLeft: String - get() = prefs.getString(SHORTCUT_ID_SWIPE_LEFT, "").toString() - set(value) = prefs.edit { putString(SHORTCUT_ID_SWIPE_LEFT, value) } - - var isShortcutSwipeLeft: Boolean - get() = prefs.getBoolean(IS_SHORTCUT_SWIPE_LEFT, false) - set(value) = prefs.edit { putBoolean(IS_SHORTCUT_SWIPE_LEFT, value) } - - var shortcutIdSwipeRight: String - get() = prefs.getString(SHORTCUT_ID_SWIPE_RIGHT, "").toString() - set(value) = prefs.edit { putString(SHORTCUT_ID_SWIPE_RIGHT, value) } - - var isShortcutSwipeRight: Boolean - get() = prefs.getBoolean(IS_SHORTCUT_SWIPE_RIGHT, false) - set(value) = prefs.edit { putBoolean(IS_SHORTCUT_SWIPE_RIGHT, value) } - - fun getAppName(location: Int): String { - return when (location) { - 1 -> prefs.getString(APP_NAME_1, "").toString() - 2 -> prefs.getString(APP_NAME_2, "").toString() - 3 -> prefs.getString(APP_NAME_3, "").toString() - 4 -> prefs.getString(APP_NAME_4, "").toString() - 5 -> prefs.getString(APP_NAME_5, "").toString() - 6 -> prefs.getString(APP_NAME_6, "").toString() - 7 -> prefs.getString(APP_NAME_7, "").toString() - 8 -> prefs.getString(APP_NAME_8, "").toString() - else -> "" - } - } - - fun getAppPackage(location: Int): String { - return when (location) { - 1 -> prefs.getString(APP_PACKAGE_1, "").toString() - 2 -> prefs.getString(APP_PACKAGE_2, "").toString() - 3 -> prefs.getString(APP_PACKAGE_3, "").toString() - 4 -> prefs.getString(APP_PACKAGE_4, "").toString() - 5 -> prefs.getString(APP_PACKAGE_5, "").toString() - 6 -> prefs.getString(APP_PACKAGE_6, "").toString() - 7 -> prefs.getString(APP_PACKAGE_7, "").toString() - 8 -> prefs.getString(APP_PACKAGE_8, "").toString() - else -> "" - } - } - - fun getAppActivityClassName(location: Int): String { - return when (location) { - 1 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_1, "").toString() - 2 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_2, "").toString() - 3 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_3, "").toString() - 4 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_4, "").toString() - 5 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_5, "").toString() - 6 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_6, "").toString() - 7 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_7, "").toString() - 8 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_8, "").toString() - else -> "" - } - } - - fun getAppUser(location: Int): String { - return when (location) { - 1 -> prefs.getString(APP_USER_1, "").toString() - 2 -> prefs.getString(APP_USER_2, "").toString() - 3 -> prefs.getString(APP_USER_3, "").toString() - 4 -> prefs.getString(APP_USER_4, "").toString() - 5 -> prefs.getString(APP_USER_5, "").toString() - 6 -> prefs.getString(APP_USER_6, "").toString() - 7 -> prefs.getString(APP_USER_7, "").toString() - 8 -> prefs.getString(APP_USER_8, "").toString() - else -> "" - } - } - - fun getShortcutId(location: Int): String { - return when (location) { - 1 -> shortcutId1 - 2 -> shortcutId2 - 3 -> shortcutId3 - 4 -> shortcutId4 - 5 -> shortcutId5 - 6 -> shortcutId6 - 7 -> shortcutId7 - 8 -> shortcutId8 - else -> "" - } - } - - fun getIsShortcut(location: Int): Boolean { - return when (location) { - 1 -> isShortcut1 - 2 -> isShortcut2 - 3 -> isShortcut3 - 4 -> isShortcut4 - 5 -> isShortcut5 - 6 -> isShortcut6 - 7 -> isShortcut7 - 8 -> isShortcut8 - else -> false - } - } - - fun setAppActivityClassName(location: Int, activityClassName: String) { - when (location) { - 1 -> appActivityClassName1 = activityClassName - 2 -> appActivityClassName2 = activityClassName - 3 -> appActivityClassName3 = activityClassName - 4 -> appActivityClassName4 = activityClassName - 5 -> appActivityClassName5 = activityClassName - 6 -> appActivityClassName6 = activityClassName - 7 -> appActivityClassName7 = activityClassName - 8 -> appActivityClassName8 = activityClassName - } - } - - fun updateAppActivityClassName(packageName: String, activityClassName: String) { - for (i in 1..8) { - if (getAppPackage(i) == packageName) setAppActivityClassName(i, activityClassName) - } - if (clockAppPackage == packageName) clockAppClassName = activityClassName - if (calendarAppPackage == packageName) calendarAppClassName = activityClassName - if (screenTimeAppPackage == packageName) screenTimeAppClassName = activityClassName - if (appPackageSwipeLeft == packageName) appActivityClassNameSwipeLeft = activityClassName - if (appPackageSwipeRight == packageName) appActivityClassNameRight = activityClassName - } - - fun getAppRenameLabel(appPackage: String): String = prefs.getString(appPackage, "").toString() - - fun setAppRenameLabel(appPackage: String, renameLabel: String) = prefs.edit { putString(appPackage, renameLabel) } -} \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/helper/AppFilterHelper.kt b/app/src/main/java/app/olauncher/helper/AppFilterHelper.kt deleted file mode 100644 index 271e96e..0000000 --- a/app/src/main/java/app/olauncher/helper/AppFilterHelper.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.olauncher.helper - -import app.olauncher.data.AppModel - -interface AppFilterHelper { - fun onAppFiltered(items:List) -} \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/helper/AppUsageStats.kt b/app/src/main/java/app/olauncher/helper/AppUsageStats.kt deleted file mode 100644 index 9b69269..0000000 --- a/app/src/main/java/app/olauncher/helper/AppUsageStats.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.olauncher.helper - -import android.os.Build -import androidx.annotation.RequiresApi - -class AppUsageStats( - val lastTimeUsedMillis: Long, - val totalTimeInForegroundMillis: Long, - @get:RequiresApi(Build.VERSION_CODES.Q) val lastTimeForegroundServiceUsedMillis: Long, - @get:RequiresApi(Build.VERSION_CODES.Q) val totalTimeForegroundServiceUsedMillis: Long, -) - -class AppUsageStatsBucket { - var startMillis: Long = 0L - var endMillis: Long = 0L - var totalTime: Long = 0L - - fun addTotalTime() { - this.totalTime += endMillis - startMillis - } -} \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/helper/Extensions.kt b/app/src/main/java/app/olauncher/helper/Extensions.kt deleted file mode 100644 index 6ec12f6..0000000 --- a/app/src/main/java/app/olauncher/helper/Extensions.kt +++ /dev/null @@ -1,192 +0,0 @@ -package app.olauncher.helper - -import android.app.Activity -import android.app.AppOpsManager -import android.app.SearchManager -import android.app.role.RoleManager -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.pm.LauncherApps -import android.content.pm.PackageManager -import android.content.res.Resources -import android.net.Uri -import android.os.Build -import android.os.UserHandle -import android.provider.Settings -import android.telephony.TelephonyManager -import android.view.View -import android.view.WindowManager -import android.view.inputmethod.InputMethodManager -import androidx.annotation.RequiresApi -import app.olauncher.BuildConfig -import app.olauncher.R -import app.olauncher.data.Constants -import java.util.Calendar -import java.util.Locale - -fun View.hideKeyboard() { - this.clearFocus() - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(windowToken, 0) -} - -fun View.showKeyboard(show: Boolean = true) { - if (show.not()) return - if (this.requestFocus()) - postDelayed({ - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) - }, 100) -} - - -@RequiresApi(Build.VERSION_CODES.Q) -fun Activity.showLauncherSelector(requestCode: Int) { - val roleManager = getSystemService(Context.ROLE_SERVICE) as RoleManager - if (roleManager.isRoleAvailable(RoleManager.ROLE_HOME)) { - val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_HOME) - startActivityForResult(intent, requestCode) - } else - resetDefaultLauncher() -} - -fun Context.resetDefaultLauncher() { - try { - val componentName = ComponentName(this, FakeHomeActivity::class.java) - packageManager.setComponentEnabledSetting( - componentName, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP - ) - val selector = Intent(Intent.ACTION_MAIN) - selector.addCategory(Intent.CATEGORY_HOME) - startActivity(selector) - packageManager.setComponentEnabledSetting( - componentName, - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, - PackageManager.DONT_KILL_APP - ) - } catch (e: Exception) { - e.printStackTrace() - } -} - -fun Context.isDefaultLauncher(): Boolean { - val launcherPackageName = getDefaultLauncherPackage(this) - return BuildConfig.APPLICATION_ID == launcherPackageName -} - -fun Context.resetLauncherViaFakeActivity() { - resetDefaultLauncher() - if (getDefaultLauncherPackage(this).contains(".")) - startActivity(Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)) -} - -fun Context.openSearch(query: String? = null) { - val intent = Intent(Intent.ACTION_WEB_SEARCH) - intent.putExtra(SearchManager.QUERY, query ?: "") - startActivity(intent) -} - -fun Context.isEinkDisplay(): Boolean { - return try { - val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager - windowManager.defaultDisplay.refreshRate <= Constants.MIN_ANIM_REFRESH_RATE - } catch (e: Exception) { - e.printStackTrace() - false - } -} - -fun Context.searchOnPlayStore(query: String? = null): Boolean { - return try { - startActivity( - Intent( - Intent.ACTION_VIEW, - Uri.parse("https://play.google.com/store/search?q=$query&c=apps") - ).addFlags( - Intent.FLAG_ACTIVITY_NO_HISTORY or - Intent.FLAG_ACTIVITY_NEW_DOCUMENT or - Intent.FLAG_ACTIVITY_MULTIPLE_TASK - ) - ) - true - } catch (e: Exception) { - e.printStackTrace() - false - } -} - -fun Context.isPackageInstalled(packageName: String, userHandle: UserHandle = android.os.Process.myUserHandle()): Boolean { - val launcher = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val activityInfo = launcher.getActivityList(packageName, userHandle) - return activityInfo.isNotEmpty() -} - -fun Context.isCountryIn(): Boolean { - val country = runCatching { - val telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager - telephonyManager?.simCountryIso?.takeIf { it.isNotBlank() } - ?: telephonyManager?.networkCountryIso?.takeIf { it.isNotBlank() } - }.getOrNull() ?: Locale.getDefault().country - return country.equals("IN", ignoreCase = true) -} - -@RequiresApi(Build.VERSION_CODES.Q) -fun Context.appUsagePermissionGranted(): Boolean { - val appOpsManager = getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager - return appOpsManager.unsafeCheckOpNoThrow( - "android:get_usage_stats", - android.os.Process.myUid(), - packageName - ) == AppOpsManager.MODE_ALLOWED -} - -fun Context.formattedTimeSpent(timeSpent: Long): String { - val seconds = timeSpent / 1000 - val minutes = seconds / 60 - val hours = minutes / 60 - val remainingMinutes = minutes % 60 - return when { - timeSpent == 0L -> "0m" - - hours > 0 -> getString( - R.string.time_spent_hour, - hours.toString(), - remainingMinutes.toString() - ) - - minutes > 0 -> { - getString(R.string.time_spent_min, minutes.toString()) - } - - else -> "<1m" - } -} - -fun Long.convertEpochToMidnight(): Long { - val calendar = Calendar.getInstance() - calendar.timeInMillis = this - calendar.set(Calendar.HOUR_OF_DAY, 0) - calendar.set(Calendar.MINUTE, 0) - calendar.set(Calendar.SECOND, 0) - calendar.set(Calendar.MILLISECOND, 0) - return calendar.timeInMillis -} - -fun Long.isDaySince(): Int = ((System.currentTimeMillis().convertEpochToMidnight() - this.convertEpochToMidnight()) - / Constants.ONE_DAY_IN_MILLIS).toInt() - -fun Long.hasBeenDays(days: Int): Boolean = - ((System.currentTimeMillis() - this) / Constants.ONE_DAY_IN_MILLIS) >= days - -fun Long.hasBeenHours(hours: Int): Boolean = - ((System.currentTimeMillis() - this) / Constants.ONE_HOUR_IN_MILLIS) >= hours - -fun Long.hasBeenMinutes(minutes: Int): Boolean = - ((System.currentTimeMillis() - this) / Constants.ONE_MINUTE_IN_MILLIS) >= minutes - -fun Int.dpToPx(): Int { - return (this * Resources.getSystem().displayMetrics.density).toInt() -} diff --git a/app/src/main/java/app/olauncher/helper/FakeHomeActivity.kt b/app/src/main/java/app/olauncher/helper/FakeHomeActivity.kt deleted file mode 100644 index 35ab546..0000000 --- a/app/src/main/java/app/olauncher/helper/FakeHomeActivity.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.olauncher.helper - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import app.olauncher.R - -class FakeHomeActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_fake_home) - } -} \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/helper/HomeNotificationListenerService.kt b/app/src/main/java/app/olauncher/helper/HomeNotificationListenerService.kt deleted file mode 100644 index 59facf8..0000000 --- a/app/src/main/java/app/olauncher/helper/HomeNotificationListenerService.kt +++ /dev/null @@ -1,27 +0,0 @@ -package app.olauncher.helper - -import android.service.notification.NotificationListenerService -import android.service.notification.StatusBarNotification - -class HomeNotificationListenerService : NotificationListenerService() { - - override fun onListenerConnected() { - super.onListenerConnected() - NotificationDotRepository.update(activeNotifications) - } - - override fun onNotificationPosted(sbn: StatusBarNotification?) { - super.onNotificationPosted(sbn) - NotificationDotRepository.update(activeNotifications) - } - - override fun onNotificationRemoved(sbn: StatusBarNotification?) { - super.onNotificationRemoved(sbn) - NotificationDotRepository.update(activeNotifications) - } - - override fun onListenerDisconnected() { - NotificationDotRepository.clear() - super.onListenerDisconnected() - } -} diff --git a/app/src/main/java/app/olauncher/helper/MyAccessibilityService.kt b/app/src/main/java/app/olauncher/helper/MyAccessibilityService.kt deleted file mode 100644 index 92e522f..0000000 --- a/app/src/main/java/app/olauncher/helper/MyAccessibilityService.kt +++ /dev/null @@ -1,45 +0,0 @@ -package app.olauncher.helper - -import android.accessibilityservice.AccessibilityService -import android.content.Intent -import android.os.Build -import android.view.accessibility.AccessibilityEvent -import android.view.accessibility.AccessibilityNodeInfo -import app.olauncher.R -import app.olauncher.data.Prefs - -class MyAccessibilityService : AccessibilityService() { - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - return START_STICKY - } - - override fun onServiceConnected() { - Prefs(applicationContext).lockModeOn = true - super.onServiceConnected() - } - - override fun onAccessibilityEvent(event: AccessibilityEvent) { - try { - val source: AccessibilityNodeInfo = event.source ?: return - if (source.className != "android.widget.FrameLayout") return - - when (source.contentDescription) { - getString(R.string.lock_layout_description) -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) - performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN) - } - // Home button for recents feature disabled - // getString(R.string.recents_layout_description) -> { - // performGlobalAction(GLOBAL_ACTION_RECENTS) - // } - } - } catch (e: Exception) { - return - } - } - - override fun onInterrupt() { - - } -} \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/helper/NotificationDotRepository.kt b/app/src/main/java/app/olauncher/helper/NotificationDotRepository.kt deleted file mode 100644 index 0040ea5..0000000 --- a/app/src/main/java/app/olauncher/helper/NotificationDotRepository.kt +++ /dev/null @@ -1,58 +0,0 @@ -package app.olauncher.helper - -import android.app.Notification -import android.service.notification.StatusBarNotification -import java.util.concurrent.CopyOnWriteArraySet - -object NotificationDotRepository { - private val listeners = CopyOnWriteArraySet<() -> Unit>() - - @Volatile - private var packagesWithNotifications: Set = emptySet() - - fun addListener(listener: () -> Unit) { - listeners += listener - } - - fun removeListener(listener: () -> Unit) { - listeners -= listener - } - - fun hasActiveNotification(packageName: String): Boolean { - if (packageName.isBlank()) return false - return packageName in packagesWithNotifications - } - - fun update(activeNotifications: Array?) { - val nextPackages = activeNotifications - ?.asSequence() - ?.filterNot(::shouldIgnore) - ?.map { it.packageName } - ?.filter { it.isNotBlank() } - ?.toSet() - ?: emptySet() - - if (nextPackages == packagesWithNotifications) return - packagesWithNotifications = nextPackages - listeners.forEach { it() } - } - - fun clear() { - if (packagesWithNotifications.isEmpty()) return - packagesWithNotifications = emptySet() - listeners.forEach { it() } - } - - private fun shouldIgnore(sbn: StatusBarNotification): Boolean { - val notification = sbn.notification - val flags = notification.flags - val category = notification.category - val isGroupSummary = flags and Notification.FLAG_GROUP_SUMMARY != 0 - val isForegroundService = flags and Notification.FLAG_FOREGROUND_SERVICE != 0 - val isOngoing = flags and Notification.FLAG_ONGOING_EVENT != 0 - val isPersistent = !sbn.isClearable - val isTransport = category == Notification.CATEGORY_TRANSPORT - val isService = category == Notification.CATEGORY_SERVICE - return isGroupSummary || isForegroundService || isOngoing || isPersistent || isTransport || isService - } -} diff --git a/app/src/main/java/app/olauncher/helper/PinItemActivity.kt b/app/src/main/java/app/olauncher/helper/PinItemActivity.kt deleted file mode 100644 index 8435ebd..0000000 --- a/app/src/main/java/app/olauncher/helper/PinItemActivity.kt +++ /dev/null @@ -1,51 +0,0 @@ -package app.olauncher.helper - -import android.content.pm.LauncherApps -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity - -class PinItemActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // Set window to be transparent - window.setBackgroundDrawable(null) - - val launcherApps = getSystemService(LauncherApps::class.java) - val pinItemRequest = launcherApps.getPinItemRequest(intent) - - when (pinItemRequest != null) { - true -> handleRequestType(pinItemRequest) - false -> showToast("Invalid pin request") - } - - finish() - } - - private fun handleRequestType(pinItemRequest: LauncherApps.PinItemRequest) { - when (pinItemRequest.requestType) { - LauncherApps.PinItemRequest.REQUEST_TYPE_SHORTCUT -> - handleShortcutRequest(pinItemRequest) - - LauncherApps.PinItemRequest.REQUEST_TYPE_APPWIDGET -> - showToast("Widgets are not supported") - - else -> showToast("Unknown action not supported") - } - } - - private fun handleShortcutRequest(pinItemRequest: LauncherApps.PinItemRequest) { - val shortcutInfo = pinItemRequest.shortcutInfo - if (shortcutInfo != null) { - val success = pinItemRequest.accept() - val message = when (success) { - true -> "Shortcut pinned successfully" - false -> "Failed to pin shortcut" - } - showToast(message) - } else { - showToast("Invalid shortcut info") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/helper/SingleLiveEvent.kt b/app/src/main/java/app/olauncher/helper/SingleLiveEvent.kt deleted file mode 100644 index de74c1f..0000000 --- a/app/src/main/java/app/olauncher/helper/SingleLiveEvent.kt +++ /dev/null @@ -1,36 +0,0 @@ -package app.olauncher.helper - -import android.util.Log -import androidx.annotation.MainThread -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer -import java.util.concurrent.atomic.AtomicBoolean - -class SingleLiveEvent : MutableLiveData() { - private val pending = AtomicBoolean(false) - - @MainThread - override fun observe(owner: LifecycleOwner, observer: Observer) { - if (hasActiveObservers()) { - Log.d("SingleLiveEvent", "Only one observer will be notified of the changes") - } - - super.observe(owner) { t -> - if (pending.compareAndSet(true, false)) { - observer.onChanged(t) - } - } - } - - @MainThread - override fun setValue(t: T?) { - pending.set(true) - super.setValue(t) - } - - @MainThread - fun call() { - value = null - } -} \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/helper/Utils.kt b/app/src/main/java/app/olauncher/helper/Utils.kt deleted file mode 100644 index 1809068..0000000 --- a/app/src/main/java/app/olauncher/helper/Utils.kt +++ /dev/null @@ -1,672 +0,0 @@ -package app.olauncher.helper - -import android.annotation.SuppressLint -import android.app.SearchManager -import android.app.WallpaperManager -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.content.pm.ApplicationInfo -import android.content.pm.LauncherApps -import android.content.res.Configuration -import android.content.res.Configuration.UI_MODE_NIGHT_YES -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.graphics.Matrix -import android.graphics.Paint -import android.graphics.Point -import android.net.Uri -import android.os.Build -import android.os.UserHandle -import android.os.UserManager -import android.provider.AlarmClock -import android.provider.CalendarContract -import android.provider.MediaStore -import android.provider.Settings -import android.util.DisplayMetrics -import android.util.Log -import android.util.TypedValue -import android.view.View -import android.view.WindowManager -import android.view.animation.LinearInterpolator -import android.widget.Toast -import androidx.annotation.AttrRes -import androidx.annotation.ColorInt -import androidx.annotation.RequiresApi -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.graphics.createBitmap -import androidx.core.net.toUri -import app.olauncher.BuildConfig -import app.olauncher.R -import app.olauncher.data.AppModel -import app.olauncher.data.Constants -import app.olauncher.data.Prefs -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.json.JSONObject -import java.io.InputStream -import java.net.HttpURLConnection -import java.net.URL -import java.text.Collator -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.Scanner -import kotlin.math.pow -import kotlin.math.sqrt - -fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) { - if (message.isNullOrBlank()) return - Toast.makeText(this, message, duration).show() -} - -fun Context.showToast(stringResource: Int, duration: Int = Toast.LENGTH_SHORT) { - Toast.makeText(this, getString(stringResource), duration).show() -} - -suspend fun getAppsList( - context: Context, - prefs: Prefs, - includeRegularApps: Boolean = true, - includeHiddenApps: Boolean = false, -): MutableList { - return withContext(Dispatchers.IO) { - val appList: MutableList = mutableListOf() - - try { - if (!Prefs(context).hiddenAppsUpdated) upgradeHiddenApps(Prefs(context)) - val hiddenApps = Prefs(context).hiddenApps - - val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager - val launcherApps = - context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val collator = Collator.getInstance() - - for (profile in userManager.userProfiles) { - if (isPrivateSpaceProfile(context, profile)) continue - for (app in launcherApps.getActivityList(null, profile)) { - val appLabelShown = prefs.getAppRenameLabel(app.applicationInfo.packageName) - .ifBlank { app.label.toString() } - val appModel = AppModel.App( - appLabel = appLabelShown, - key = collator.getCollationKey(app.label.toString()), - appPackage = app.applicationInfo.packageName, - activityClassName = app.componentName.className, - isNew = (System.currentTimeMillis() - app.firstInstallTime) < Constants.ONE_HOUR_IN_MILLIS, - user = profile - ) - - // if the current app is not OLauncher - if (app.applicationInfo.packageName != BuildConfig.APPLICATION_ID) { - // is this a hidden app? - if (hiddenApps.contains(app.applicationInfo.packageName + "|" + profile.toString())) { - if (includeHiddenApps) { - appList.add(appModel) - } - } else { - // this is a regular app - if (includeRegularApps) { - appList.add(appModel) - } - } - } - } - } - - // Add shortcuts if we're getting regular apps - if (includeRegularApps && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val pinned = try { - getPinnedShortcuts(context, prefs, collator) - } catch (e: Exception) { - emptyList() - } - appList.addAll(pinned) - } - - appList.sortWith(compareBy(collator) { it.appLabel }) - } catch (e: Exception) { - e.printStackTrace() - } - appList - } -} - -@RequiresApi(Build.VERSION_CODES.O) -private suspend fun getPinnedShortcuts( - context: Context, - prefs: Prefs, - collator: Collator, -): List = - withContext(Dispatchers.IO) { - val pinnedShortcuts = mutableListOf() - val shortcuts = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as? LauncherApps - if (shortcuts?.hasShortcutHostPermission() == true) { - val query = LauncherApps.ShortcutQuery().apply { - setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED) - } - shortcuts.profiles.forEach { profile -> - if (isPrivateSpaceProfile(context, profile)) return@forEach - try { - shortcuts.getShortcuts(query, profile)?.forEach { shortcut -> - if (shortcut.isPinned && pinnedShortcuts.none { it.shortcutId == shortcut.id }) { - val label = prefs.getAppRenameLabel(shortcut.id) - .takeIf { it.isNotBlank() } - ?: shortcut.shortLabel?.toString() - ?: shortcut.longLabel?.toString().orEmpty() - pinnedShortcuts.add( - AppModel.PinnedShortcut( - appLabel = label, - key = collator.getCollationKey(label), - appPackage = shortcut.`package`, - shortcutId = shortcut.id, - isNew = false, - user = profile - ) - ) - } - } - } catch (e: Exception) { - e.printStackTrace() - } - } - } - pinnedShortcuts - } - -// This is to ensure backward compatibility with older app versions -// which did not support multiple user profiles -private fun upgradeHiddenApps(prefs: Prefs) { - val hiddenAppsSet = prefs.hiddenApps - val newHiddenAppsSet = mutableSetOf() - for (hiddenPackage in hiddenAppsSet) { - if (hiddenPackage.contains("|")) newHiddenAppsSet.add(hiddenPackage) - else newHiddenAppsSet.add(hiddenPackage + android.os.Process.myUserHandle().toString()) - } - prefs.hiddenApps = newHiddenAppsSet - prefs.hiddenAppsUpdated = true -} - -fun isPackageInstalled(context: Context, packageName: String, userString: String): Boolean { - val launcher = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val activityInfo = launcher.getActivityList(packageName, getUserHandleFromString(context, userString)) - if (activityInfo.isNotEmpty()) return true - return false -} - -fun isPrivateSpaceProfile(context: Context, userHandle: UserHandle): Boolean { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) return false - return try { - val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - launcherApps.getLauncherUserInfo(userHandle)?.userType == "android.os.usertype.profile.PRIVATE" - } catch (e: Exception) { - false - } -} - -fun isPrivateSpaceLocked(context: Context, userHandle: UserHandle): Boolean { - return try { - val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager - userManager.isQuietModeEnabled(userHandle) - } catch (e: Exception) { - true - } -} - -fun getPrivateSpaceUserHandle(context: Context): UserHandle? { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) return null - val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager - for (profile in userManager.userProfiles) { - if (isPrivateSpaceProfile(context, profile)) return profile - } - return null -} - -suspend fun getPrivateSpaceApps( - context: Context, - prefs: Prefs, -): MutableList { - return withContext(Dispatchers.IO) { - val appList: MutableList = mutableListOf() - try { - val privateSpaceHandle = getPrivateSpaceUserHandle(context) ?: return@withContext appList - val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val collator = Collator.getInstance() - - for (app in launcherApps.getActivityList(null, privateSpaceHandle)) { - if (app.applicationInfo.packageName == BuildConfig.APPLICATION_ID) continue - val appLabelShown = prefs.getAppRenameLabel(app.applicationInfo.packageName) - .ifBlank { app.label.toString() } - appList.add( - AppModel.App( - appLabel = appLabelShown, - key = collator.getCollationKey(app.label.toString()), - appPackage = app.applicationInfo.packageName, - activityClassName = app.componentName.className, - isNew = false, - user = privateSpaceHandle - ) - ) - } - appList.sortWith(compareBy(collator) { it.appLabel }) - } catch (e: Exception) { - e.printStackTrace() - } - appList - } -} - -fun getUserHandleFromString(context: Context, userHandleString: String): UserHandle { - val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager - for (userHandle in userManager.userProfiles) { - if (userHandle.toString() == userHandleString) { - return userHandle - } - } - return android.os.Process.myUserHandle() -} - -fun isOlauncherDefault(context: Context): Boolean { - val launcherPackageName = getDefaultLauncherPackage(context) - return BuildConfig.APPLICATION_ID == launcherPackageName -} - -fun getDefaultLauncherPackage(context: Context): String { - val intent = Intent() - intent.action = Intent.ACTION_MAIN - intent.addCategory(Intent.CATEGORY_HOME) - val packageManager = context.packageManager - val result = packageManager.resolveActivity(intent, 0) - return if (result?.activityInfo != null) { - result.activityInfo.packageName - } else "android" -} - -fun setPlainWallpaperByTheme(context: Context, appTheme: Int) { - when (appTheme) { - AppCompatDelegate.MODE_NIGHT_YES -> setPlainWallpaper(context, android.R.color.black) - AppCompatDelegate.MODE_NIGHT_NO -> setPlainWallpaper(context, android.R.color.white) - else -> { - if (context.isDarkThemeOn()) - setPlainWallpaper(context, android.R.color.black) - else setPlainWallpaper(context, android.R.color.white) - } - } -} - -fun setPlainWallpaper(context: Context, color: Int) { - try { - val bitmap = createBitmap(1000, 2000) - bitmap.eraseColor(context.getColor(color)) - val manager = WallpaperManager.getInstance(context) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - manager.setBitmap(bitmap, null, false, WallpaperManager.FLAG_SYSTEM) - manager.setBitmap(bitmap, null, false, WallpaperManager.FLAG_LOCK) - } else - manager.setBitmap(bitmap) - bitmap.recycle() - } catch (e: Exception) { - e.printStackTrace() - } -} - -fun getChangedAppTheme(context: Context, currentAppTheme: Int): Int { - return when (currentAppTheme) { - AppCompatDelegate.MODE_NIGHT_YES -> AppCompatDelegate.MODE_NIGHT_NO - AppCompatDelegate.MODE_NIGHT_NO -> AppCompatDelegate.MODE_NIGHT_YES - else -> { - if (context.isDarkThemeOn()) - AppCompatDelegate.MODE_NIGHT_NO - else AppCompatDelegate.MODE_NIGHT_YES - } - } -} - -fun openAppInfo(context: Context, userHandle: UserHandle, packageName: String) { - val launcher = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val component = launcher.getActivityList(packageName, userHandle).firstOrNull()?.componentName - if (component != null) - launcher.startAppDetailsActivity(component, userHandle, null, null) - else - context.showToast(context.getString(R.string.unable_to_open_app_info)) -} - -suspend fun getBitmapFromURL(src: String?): Bitmap? { - return withContext(Dispatchers.IO) { - var bitmap: Bitmap? = null - try { - val url = URL(src) - val connection: HttpURLConnection = url - .openConnection() as HttpURLConnection - connection.doInput = true - connection.connect() - val input: InputStream = connection.inputStream - bitmap = BitmapFactory.decodeStream(input) - } catch (e: Exception) { - e.printStackTrace() - } - bitmap - } -} - -suspend fun getWallpaperBitmap(originalImage: Bitmap, width: Int, height: Int): Bitmap { - return withContext(Dispatchers.IO) { - - val background = createBitmap(width, height) - - val originalWidth: Float = originalImage.width.toFloat() - val originalHeight: Float = originalImage.height.toFloat() - - val canvas = Canvas(background) - val heightScale: Float = height / originalHeight - val widthScale: Float = width / originalWidth - val scale = maxOf(heightScale, widthScale) - - val (xTranslation, yTranslation) = if (heightScale > widthScale) - Pair((width - originalWidth * heightScale) / 2.0f, 0f) - else - Pair(0f, (height - originalHeight * widthScale) / 2.0f) - - val transformation = Matrix() - transformation.postTranslate(xTranslation, yTranslation) - transformation.preScale(scale, scale) - - val paint = Paint() - paint.isFilterBitmap = true - canvas.drawBitmap(originalImage, transformation, paint) - - background - } -} - -suspend fun setWallpaper(appContext: Context, url: String): Boolean { - return withContext(Dispatchers.IO) { - val originalImageBitmap = getBitmapFromURL(url) ?: return@withContext false - if (appContext.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && isTablet(appContext).not()) - return@withContext false - - val wallpaperManager = WallpaperManager.getInstance(appContext) - val (width, height) = getScreenDimensions(appContext) - val scaledBitmap = getWallpaperBitmap(originalImageBitmap, width, height) - - try { - wallpaperManager.setBitmap(scaledBitmap, null, false, WallpaperManager.FLAG_SYSTEM) - wallpaperManager.setBitmap(scaledBitmap, null, false, WallpaperManager.FLAG_LOCK) - } catch (e: Exception) { - return@withContext false - } - - try { - originalImageBitmap.recycle() - scaledBitmap.recycle() - } catch (e: Exception) { - e.printStackTrace() - } - true - } -} - -fun getScreenDimensions(context: Context): Pair { - val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - val point = Point() - windowManager.defaultDisplay.getRealSize(point) - return Pair(point.x, point.y) -} - -suspend fun getTodaysWallpaper(wallType: String, firstOpenTime: Long): String { - return withContext(Dispatchers.IO) { - var wallpaperUrl: String - try { - val key = if (firstOpenTime.isDaySince() < 10) - String.format("0_%s", firstOpenTime.isDaySince().toString()) - else { - val month = SimpleDateFormat("M", Locale.ENGLISH).format(Date()) ?: "0" - val day = SimpleDateFormat("d", Locale.ENGLISH).format(Date()) ?: "0" - String.format("%s_%s", month, day) - } - - val url = URL(Constants.URL_WALLPAPERS) - val connection: HttpURLConnection = url.openConnection() as HttpURLConnection - connection.doInput = true - connection.connect() - - val inputStream = connection.inputStream - val scanner = Scanner(inputStream) - val stringBuffer = StringBuffer() - while (scanner.hasNext()) { - stringBuffer.append(scanner.nextLine()) - } - - val json = JSONObject(stringBuffer.toString()) - val wallpapers = json.getString(key) - val wallpapersJson = JSONObject(wallpapers) - wallpaperUrl = wallpapersJson.getString(wallType) - wallpaperUrl - - } catch (e: Exception) { - wallpaperUrl = getBackupWallpaper(wallType) - wallpaperUrl - } - } -} - -fun getBackupWallpaper(wallType: String): String { - return if (wallType == Constants.WALL_TYPE_LIGHT) - Constants.URL_DEFAULT_LIGHT_WALLPAPER - else Constants.URL_DEFAULT_DARK_WALLPAPER -} - -fun openSearch(context: Context) { - val intent = Intent(Intent.ACTION_WEB_SEARCH) - intent.putExtra(SearchManager.QUERY, "") - context.startActivity(intent) -} - -@SuppressLint("WrongConstant", "PrivateApi") -fun expandNotificationDrawer(context: Context) { - // Source: https://stackoverflow.com/a/51132142 - try { - val statusBarService = context.getSystemService("statusbar") - val statusBarManager = Class.forName("android.app.StatusBarManager") - val method = statusBarManager.getMethod("expandNotificationsPanel") - method.invoke(statusBarService) - } catch (e: Exception) { - e.printStackTrace() - } -} - -fun openDialerApp(context: Context) { - try { - val sendIntent = Intent(Intent.ACTION_DIAL) - context.startActivity(sendIntent) - } catch (e: Exception) { - e.printStackTrace() - } -} - -fun openCameraApp(context: Context) { - try { - val sendIntent = Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA) - context.startActivity(sendIntent) - } catch (e: Exception) { - e.printStackTrace() - } -} - -fun openAlarmApp(context: Context) { - try { - val intent = Intent(AlarmClock.ACTION_SHOW_ALARMS) - context.startActivity(intent) - } catch (e: Exception) { - Log.d("TAG", e.toString()) - } -} - -fun openCalendar(context: Context) { - try { - val calendarUri = CalendarContract.CONTENT_URI - .buildUpon() - .appendPath("time") - .build() - context.startActivity(Intent(Intent.ACTION_VIEW, calendarUri)) - } catch (e: Exception) { - try { - val intent = Intent(Intent.ACTION_MAIN) - intent.addCategory(Intent.CATEGORY_APP_CALENDAR) - context.startActivity(intent) - } catch (e: Exception) { - e.printStackTrace() - } - } -} - -fun isAccessServiceEnabled(context: Context): Boolean { - val enabled = try { - Settings.Secure.getInt(context.applicationContext.contentResolver, Settings.Secure.ACCESSIBILITY_ENABLED) - } catch (e: Exception) { - 0 - } - if (enabled == 1) { - val enabledServicesString: String? = Settings.Secure.getString(context.contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES) - return enabledServicesString?.contains(context.packageName + "/" + MyAccessibilityService::class.java.name) ?: false - } - return false -} - -fun isTablet(context: Context): Boolean { - val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - val metrics = DisplayMetrics() - windowManager.defaultDisplay.getMetrics(metrics) - val widthInches = metrics.widthPixels / metrics.xdpi - val heightInches = metrics.heightPixels / metrics.ydpi - val diagonalInches = sqrt(widthInches.toDouble().pow(2.0) + heightInches.toDouble().pow(2.0)) - if (diagonalInches >= 7.0) return true - return false -} - -fun Context.isDarkThemeOn(): Boolean { - return resources.configuration.uiMode and - Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES -} - -fun Context.copyToClipboard(text: String) { - val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clipData = ClipData.newPlainText(getString(R.string.app_name), text) - clipboardManager.setPrimaryClip(clipData) - showToast("") -} - -fun Context.openUrl(url: String) { - if (url.isEmpty()) return - val intent = Intent(Intent.ACTION_VIEW) - intent.data = Uri.parse(url) - startActivity(intent) -} - -fun Context.isSystemApp(packageName: String, user: UserHandle? = null): Boolean { - if (packageName.isBlank()) return true - return try { - val launcherApps = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val targetUser = user ?: android.os.Process.myUserHandle() - val activityList = launcherApps.getActivityList(packageName, targetUser) - if (activityList.isNotEmpty()) { - val applicationInfo = activityList.first().applicationInfo - ((applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) - || (applicationInfo.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP != 0)) - } else { - val applicationInfo = packageManager.getApplicationInfo(packageName, 0) - ((applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) - || (applicationInfo.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP != 0)) - } - } catch (e: Exception) { - e.printStackTrace() - false - } -} - -fun Context.uninstall(packageName: String) { - val intent = Intent(Intent.ACTION_DELETE) - intent.data = Uri.parse("package:$packageName") - startActivity(intent) -} - -@ColorInt -fun Context.getColorFromAttr( - @AttrRes attrColor: Int, - typedValue: TypedValue = TypedValue(), - resolveRefs: Boolean = true, -): Int { - theme.resolveAttribute(attrColor, typedValue, resolveRefs) - return typedValue.data -} - -fun View.animateAlpha(alpha: Float = 1.0f) { - this.animate().apply { - interpolator = LinearInterpolator() - duration = 200 - alpha(alpha) - start() - } -} - -fun Context.shareApp() { - val message = getString(R.string.are_you_using_your_phone_or_is_your_phone_using_you) + - "\n" + Constants.URL_OLAUNCHER_PLAY_STORE - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, message) - type = "text/plain" - } - - val shareIntent = Intent.createChooser(sendIntent, null) - startActivity(shareIntent) -} - -fun Context.rateApp() { - val intent = Intent( - Intent.ACTION_VIEW, - Constants.URL_OLAUNCHER_PLAY_STORE.toUri() - ) - var flags = Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_MULTIPLE_TASK - flags = flags or Intent.FLAG_ACTIVITY_NEW_DOCUMENT - intent.addFlags(flags) - startActivity(intent) -} - -@RequiresApi(Build.VERSION_CODES.N_MR1) -fun Context.deletePinnedShortcut(packageName: String, shortcutIdToDelete: String, user: UserHandle) { - val launcherApps = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - - // 1. Query for existing pinned shortcuts for the package - val query = LauncherApps.ShortcutQuery().apply { - setPackage(packageName) - // Query only for pinned shortcuts - setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED) - } - - try { - val pinnedShortcuts = launcherApps.getShortcuts(query, user) - - if (pinnedShortcuts != null) { - // 2. Filter out the shortcut to be deleted - val updatedPinnedIds = pinnedShortcuts - .filter { it.id != shortcutIdToDelete } - .map { it.id } - - // 3. Re-pin the remaining shortcuts - // This replaces the existing set of pinned shortcuts for this package - launcherApps.pinShortcuts(packageName, updatedPinnedIds, user) - } - } catch (e: SecurityException) { - // Handle cases where the app doesn't have permission - // (e.g., not the default launcher or active voice interaction service) - Log.e("ShortcutHelper", "Permission denied to modify pinned shortcuts for $packageName", e) - } catch (e: IllegalStateException) { - // Handle cases where the user profile is locked or not running - Log.e("ShortcutHelper", "User profile unavailable for modifying pinned shortcuts for $packageName", e) - } catch (e: Exception) { - // Handle other potential exceptions (like RemoteException wrapped) - Log.e("ShortcutHelper", "Failed to modify pinned shortcuts for $packageName", e) - } -} diff --git a/app/src/main/java/app/olauncher/helper/WallpaperWorker.kt b/app/src/main/java/app/olauncher/helper/WallpaperWorker.kt deleted file mode 100644 index 9ca71c1..0000000 --- a/app/src/main/java/app/olauncher/helper/WallpaperWorker.kt +++ /dev/null @@ -1,47 +0,0 @@ -package app.olauncher.helper - -import android.content.Context -import androidx.appcompat.app.AppCompatDelegate -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters -import app.olauncher.data.Constants -import app.olauncher.data.Prefs -import kotlinx.coroutines.coroutineScope - -class WallpaperWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { - - private val prefs = Prefs(applicationContext) - - override suspend fun doWork(): Result = coroutineScope { - val success = - if (prefs.appTheme == AppCompatDelegate.MODE_NIGHT_YES && isOlauncherDefault(applicationContext).not()) - true - else if (prefs.dailyWallpaper) { - val wallType = checkWallpaperType() - val wallpaperUrl = getTodaysWallpaper(wallType, prefs.firstOpenTime) - if (prefs.dailyWallpaperUrl == wallpaperUrl) - true - else { - prefs.dailyWallpaperUrl = wallpaperUrl - setWallpaper(applicationContext, wallpaperUrl) - } - } else - true - - if (success) - Result.success() - else - Result.retry() - } - - private fun checkWallpaperType(): String { - return when (prefs.appTheme) { - AppCompatDelegate.MODE_NIGHT_YES -> Constants.WALL_TYPE_DARK - AppCompatDelegate.MODE_NIGHT_NO -> Constants.WALL_TYPE_LIGHT - else -> if (applicationContext.isDarkThemeOn()) - Constants.WALL_TYPE_DARK - else - Constants.WALL_TYPE_LIGHT - } - } -} diff --git a/app/src/main/java/app/olauncher/helper/usageStats/ComponentForegroundStat.kt b/app/src/main/java/app/olauncher/helper/usageStats/ComponentForegroundStat.kt deleted file mode 100644 index ca7d033..0000000 --- a/app/src/main/java/app/olauncher/helper/usageStats/ComponentForegroundStat.kt +++ /dev/null @@ -1,28 +0,0 @@ -package app.olauncher.helper.usageStats - -import java.util.Date - -/** - * Object representing a timespan that an application was in the foreground. - * - * @property beginTime The start of the timespan, in milliseconds since the epoch. - * @property endTime The end of the timespan, in milliseconds since the epoch. - * @property packageName The package name of the application. - */ -data class ComponentForegroundStat( - val beginTime: Long, - val endTime: Long, - val packageName: String -) { - /** - * Overriding toString for a more readable log output, similar to the original Java class. - * The default data class toString() would also work, but this one is more explicit - * about formatting the timestamps as Instants. - */ - override fun toString(): String { - return "ComponentForegroundStat(" + - "beginTime=${Date(beginTime)}, " + - "endTime=${Date(endTime)}, " + - "packageName='$packageName')" - } -} diff --git a/app/src/main/java/app/olauncher/helper/usageStats/EventLogWrapper.kt b/app/src/main/java/app/olauncher/helper/usageStats/EventLogWrapper.kt deleted file mode 100644 index 15eccfa..0000000 --- a/app/src/main/java/app/olauncher/helper/usageStats/EventLogWrapper.kt +++ /dev/null @@ -1,391 +0,0 @@ -package app.olauncher.helper.usageStats - -import android.app.ActivityManager -import android.app.usage.UsageEvents -import android.app.usage.UsageStatsManager -import android.content.Context -import android.icu.util.Calendar -import android.util.Log -import java.util.concurrent.TimeUnit -import java.util.function.BiConsumer -import kotlin.math.max -import kotlin.math.min - -class EventLogWrapper(private val context: Context) { - private val usageStatsManager by lazy { context.getSystemService("usagestats") as UsageStatsManager } - private val guardian = UnmatchedCloseEventGuardian(usageStatsManager) - - - /** - * Collects event information from system to calculate and aggregate precise - * foreground time statistics for the specified period. - * - * Comments refer to the cases from - * [the documentation.](https://codeberg.org/fynngodau/usageDirect/wiki/Event-log-wrapper-scenarios) - * - * @param start First point in time to include in results - * @param end Last point in time to include in results - * @return A list of foreground stats for the specified period - */ - fun getForegroundStatsByTimestamps(start: Long, end: Long): List { - var queryStart = start // Can be mutated by DEVICE_STARTUP event - - /* - * Because sometimes, open events do not have close events when they should, as a hack / workaround, - * we query the apps currently in the foreground and match them against the apps that are currently - * in the foreground if the query start date is very recent or in the future. Thus, we are using this - * to tell apart True from Faulty unmatched open events. - * - * We query processes in the beginning of this method call in case querying the event log takes a - * little longer. - */ - val foregroundProcesses = mutableListOf() - if (end >= System.currentTimeMillis() - 1500) { - val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager - activityManager?.runningAppProcesses?.forEach { appProcess -> - if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND || - appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE - ) { - if (context.packageName != appProcess.processName) { - foregroundProcesses.add(appProcess.processName) - } - } - } - } - - // Assumption: events are ordered chronologically - val events = usageStatsManager.queryEvents(queryStart, end) - - /* …except that sometimes, the events that are close to each other are swapped in a way that - * breaks the assumption that all end times which do not have a matching start time have - * started before start. We handle those as Duplicate close event and Duplicate open event. - * Therefore, we keep null entries in our moveToForegroundMap instead of removing the entries - * to prevent apps that had been opened previously in a period from being counted as "opened - * before start" (as they are not a True unmatched close event). - */ - // Map components to the last moveToForeground event. Null value means it was closed. - val moveToForegroundMap = mutableMapOf() - val componentForegroundStats = mutableListOf() - - // Iterate over events - val event = UsageEvents.Event() - - while (events.hasNextEvent()) { - events.getNextEvent(event) - if (context.packageName == event.packageName) continue - - val appClass = AppClass(event.packageName, event.className) - - when (event.eventType) { - /* - * "An event type denoting that an android.app.Activity moved to the foreground." - * (old definition: "An event type denoting that a component moved to the foreground.") - */ - UsageEvents.Event.ACTIVITY_RESUMED, - /* - * public static final int android.app.usage.UsageEvents.Event.CONTINUE_PREVIOUS_DAY = 4; - * (annotated as @hide) - * "An event type denoting that a component was in the foreground the previous day. - * This is effectively treated as a MOVE_TO_FOREGROUND." - */ - 4 -> { - // Store open timestamp in map, overwriting earlier timestamps in case of Duplicate open event - moveToForegroundMap[appClass] = event.timeStamp - } - - /* - * "An event type denoting that an android.app.Activity moved to the background." - * (old definition: "An event type denoting that a component moved to the background.") - */ - UsageEvents.Event.ACTIVITY_PAUSED, - /* - * "An activity becomes invisible on the UI, corresponding to Activity.onStop() - * of the activity's lifecycle." - */ - UsageEvents.Event.ACTIVITY_STOPPED, - /* - * public static final int android.app.usage.UsageEvents.Event.END_OF_DAY = 3; - * (annotated as @hide) - * "An event type denoting that a component was in the foreground when the stats - * rolled-over. This is effectively treated as a {@link #MOVE_TO_BACKGROUND}." - */ - 3 -> { - val eventBeginTime: Long? = moveToForegroundMap[appClass]?.also { - // Open and close events in order. Mark as closed. - moveToForegroundMap[appClass] = null - } ?: if ( - // App has not been in this query yet (test for Duplicate close event) - moveToForegroundMap.keys.none { it.packageName == event.packageName } && - // Test if this unmatched close event is True by asking the Guardian to scan for it - guardian.test(event, queryStart) - ) { - // Identified as True unmatched close event. Take start as a starting timestamp. - queryStart - } else { - null // Ignore Faulty unmatched close event - } - - if (eventBeginTime != null) { - // Check if another of the app's components have moved to the foreground in the meantime - val endTime = moveToForegroundMap.entries - .filter { (key, value) -> key.packageName == event.packageName && value != null } - .mapNotNull { it.value } - .minOrNull() ?: event.timeStamp - - componentForegroundStats.add( - ComponentForegroundStat(eventBeginTime, endTime, event.packageName) - ) - } - } - - /* - * "An event type denoting that the Android runtime underwent a shutdown process..." - */ - UsageEvents.Event.DEVICE_SHUTDOWN -> { - // Per docs: iterate over remaining start events and treat them as closed - moveToForegroundMap.forEach { (key, value) -> - if (value != null) { // If it's a remaining start event - componentForegroundStats.add( - ComponentForegroundStat(value, event.timeStamp, key.packageName) - ) - // Set entire app to closed - moveToForegroundMap.keys - .filter { it.packageName == key.packageName } - .forEach { samePackageKey -> moveToForegroundMap[samePackageKey] = null } - } - } - } - - /* - * "An event type denoting that the Android runtime started up..." - */ - UsageEvents.Event.DEVICE_STARTUP -> { - // Per docs: remove pending open events - moveToForegroundMap.clear() - - /* No package could be open longer than a reboot. Thus, we set the `start` - * timestamp to the boot event's timestamp in case we later assume that a - * package has been open "since the start of the period". It is not logical - * that this would happen but we can never know with this API. - */ - queryStart = event.timeStamp - } - } - } - - // Iterate over remaining start events - moveToForegroundMap.forEach { (key, value) -> - if (value != null) { // If it's a remaining start event - // Test if it's a foreground app (True unmatched open event) - if (foregroundProcesses.any { it.contains(key.packageName) }) { - componentForegroundStats.add( - ComponentForegroundStat( - value, - min(System.currentTimeMillis(), end), - key.packageName - ) - ) - } - // If app is not in foreground, drop event (Assume Faulty unmatched open event) - } - } - - /* If nothing happened during the timespan but there is an app in the foreground, - * then this app was used the whole period time and there was No event for it. - * Because the foreground applications API call is documented as not to be used - * for purposes like this, we first query whether the process name is a valid - * package name and if not, we drop it. - */ - if (moveToForegroundMap.isEmpty()) { - val packageManager = context.packageManager - foregroundProcesses.forEach { foregroundProcess -> - if (packageManager.getLaunchIntentForPackage(foregroundProcess) != null) { - componentForegroundStats.add( - ComponentForegroundStat( - queryStart, - min(System.currentTimeMillis(), end), - foregroundProcess - ) - ) - Log.d("EventLogWrapper", "Assuming that application $foregroundProcess has been used the whole query time") - } - } - } - - return componentForegroundStats - } - - /** - * Collects event information from system to calculate and aggregate precise - * foreground time statistics for the specified relative day. - * - * @param offset Day to query back in time relative to today - */ - fun getForegroundStatsByRelativeDay(offset: Int): List { - val cal = Calendar.getInstance() - cal.add(Calendar.DAY_OF_YEAR, -offset) - // Set to start of the day - cal.set(Calendar.HOUR_OF_DAY, 0) - cal.set(Calendar.MINUTE, 0) - cal.set(Calendar.SECOND, 0) - cal.set(Calendar.MILLISECOND, 0) - val beginTime = cal.timeInMillis - - // Set to start of the next day - cal.add(Calendar.DAY_OF_YEAR, 1) - val endTime = cal.timeInMillis - - return getForegroundStatsByTimestamps(beginTime, endTime) - } - - /** - * Collects event information from system to calculate and aggregate precise - * foreground time statistics starting at `start` and ending at - * the end of the day that contains `start`. - * - * @param start Starting time of query and point in time in day to query - */ - fun getForegroundStatsByPartialDay(start: Long): List { - val cal = Calendar.getInstance() - cal.timeInMillis = start - // Set to start of the next day - cal.add(Calendar.DAY_OF_YEAR, 1) - cal.set(Calendar.HOUR_OF_DAY, 0) - cal.set(Calendar.MINUTE, 0) - cal.set(Calendar.SECOND, 0) - cal.set(Calendar.MILLISECOND, 0) - val endTime = cal.timeInMillis - return getForegroundStatsByTimestamps(start, endTime) - } - - /** - * Takes a list of foreground stats and aggregates them to usage stats. - * Assumes all provided usage stats to be on the same day. - * - * @param endConsumer Consumer that accepts ending times of component - * foreground stats with their package name - */ - @JvmOverloads - fun aggregateForegroundStats( - foregroundStats: List, - endConsumer: BiConsumer? = null - ): List { - if (foregroundStats.isEmpty()) return emptyList() - - // Group by package name, merge overlapping intervals, then sum durations - val applicationTotalTime = foregroundStats - .groupBy { it.packageName } - .mapValues { (_, stats) -> - mergeOverlappingIntervals(stats).sumOf { it.endTime - it.beginTime } - } - - val firstBeginTime = foregroundStats.first().beginTime - val timeZoneOffset = Calendar.getInstance().timeZone.getOffset(firstBeginTime) - val day = TimeUnit.MILLISECONDS.toDays(firstBeginTime + timeZoneOffset) - - // Optionally consume end times - endConsumer?.let { consumer -> - foregroundStats.forEach { consumer.accept(it.packageName, it.endTime) } - } - - // Map the aggregated times to SimpleUsageStat objects - return applicationTotalTime.map { (packageName, totalTime) -> - SimpleUsageStat(day, totalTime, packageName) - } - } - - /** - * Collects all event information from system to calculate and aggregate precise - * foreground time statistics for the provided day and presents this information as - * [SimpleUsageStat]s. - * - * @param day Day since epoch - */ - private fun getForegroundStatsByDay(day: Long): List { - val cal = Calendar.getInstance() - val timeZoneOffset = cal.timeZone.getOffset(System.currentTimeMillis()) - - // To convert epoch day to milliseconds, we first get the UTC millis and then adjust for timezone - val start = TimeUnit.DAYS.toMillis(day) - timeZoneOffset - val end = TimeUnit.DAYS.toMillis(day + 1) - timeZoneOffset - return getForegroundStatsByTimestamps(start, end) - } - - /** - * Collects all event information from system to calculate and aggregate precise - * foreground time statistics and presents this information as [SimpleUsageStat]s. - * **This method call causes lag** if called with a low since value. - * - * @param daySince Return data from this day on - * @param endConsumer Consumer that accepts ending times of component - * foreground stats with their package name - */ - fun getAllSimpleUsageStats(daySince: Long, endConsumer: BiConsumer): List { - val usageStats = mutableListOf() - var currentDay = daySince - - val timeZoneOffset = Calendar.getInstance().timeZone.getOffset(System.currentTimeMillis()) - val today = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() + timeZoneOffset) - - // Maximum event log size - currentDay = max(today - 10, currentDay) - - while (currentDay <= today) { - val foregroundStats = getForegroundStatsByDay(currentDay) - usageStats.addAll(aggregateForegroundStats(foregroundStats, endConsumer)) - currentDay++ - } - - return usageStats - } - - /** - * Returns only usage statistics that have not been counted yet for - * only the day that contains `timestamp` - * - * @param endConsumer Consumer that accepts ending times of component - * foreground stats with their package name - */ - fun getIncrementalSimpleUsageStats(timestamp: Long, endConsumer: BiConsumer): List { - val foregroundStats = getForegroundStatsByPartialDay(timestamp) - return aggregateForegroundStats(foregroundStats, endConsumer) - } - - fun aggregateSimpleUsageStats(usageStats: List): Long { - return usageStats.sumOf { it.timeUsed } - } - /** - * Merges overlapping or adjacent time intervals to prevent double-counting - * when multiple components of the same package have overlapping foreground times. - */ - private fun mergeOverlappingIntervals( - stats: List - ): List { - if (stats.size <= 1) return stats - - val sorted = stats.sortedBy { it.beginTime } - val merged = mutableListOf() - var current = sorted[0] - - for (i in 1 until sorted.size) { - val next = sorted[i] - if (next.beginTime <= current.endTime) { - current = ComponentForegroundStat( - current.beginTime, - max(current.endTime, next.endTime), - current.packageName - ) - } else { - merged.add(current) - current = next - } - } - merged.add(current) - return merged - } - - /** - * Stores a class name and its corresponding package. - */ - private data class AppClass(val packageName: String, val className: String?) -} \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/helper/usageStats/SimpleUsageStat.kt b/app/src/main/java/app/olauncher/helper/usageStats/SimpleUsageStat.kt deleted file mode 100644 index 93bac4f..0000000 --- a/app/src/main/java/app/olauncher/helper/usageStats/SimpleUsageStat.kt +++ /dev/null @@ -1,48 +0,0 @@ -package app.olauncher.helper.usageStats - -import android.app.usage.UsageStats -import android.icu.util.Calendar -import java.util.concurrent.TimeUnit - -/** - * A data class to hold simplified usage statistics. - * - * @property day The day since epoch that this object concerns. - * @property timeUsed The time that the application has been in the foreground in milliseconds on this day. - * @property applicationId The package name of the application that this object concerns. - */ -data class SimpleUsageStat( - val day: Long, - val timeUsed: Long, - val applicationId: String -) { - /** - * Secondary constructor to create a SimpleUsageStat from the system's [UsageStats]. - */ - constructor(systemUsageStat: UsageStats) : this( - day = getEpochDay(systemUsageStat.lastTimeUsed), - timeUsed = systemUsageStat.totalTimeInForeground, - applicationId = systemUsageStat.packageName - ) - - companion object { - /** - * Converts a list of system [UsageStats] to a list of [SimpleUsageStat]. - * This function is kept for direct compatibility with the original Java static method. - */ - @JvmStatic - fun asSimpleStats(usageStats: List): List { - return usageStats.map { SimpleUsageStat(it) } - } - - /** - * Calculates the epoch day from a timestamp in milliseconds, compatible with API 24+. - * It manually calculates the day by using integer division on the milliseconds. - */ - private fun getEpochDay(lastTimeUsed: Long): Long { - val timeZoneOffset = Calendar.getInstance().timeZone.getOffset(lastTimeUsed) - return TimeUnit.MILLISECONDS.toDays(lastTimeUsed + timeZoneOffset) - } - } -} - diff --git a/app/src/main/java/app/olauncher/helper/usageStats/UnmatchedCloseEventGuardian.kt b/app/src/main/java/app/olauncher/helper/usageStats/UnmatchedCloseEventGuardian.kt deleted file mode 100644 index 9d550e2..0000000 --- a/app/src/main/java/app/olauncher/helper/usageStats/UnmatchedCloseEventGuardian.kt +++ /dev/null @@ -1,65 +0,0 @@ -package app.olauncher.helper.usageStats - -import android.app.usage.UsageEvents -import android.app.usage.UsageStatsManager -import android.util.Log - -/** - * “…a diminutive Guardian who traveled backward through time…” - * - * Guards [EventLogWrapper] against Faulty unmatched close events (per - * [the documentation](https://codeberg.org/fynngodau/usageDirect/wiki/Event-log-wrapper-scenarios)) - * by seeking backwards through time and scanning for the open event. - */ -class UnmatchedCloseEventGuardian(private val usageStatsManager: UsageStatsManager) { - - companion object { - private const val SCAN_INTERVAL = 1000L * 60 * 60 * 24 // 24 hours - } - - /** - * @param event Event to validate - * @param queryStart Timestamp at which original query o - * @return True if the event is valid, false otherwise - */ - fun test(event: UsageEvents.Event, queryStart: Long): Boolean { - val events = usageStatsManager.queryEvents(queryStart - SCAN_INTERVAL, queryStart) - - // Reusable event object for iteration - val e = UsageEvents.Event() - - // Track whether the package is currently in foreground or background - var open = false // Not open until opened - - while (events.hasNextEvent()) { - events.getNextEvent(e) - - if (e.eventType == UsageEvents.Event.DEVICE_STARTUP) { - // Consider all apps closed after startup according to docs - open = false - } - - // Only consider events concerning our package otherwise - if (event.packageName == e.packageName) { - when (e.eventType) { - // see EventLogWrapper - UsageEvents.Event.ACTIVITY_RESUMED, 4 -> { - open = true - } - UsageEvents.Event.ACTIVITY_PAUSED, 3 -> { - if (e.timeStamp != event.timeStamp) { - // Don't flip to 'false' if we're looking at the original event itself - open = false - } - } - } - } - } - - val result = if (open) "True" else "Faulty" - Log.d("Guardian", "Scanned for package ${event.packageName} and determined event to be $result") - - // Event is valid if it was previously opened (within SCAN_INTERVAL) - return open - } -} diff --git a/app/src/main/java/app/olauncher/listener/DeviceAdmin.kt b/app/src/main/java/app/olauncher/listener/DeviceAdmin.kt deleted file mode 100644 index 14a280f..0000000 --- a/app/src/main/java/app/olauncher/listener/DeviceAdmin.kt +++ /dev/null @@ -1,19 +0,0 @@ -package app.olauncher.listener - -import android.app.admin.DeviceAdminReceiver -import android.content.Context -import android.content.Intent -import android.widget.Toast -import app.olauncher.R - -class DeviceAdmin : DeviceAdminReceiver() { - fun onEnabled(intent: Intent?, context: Context) { - super.onEnabled(context, intent!!) - Toast.makeText(context, context.getString(R.string.enabled), Toast.LENGTH_SHORT).show() - } - - fun onDisabled(intent: Intent?, context: Context) { - super.onDisabled(context, intent!!) - Toast.makeText(context, context.getString(R.string.enabled), Toast.LENGTH_SHORT).show() - } -} \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/listener/OnSwipeTouchListener.kt b/app/src/main/java/app/olauncher/listener/OnSwipeTouchListener.kt deleted file mode 100644 index 3197b3c..0000000 --- a/app/src/main/java/app/olauncher/listener/OnSwipeTouchListener.kt +++ /dev/null @@ -1,113 +0,0 @@ -package app.olauncher.listener - -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 app.olauncher.data.Constants -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlin.math.abs - -/* -Swipe, double tap and long press touch listener for a view -Source: https://www.tutorialspoint.com/how-to-handle-swipe-gestures-in-kotlin -*/ - -internal open class OnSwipeTouchListener(c: Context?) : OnTouchListener { - private var longPressOn = false - - // private var doubleTapOn = false - private val gestureDetector: GestureDetector - - override fun onTouch(view: View, motionEvent: MotionEvent): Boolean { - if (motionEvent.action == MotionEvent.ACTION_UP) - longPressOn = false - return gestureDetector.onTouchEvent(motionEvent) - } - - private inner class GestureListener : SimpleOnGestureListener() { - private val SWIPE_THRESHOLD: Int = 100 - private val SWIPE_VELOCITY_THRESHOLD: Int = 100 - - override fun onDown(e: MotionEvent): Boolean { - return true - } - - override fun onSingleTapUp(e: MotionEvent): Boolean { -// if (doubleTapOn) { -// doubleTapOn = false -// onTripleClick() -// } - onClick() - return super.onSingleTapUp(e) - } - - override fun onDoubleTap(e: MotionEvent): Boolean { -// doubleTapOn = true -// Timer().schedule(Constants.TRIPLE_TAP_DELAY_MS) { -// if (doubleTapOn) { -// doubleTapOn = false -// onDoubleClick() -// } -// } - onDoubleClick() - return super.onDoubleTap(e) - } - - override fun onLongPress(e: MotionEvent) { - longPressOn = true - GlobalScope.launch { - delay(Constants.LONG_PRESS_DELAY_MS) - withContext(Dispatchers.Main) { - if (isActive && longPressOn) - onLongClick() - } - } - super.onLongPress(e) - } - - override fun onFling( - event1: MotionEvent?, - event2: MotionEvent, - velocityX: Float, - velocityY: Float, - ): Boolean { - try { - val diffY = event2.y - (event1?.y ?: 0F) - val diffX = event2.x - (event1?.x ?: 0F) - if (abs(diffX) > abs(diffY)) { - if (abs(diffX) > SWIPE_THRESHOLD && abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { - if (diffX > 0) onSwipeRight() else onSwipeLeft() - } - } else { - if (abs(diffY) > SWIPE_THRESHOLD && abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) { - if (diffY < 0) onSwipeUp() else onSwipeDown() - } - } - } catch (exception: Exception) { - exception.printStackTrace() - } - return false - } - } - - open fun onSwipeRight() {} - open fun onSwipeLeft() {} - open fun onSwipeUp() {} - open fun onSwipeDown() {} - open fun onLongClick() {} - open fun onDoubleClick() {} - open fun onTripleClick() {} - open fun onClick() {} - - init { - gestureDetector = GestureDetector(c, GestureListener()) - } -} \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/listener/ViewSwipeTouchListener.kt b/app/src/main/java/app/olauncher/listener/ViewSwipeTouchListener.kt deleted file mode 100644 index 633b779..0000000 --- a/app/src/main/java/app/olauncher/listener/ViewSwipeTouchListener.kt +++ /dev/null @@ -1,96 +0,0 @@ -package app.olauncher.listener - -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 app.olauncher.data.Constants -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlin.math.abs - -internal open class ViewSwipeTouchListener(c: Context?, v: View) : OnTouchListener { - private var longPressOn = false - private val gestureDetector: GestureDetector - - override fun onTouch(view: View, motionEvent: MotionEvent): Boolean { - when (motionEvent.action) { - MotionEvent.ACTION_DOWN -> view.isPressed = true - MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> view.isPressed = false - } - return gestureDetector.onTouchEvent(motionEvent) - } - - private inner class GestureListener(private val view: View) : SimpleOnGestureListener() { - private val SWIPE_THRESHOLD: Int = 100 - private val SWIPE_VELOCITY_THRESHOLD: Int = 100 - - override fun onDown(e: MotionEvent): Boolean { - return true - } - - override fun onSingleTapUp(e: MotionEvent): Boolean { - view.performClick() - return true - } - - override fun onDoubleTap(e: MotionEvent): Boolean { - onDoubleClick() - return super.onDoubleTap(e) - } - - override fun onLongPress(e: MotionEvent) { - longPressOn = true - GlobalScope.launch { - delay(Constants.LONG_PRESS_DELAY_MS) - withContext(Dispatchers.Main) { - if (isActive && longPressOn) - view.performLongClick() - } - } - super.onLongPress(e) - } - - override fun onFling( - event1: MotionEvent?, - event2: MotionEvent, - velocityX: Float, - velocityY: Float, - ): Boolean { - try { - val diffY = event2.y - (event1?.y ?: 0F) - val diffX = event2.x - (event1?.x ?: 0F) - if (abs(diffX) > abs(diffY)) { - if (abs(diffX) > SWIPE_THRESHOLD && abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { - if (diffX > 0) onSwipeRight() else onSwipeLeft() - } - } else { - if (abs(diffY) > SWIPE_THRESHOLD && abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) { - if (diffY < 0) onSwipeUp() else onSwipeDown() - } - } - } catch (exception: Exception) { - exception.printStackTrace() - } - return false - } - } - - open fun onSwipeRight() {} - open fun onSwipeLeft() {} - open fun onSwipeUp() {} - open fun onSwipeDown() {} - open fun onLongClick(view: View) {} - private fun onDoubleClick() {} - open fun onClick(view: View) {} - - init { - gestureDetector = GestureDetector(c, GestureListener(v)) - } -} \ No newline at end of file diff --git a/app/src/main/java/app/olauncher/ui/AppDrawerAdapter.kt b/app/src/main/java/app/olauncher/ui/AppDrawerAdapter.kt deleted file mode 100644 index 07428b5..0000000 --- a/app/src/main/java/app/olauncher/ui/AppDrawerAdapter.kt +++ /dev/null @@ -1,378 +0,0 @@ -package app.olauncher.ui - -import android.content.Context -import android.content.pm.LauncherApps -import android.os.UserHandle -import android.text.Editable -import android.text.TextWatcher -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.widget.Filter -import android.widget.Filterable -import androidx.core.view.isVisible -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import app.olauncher.R -import app.olauncher.data.AppModel -import app.olauncher.data.Constants -import app.olauncher.databinding.AdapterAppDrawerBinding -import app.olauncher.databinding.AdapterPrivateSpaceHeaderBinding -import app.olauncher.helper.hideKeyboard -import app.olauncher.helper.isSystemApp -import app.olauncher.helper.showKeyboard -import java.text.Normalizer - -class AppDrawerAdapter( - private var flag: Int, - private val appLabelGravity: Int, - private val appClickListener: (AppModel) -> Unit, - private val appInfoListener: (AppModel) -> Unit, - private val appDeleteListener: (AppModel) -> Unit, - private val appHideListener: (AppModel, Int) -> Unit, - private val appRenameListener: (AppModel, String) -> Unit, - private val privateSpaceToggleListener: () -> Unit = {}, - private val privateSpaceSettingsListener: () -> Unit = {}, -) : ListAdapter(DIFF_CALLBACK), Filterable { - - companion object { - const val VIEW_TYPE_APP = 0 - const val VIEW_TYPE_PRIVATE_HEADER = 1 - - val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: AppModel, newItem: AppModel): Boolean = when { - oldItem is AppModel.App && newItem is AppModel.App -> - oldItem.appPackage == newItem.appPackage && oldItem.user == newItem.user - - oldItem is AppModel.PinnedShortcut && newItem is AppModel.PinnedShortcut -> - oldItem.shortcutId == newItem.shortcutId && oldItem.user == newItem.user - - oldItem is AppModel.PrivateSpaceHeader && newItem is AppModel.PrivateSpaceHeader -> true - - else -> false - } - - override fun areContentsTheSame(oldItem: AppModel, newItem: AppModel): Boolean = - oldItem == newItem - } - } - - private var autoLaunch = true - private var isBangSearch = false - var allowAutoLaunch = true - private val diacriticsRegex = Regex("\\p{InCombiningDiacriticalMarks}+") - private val separatorsRegex = Regex("[-_+,.`'\\s\\p{Z}]") - private val appFilter = createAppFilter() - private val myUserHandle = android.os.Process.myUserHandle() - - var appsList: MutableList = mutableListOf() - var appFilteredList: MutableList = mutableListOf() - - override fun getItemViewType(position: Int): Int { - return when (appFilteredList.getOrNull(position)) { - is AppModel.PrivateSpaceHeader -> VIEW_TYPE_PRIVATE_HEADER - else -> VIEW_TYPE_APP - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - VIEW_TYPE_PRIVATE_HEADER -> PrivateSpaceHeaderViewHolder( - AdapterPrivateSpaceHeaderBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - else -> ViewHolder( - AdapterAppDrawerBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - try { - if (appFilteredList.isEmpty() || position == RecyclerView.NO_POSITION) return - val appModel = appFilteredList[holder.bindingAdapterPosition] - when (holder) { - is PrivateSpaceHeaderViewHolder -> { - holder.bind( - appLabelGravity, - privateSpaceToggleListener, - privateSpaceSettingsListener, - ) - } - is ViewHolder -> holder.bind( - flag, - appLabelGravity, - myUserHandle, - appModel, - appClickListener, - appDeleteListener, - appInfoListener, - appHideListener, - appRenameListener - ) - } - } catch (e: Exception) { - e.printStackTrace() - } - } - - override fun getFilter(): Filter = this.appFilter - - private fun createAppFilter(): Filter { - return object : Filter() { - override fun performFiltering(charSearch: CharSequence?): FilterResults { - isBangSearch = charSearch?.startsWith("!") ?: false - autoLaunch = allowAutoLaunch && (charSearch?.startsWith(" ")?.not() ?: true) - - val appFilteredList = (if (charSearch.isNullOrBlank()) appsList - else appsList.filter { app -> - app !is AppModel.PrivateSpaceHeader && appLabelMatches(app.appLabel, charSearch) - } as MutableList) - - val filterResults = FilterResults() - filterResults.values = appFilteredList - return filterResults - } - - @Suppress("UNCHECKED_CAST") - override fun publishResults(constraint: CharSequence?, results: FilterResults?) { - results?.values?.let { - val items = it as MutableList - appFilteredList = items - submitList(appFilteredList) { - autoLaunch() - } - } - } - } - } - - private fun autoLaunch() { - try { - if (itemCount == 1 - && autoLaunch - && isBangSearch.not() - && flag == Constants.FLAG_LAUNCH_APP - && appFilteredList.isNotEmpty() - && appFilteredList[0] !is AppModel.PrivateSpaceHeader - ) appClickListener(appFilteredList[0]) - } catch (e: Exception) { - e.printStackTrace() - } - } - - private fun appLabelMatches(appLabel: String, charSearch: CharSequence): Boolean { - if (appLabel.contains(charSearch.trim(), true)) return true - val query = charSearch.normalizeForSearch() - return query.isNotEmpty() && appLabel.normalizeForSearch().contains(query, true) - } - - private fun CharSequence.normalizeForSearch(): String = - Normalizer.normalize(this, Normalizer.Form.NFD) - .replace(diacriticsRegex, "") - .replace(separatorsRegex, "") - - fun setAppList(appsList: MutableList) { - appsList.add( - AppModel.App( - appLabel = "", - key = null, - appPackage = "", - activityClassName = "", - isNew = false, - user = android.os.Process.myUserHandle() - ) - ) - this.appsList = appsList - this.appFilteredList = appsList - submitList(appsList) - } - - fun launchFirstInList() { - val first = appFilteredList.firstOrNull { it !is AppModel.PrivateSpaceHeader } - if (first != null) appClickListener(first) - } - - class PrivateSpaceHeaderViewHolder(private val binding: AdapterPrivateSpaceHeaderBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind( - appLabelGravity: Int, - toggleListener: () -> Unit, - settingsListener: () -> Unit, - ) = with(binding) { - privateSpaceTitle.gravity = appLabelGravity - privateSpaceTitle.setOnClickListener { toggleListener() } - privateSpaceTitle.setOnLongClickListener { - settingsListener() - true - } - } - } - - class ViewHolder(private val binding: AdapterAppDrawerBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind( - flag: Int, - appLabelGravity: Int, - myUserHandle: UserHandle, - appModel: AppModel, - clickListener: (AppModel) -> Unit, - appDeleteListener: (AppModel) -> Unit, - appInfoListener: (AppModel) -> Unit, - appHideListener: (AppModel, Int) -> Unit, - appRenameListener: (AppModel, String) -> Unit, - ) = with(binding) { - appHideLayout.visibility = View.GONE - renameLayout.visibility = View.GONE - appTitle.visibility = View.VISIBLE - - appTitle.text = buildString { - append(appModel.appLabel) - if (appModel.isNew) append(" ✦") - } - appTitle.gravity = appLabelGravity - otherProfileIndicator.isVisible = appModel.user != myUserHandle - - appRow.setOnClickListener { clickListener(appModel) } - appRow.setOnLongClickListener { - if (appModel.appPackage.isNotEmpty()) { - appDelete.alpha = when ( - appModel is AppModel.PinnedShortcut || !root.context.isSystemApp(appModel.appPackage, appModel.user) - ) { - true -> 1.0f - false -> 0.5f - } - appHide.text = if (flag == Constants.FLAG_HIDDEN_APPS) - root.context.getString(R.string.adapter_show) - else - root.context.getString(R.string.adapter_hide) - appTitle.visibility = View.INVISIBLE - appHide.alpha = when (appModel is AppModel.PinnedShortcut) { - true -> 0.5f - false -> 1.0f - } - appHideLayout.visibility = View.VISIBLE - appRename.isVisible = flag != Constants.FLAG_HIDDEN_APPS - } - true - } - appRow.setOnKeyListener { _, keyCode, event -> - when (keyCode) { - KeyEvent.KEYCODE_ENTER, - KeyEvent.KEYCODE_DPAD_CENTER, - KeyEvent.KEYCODE_NUMPAD_ENTER -> { - when (event.action) { - KeyEvent.ACTION_DOWN -> { - if (event.repeatCount > 0) { - appRow.performLongClick() - } - true - } - KeyEvent.ACTION_UP -> { - if (event.repeatCount == 0) appRow.performClick() - true - } - else -> false - } - } - else -> false - } - } - - appRename.setOnClickListener { - if (appModel.appPackage.isNotEmpty()) { - etAppRename.hint = getAppName(etAppRename.context, appModel.appPackage, appModel.user) - etAppRename.setText(appModel.appLabel) - etAppRename.setSelectAllOnFocus(true) - renameLayout.visibility = View.VISIBLE - appHideLayout.visibility = View.GONE - etAppRename.showKeyboard() - etAppRename.imeOptions = EditorInfo.IME_ACTION_DONE - } - } - etAppRename.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> - appTitle.visibility = if (hasFocus) View.INVISIBLE else View.VISIBLE - } - etAppRename.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - etAppRename.hint = getAppName(etAppRename.context, appModel.appPackage, appModel.user) - } - - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int, - ) { - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - etAppRename.hint = "" - } - }) - etAppRename.setOnEditorActionListener { _, actionCode, _ -> - if (actionCode == EditorInfo.IME_ACTION_DONE) { - val renameLabel = etAppRename.text.toString().trim() - if (renameLabel.isNotBlank() && appModel.appPackage.isNotBlank()) { - appRenameListener(appModel, renameLabel) - renameLayout.visibility = View.GONE - } - true - } - false - } - tvSaveRename.setOnClickListener { - etAppRename.hideKeyboard() - val renameLabel = etAppRename.text.toString().trim() - if (renameLabel.isNotBlank() && appModel.appPackage.isNotBlank()) { - appRenameListener(appModel, renameLabel) - renameLayout.visibility = View.GONE - } else { - appRenameListener( - appModel, - getAppName(etAppRename.context, appModel.appPackage, appModel.user) - ) - renameLayout.visibility = View.GONE - } - } - appInfo.setOnClickListener { appInfoListener(appModel) } - appDelete.setOnClickListener { appDeleteListener(appModel) } - appMenuClose.setOnClickListener { - appHideLayout.visibility = View.GONE - appTitle.visibility = View.VISIBLE - } - appRenameClose.setOnClickListener { - renameLayout.visibility = View.GONE - appTitle.visibility = View.VISIBLE - } - appHide.setOnClickListener { appHideListener(appModel, bindingAdapterPosition) } - } - - private fun getAppName(context: Context, appPackage: String, user: UserHandle): String { - val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - return try { - val activityList = launcherApps.getActivityList(appPackage, user) - if (activityList.isNotEmpty()) { - activityList.first().label.toString() - } else { - val packageManager = context.packageManager - packageManager.getApplicationLabel( - packageManager.getApplicationInfo(appPackage, 0) - ).toString() - } - } catch (_: Exception) { - "" - } - } - } -} diff --git a/app/src/main/java/app/olauncher/ui/AppDrawerFragment.kt b/app/src/main/java/app/olauncher/ui/AppDrawerFragment.kt deleted file mode 100644 index 56af3dc..0000000 --- a/app/src/main/java/app/olauncher/ui/AppDrawerFragment.kt +++ /dev/null @@ -1,461 +0,0 @@ -package app.olauncher.ui - -import android.content.Context -import android.graphics.Typeface -import android.os.Build -import android.os.Bundle -import android.os.Process -import android.text.InputType -import android.text.Spannable -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.animation.AnimationUtils -import android.view.inputmethod.BaseInputConnection -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager -import android.widget.TextView -import androidx.appcompat.widget.SearchView -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.Recycler -import app.olauncher.MainViewModel -import app.olauncher.R -import app.olauncher.data.AppModel -import app.olauncher.data.Constants -import app.olauncher.data.Prefs -import app.olauncher.databinding.FragmentAppDrawerBinding -import app.olauncher.helper.deletePinnedShortcut -import app.olauncher.helper.hideKeyboard -import app.olauncher.helper.isEinkDisplay -import app.olauncher.helper.isSystemApp -import app.olauncher.helper.openAppInfo -import app.olauncher.helper.openSearch -import app.olauncher.helper.openUrl -import app.olauncher.helper.showKeyboard -import app.olauncher.helper.showToast -import app.olauncher.helper.uninstall - -class AppDrawerFragment : Fragment() { - - private lateinit var prefs: Prefs - private lateinit var adapter: AppDrawerAdapter - private lateinit var linearLayoutManager: LinearLayoutManager - private var searchTextView: TextView? = null - private var cachedIsCjkKeyboard: Boolean? = null - - private var flag = Constants.FLAG_LAUNCH_APP - private var canRename = false - private var currentAppList: List? = null - private var currentPrivateSpaceApps: List? = null - private var currentPrivateSpaceLocked: Boolean = true - private var currentPrivateSpaceAvailable: Boolean = false - - private val viewModel: MainViewModel by activityViewModels() - private var _binding: FragmentAppDrawerBinding? = null - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - _binding = FragmentAppDrawerBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - prefs = Prefs(requireContext()) - arguments?.let { - flag = it.getInt(Constants.Key.FLAG, Constants.FLAG_LAUNCH_APP) - canRename = it.getBoolean(Constants.Key.RENAME, false) - } - - initViews() - initSearch() - initAdapter() - initObservers() - initClickListeners() - initKeyNavigation() - } - - private fun initKeyNavigation() { - binding.recyclerView.setOnKeyListener { _, keyCode, event -> - if (event.action != KeyEvent.ACTION_DOWN) return@setOnKeyListener false - when (keyCode) { - KeyEvent.KEYCODE_DPAD_UP -> { - if (!binding.recyclerView.canScrollVertically(-1)) { - val firstVisible = (binding.recyclerView.layoutManager as? LinearLayoutManager) - ?.findFirstVisibleItemPosition() ?: -1 - if (firstVisible <= 0) { - binding.search.requestFocus() - return@setOnKeyListener true - } - } - false - } - KeyEvent.KEYCODE_DEL -> { - val query = binding.search.query - if (query.isNullOrEmpty()) { - findNavController().popBackStack() - true - } else false - } - else -> false - } - } - - binding.search.setOnKeyListener { _, keyCode, event -> - if (event.action != KeyEvent.ACTION_DOWN) return@setOnKeyListener false - if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { - focusFirstLaunchableItem() - } else { - false - } - } - - searchTextView?.setOnKeyListener { _, keyCode, event -> - if (event.action != KeyEvent.ACTION_DOWN) return@setOnKeyListener false - when (keyCode) { - KeyEvent.KEYCODE_DPAD_DOWN -> focusFirstLaunchableItem() - KeyEvent.KEYCODE_DPAD_LEFT -> { - val textView = searchTextView - val selectionStart = textView?.selectionStart ?: -1 - val selectionEnd = textView?.selectionEnd ?: -1 - if (selectionStart <= 0 && selectionEnd <= 0) { - findNavController().popBackStack() - true - } else { - false - } - } - else -> false - } - } - } - - private fun initViews() { - if (flag == Constants.FLAG_HIDDEN_APPS) - binding.search.queryHint = getString(R.string.hidden_apps) - else if (flag in Constants.FLAG_SET_HOME_APP_1..Constants.FLAG_SET_CALENDAR_APP) - binding.search.queryHint = "Please select an app" - try { - searchTextView = binding.search.findViewById(R.id.search_src_text) - searchTextView?.gravity = prefs.appLabelAlignment - searchTextView?.inputType = - InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD - searchTextView?.typeface = Typeface.DEFAULT - searchTextView?.imeOptions = - EditorInfo.IME_ACTION_SEARCH or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING - } catch (e: Exception) { - e.printStackTrace() - } - } - - private fun initSearch() { - binding.search.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String?): Boolean { - if (query?.startsWith("!") == true) - requireContext().openUrl(Constants.URL_DUCK_SEARCH + query.replace(" ", "%20")) - else if (adapter.itemCount == 0) - requireContext().openSearch(query?.trim()) - else - adapter.launchFirstInList() - return true - } - - override fun onQueryTextChange(newText: String): Boolean { - try { - adapter.allowAutoLaunch = !isSearchComposing() - adapter.filter.filter(newText) - binding.appRename.visibility = - if (canRename && newText.isNotBlank()) View.VISIBLE else View.GONE - return true - } catch (e: Exception) { - e.printStackTrace() - } - return false - } - }) - } - - private fun isSearchComposing(): Boolean { - val text = searchTextView?.text - if (text !is Spannable) return false - val start = BaseInputConnection.getComposingSpanStart(text) - val end = BaseInputConnection.getComposingSpanEnd(text) - if (start !in 0 until end) return false - return isCjkKeyboard() - } - - private fun isCjkKeyboard(): Boolean { - cachedIsCjkKeyboard?.let { return it } - val result = try { - val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - val subtype = imm.currentInputMethodSubtype - val language = when { - subtype == null -> "" - subtype.languageTag.isNotEmpty() -> subtype.languageTag - else -> subtype.locale - } - language.startsWith("zh") || language.startsWith("ja") || language.startsWith("ko") - } catch (e: Exception) { - false - } - cachedIsCjkKeyboard = result - return result - } - - private fun initAdapter() { - adapter = AppDrawerAdapter( - flag, - prefs.appLabelAlignment, - appClickListener = { appModel -> - viewModel.selectedApp(appModel, flag) - if (flag == Constants.FLAG_LAUNCH_APP || flag == Constants.FLAG_HIDDEN_APPS) - findNavController().popBackStack(R.id.mainFragment, false) - else - findNavController().popBackStack() - }, - appInfoListener = { - openAppInfo( - requireContext(), - it.user, - it.appPackage - ) - findNavController().popBackStack(R.id.mainFragment, false) - }, - appDeleteListener = { appModel -> - when (appModel) { - is AppModel.PrivateSpaceHeader -> {} - is AppModel.PinnedShortcut -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - requireContext().deletePinnedShortcut( - packageName = appModel.appPackage, - shortcutIdToDelete = appModel.shortcutId, - user = appModel.user, - ) - } - - is AppModel.App -> { - if (appModel.user != Process.myUserHandle()) { - openAppInfo(requireContext(), appModel.user, appModel.appPackage) - } else if (requireContext().isSystemApp(appModel.appPackage, appModel.user)) { - requireContext().showToast(getString(R.string.system_app_cannot_delete)) - openAppInfo(requireContext(), appModel.user, appModel.appPackage) - } else { - requireContext().uninstall(appModel.appPackage) - } - } - } - viewModel.getAppList() - }, - appHideListener = { appModel, position -> - if (appModel is AppModel.PinnedShortcut) { - requireContext().showToast("Hiding pinned shortcuts is not supported") - return@AppDrawerAdapter - } - adapter.appFilteredList.removeAt(position) - adapter.notifyItemRemoved(position) - adapter.appsList.remove(appModel) - - val newSet = mutableSetOf() - newSet.addAll(prefs.hiddenApps) - if (flag == Constants.FLAG_HIDDEN_APPS) - newSet.remove(appModel.appPackage + "|" + appModel.user.toString()) - else - newSet.add(appModel.appPackage + "|" + appModel.user.toString()) - - prefs.hiddenApps = newSet - if (newSet.isEmpty()) - findNavController().popBackStack() - if (prefs.firstHide) { - binding.search.hideKeyboard() - prefs.firstHide = false - viewModel.showDialog.postValue(Constants.Dialog.HIDDEN) - findNavController().navigate(R.id.action_appListFragment_to_settingsFragment2) - } - viewModel.getAppList() - viewModel.getHiddenApps() - }, - appRenameListener = { appModel, renameLabel -> - val identifier = when (appModel) { - is AppModel.PinnedShortcut -> appModel.shortcutId - is AppModel.App -> appModel.appPackage - else -> return@AppDrawerAdapter - } - prefs.setAppRenameLabel(identifier, renameLabel) - viewModel.getAppList() - }, - privateSpaceToggleListener = { - viewModel.togglePrivateSpaceLock() - }, - privateSpaceSettingsListener = { - viewModel.openPrivateSpaceSettings() - findNavController().popBackStack(R.id.mainFragment, false) - } - ) - - linearLayoutManager = object : LinearLayoutManager(requireContext()) { - override fun scrollVerticallyBy( - dx: Int, - recycler: Recycler, - state: RecyclerView.State, - ): Int { - val scrollRange = super.scrollVerticallyBy(dx, recycler, state) - val overScroll = dx - scrollRange - if (overScroll < -10 && binding.recyclerView.scrollState == RecyclerView.SCROLL_STATE_DRAGGING) - checkMessageAndExit() - return scrollRange - } - } - - binding.recyclerView.layoutManager = linearLayoutManager - binding.recyclerView.adapter = adapter - binding.recyclerView.addOnScrollListener(getRecyclerViewOnScrollListener()) - binding.recyclerView.itemAnimator = null - if (requireContext().isEinkDisplay().not()) - binding.recyclerView.layoutAnimation = - AnimationUtils.loadLayoutAnimation(requireContext(), R.anim.layout_anim_from_bottom) - } - - private fun initObservers() { - viewModel.firstOpen.observe(viewLifecycleOwner) {} - if (flag == Constants.FLAG_HIDDEN_APPS) { - viewModel.hiddenApps.observe(viewLifecycleOwner) { - it?.let { - adapter.setAppList(it.toMutableList()) - } - } - } else { - viewModel.appList.observe(viewLifecycleOwner) { - currentAppList = it - updateCombinedAppList() - } - if (flag == Constants.FLAG_LAUNCH_APP) { - viewModel.privateSpaceAvailable.observe(viewLifecycleOwner) { - currentPrivateSpaceAvailable = it - updateCombinedAppList() - } - viewModel.privateSpaceLocked.observe(viewLifecycleOwner) { - currentPrivateSpaceLocked = it - updateCombinedAppList() - } - viewModel.privateSpaceApps.observe(viewLifecycleOwner) { - currentPrivateSpaceApps = it - updateCombinedAppList() - } - } - } - } - - private fun updateCombinedAppList() { - val apps = currentAppList ?: return - val combined = apps.toMutableList() - - if (flag == Constants.FLAG_LAUNCH_APP && currentPrivateSpaceAvailable) { - combined.add(AppModel.PrivateSpaceHeader(isLocked = currentPrivateSpaceLocked)) - if (!currentPrivateSpaceLocked) { - currentPrivateSpaceApps?.let { combined.addAll(it) } - } - } - - adapter.setAppList(combined) - adapter.filter.filter(binding.search.query) - } - - private fun findFirstLaunchablePosition(): Int { - return adapter.appFilteredList.indexOfFirst { appModel -> - appModel !is AppModel.PrivateSpaceHeader && appModel.appPackage.isNotBlank() - }.takeIf { it >= 0 } ?: RecyclerView.NO_POSITION - } - - private fun focusFirstLaunchableItem(): Boolean { - val position = findFirstLaunchablePosition() - if (position == RecyclerView.NO_POSITION) return false - - binding.recyclerView.requestFocus() - binding.recyclerView.scrollToPosition(position) - binding.recyclerView.post { - val holder = binding.recyclerView.findViewHolderForAdapterPosition(position) - val itemView = holder?.itemView?.findViewById(R.id.appRow) ?: holder?.itemView - itemView?.requestFocus() - } - return true - } - - private fun initClickListeners() { - binding.appRename.setOnClickListener { - val name = binding.search.query.toString().trim() - if (name.isEmpty()) { - requireContext().showToast(getString(R.string.type_a_new_app_name_first)) - binding.search.showKeyboard() - return@setOnClickListener - } - - when (flag) { - Constants.FLAG_SET_HOME_APP_1 -> prefs.appName1 = name - Constants.FLAG_SET_HOME_APP_2 -> prefs.appName2 = name - Constants.FLAG_SET_HOME_APP_3 -> prefs.appName3 = name - Constants.FLAG_SET_HOME_APP_4 -> prefs.appName4 = name - Constants.FLAG_SET_HOME_APP_5 -> prefs.appName5 = name - Constants.FLAG_SET_HOME_APP_6 -> prefs.appName6 = name - Constants.FLAG_SET_HOME_APP_7 -> prefs.appName7 = name - Constants.FLAG_SET_HOME_APP_8 -> prefs.appName8 = name - } - findNavController().popBackStack() - } - } - - private fun getRecyclerViewOnScrollListener(): RecyclerView.OnScrollListener { - return object : RecyclerView.OnScrollListener() { - var onTop = false - - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - super.onScrollStateChanged(recyclerView, newState) - when (newState) { - RecyclerView.SCROLL_STATE_DRAGGING -> { - onTop = !recyclerView.canScrollVertically(-1) - if (onTop) - binding.search.hideKeyboard() - } - RecyclerView.SCROLL_STATE_IDLE -> { - if (!recyclerView.canScrollVertically(1)) - binding.search.hideKeyboard() - else if (!recyclerView.canScrollVertically(-1)) - if (!onTop && isRemoving.not()) - binding.search.showKeyboard(prefs.autoShowKeyboard) - } - } - } - } - } - - private fun checkMessageAndExit() { - findNavController().popBackStack() - if (flag == Constants.FLAG_LAUNCH_APP) - viewModel.checkForMessages.call() - } - - override fun onStart() { - super.onStart() - cachedIsCjkKeyboard = null - binding.search.showKeyboard(prefs.autoShowKeyboard) - } - - override fun onStop() { - binding.search.hideKeyboard() - super.onStop() - } - - override fun onDestroyView() { - super.onDestroyView() - searchTextView = null - _binding = null - } -} diff --git a/app/src/main/java/app/olauncher/ui/HomeAppTextView.kt b/app/src/main/java/app/olauncher/ui/HomeAppTextView.kt deleted file mode 100644 index 144b6b9..0000000 --- a/app/src/main/java/app/olauncher/ui/HomeAppTextView.kt +++ /dev/null @@ -1,51 +0,0 @@ -package app.olauncher.ui - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint -import android.util.AttributeSet -import android.widget.TextView -import app.olauncher.helper.dpToPx - -class HomeAppTextView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = android.R.attr.textViewStyle, -) : TextView(context, attrs, defStyleAttr) { - - private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = Color.WHITE - alpha = 220 - style = Paint.Style.FILL - } - - private val dotRadiusPx = 3.dpToPx().toFloat() - private val reservedSideSpacePx = 14.dpToPx() - - private var showNotificationDot = false - - init { - setPaddingRelative( - paddingStart + reservedSideSpacePx, - paddingTop, - paddingEnd + reservedSideSpacePx, - paddingBottom, - ) - } - - fun setShowNotificationDot(show: Boolean) { - if (showNotificationDot == show) return - showNotificationDot = show - invalidate() - } - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - if (!showNotificationDot) return - - val cx = width - reservedSideSpacePx / 2f - val cy = height / 2f - canvas.drawCircle(cx, cy, dotRadiusPx, dotPaint) - } -} diff --git a/app/src/main/java/app/olauncher/ui/HomeFragment.kt b/app/src/main/java/app/olauncher/ui/HomeFragment.kt deleted file mode 100644 index 43ddbb1..0000000 --- a/app/src/main/java/app/olauncher/ui/HomeFragment.kt +++ /dev/null @@ -1,905 +0,0 @@ -package app.olauncher.ui - -import android.app.admin.DevicePolicyManager -import android.content.Context -import android.content.Intent -import android.content.pm.LauncherApps -import android.content.res.Configuration -import android.os.BatteryManager -import android.os.Build -import android.os.Bundle -import android.view.Gravity -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.WindowInsets -import android.widget.FrameLayout -import android.widget.TextView -import android.widget.Toast -import androidx.annotation.RequiresApi -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.os.bundleOf -import androidx.core.view.isVisible -import androidx.core.view.setPadding -import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.findNavController -import app.olauncher.MainViewModel -import app.olauncher.R -import app.olauncher.data.AppModel -import app.olauncher.data.Constants -import app.olauncher.data.Prefs -import app.olauncher.databinding.FragmentHomeBinding -import app.olauncher.helper.NotificationDotRepository -import app.olauncher.helper.appUsagePermissionGranted -import app.olauncher.helper.dpToPx -import app.olauncher.helper.expandNotificationDrawer -import app.olauncher.helper.getChangedAppTheme -import app.olauncher.helper.getUserHandleFromString -import app.olauncher.helper.isPackageInstalled -import app.olauncher.helper.openAlarmApp -import app.olauncher.helper.openCalendar -import app.olauncher.helper.openCameraApp -import app.olauncher.helper.openDialerApp -import app.olauncher.helper.openSearch -import app.olauncher.helper.setPlainWallpaperByTheme -import app.olauncher.helper.showToast -import app.olauncher.listener.OnSwipeTouchListener -import app.olauncher.listener.ViewSwipeTouchListener -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -class HomeFragment : Fragment(), View.OnClickListener, View.OnLongClickListener { - - private lateinit var prefs: Prefs - private lateinit var viewModel: MainViewModel - private lateinit var deviceManager: DevicePolicyManager - - private var _binding: FragmentHomeBinding? = null - private val binding get() = _binding!! - - private var dpadLeftKeyDownTime = 0L - private val notificationDotListener: () -> Unit = { - activity?.runOnUiThread { - if (_binding != null) updateHomeNotificationDots() - } - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - _binding = FragmentHomeBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - prefs = Prefs(requireContext()) - viewModel = activity?.run { - ViewModelProvider(this)[MainViewModel::class.java] - } ?: throw Exception("Invalid Activity") - - deviceManager = context?.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - - initObservers() - setHomeAlignment(prefs.homeAlignment) - initSwipeTouchListener() - initClickListeners() - initKeyNavigation() - } - - override fun onResume() { - super.onResume() - NotificationDotRepository.addListener(notificationDotListener) - dpadLeftKeyDownTime = 0L - populateHomeScreen(false) - viewModel.isOlauncherDefault() - if (prefs.showStatusBar) showStatusBar() - else hideStatusBar() - requestInitialFocus() - } - - override fun onClick(view: View) { - when (view.id) { - R.id.lock -> {} - R.id.clock -> openClockApp() - R.id.date -> openCalendarApp() - R.id.setDefaultLauncher -> viewModel.resetLauncherLiveData.call() - R.id.tvScreenTime -> openScreenTimeDigitalWellbeing() - else -> { - try { - val appLocation = view.tag.toString().toInt() - homeAppClicked(appLocation) - } catch (e: Exception) { - e.printStackTrace() - } - } - } - } - - private fun openClockApp() { - if (prefs.clockAppPackage.isBlank()) - openAlarmApp(requireContext()) - else - launchApp( - "Clock", - prefs.clockAppPackage, - prefs.clockAppClassName, - prefs.clockAppUser - ) - } - - private fun openCalendarApp() { - if (prefs.calendarAppPackage.isBlank()) - openCalendar(requireContext()) - else - launchApp( - "Calendar", - prefs.calendarAppPackage, - prefs.calendarAppClassName, - prefs.calendarAppUser - ) - } - - override fun onLongClick(view: View): Boolean { - when (view.id) { - R.id.homeApp1 -> showAppList(Constants.FLAG_SET_HOME_APP_1, prefs.appName1.isNotEmpty(), true) - R.id.homeApp2 -> showAppList(Constants.FLAG_SET_HOME_APP_2, prefs.appName2.isNotEmpty(), true) - R.id.homeApp3 -> showAppList(Constants.FLAG_SET_HOME_APP_3, prefs.appName3.isNotEmpty(), true) - R.id.homeApp4 -> showAppList(Constants.FLAG_SET_HOME_APP_4, prefs.appName4.isNotEmpty(), true) - R.id.homeApp5 -> showAppList(Constants.FLAG_SET_HOME_APP_5, prefs.appName5.isNotEmpty(), true) - R.id.homeApp6 -> showAppList(Constants.FLAG_SET_HOME_APP_6, prefs.appName6.isNotEmpty(), true) - R.id.homeApp7 -> showAppList(Constants.FLAG_SET_HOME_APP_7, prefs.appName7.isNotEmpty(), true) - R.id.homeApp8 -> showAppList(Constants.FLAG_SET_HOME_APP_8, prefs.appName8.isNotEmpty(), true) - R.id.clock -> { - showAppList(Constants.FLAG_SET_CLOCK_APP) - prefs.clockAppPackage = "" - prefs.clockAppClassName = "" - prefs.clockAppUser = "" - } - R.id.date -> { - showAppList(Constants.FLAG_SET_CALENDAR_APP) - prefs.calendarAppPackage = "" - prefs.calendarAppClassName = "" - prefs.calendarAppUser = "" - } - R.id.tvScreenTime -> { - showAppList(Constants.FLAG_SET_SCREEN_TIME_APP) - prefs.screenTimeAppPackage = "" - prefs.screenTimeAppClassName = "" - prefs.screenTimeAppUser = "" - } - R.id.setDefaultLauncher -> { - prefs.hideSetDefaultLauncher = true - binding.setDefaultLauncher.visibility = View.GONE - if (viewModel.isOlauncherDefault.value != true) { - requireContext().showToast(R.string.set_as_default_launcher) - findNavController().navigate(R.id.action_mainFragment_to_settingsFragment) - } - } - } - return true - } - - private fun initObservers() { - if (prefs.firstSettingsOpen) { - binding.firstRunTips.visibility = View.VISIBLE - binding.setDefaultLauncher.visibility = View.GONE - } else binding.firstRunTips.visibility = View.GONE - - viewModel.refreshHome.observe(viewLifecycleOwner) { - populateHomeScreen(it) - } - viewModel.isOlauncherDefault.observe(viewLifecycleOwner, Observer { - if (it != true) { - if (prefs.dailyWallpaper && prefs.appTheme == AppCompatDelegate.MODE_NIGHT_YES) { - prefs.dailyWallpaper = false - viewModel.cancelWallpaperWorker() - } - prefs.homeBottomAlignment = false - setHomeAlignment() - } - if (binding.firstRunTips.isVisible) return@Observer - binding.setDefaultLauncher.isVisible = it.not() && prefs.hideSetDefaultLauncher.not() - }) - viewModel.homeAppAlignment.observe(viewLifecycleOwner) { - setHomeAlignment(it) - } - viewModel.toggleDateTime.observe(viewLifecycleOwner) { - populateDateTime() - } - viewModel.screenTimeValue.observe(viewLifecycleOwner) { - it?.let { binding.tvScreenTime.text = it } - } - } - - private fun initKeyNavigation() { - binding.mainLayout.isFocusable = true - binding.mainLayout.setOnKeyListener { _, keyCode, event -> - handleHomeKeyEvent(keyCode, event) - } - } - - private fun handleHomeKeyEvent(keyCode: Int, event: KeyEvent): Boolean { - val visibleApps = getVisibleHomeApps() - if (visibleApps.isEmpty()) return false - - val focusedIndex = visibleApps.indexOf(activity?.currentFocus) - - when (event.action) { - KeyEvent.ACTION_DOWN -> { - when (keyCode) { - KeyEvent.KEYCODE_DPAD_UP -> { - if (event.repeatCount > 0) return true - if (focusedIndex > 0) { - visibleApps[focusedIndex - 1].requestFocus() - } else { - swipeDownAction() - } - return true - } - KeyEvent.KEYCODE_DPAD_DOWN -> { - if (event.repeatCount > 0) return true - if (focusedIndex < visibleApps.size - 1) { - visibleApps[focusedIndex + 1].requestFocus() - } - return true - } - KeyEvent.KEYCODE_DPAD_LEFT -> { - if (dpadLeftKeyDownTime == 0L) dpadLeftKeyDownTime = System.currentTimeMillis() - if (event.repeatCount > 0) { - val elapsed = System.currentTimeMillis() - dpadLeftKeyDownTime - if (elapsed >= Constants.LONG_PRESS_DELAY_MS) { - dpadLeftKeyDownTime = 0L - openSwipeLeftApp() - } - } - return true - } - KeyEvent.KEYCODE_DPAD_RIGHT -> { - if (event.repeatCount > 0) return true - showAppList(Constants.FLAG_LAUNCH_APP) - return true - } - KeyEvent.KEYCODE_DPAD_CENTER, - KeyEvent.KEYCODE_ENTER, - KeyEvent.KEYCODE_NUMPAD_ENTER -> return false - } - } - KeyEvent.ACTION_UP -> { - when (keyCode) { - KeyEvent.KEYCODE_DPAD_DOWN -> return true - KeyEvent.KEYCODE_DPAD_LEFT -> { - dpadLeftKeyDownTime = 0L - return true - } - KeyEvent.KEYCODE_DPAD_RIGHT -> return true - KeyEvent.KEYCODE_DPAD_CENTER, - KeyEvent.KEYCODE_ENTER, - KeyEvent.KEYCODE_NUMPAD_ENTER -> return false - } - } - } - return false - } - - private fun requestInitialFocus() { - if (binding.mainLayout.isInTouchMode) return - getVisibleHomeApps().firstOrNull()?.requestFocus() - } - - private fun getVisibleHomeApps(): List { - return listOf( - binding.homeApp1, - binding.homeApp2, - binding.homeApp3, - binding.homeApp4, - binding.homeApp5, - binding.homeApp6, - binding.homeApp7, - binding.homeApp8, - ).filter { it.visibility == View.VISIBLE } - } - - private fun initSwipeTouchListener() { - val context = requireContext() - binding.mainLayout.setOnTouchListener(getSwipeGestureListener(context)) - binding.homeApp1.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp1)) - binding.homeApp2.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp2)) - binding.homeApp3.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp3)) - binding.homeApp4.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp4)) - binding.homeApp5.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp5)) - binding.homeApp6.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp6)) - binding.homeApp7.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp7)) - binding.homeApp8.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp8)) - } - - private fun initClickListeners() { - binding.lock.setOnClickListener(this) - binding.clock.setOnClickListener(this) - binding.date.setOnClickListener(this) - binding.clock.setOnLongClickListener(this) - binding.date.setOnLongClickListener(this) - binding.setDefaultLauncher.setOnClickListener(this) - binding.setDefaultLauncher.setOnLongClickListener(this) - binding.tvScreenTime.setOnClickListener(this) - binding.tvScreenTime.setOnLongClickListener(this) - - listOf( - binding.homeApp1, - binding.homeApp2, - binding.homeApp3, - binding.homeApp4, - binding.homeApp5, - binding.homeApp6, - binding.homeApp7, - binding.homeApp8, - ).forEach { homeApp -> - homeApp.setOnClickListener(this) - homeApp.setOnLongClickListener(this) - homeApp.setOnKeyListener { view, keyCode, event -> - when (keyCode) { - KeyEvent.KEYCODE_DPAD_DOWN -> { - if (event.action != KeyEvent.ACTION_DOWN) return@setOnKeyListener true - if (event.repeatCount > 0) return@setOnKeyListener true - val visibleApps = getVisibleHomeApps() - val focusedIndex = visibleApps.indexOf(view) - if (focusedIndex < 0) return@setOnKeyListener true - if (focusedIndex < visibleApps.size - 1) { - visibleApps[focusedIndex + 1].requestFocus() - } - true - } - KeyEvent.KEYCODE_DPAD_RIGHT -> { - if (event.action != KeyEvent.ACTION_DOWN) return@setOnKeyListener true - if (event.repeatCount > 0) return@setOnKeyListener true - showAppList(Constants.FLAG_LAUNCH_APP) - true - } - KeyEvent.KEYCODE_ENTER, - KeyEvent.KEYCODE_DPAD_CENTER, - KeyEvent.KEYCODE_NUMPAD_ENTER -> { - when (event.action) { - KeyEvent.ACTION_DOWN -> { - if (event.repeatCount > 0) { - view.performLongClick() - } - true - } - KeyEvent.ACTION_UP -> { - if (event.repeatCount == 0) view.performClick() - true - } - else -> false - } - } - else -> false - } - } - } - } - - private fun setHomeAlignment(horizontalGravity: Int = prefs.homeAlignment) { - val verticalGravity = if (prefs.homeBottomAlignment) Gravity.BOTTOM else Gravity.CENTER_VERTICAL - binding.homeAppsLayout.gravity = horizontalGravity or verticalGravity - binding.dateTimeLayout.gravity = horizontalGravity - homeAppTextViews().forEach { it.gravity = horizontalGravity } - } - - private fun populateDateTime() { - binding.dateTimeLayout.isVisible = prefs.dateTimeVisibility != Constants.DateTime.OFF - binding.clock.isVisible = Constants.DateTime.isTimeVisible(prefs.dateTimeVisibility) - binding.date.isVisible = Constants.DateTime.isDateVisible(prefs.dateTimeVisibility) - - val dateFormat = SimpleDateFormat("EEE, d MMM", Locale.getDefault()) - var dateText = dateFormat.format(Date()) - - if (!prefs.showStatusBar) { - val battery = (requireContext().getSystemService(Context.BATTERY_SERVICE) as BatteryManager) - .getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) - if (battery > 0) - dateText = getString(R.string.day_battery, dateText, battery) - } - binding.date.text = dateText.replace(".,", ",") - } - - @RequiresApi(Build.VERSION_CODES.Q) - private fun populateScreenTime() { - if (requireContext().appUsagePermissionGranted().not()) return - - viewModel.getTodaysScreenTime() - binding.tvScreenTime.visibility = View.VISIBLE - - val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - val horizontalMargin = if (isLandscape) 64.dpToPx() else 10.dpToPx() - val marginTop = if (isLandscape) { - if (prefs.dateTimeVisibility == Constants.DateTime.DATE_ONLY) 36.dpToPx() else 56.dpToPx() - } else { - if (prefs.dateTimeVisibility == Constants.DateTime.DATE_ONLY) 45.dpToPx() else 72.dpToPx() - } - val params = FrameLayout.LayoutParams( - FrameLayout.LayoutParams.WRAP_CONTENT, - FrameLayout.LayoutParams.WRAP_CONTENT - ).apply { - topMargin = marginTop - marginStart = horizontalMargin - marginEnd = horizontalMargin - gravity = if (prefs.homeAlignment == Gravity.END) Gravity.START else Gravity.END - } - binding.tvScreenTime.layoutParams = params - binding.tvScreenTime.setPadding(10.dpToPx()) - } - - private fun populateHomeScreen(appCountUpdated: Boolean) { - if (appCountUpdated) hideHomeApps() - restoreHomeAppFocusability() - populateDateTime() - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - populateScreenTime() - - val homeAppsNum = prefs.homeAppsNum - if (homeAppsNum == 0) return - - binding.homeApp1.visibility = View.VISIBLE - if (!setHomeAppText(binding.homeApp1 as TextView, prefs.appName1, prefs.appPackage1, prefs.appUser1, prefs.isShortcut1, prefs.shortcutId1)) { - prefs.appName1 = "" - prefs.appPackage1 = "" - } - if (homeAppsNum == 1) return - - binding.homeApp2.visibility = View.VISIBLE - if (!setHomeAppText(binding.homeApp2 as TextView, prefs.appName2, prefs.appPackage2, prefs.appUser2, prefs.isShortcut2, prefs.shortcutId2)) { - prefs.appName2 = "" - prefs.appPackage2 = "" - } - if (homeAppsNum == 2) return - - binding.homeApp3.visibility = View.VISIBLE - if (!setHomeAppText(binding.homeApp3 as TextView, prefs.appName3, prefs.appPackage3, prefs.appUser3, prefs.isShortcut3, prefs.shortcutId3)) { - prefs.appName3 = "" - prefs.appPackage3 = "" - } - if (homeAppsNum == 3) return - - binding.homeApp4.visibility = View.VISIBLE - if (!setHomeAppText(binding.homeApp4 as TextView, prefs.appName4, prefs.appPackage4, prefs.appUser4, prefs.isShortcut4, prefs.shortcutId4)) { - prefs.appName4 = "" - prefs.appPackage4 = "" - } - if (homeAppsNum == 4) return - - binding.homeApp5.visibility = View.VISIBLE - if (!setHomeAppText(binding.homeApp5 as TextView, prefs.appName5, prefs.appPackage5, prefs.appUser5, prefs.isShortcut5, prefs.shortcutId5)) { - prefs.appName5 = "" - prefs.appPackage5 = "" - } - if (homeAppsNum == 5) return - - binding.homeApp6.visibility = View.VISIBLE - if (!setHomeAppText(binding.homeApp6 as TextView, prefs.appName6, prefs.appPackage6, prefs.appUser6, prefs.isShortcut6, prefs.shortcutId6)) { - prefs.appName6 = "" - prefs.appPackage6 = "" - } - if (homeAppsNum == 6) return - - binding.homeApp7.visibility = View.VISIBLE - if (!setHomeAppText(binding.homeApp7 as TextView, prefs.appName7, prefs.appPackage7, prefs.appUser7, prefs.isShortcut7, prefs.shortcutId7)) { - prefs.appName7 = "" - prefs.appPackage7 = "" - } - if (homeAppsNum == 7) return - - binding.homeApp8.visibility = View.VISIBLE - if (!setHomeAppText(binding.homeApp8 as TextView, prefs.appName8, prefs.appPackage8, prefs.appUser8, prefs.isShortcut8, prefs.shortcutId8)) { - prefs.appName8 = "" - prefs.appPackage8 = "" - } - } - - private fun setHomeAppText( - textView: TextView, - appName: String, - packageName: String, - userString: String, - isShortcut: Boolean, - shortcutId: String?, - ): Boolean { - val userHandle = getUserHandleFromString(requireContext(), userString) - - if (isShortcut) { - val launcherApps = requireContext().getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val query = LauncherApps.ShortcutQuery().apply { - setPackage(packageName) - setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED) - } - - try { - val shortcuts = launcherApps.getShortcuts(query, userHandle) - if (shortcuts?.any { it.id == shortcutId } == true) { - textView.text = appName - updateHomeAppDot(textView, packageName) - return true - } - textView.text = "" - updateHomeAppDot(textView, null) - return false - } catch (e: Exception) { - e.printStackTrace() - textView.text = "" - updateHomeAppDot(textView, null) - return false - } - } - - if (isPackageInstalled(requireContext(), packageName, userString)) { - textView.text = appName - updateHomeAppDot(textView, packageName) - return true - } - textView.text = "" - updateHomeAppDot(textView, null) - return false - } - - private fun updateHomeNotificationDots() { - updateHomeAppDot(binding.homeApp1 as TextView, prefs.appPackage1) - updateHomeAppDot(binding.homeApp2 as TextView, prefs.appPackage2) - updateHomeAppDot(binding.homeApp3 as TextView, prefs.appPackage3) - updateHomeAppDot(binding.homeApp4 as TextView, prefs.appPackage4) - updateHomeAppDot(binding.homeApp5 as TextView, prefs.appPackage5) - updateHomeAppDot(binding.homeApp6 as TextView, prefs.appPackage6) - updateHomeAppDot(binding.homeApp7 as TextView, prefs.appPackage7) - updateHomeAppDot(binding.homeApp8 as TextView, prefs.appPackage8) - } - - private fun homeAppTextViews(): List { - return listOf( - binding.homeApp1 as TextView, - binding.homeApp2 as TextView, - binding.homeApp3 as TextView, - binding.homeApp4 as TextView, - binding.homeApp5 as TextView, - binding.homeApp6 as TextView, - binding.homeApp7 as TextView, - binding.homeApp8 as TextView, - ) - } - - private fun updateHomeAppDot(textView: TextView, packageName: String?) { - (textView as? HomeAppTextView)?.setShowNotificationDot( - packageName.isNullOrBlank().not() && NotificationDotRepository.hasActiveNotification(packageName) - ) - } - - private fun restoreHomeAppFocusability() { - listOf( - binding.homeApp1, - binding.homeApp2, - binding.homeApp3, - binding.homeApp4, - binding.homeApp5, - binding.homeApp6, - binding.homeApp7, - binding.homeApp8, - ).forEach { it.isFocusable = true } - } - - private fun hideHomeApps() { - listOf( - binding.homeApp1, - binding.homeApp2, - binding.homeApp3, - binding.homeApp4, - binding.homeApp5, - binding.homeApp6, - binding.homeApp7, - binding.homeApp8, - ).forEach { - it.visibility = View.GONE - it.isFocusable = false - } - } - - private fun launchAppOrShortcut( - appName: String, - packageName: String, - activityClassName: String?, - shortcutId: String?, - isShortcut: Boolean, - userString: String, - fallback: (() -> Unit)? = null, - ) { - if (appName.isEmpty()) { - showLongPressToast() - return - } - if (isShortcut && !shortcutId.isNullOrEmpty()) { - launchShortcut( - packageName = packageName, - shortcutId = shortcutId, - shortcutLabel = appName, - userString = userString - ) - } else if (packageName.isNotEmpty()) { - launchApp( - appName = appName, - packageName = packageName, - activityClassName = activityClassName, - userString = userString - ) - } else { - fallback?.invoke() - } - } - - private fun launchShortcut(shortcutId: String, packageName: String, shortcutLabel: String, userString: String) { - viewModel.selectedApp( - AppModel.PinnedShortcut( - shortcutId = shortcutId, - appLabel = shortcutLabel, - user = getUserHandleFromString(requireContext(), userString), - key = null, - appPackage = packageName, - isNew = false, - ), - Constants.FLAG_LAUNCH_APP - ) - } - - private fun launchApp(appName: String, packageName: String, activityClassName: String?, userString: String) { - viewModel.selectedApp( - AppModel.App( - appLabel = appName, - key = null, - appPackage = packageName, - activityClassName = activityClassName, - isNew = false, - user = getUserHandleFromString(requireContext(), userString) - ), - Constants.FLAG_LAUNCH_APP - ) - } - - private fun homeAppClicked(location: Int) { - launchAppOrShortcut( - appName = prefs.getAppName(location), - packageName = prefs.getAppPackage(location), - activityClassName = prefs.getAppActivityClassName(location), - shortcutId = prefs.getShortcutId(location), - isShortcut = prefs.getIsShortcut(location), - userString = prefs.getAppUser(location) - ) - } - - private fun openSwipeRightApp() { - if (!prefs.swipeRightEnabled) return - launchAppOrShortcut( - appName = prefs.appNameSwipeRight, - packageName = prefs.appPackageSwipeRight, - activityClassName = prefs.appActivityClassNameRight, - shortcutId = prefs.shortcutIdSwipeRight, - isShortcut = prefs.isShortcutSwipeRight, - userString = prefs.appUserSwipeRight, - fallback = { openDialerApp(requireContext()) } - ) - } - - private fun openSwipeLeftApp() { - if (!prefs.swipeLeftEnabled) return - launchAppOrShortcut( - appName = prefs.appNameSwipeLeft, - packageName = prefs.appPackageSwipeLeft, - activityClassName = prefs.appActivityClassNameSwipeLeft, - shortcutId = prefs.shortcutIdSwipeLeft, - isShortcut = prefs.isShortcutSwipeLeft, - userString = prefs.appUserSwipeLeft, - fallback = { openCameraApp(requireContext()) } - ) - } - - private fun showAppList(flag: Int, rename: Boolean = false, includeHiddenApps: Boolean = false) { - viewModel.getAppList(includeHiddenApps) - try { - findNavController().navigate( - R.id.action_mainFragment_to_appListFragment, - bundleOf( - Constants.Key.FLAG to flag, - Constants.Key.RENAME to rename - ) - ) - } catch (e: Exception) { - findNavController().navigate( - R.id.appListFragment, - bundleOf( - Constants.Key.FLAG to flag, - Constants.Key.RENAME to rename - ) - ) - e.printStackTrace() - } - } - - private fun swipeDownAction() { - when (prefs.swipeDownAction) { - Constants.SwipeDownAction.SEARCH -> openSearch(requireContext()) - else -> expandNotificationDrawer(requireContext()) - } - } - - private fun lockPhone() { - requireActivity().runOnUiThread { - try { - deviceManager.lockNow() - } catch (e: SecurityException) { - requireContext().showToast(getString(R.string.please_turn_on_double_tap_to_unlock), Toast.LENGTH_LONG) - findNavController().navigate(R.id.action_mainFragment_to_settingsFragment) - } catch (e: Exception) { - requireContext().showToast(getString(R.string.launcher_failed_to_lock_device), Toast.LENGTH_LONG) - prefs.lockModeOn = false - } - } - } - - private fun showStatusBar() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) - requireActivity().window.insetsController?.show(WindowInsets.Type.statusBars()) - else - @Suppress("DEPRECATION", "InlinedApi") - requireActivity().window.decorView.apply { - systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - } - } - - private fun hideStatusBar() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) - requireActivity().window.insetsController?.hide(WindowInsets.Type.statusBars()) - else { - @Suppress("DEPRECATION") - requireActivity().window.decorView.apply { - systemUiVisibility = View.SYSTEM_UI_FLAG_IMMERSIVE or View.SYSTEM_UI_FLAG_FULLSCREEN - } - } - } - - private fun changeAppTheme() { - if (prefs.dailyWallpaper.not()) return - val changedAppTheme = getChangedAppTheme(requireContext(), prefs.appTheme) - prefs.appTheme = changedAppTheme - if (prefs.dailyWallpaper) { - setPlainWallpaperByTheme(requireContext(), changedAppTheme) - viewModel.setWallpaperWorker() - } - requireActivity().recreate() - } - - private fun openScreenTimeDigitalWellbeing() { - if (prefs.screenTimeAppPackage.isNotBlank()) { - launchApp( - "Screen Time", - prefs.screenTimeAppPackage, - prefs.screenTimeAppClassName, - prefs.screenTimeAppUser - ) - return - } - val intent = Intent() - try { - intent.setClassName( - Constants.DIGITAL_WELLBEING_PACKAGE_NAME, - Constants.DIGITAL_WELLBEING_ACTIVITY - ) - startActivity(intent) - } catch (e: Exception) { - e.printStackTrace() - try { - intent.setClassName( - Constants.DIGITAL_WELLBEING_SAMSUNG_PACKAGE_NAME, - Constants.DIGITAL_WELLBEING_SAMSUNG_ACTIVITY - ) - startActivity(intent) - } catch (e: Exception) { - e.printStackTrace() - } - } - } - - private fun showLongPressToast() = requireContext().showToast(getString(R.string.long_press_to_select_app)) - - private fun textOnClick(view: View) = onClick(view) - - private fun textOnLongClick(view: View) = onLongClick(view) - - private fun getSwipeGestureListener(context: Context): View.OnTouchListener { - return object : OnSwipeTouchListener(context) { - override fun onSwipeLeft() { - super.onSwipeLeft() - openSwipeLeftApp() - } - - override fun onSwipeRight() { - super.onSwipeRight() - openSwipeRightApp() - } - - override fun onSwipeUp() { - super.onSwipeUp() - showAppList(Constants.FLAG_LAUNCH_APP) - } - - override fun onSwipeDown() { - super.onSwipeDown() - swipeDownAction() - } - - override fun onLongClick() { - super.onLongClick() - try { - findNavController().navigate(R.id.action_mainFragment_to_settingsFragment) - viewModel.firstOpen(false) - } catch (e: Exception) { - e.printStackTrace() - } - } - - override fun onDoubleClick() { - super.onDoubleClick() - if (!prefs.lockModeOn) return - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) - binding.lock.performClick() - else - lockPhone() - } - - override fun onClick() { - super.onClick() - viewModel.checkForMessages.call() - } - } - } - - private fun getViewSwipeTouchListener(context: Context, view: View): View.OnTouchListener { - return object : ViewSwipeTouchListener(context, view) { - override fun onSwipeLeft() { - super.onSwipeLeft() - openSwipeLeftApp() - } - - override fun onSwipeRight() { - super.onSwipeRight() - openSwipeRightApp() - } - - override fun onSwipeUp() { - super.onSwipeUp() - showAppList(Constants.FLAG_LAUNCH_APP) - } - - override fun onSwipeDown() { - super.onSwipeDown() - swipeDownAction() - } - - override fun onLongClick(view: View) { - super.onLongClick(view) - textOnLongClick(view) - } - - override fun onClick(view: View) { - super.onClick(view) - textOnClick(view) - } - } - } - - override fun onPause() { - NotificationDotRepository.removeListener(notificationDotListener) - super.onPause() - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} diff --git a/app/src/main/java/app/olauncher/ui/SettingsFragment.kt b/app/src/main/java/app/olauncher/ui/SettingsFragment.kt deleted file mode 100644 index a9c70e2..0000000 --- a/app/src/main/java/app/olauncher/ui/SettingsFragment.kt +++ /dev/null @@ -1,695 +0,0 @@ -package app.olauncher.ui - -import android.app.admin.DevicePolicyManager -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.os.Process -import android.provider.Settings -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.WindowInsets -import android.widget.Toast -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.os.bundleOf -import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.findNavController -import app.olauncher.BuildConfig -import app.olauncher.MainViewModel -import app.olauncher.R -import app.olauncher.data.Constants -import app.olauncher.data.Prefs -import app.olauncher.databinding.FragmentSettingsBinding -import app.olauncher.helper.animateAlpha -import app.olauncher.helper.appUsagePermissionGranted -import app.olauncher.helper.getColorFromAttr -import app.olauncher.helper.isAccessServiceEnabled -import app.olauncher.helper.isDarkThemeOn -import app.olauncher.helper.isEinkDisplay -import app.olauncher.helper.isCountryIn -import app.olauncher.helper.isOlauncherDefault -import app.olauncher.helper.isTablet -import app.olauncher.helper.openAppInfo -import app.olauncher.helper.openUrl -import app.olauncher.helper.rateApp -import app.olauncher.helper.setPlainWallpaper -import app.olauncher.helper.shareApp -import app.olauncher.helper.showToast -import app.olauncher.listener.DeviceAdmin - -class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongClickListener { - - private lateinit var prefs: Prefs - private lateinit var viewModel: MainViewModel - private lateinit var deviceManager: DevicePolicyManager - private lateinit var componentName: ComponentName - - private var _binding: FragmentSettingsBinding? = null - private val binding get() = _binding!! - private val showPentastic = System.currentTimeMillis() % 2 == 0L - private var showInstagram = false - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - _binding = FragmentSettingsBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - prefs = Prefs(requireContext()) - viewModel = activity?.run { - ViewModelProvider(this)[MainViewModel::class.java] - } ?: throw Exception("Invalid Activity") - viewModel.isOlauncherDefault() - - deviceManager = requireContext().getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - componentName = ComponentName(requireContext(), DeviceAdmin::class.java) - checkAdminPermission() - - binding.homeAppsNum.text = prefs.homeAppsNum.toString() - populateProMessage() - populateKeyboardText() - populateScreenTimeOnOff() - populateLockSettings() - // Home button for recents feature disabled - // populateHomeButtonRecents() - populateWallpaperText() - populateAppThemeText() - populateTextSize() - populateAlignment() - populateStatusBar() - populateDateTime() - populateSwipeApps() - populateSwipeDownAction() - populateActionHints() - showInstagram = requireContext().isCountryIn() - if (showInstagram) binding.twitter.text = getString(R.string.instagram) - initClickListeners() - initObservers() - - if (showPentastic) - binding.footer.text = getText(R.string.new_app_minimal_todo_lists) - } - - override fun onClick(view: View) { - binding.appsNumSelectLayout.visibility = View.GONE - binding.dateTimeSelectLayout.visibility = View.GONE - binding.appThemeSelectLayout.visibility = View.GONE - binding.swipeDownSelectLayout.visibility = View.GONE - if (view.id != R.id.textSizeMinus && view.id != R.id.textSizePlus) { - if (binding.textSizesLayout.isVisible) { - binding.textSizesLayout.visibility = View.GONE - applyTextSizeScale() - } - } - if (view.id != R.id.alignmentBottom) - binding.alignmentSelectLayout.visibility = View.GONE - - when (view.id) { - R.id.olauncherHiddenApps -> showHiddenApps() - R.id.moreFeatures -> viewModel.showDialog.postValue(Constants.Dialog.PRO_MESSAGE) - R.id.screenTimeOnOff -> viewModel.showDialog.postValue(Constants.Dialog.DIGITAL_WELLBEING) - R.id.appInfo -> openAppInfo(requireContext(), Process.myUserHandle(), BuildConfig.APPLICATION_ID) - R.id.setLauncher -> viewModel.resetLauncherLiveData.call() - R.id.toggleLock -> toggleLockMode() - // Home button for recents feature disabled - // R.id.homeButtonRecents -> toggleHomeButtonRecents() - R.id.autoShowKeyboard -> toggleKeyboardText() - R.id.homeAppsNum -> binding.appsNumSelectLayout.visibility = View.VISIBLE - R.id.dailyWallpaperUrl -> requireContext().openUrl(prefs.dailyWallpaperUrl) - R.id.dailyWallpaper -> toggleDailyWallpaperUpdate() - R.id.alignment -> binding.alignmentSelectLayout.visibility = View.VISIBLE - R.id.alignmentLeft -> viewModel.updateHomeAlignment(Gravity.START) - R.id.alignmentCenter -> viewModel.updateHomeAlignment(Gravity.CENTER) - R.id.alignmentRight -> viewModel.updateHomeAlignment(Gravity.END) - R.id.alignmentBottom -> updateHomeBottomAlignment() - R.id.statusBar -> toggleStatusBar() - R.id.dateTime -> binding.dateTimeSelectLayout.visibility = View.VISIBLE - R.id.dateTimeOn -> toggleDateTime(Constants.DateTime.ON) - R.id.dateTimeOff -> toggleDateTime(Constants.DateTime.OFF) - R.id.dateOnly -> toggleDateTime(Constants.DateTime.DATE_ONLY) - R.id.appThemeText -> binding.appThemeSelectLayout.visibility = View.VISIBLE - R.id.themeLight -> updateTheme(AppCompatDelegate.MODE_NIGHT_NO) - R.id.themeDark -> updateTheme(AppCompatDelegate.MODE_NIGHT_YES) - R.id.themeSystem -> updateTheme(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) - R.id.textSizeValue -> binding.textSizesLayout.visibility = View.VISIBLE - R.id.actionAccessibility -> openAccessibilityService() - R.id.closeAccessibility -> toggleAccessibilityVisibility(false) - R.id.notWorking -> requireContext().openUrl(Constants.URL_DOUBLE_TAP) - - R.id.tvGestures -> binding.flSwipeDown.visibility = View.VISIBLE - - R.id.maxApps0 -> updateHomeAppsNum(0) - R.id.maxApps1 -> updateHomeAppsNum(1) - R.id.maxApps2 -> updateHomeAppsNum(2) - R.id.maxApps3 -> updateHomeAppsNum(3) - R.id.maxApps4 -> updateHomeAppsNum(4) - R.id.maxApps5 -> updateHomeAppsNum(5) - R.id.maxApps6 -> updateHomeAppsNum(6) - R.id.maxApps7 -> updateHomeAppsNum(7) - R.id.maxApps8 -> updateHomeAppsNum(8) - - R.id.textSizeMinus -> adjustTextSizePreview(-0.1f) - R.id.textSizePlus -> adjustTextSizePreview(0.1f) - - R.id.swipeLeftApp -> showAppListIfEnabled(Constants.FLAG_SET_SWIPE_LEFT_APP) - R.id.swipeRightApp -> showAppListIfEnabled(Constants.FLAG_SET_SWIPE_RIGHT_APP) - R.id.swipeDownAction -> binding.swipeDownSelectLayout.visibility = View.VISIBLE - R.id.notifications -> updateSwipeDownAction(Constants.SwipeDownAction.NOTIFICATIONS) - R.id.search -> updateSwipeDownAction(Constants.SwipeDownAction.SEARCH) - - R.id.aboutOlauncher -> { - prefs.aboutClicked = true - requireContext().openUrl(Constants.URL_ABOUT_OLAUNCHER) - } - - R.id.share -> requireActivity().shareApp() - R.id.rate -> { - prefs.rateClicked = true - requireActivity().rateApp() - } - - R.id.twitter -> requireContext().openUrl( - if (showInstagram) Constants.URL_INSTA_TANUJ else Constants.URL_TWITTER_TANUJ - ) - R.id.github -> requireContext().openUrl(Constants.URL_OLAUNCHER_GITHUB) - R.id.privacy -> requireContext().openUrl(Constants.URL_OLAUNCHER_PRIVACY) - R.id.footer -> { - requireContext().openUrl( - if (showPentastic) Constants.URL_PENTASTIC else Constants.URL_NTS - ) - } - } - } - - override fun onLongClick(view: View): Boolean { - when (view.id) { - R.id.alignment -> { - prefs.appLabelAlignment = prefs.homeAlignment - findNavController().navigate(R.id.action_settingsFragment_to_appListFragment) - requireContext().showToast(getString(R.string.alignment_changed)) - } - - R.id.dailyWallpaper -> removeWallpaper() - R.id.appThemeText -> { - binding.appThemeSelectLayout.visibility = View.VISIBLE - binding.themeSystem.visibility = View.VISIBLE - } - - R.id.swipeLeftApp -> toggleSwipeLeft() - R.id.swipeRightApp -> toggleSwipeRight() - R.id.toggleLock -> startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)) - } - return true - } - - private fun initClickListeners() { - binding.olauncherHiddenApps.setOnClickListener(this) - binding.scrollLayout.setOnClickListener(this) - binding.appInfo.setOnClickListener(this) - binding.setLauncher.setOnClickListener(this) - binding.aboutOlauncher.setOnClickListener(this) - binding.moreFeatures.setOnClickListener(this) - binding.autoShowKeyboard.setOnClickListener(this) - binding.toggleLock.setOnClickListener(this) - // Home button for recents feature disabled - // binding.homeButtonRecents.setOnClickListener(this) - binding.homeAppsNum.setOnClickListener(this) - binding.screenTimeOnOff.setOnClickListener(this) - binding.dailyWallpaperUrl.setOnClickListener(this) - binding.dailyWallpaper.setOnClickListener(this) - binding.alignment.setOnClickListener(this) - binding.alignmentLeft.setOnClickListener(this) - binding.alignmentCenter.setOnClickListener(this) - binding.alignmentRight.setOnClickListener(this) - binding.alignmentBottom.setOnClickListener(this) - binding.statusBar.setOnClickListener(this) - binding.dateTime.setOnClickListener(this) - binding.dateTimeOn.setOnClickListener(this) - binding.dateTimeOff.setOnClickListener(this) - binding.dateOnly.setOnClickListener(this) - binding.swipeLeftApp.setOnClickListener(this) - binding.swipeRightApp.setOnClickListener(this) - binding.swipeDownAction.setOnClickListener(this) - binding.search.setOnClickListener(this) - binding.notifications.setOnClickListener(this) - binding.appThemeText.setOnClickListener(this) - binding.themeLight.setOnClickListener(this) - binding.themeDark.setOnClickListener(this) - binding.themeSystem.setOnClickListener(this) - binding.textSizeValue.setOnClickListener(this) - binding.actionAccessibility.setOnClickListener(this) - binding.closeAccessibility.setOnClickListener(this) - binding.notWorking.setOnClickListener(this) - - binding.share.setOnClickListener(this) - binding.rate.setOnClickListener(this) - binding.twitter.setOnClickListener(this) - binding.github.setOnClickListener(this) - binding.privacy.setOnClickListener(this) - binding.footer.setOnClickListener(this) - - binding.maxApps0.setOnClickListener(this) - binding.maxApps1.setOnClickListener(this) - binding.maxApps2.setOnClickListener(this) - binding.maxApps3.setOnClickListener(this) - binding.maxApps4.setOnClickListener(this) - binding.maxApps5.setOnClickListener(this) - binding.maxApps6.setOnClickListener(this) - binding.maxApps7.setOnClickListener(this) - binding.maxApps8.setOnClickListener(this) - - binding.textSizeMinus.setOnClickListener(this) - binding.textSizePlus.setOnClickListener(this) - - binding.dailyWallpaper.setOnLongClickListener(this) - binding.alignment.setOnLongClickListener(this) - binding.appThemeText.setOnLongClickListener(this) - binding.swipeLeftApp.setOnLongClickListener(this) - binding.swipeRightApp.setOnLongClickListener(this) - binding.toggleLock.setOnLongClickListener(this) - } - - private fun initObservers() { - if (prefs.firstSettingsOpen) { - viewModel.showDialog.postValue(Constants.Dialog.ABOUT) - prefs.firstSettingsOpen = false - } - viewModel.isOlauncherDefault.observe(viewLifecycleOwner) { - if (it) { - binding.setLauncher.text = getString(R.string.change_default_launcher) - prefs.toShowHintCounter += 1 - } - } - viewModel.homeAppAlignment.observe(viewLifecycleOwner) { - populateAlignment() - } - viewModel.updateSwipeApps.observe(viewLifecycleOwner) { - populateSwipeApps() - } - } - - private fun toggleSwipeLeft() { - prefs.swipeLeftEnabled = !prefs.swipeLeftEnabled - if (prefs.swipeLeftEnabled) { - binding.swipeLeftApp.setTextColor(requireContext().getColorFromAttr(R.attr.primaryColor)) - requireContext().showToast(getString(R.string.swipe_left_app_enabled)) - } else { - binding.swipeLeftApp.setTextColor(requireContext().getColorFromAttr(R.attr.primaryColorTrans50)) - requireContext().showToast(getString(R.string.swipe_left_app_disabled)) - } - } - - private fun toggleSwipeRight() { - prefs.swipeRightEnabled = !prefs.swipeRightEnabled - if (prefs.swipeRightEnabled) { - binding.swipeRightApp.setTextColor(requireContext().getColorFromAttr(R.attr.primaryColor)) - requireContext().showToast(getString(R.string.swipe_right_app_enabled)) - } else { - binding.swipeRightApp.setTextColor(requireContext().getColorFromAttr(R.attr.primaryColorTrans50)) - requireContext().showToast(getString(R.string.swipe_right_app_disabled)) - } - } - - private fun toggleStatusBar() { - prefs.showStatusBar = !prefs.showStatusBar - populateStatusBar() - } - - private fun populateStatusBar() { - if (prefs.showStatusBar) { - showStatusBar() - binding.statusBar.text = getString(R.string.on) - } else { - hideStatusBar() - binding.statusBar.text = getString(R.string.off) - } - } - - private fun toggleDateTime(selected: Int) { - prefs.dateTimeVisibility = selected - populateDateTime() - viewModel.toggleDateTime() - } - - private fun populateDateTime() { - binding.dateTime.text = getString( - when (prefs.dateTimeVisibility) { - Constants.DateTime.DATE_ONLY -> R.string.date - Constants.DateTime.ON -> R.string.on - else -> R.string.off - } - ) - } - - private fun showStatusBar() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) - requireActivity().window.insetsController?.show(WindowInsets.Type.statusBars()) - else - @Suppress("DEPRECATION", "InlinedApi") - requireActivity().window.decorView.apply { - systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - } - } - - private fun hideStatusBar() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) - requireActivity().window.insetsController?.hide(WindowInsets.Type.statusBars()) - else { - @Suppress("DEPRECATION") - requireActivity().window.decorView.apply { - systemUiVisibility = View.SYSTEM_UI_FLAG_IMMERSIVE or View.SYSTEM_UI_FLAG_FULLSCREEN - } - } - } - - private fun showHiddenApps() { - if (prefs.hiddenApps.isEmpty()) { - requireContext().showToast(getString(R.string.no_hidden_apps)) - return - } - viewModel.getHiddenApps() - findNavController().navigate( - R.id.action_settingsFragment_to_appListFragment, - bundleOf(Constants.Key.FLAG to Constants.FLAG_HIDDEN_APPS) - ) - } - - private fun checkAdminPermission() { - val isAdmin: Boolean = deviceManager.isAdminActive(componentName) - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) - prefs.lockModeOn = isAdmin - } - - private fun toggleAccessibilityVisibility(show: Boolean) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) - binding.notWorking.visibility = View.VISIBLE - if (isAccessServiceEnabled(requireContext())) - binding.actionAccessibility.text = getString(R.string.disable) - binding.accessibilityLayout.isVisible = show - binding.scrollView.animateAlpha(if (show) 0.5f else 1f) - } - - private fun openAccessibilityService() { - toggleAccessibilityVisibility(false) - // prefs.lockModeOn = true - populateLockSettings() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) - startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)) - } - - private fun toggleLockMode() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - if (!prefs.lockModeOn && !isAccessServiceEnabled(requireContext())) { - toggleAccessibilityVisibility(true) - return - } - prefs.lockModeOn = !prefs.lockModeOn - } else { - val isAdmin: Boolean = deviceManager.isAdminActive(componentName) - if (isAdmin) { - removeActiveAdmin("Admin permission removed.") - prefs.lockModeOn = false - } else { - val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN) - intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, componentName) - intent.putExtra( - DevicePolicyManager.EXTRA_ADD_EXPLANATION, - getString(R.string.admin_permission_message) - ) - requireActivity().startActivityForResult(intent, Constants.REQUEST_CODE_ENABLE_ADMIN) - } - } - populateLockSettings() - } - - private fun removeActiveAdmin(toastMessage: String? = null) { - try { - deviceManager.removeActiveAdmin(componentName) // for backward compatibility - requireContext().showToast(toastMessage) - } catch (e: Exception) { - e.printStackTrace() - } - } - - private fun removeWallpaper() { - if (requireContext().isEinkDisplay()) { - prefs.appTheme = AppCompatDelegate.MODE_NIGHT_NO - setPlainWallpaper(requireContext(), android.R.color.white) - } else { - prefs.appTheme = AppCompatDelegate.MODE_NIGHT_YES - setPlainWallpaper(requireContext(), android.R.color.black) - } - if (!prefs.dailyWallpaper) return - prefs.dailyWallpaper = false - populateWallpaperText() - viewModel.cancelWallpaperWorker() - } - - private fun toggleDailyWallpaperUpdate() { - if (prefs.dailyWallpaper.not() && prefs.appTheme == AppCompatDelegate.MODE_NIGHT_YES && viewModel.isOlauncherDefault.value == false) { - requireContext().showToast(R.string.set_as_default_launcher_first) - return - } - prefs.dailyWallpaper = !prefs.dailyWallpaper - populateWallpaperText() - if (prefs.dailyWallpaper) { - viewModel.setWallpaperWorker() - showWallpaperToasts() - } else viewModel.cancelWallpaperWorker() - } - - private fun showWallpaperToasts() { - if (isOlauncherDefault(requireContext())) - requireContext().showToast(getString(R.string.your_wallpaper_will_update_shortly)) - else - requireContext().showToast(getString(R.string.olauncher_is_not_default_launcher), Toast.LENGTH_LONG) - } - - private fun updateHomeAppsNum(num: Int) { - binding.homeAppsNum.text = num.toString() - binding.appsNumSelectLayout.visibility = View.GONE - prefs.homeAppsNum = num - viewModel.refreshHome(true) - } - - private var pendingTextSizeScale: Float = -1f - - private fun adjustTextSizePreview(delta: Float) { - val maxScale = if (isTablet(requireContext())) 2.0f else 1.5f - val current = if (pendingTextSizeScale > 0) pendingTextSizeScale else prefs.textSizeScale - val newScale = Math.round((current + delta) * 10f) / 10f - val clamped = newScale.coerceIn(0.5f, maxScale) - if (clamped == current) return - pendingTextSizeScale = clamped - val formatted = String.format("%.1f", clamped) - binding.textSizeValue.text = formatted - binding.textSizeCurrent.text = formatted - } - - private fun applyTextSizeScale() { - if (pendingTextSizeScale < 0 || prefs.textSizeScale == pendingTextSizeScale) { - pendingTextSizeScale = -1f - return - } - prefs.textSizeScale = pendingTextSizeScale - pendingTextSizeScale = -1f - requireActivity().recreate() - } - - private fun toggleKeyboardText() { - if (prefs.autoShowKeyboard && prefs.keyboardMessageShown.not()) { - viewModel.showDialog.postValue(Constants.Dialog.KEYBOARD) - prefs.keyboardMessageShown = true - } else { - prefs.autoShowKeyboard = !prefs.autoShowKeyboard - populateKeyboardText() - } - } - - private fun updateTheme(appTheme: Int) { - if (AppCompatDelegate.getDefaultNightMode() == appTheme) return - prefs.appTheme = appTheme - populateAppThemeText(appTheme) - setAppTheme(appTheme) - } - - private fun setAppTheme(theme: Int) { - if (AppCompatDelegate.getDefaultNightMode() == theme) return - if (prefs.dailyWallpaper) { - setPlainWallpaper(theme) - viewModel.setWallpaperWorker() - } - requireActivity().recreate() - } - - private fun setPlainWallpaper(appTheme: Int) { - when (appTheme) { - AppCompatDelegate.MODE_NIGHT_YES -> setPlainWallpaper(requireContext(), android.R.color.black) - AppCompatDelegate.MODE_NIGHT_NO -> setPlainWallpaper(requireContext(), android.R.color.white) - else -> { - if (requireContext().isDarkThemeOn()) - setPlainWallpaper(requireContext(), android.R.color.black) - else setPlainWallpaper(requireContext(), android.R.color.white) - } - } - } - - private fun populateAppThemeText(appTheme: Int = prefs.appTheme) { - when (appTheme) { - AppCompatDelegate.MODE_NIGHT_YES -> binding.appThemeText.text = getString(R.string.dark) - AppCompatDelegate.MODE_NIGHT_NO -> binding.appThemeText.text = getString(R.string.light) - else -> binding.appThemeText.text = getString(R.string.system_default) - } - } - - private fun populateTextSize() { - val formatted = String.format("%.1f", prefs.textSizeScale) - binding.textSizeValue.text = formatted - binding.textSizeCurrent.text = formatted - } - - private fun populateScreenTimeOnOff() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - if (requireContext().appUsagePermissionGranted()) binding.screenTimeOnOff.text = getString(R.string.on) - else binding.screenTimeOnOff.text = getString(R.string.off) - } else binding.screenTimeLayout.visibility = View.GONE - } - - private fun populateKeyboardText() { - if (prefs.autoShowKeyboard) binding.autoShowKeyboard.text = getString(R.string.on) - else binding.autoShowKeyboard.text = getString(R.string.off) - } - - private fun populateWallpaperText() { - if (prefs.dailyWallpaper) binding.dailyWallpaper.text = getString(R.string.on) - else binding.dailyWallpaper.text = getString(R.string.off) - } - - private fun updateHomeBottomAlignment() { - if (viewModel.isOlauncherDefault.value != true) { - requireContext().showToast(getString(R.string.please_set_olauncher_as_default_first), Toast.LENGTH_LONG) - return - } - prefs.homeBottomAlignment = !prefs.homeBottomAlignment - populateAlignment() - viewModel.updateHomeAlignment(prefs.homeAlignment) - } - - private fun populateAlignment() { - when (prefs.homeAlignment) { - Gravity.START -> binding.alignment.text = getString(R.string.left) - Gravity.CENTER -> binding.alignment.text = getString(R.string.center) - Gravity.END -> binding.alignment.text = getString(R.string.right) - } - binding.alignmentBottom.text = if (prefs.homeBottomAlignment) - getString(R.string.bottom_on) - else getString(R.string.bottom_off) - } - - // Home button for recents feature disabled - // private fun toggleHomeButtonRecents() { - // if (!prefs.homeButtonShowRecents && !isAccessServiceEnabled(requireContext())) { - // toggleAccessibilityVisibility(true) - // return - // } - // prefs.homeButtonShowRecents = !prefs.homeButtonShowRecents - // populateHomeButtonRecents() - // } - - // private fun populateHomeButtonRecents() { - // binding.homeButtonRecents.text = getString( - // if (prefs.homeButtonShowRecents && isAccessServiceEnabled(requireContext())) R.string.on - // else R.string.off - // ) - // } - - private fun populateLockSettings() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - binding.toggleLock.text = getString( - if (prefs.lockModeOn && isAccessServiceEnabled(requireContext())) R.string.on - else R.string.off - ) - } else { - binding.toggleLock.text = getString( - if (prefs.lockModeOn) R.string.on - else R.string.off - ) - } - } - - private fun populateSwipeDownAction() { - binding.swipeDownAction.text = when (prefs.swipeDownAction) { - Constants.SwipeDownAction.NOTIFICATIONS -> getString(R.string.notifications) - else -> getString(R.string.search) - } - } - - private fun updateSwipeDownAction(swipeDownFor: Int) { - if (prefs.swipeDownAction == swipeDownFor) return - prefs.swipeDownAction = swipeDownFor - populateSwipeDownAction() - } - - private fun populateSwipeApps() { - binding.swipeLeftApp.text = prefs.appNameSwipeLeft - binding.swipeRightApp.text = prefs.appNameSwipeRight - if (!prefs.swipeLeftEnabled) - binding.swipeLeftApp.setTextColor(requireContext().getColorFromAttr(R.attr.primaryColorTrans50)) - if (!prefs.swipeRightEnabled) - binding.swipeRightApp.setTextColor(requireContext().getColorFromAttr(R.attr.primaryColorTrans50)) - } - -// private fun populateDigitalWellbeing() { -// binding.digitalWellbeing.isVisible = requireContext().isPackageInstalled(Constants.DIGITAL_WELLBEING_PACKAGE_NAME).not() -// && requireContext().isPackageInstalled(Constants.DIGITAL_WELLBEING_SAMSUNG_PACKAGE_NAME).not() -// && prefs.hideDigitalWellbeing.not() -// } - - private fun showAppListIfEnabled(flag: Int) { - if ((flag == Constants.FLAG_SET_SWIPE_LEFT_APP) and !prefs.swipeLeftEnabled) { - requireContext().showToast(getString(R.string.long_press_to_enable)) - return - } - if ((flag == Constants.FLAG_SET_SWIPE_RIGHT_APP) and !prefs.swipeRightEnabled) { - requireContext().showToast(getString(R.string.long_press_to_enable)) - return - } - viewModel.getAppList(true) - findNavController().navigate( - R.id.action_settingsFragment_to_appListFragment, - bundleOf(Constants.Key.FLAG to flag) - ) - } - - private fun populateActionHints() { - if (prefs.aboutClicked.not()) - binding.aboutOlauncher.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_info, 0) - if (viewModel.isOlauncherDefault.value != true) return - if (prefs.rateClicked.not() && prefs.toShowHintCounter > Constants.HINT_RATE_US && prefs.toShowHintCounter < Constants.HINT_RATE_US + 100) - binding.rate.setCompoundDrawablesWithIntrinsicBounds(0, android.R.drawable.arrow_down_float, 0, 0) - } - - private fun populateProMessage() { - if (prefs.proMessageShown.not() && prefs.userState == Constants.UserState.SHARE) { - prefs.proMessageShown = true - viewModel.showDialog.postValue(Constants.Dialog.PRO_MESSAGE) - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - override fun onDestroy() { - viewModel.checkForMessages.call() - super.onDestroy() - } -} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/MainActivity.kt b/app/src/main/java/app/sidephonelauncher/MainActivity.kt new file mode 100644 index 0000000..2454e52 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/MainActivity.kt @@ -0,0 +1,418 @@ +package app.sidephonelauncher + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.ActivityInfo +import android.content.res.Configuration +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import android.view.KeyEvent +import android.view.View +import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.navigation.NavController +import androidx.navigation.findNavController +import app.sidephonelauncher.data.Constants +import app.sidephonelauncher.data.Prefs +import app.sidephonelauncher.databinding.ActivityMainBinding +import app.sidephonelauncher.helper.getColorFromAttr +import app.sidephonelauncher.helper.hasBeenDays +import app.sidephonelauncher.helper.hasBeenHours +import app.sidephonelauncher.helper.hasBeenMinutes +import app.sidephonelauncher.helper.isDarkThemeOn +import app.sidephonelauncher.helper.isDaySince +import app.sidephonelauncher.helper.isDefaultLauncher +import app.sidephonelauncher.helper.isEinkDisplay +import app.sidephonelauncher.helper.isOlauncherDefault +import app.sidephonelauncher.helper.isTablet +import app.sidephonelauncher.helper.openUrl +import app.sidephonelauncher.helper.rateApp +import app.sidephonelauncher.helper.resetLauncherViaFakeActivity +import app.sidephonelauncher.helper.setPlainWallpaper +import app.sidephonelauncher.helper.shareApp +import app.sidephonelauncher.helper.showLauncherSelector +import app.sidephonelauncher.helper.showToast +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.Calendar + +class MainActivity : AppCompatActivity() { + + private lateinit var prefs: Prefs + private lateinit var navController: NavController + private lateinit var viewModel: MainViewModel + private lateinit var binding: ActivityMainBinding + private var timerJob: Job? = null + private var isResumed = false + private var profileReceiver: BroadcastReceiver? = null + +// override fun onBackPressed() { +// if (navController.currentDestination?.id != R.id.mainFragment) +// super.onBackPressed() +// } + + override fun attachBaseContext(context: Context) { + val newConfig = Configuration(context.resources.configuration) + newConfig.fontScale = Prefs(context).textSizeScale + applyOverrideConfiguration(newConfig) + super.attachBaseContext(context) + } + + override fun onCreate(savedInstanceState: Bundle?) { + prefs = Prefs(this) + if (isEinkDisplay()) prefs.appTheme = AppCompatDelegate.MODE_NIGHT_NO + AppCompatDelegate.setDefaultNightMode(prefs.appTheme) + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + setDefaultKeyMode(Activity.DEFAULT_KEYS_DISABLE) + + navController = this.findNavController(R.id.nav_host_fragment) + viewModel = ViewModelProvider(this)[MainViewModel::class.java] + + val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (navController.currentDestination?.id != R.id.mainFragment) { + // then we might want to finish the activity or disable this callback. + if (navController.popBackStack()) { + // Successfully popped back + } else { + // if you want other system/activity level handling + } + } else { + binding.messageLayout.visibility = View.GONE + } + } + } + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + + if (prefs.firstOpen) { + viewModel.firstOpen(true) + prefs.firstOpen = false + prefs.firstOpenTime = System.currentTimeMillis() + viewModel.setDefaultClockApp() + viewModel.resetLauncherLiveData.call() + } + + initClickListeners() + initObservers(viewModel) + viewModel.getAppList() + setupOrientation() + + window.addFlags(FLAG_LAYOUT_NO_LIMITS) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + profileReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + viewModel.isPrivateSpaceToggling = false + viewModel.getPrivateSpaceAppList() + } + } + val filter = IntentFilter().apply { + addAction(Intent.ACTION_PROFILE_AVAILABLE) + addAction(Intent.ACTION_PROFILE_UNAVAILABLE) + } + registerReceiver(profileReceiver, filter) + } + } + + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + val dpadKey = when (event.keyCode) { + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_DPAD_CENTER, + KeyEvent.KEYCODE_ENTER -> true + else -> false + } + if (dpadKey) { + val focusedView = currentFocus + if (focusedView == null || focusedView == window.decorView) { + val navHost = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) + val currentFragment = navHost?.childFragmentManager?.fragments?.firstOrNull() + val fragmentView = currentFragment?.view + if (fragmentView != null && fragmentView.dispatchKeyEvent(event)) return true + } + } + return super.dispatchKeyEvent(event) + } + + override fun onStart() { + super.onStart() + restartLauncherOrCheckTheme() + } + + override fun onResume() { + super.onResume() + isResumed = true + viewModel.isPrivateSpaceToggling = false + } + + override fun onStop() { + isResumed = false + backToHomeScreen() + super.onStop() + } + + override fun onUserLeaveHint() { + backToHomeScreen() + super.onUserLeaveHint() + } + + override fun onNewIntent(intent: Intent?) { + // Home button for recents feature disabled + // val alreadyHome = navController.currentDestination?.id == R.id.mainFragment + backToHomeScreen() + // if (alreadyHome && isResumed && prefs.homeButtonShowRecents) + // viewModel.showRecentApps.call() + super.onNewIntent(intent) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + AppCompatDelegate.setDefaultNightMode(prefs.appTheme) + if (prefs.dailyWallpaper && AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) { + setPlainWallpaper() + viewModel.setWallpaperWorker() + recreate() + } + } + + private fun initClickListeners() { + binding.ivClose.setOnClickListener { + binding.messageLayout.visibility = View.GONE + } + } + + private fun initObservers(viewModel: MainViewModel) { + viewModel.launcherResetFailed.observe(this) { + openLauncherChooser(it) + } + viewModel.resetLauncherLiveData.observe(this) { + if (isDefaultLauncher() || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) + resetLauncherViaFakeActivity() + else + showLauncherSelector(Constants.REQUEST_CODE_LAUNCHER_SELECTOR) + } + viewModel.checkForMessages.observe(this) { + checkForMessages() + } + viewModel.showDialog.observe(this) { + when (it) { + Constants.Dialog.ABOUT -> { + showMessageDialog(R.string.app_name, R.string.welcome_to_olauncher_settings, R.string.okay) { + binding.messageLayout.visibility = View.GONE + } + } + + Constants.Dialog.WALLPAPER -> { + prefs.wallpaperMsgShown = true + prefs.userState = Constants.UserState.REVIEW + showMessageDialog(R.string.did_you_know, R.string.wallpaper_message, R.string.enable) { + prefs.dailyWallpaper = true + viewModel.setWallpaperWorker() + showToast(getString(R.string.your_wallpaper_will_update_shortly)) + } + } + + Constants.Dialog.REVIEW -> { + prefs.userState = Constants.UserState.RATE + showMessageDialog(R.string.hey, R.string.review_message, R.string.leave_a_review) { + prefs.rateClicked = true + showToast("😇❤️") + rateApp() + } + } + + Constants.Dialog.RATE -> { + prefs.userState = Constants.UserState.SHARE + showMessageDialog(R.string.app_name, R.string.rate_us_message, R.string.rate_now) { + prefs.rateClicked = true + showToast("🤩❤️") + rateApp() + } + } + + Constants.Dialog.SHARE -> { + prefs.shareShownTime = System.currentTimeMillis() + showMessageDialog(R.string.hey, R.string.share_message, R.string.share_now) { + showToast("😊❤️") + shareApp() + } + } + + Constants.Dialog.HIDDEN -> { + showMessageDialog(R.string.hidden_apps, R.string.hidden_apps_message, R.string.okay) { + } + } + + Constants.Dialog.KEYBOARD -> { + showMessageDialog(R.string.app_name, R.string.keyboard_message, R.string.okay) { + } + } + + Constants.Dialog.DIGITAL_WELLBEING -> { + showMessageDialog(R.string.screen_time, R.string.app_usage_message, R.string.permission) { + startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)) + } + } + + Constants.Dialog.PRO_MESSAGE -> { + showMessageDialog(R.string.hey, R.string.pro_message, R.string.olauncher_pro) { + openUrl(Constants.URL_OLAUNCHER_PRO) + } + } + } + } + } + + private fun showMessageDialog(title: Int, message: Int, action: Int, clickListener: () -> Unit) { + binding.tvTitle.text = getString(title) + binding.tvMessage.text = getString(message) + binding.tvAction.text = getString(action) + binding.tvAction.setOnClickListener { + clickListener() + binding.messageLayout.visibility = View.GONE + } + binding.messageLayout.visibility = View.VISIBLE + } + + private fun checkForMessages() { + if (prefs.firstOpenTime == 0L) + prefs.firstOpenTime = System.currentTimeMillis() + + val calendar = Calendar.getInstance() + val dayOfYear = calendar.get(Calendar.DAY_OF_YEAR) + if (dayOfYear == 1 && dayOfYear != prefs.shownOnDayOfYear) { + prefs.shownOnDayOfYear = dayOfYear + showMessageDialog(R.string.hey, R.string.new_year_wish, R.string.cheers) {} + return + } else if (dayOfYear == 32 && dayOfYear != prefs.shownOnDayOfYear) { + prefs.shownOnDayOfYear = dayOfYear + showMessageDialog(R.string.hey, R.string.new_year_wish_1, R.string.cheers) {} + return + } + + when (prefs.userState) { + Constants.UserState.START -> { + if (prefs.firstOpenTime.hasBeenMinutes(10)) + prefs.userState = Constants.UserState.WALLPAPER + } + + Constants.UserState.WALLPAPER -> { + if (prefs.wallpaperMsgShown || prefs.dailyWallpaper) + prefs.userState = Constants.UserState.REVIEW + else if (isOlauncherDefault(this)) + viewModel.showDialog.postValue(Constants.Dialog.WALLPAPER) + } + + Constants.UserState.REVIEW -> { + if (prefs.rateClicked) + prefs.userState = Constants.UserState.SHARE + else if (isOlauncherDefault(this) && prefs.firstOpenTime.hasBeenHours(1)) + viewModel.showDialog.postValue(Constants.Dialog.REVIEW) + } + + Constants.UserState.RATE -> { + if (prefs.rateClicked) + prefs.userState = Constants.UserState.SHARE + else if (isOlauncherDefault(this) + && prefs.firstOpenTime.isDaySince() >= 7 + && calendar.get(Calendar.HOUR_OF_DAY) >= 16 + ) viewModel.showDialog.postValue(Constants.Dialog.RATE) + } + + Constants.UserState.SHARE -> { + if (isOlauncherDefault(this) && prefs.firstOpenTime.hasBeenDays(14) + && prefs.shareShownTime.isDaySince() >= 70 + && calendar.get(Calendar.HOUR_OF_DAY) >= 16 + ) viewModel.showDialog.postValue(Constants.Dialog.SHARE) + } + } + } + + @SuppressLint("SourceLockedOrientationActivity") + private fun setupOrientation() { + if (isTablet(this) || Build.VERSION.SDK_INT == Build.VERSION_CODES.O) + return + // In Android 8.0, windowIsTranslucent cannot be used with screenOrientation=portrait + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + + private fun backToHomeScreen() { + if (viewModel.isPrivateSpaceToggling) return + binding.messageLayout.visibility = View.GONE + if (navController.currentDestination?.id != R.id.mainFragment) + navController.popBackStack(R.id.mainFragment, false) + } + + private fun setPlainWallpaper() { + if (this.isDarkThemeOn()) + setPlainWallpaper(this, android.R.color.black) + else setPlainWallpaper(this, android.R.color.white) + } + + private fun openLauncherChooser(resetFailed: Boolean) { + if (resetFailed) { + val intent = Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS) + startActivity(intent) + } + } + + private fun restartLauncherOrCheckTheme(forceRestart: Boolean = false) { + if (forceRestart || prefs.launcherRestartTimestamp.hasBeenHours(4)) { + prefs.launcherRestartTimestamp = System.currentTimeMillis() + cacheDir.deleteRecursively() + recreate() + } else + checkTheme() + } + + private fun checkTheme() { + timerJob?.cancel() + timerJob = lifecycleScope.launch { + delay(200) + if ((prefs.appTheme == AppCompatDelegate.MODE_NIGHT_YES && getColorFromAttr(R.attr.primaryColor) != getColor(R.color.white)) + || (prefs.appTheme == AppCompatDelegate.MODE_NIGHT_NO && getColorFromAttr(R.attr.primaryColor) != getColor(R.color.black)) + ) + restartLauncherOrCheckTheme(true) + } + } + + override fun onDestroy() { + profileReceiver?.let { + try { + unregisterReceiver(it) + } catch (_: Exception) { + } + } + super.onDestroy() + } + + @Deprecated("Deprecated in Java") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + Constants.REQUEST_CODE_ENABLE_ADMIN -> { + if (resultCode == Activity.RESULT_OK) + prefs.lockModeOn = true + } + + Constants.REQUEST_CODE_LAUNCHER_SELECTOR -> { + if (resultCode == Activity.RESULT_OK) + resetLauncherViaFakeActivity() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/MainViewModel.kt b/app/src/main/java/app/sidephonelauncher/MainViewModel.kt new file mode 100644 index 0000000..4c80fc3 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/MainViewModel.kt @@ -0,0 +1,521 @@ +package app.sidephonelauncher + +import android.app.Application +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherApps +import android.os.Build +import android.os.UserHandle +import android.os.UserManager +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import app.sidephonelauncher.data.AppModel +import app.sidephonelauncher.data.Constants +import app.sidephonelauncher.data.Prefs +import app.sidephonelauncher.helper.SingleLiveEvent +import app.sidephonelauncher.helper.WallpaperWorker +import app.sidephonelauncher.helper.formattedTimeSpent +import app.sidephonelauncher.helper.getAppsList +import app.sidephonelauncher.helper.getPrivateSpaceApps +import app.sidephonelauncher.helper.getPrivateSpaceUserHandle +import app.sidephonelauncher.helper.hasBeenMinutes +import app.sidephonelauncher.helper.isOlauncherDefault +import app.sidephonelauncher.helper.isPackageInstalled +import app.sidephonelauncher.helper.isPrivateSpaceLocked +import app.sidephonelauncher.helper.showToast +import app.sidephonelauncher.helper.usageStats.EventLogWrapper +import kotlinx.coroutines.launch +import java.util.Calendar +import java.util.concurrent.TimeUnit + + +class MainViewModel(application: Application) : AndroidViewModel(application) { + private val appContext by lazy { application.applicationContext } + private val prefs = Prefs(appContext) + + val firstOpen = MutableLiveData() + val refreshHome = MutableLiveData() + val toggleDateTime = MutableLiveData() + val updateSwipeApps = MutableLiveData() + val appList = MutableLiveData?>() + val hiddenApps = MutableLiveData?>() + val isOlauncherDefault = MutableLiveData() + val launcherResetFailed = MutableLiveData() + val homeAppAlignment = MutableLiveData() + val screenTimeValue = MutableLiveData() + + val privateSpaceApps = MutableLiveData?>() + val privateSpaceLocked = MutableLiveData() + val privateSpaceAvailable = MutableLiveData() + + // Suppress backToHomeScreen during Private Space lock/unlock auth + var isPrivateSpaceToggling = false + + val showDialog = SingleLiveEvent() + val checkForMessages = SingleLiveEvent() + val resetLauncherLiveData = SingleLiveEvent() + // Home button for recents feature disabled + // val showRecentApps = SingleLiveEvent() + + fun selectedApp(appModel: AppModel, flag: Int) { + if (appModel is AppModel.PrivateSpaceHeader) return + when (flag) { + Constants.FLAG_LAUNCH_APP -> { + when (appModel) { + is AppModel.PinnedShortcut -> launchShortcut(appModel) + is AppModel.App -> + launchApp(appModel.appPackage, appModel.activityClassName, appModel.user) + + else -> {} + } + } + + Constants.FLAG_HIDDEN_APPS -> { + if (appModel is AppModel.App) { + launchApp(appModel.appPackage, appModel.activityClassName, appModel.user) + } + } + + Constants.FLAG_SET_HOME_APP_1 -> saveHomeApp(appModel, 1) + Constants.FLAG_SET_HOME_APP_2 -> saveHomeApp(appModel, 2) + Constants.FLAG_SET_HOME_APP_3 -> saveHomeApp(appModel, 3) + Constants.FLAG_SET_HOME_APP_4 -> saveHomeApp(appModel, 4) + Constants.FLAG_SET_HOME_APP_5 -> saveHomeApp(appModel, 5) + Constants.FLAG_SET_HOME_APP_6 -> saveHomeApp(appModel, 6) + Constants.FLAG_SET_HOME_APP_7 -> saveHomeApp(appModel, 7) + Constants.FLAG_SET_HOME_APP_8 -> saveHomeApp(appModel, 8) + + Constants.FLAG_SET_SWIPE_LEFT_APP -> saveSwipeApp(appModel, isLeft = true) + Constants.FLAG_SET_SWIPE_RIGHT_APP -> saveSwipeApp(appModel, isLeft = false) + Constants.FLAG_SET_CLOCK_APP -> saveClockApp(appModel) + Constants.FLAG_SET_CALENDAR_APP -> saveCalendarApp(appModel) + Constants.FLAG_SET_SCREEN_TIME_APP -> saveScreenTimeApp(appModel) + } + } + + private fun launchShortcut(appModel: AppModel.PinnedShortcut) { + val launcher = appContext.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + val query = LauncherApps.ShortcutQuery().apply { + setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED) + } + launcher.getShortcuts(query, appModel.user)?.find { it.id == appModel.shortcutId } + ?.let { shortcut -> + launcher.startShortcut(shortcut, null, null) + } + } + + private fun saveHomeApp(appModel: AppModel, position: Int) { + when (appModel) { + is AppModel.PrivateSpaceHeader -> return + is AppModel.App -> { + when (position) { + 1 -> { + prefs.appName1 = appModel.appLabel + prefs.appPackage1 = appModel.appPackage + prefs.appUser1 = appModel.user.toString() + prefs.appActivityClassName1 = appModel.activityClassName + prefs.isShortcut1 = false + prefs.shortcutId1 = "" + } + + 2 -> { + prefs.appName2 = appModel.appLabel + prefs.appPackage2 = appModel.appPackage + prefs.appUser2 = appModel.user.toString() + prefs.appActivityClassName2 = appModel.activityClassName + prefs.isShortcut2 = false + prefs.shortcutId2 = "" + } + + 3 -> { + prefs.appName3 = appModel.appLabel + prefs.appPackage3 = appModel.appPackage + prefs.appUser3 = appModel.user.toString() + prefs.appActivityClassName3 = appModel.activityClassName + prefs.isShortcut3 = false + prefs.shortcutId3 = "" + } + + 4 -> { + prefs.appName4 = appModel.appLabel + prefs.appPackage4 = appModel.appPackage + prefs.appUser4 = appModel.user.toString() + prefs.appActivityClassName4 = appModel.activityClassName + prefs.isShortcut4 = false + prefs.shortcutId4 = "" + } + + 5 -> { + prefs.appName5 = appModel.appLabel + prefs.appPackage5 = appModel.appPackage + prefs.appUser5 = appModel.user.toString() + prefs.appActivityClassName5 = appModel.activityClassName + prefs.isShortcut5 = false + prefs.shortcutId5 = "" + } + + 6 -> { + prefs.appName6 = appModel.appLabel + prefs.appPackage6 = appModel.appPackage + prefs.appUser6 = appModel.user.toString() + prefs.appActivityClassName6 = appModel.activityClassName + prefs.isShortcut6 = false + prefs.shortcutId6 = "" + } + + 7 -> { + prefs.appName7 = appModel.appLabel + prefs.appPackage7 = appModel.appPackage + prefs.appUser7 = appModel.user.toString() + prefs.appActivityClassName7 = appModel.activityClassName + prefs.isShortcut7 = false + prefs.shortcutId7 = "" + } + + 8 -> { + prefs.appName8 = appModel.appLabel + prefs.appPackage8 = appModel.appPackage + prefs.appUser8 = appModel.user.toString() + prefs.appActivityClassName8 = appModel.activityClassName + prefs.isShortcut8 = false + prefs.shortcutId8 = "" + } + } + } + + is AppModel.PinnedShortcut -> { + when (position) { + 1 -> { + prefs.appName1 = appModel.appLabel + prefs.appPackage1 = appModel.appPackage + prefs.appUser1 = appModel.user.toString() + prefs.appActivityClassName1 = null + prefs.isShortcut1 = true + prefs.shortcutId1 = appModel.shortcutId + } + + 2 -> { + prefs.appName2 = appModel.appLabel + prefs.appPackage2 = appModel.appPackage + prefs.appUser2 = appModel.user.toString() + prefs.appActivityClassName2 = null + prefs.isShortcut2 = true + prefs.shortcutId2 = appModel.shortcutId + } + + 3 -> { + prefs.appName3 = appModel.appLabel + prefs.appPackage3 = appModel.appPackage + prefs.appUser3 = appModel.user.toString() + prefs.appActivityClassName3 = null + prefs.isShortcut3 = true + prefs.shortcutId3 = appModel.shortcutId + } + + 4 -> { + prefs.appName4 = appModel.appLabel + prefs.appPackage4 = appModel.appPackage + prefs.appUser4 = appModel.user.toString() + prefs.appActivityClassName4 = null + prefs.isShortcut4 = true + prefs.shortcutId4 = appModel.shortcutId + } + + 5 -> { + prefs.appName5 = appModel.appLabel + prefs.appPackage5 = appModel.appPackage + prefs.appUser5 = appModel.user.toString() + prefs.appActivityClassName5 = null + prefs.isShortcut5 = true + prefs.shortcutId5 = appModel.shortcutId + } + + 6 -> { + prefs.appName6 = appModel.appLabel + prefs.appPackage6 = appModel.appPackage + prefs.appUser6 = appModel.user.toString() + prefs.appActivityClassName6 = null + prefs.isShortcut6 = true + prefs.shortcutId6 = appModel.shortcutId + } + + 7 -> { + prefs.appName7 = appModel.appLabel + prefs.appPackage7 = appModel.appPackage + prefs.appUser7 = appModel.user.toString() + prefs.appActivityClassName7 = null + prefs.isShortcut7 = true + prefs.shortcutId7 = appModel.shortcutId + } + + 8 -> { + prefs.appName8 = appModel.appLabel + prefs.appPackage8 = appModel.appPackage + prefs.appUser8 = appModel.user.toString() + prefs.appActivityClassName8 = null + prefs.isShortcut8 = true + prefs.shortcutId8 = appModel.shortcutId + } + } + } + } + refreshHome(false) + } + + private fun saveSwipeApp(appModel: AppModel, isLeft: Boolean) { + when (appModel) { + is AppModel.PrivateSpaceHeader -> return + is AppModel.App -> { + if (isLeft) { + prefs.appNameSwipeLeft = appModel.appLabel + prefs.appPackageSwipeLeft = appModel.appPackage + prefs.appUserSwipeLeft = appModel.user.toString() + prefs.appActivityClassNameSwipeLeft = appModel.activityClassName + prefs.isShortcutSwipeLeft = false + prefs.shortcutIdSwipeLeft = "" + } else { + prefs.appNameSwipeRight = appModel.appLabel + prefs.appPackageSwipeRight = appModel.appPackage + prefs.appUserSwipeRight = appModel.user.toString() + prefs.appActivityClassNameRight = appModel.activityClassName + prefs.isShortcutSwipeRight = false + prefs.shortcutIdSwipeRight = "" + } + } + + is AppModel.PinnedShortcut -> { + if (isLeft) { + prefs.appNameSwipeLeft = appModel.appLabel + prefs.appPackageSwipeLeft = appModel.appPackage + prefs.appUserSwipeLeft = appModel.user.toString() + prefs.appActivityClassNameSwipeLeft = null + prefs.isShortcutSwipeLeft = true + prefs.shortcutIdSwipeLeft = appModel.shortcutId + } else { + prefs.appNameSwipeRight = appModel.appLabel + prefs.appPackageSwipeRight = appModel.appPackage + prefs.appUserSwipeRight = appModel.user.toString() + prefs.appActivityClassNameRight = null + prefs.isShortcutSwipeRight = true + prefs.shortcutIdSwipeRight = appModel.shortcutId + } + } + } + updateSwipeApps() + } + + private fun saveClockApp(appModel: AppModel) { + if (appModel is AppModel.App) { + prefs.clockAppPackage = appModel.appPackage + prefs.clockAppUser = appModel.user.toString() + prefs.clockAppClassName = appModel.activityClassName + } + } + + private fun saveCalendarApp(appModel: AppModel) { + if (appModel is AppModel.App) { + prefs.calendarAppPackage = appModel.appPackage + prefs.calendarAppUser = appModel.user.toString() + prefs.calendarAppClassName = appModel.activityClassName + } + } + + private fun saveScreenTimeApp(appModel: AppModel) { + if (appModel is AppModel.App) { + prefs.screenTimeAppPackage = appModel.appPackage + prefs.screenTimeAppUser = appModel.user.toString() + prefs.screenTimeAppClassName = appModel.activityClassName + } + } + + fun firstOpen(value: Boolean) { + firstOpen.postValue(value) + } + + fun refreshHome(appCountUpdated: Boolean) { + refreshHome.value = appCountUpdated + } + + fun toggleDateTime() { + toggleDateTime.postValue(Unit) + } + + private fun updateSwipeApps() { + updateSwipeApps.postValue(Unit) + } + + private fun launchApp(packageName: String, activityClassName: String?, userHandle: UserHandle) { + val launcher = appContext.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + val activityInfo = launcher.getActivityList(packageName, userHandle) + + val isActivityValid = activityClassName.isNullOrBlank().not() + && activityInfo.any { it.componentName.className == activityClassName } + + val component = if (isActivityValid) + ComponentName(packageName, activityClassName) + else { + when (activityInfo.size) { + 0 -> { + appContext.showToast(appContext.getString(R.string.app_not_found)) + return + } + + 1 -> ComponentName(packageName, activityInfo[0].name) + else -> ComponentName(packageName, activityInfo[activityInfo.size - 1].name) + }.also { prefs.updateAppActivityClassName(packageName, it.className) } + } + + try { + launcher.startMainActivity(component, userHandle, null, null) + } catch (e: SecurityException) { + try { + launcher.startMainActivity(component, android.os.Process.myUserHandle(), null, null) + } catch (e: Exception) { + appContext.showToast(appContext.getString(R.string.unable_to_open_app)) + } + } catch (e: Exception) { + appContext.showToast(appContext.getString(R.string.unable_to_open_app)) + } + } + + fun getAppList(includeHiddenApps: Boolean = false) { + viewModelScope.launch { + val apps = getAppsList(appContext, prefs, includeRegularApps = true, includeHiddenApps) + appList.value = apps + } + getPrivateSpaceAppList() + } + + fun getHiddenApps() { + viewModelScope.launch { + hiddenApps.value = + getAppsList(appContext, prefs, includeRegularApps = false, includeHiddenApps = true) + } + } + + fun isOlauncherDefault() { + isOlauncherDefault.value = isOlauncherDefault(appContext) + } + + fun setWallpaperWorker() { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + val uploadWorkRequest = PeriodicWorkRequestBuilder(4, TimeUnit.HOURS) + .setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.HOURS) + .setConstraints(constraints) + .build() + WorkManager + .getInstance(appContext) + .enqueueUniquePeriodicWork( + Constants.WALLPAPER_WORKER_NAME, + ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, + uploadWorkRequest + ) + } + + fun cancelWallpaperWorker() { + WorkManager.getInstance(appContext).cancelUniqueWork(Constants.WALLPAPER_WORKER_NAME) + prefs.dailyWallpaperUrl = "" + prefs.dailyWallpaper = false + } + + fun updateHomeAlignment(gravity: Int) { + prefs.homeAlignment = gravity + homeAppAlignment.value = prefs.homeAlignment + } + + fun getTodaysScreenTime() { + if (prefs.screenTimeLastUpdated.hasBeenMinutes(1).not()) return + + val eventLogWrapper = EventLogWrapper( + appContext + ) + // Start of today in millis + val calendar = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + } + val startTime = calendar.timeInMillis + val endTime = System.currentTimeMillis() + + val timeSpent = eventLogWrapper.aggregateSimpleUsageStats( + eventLogWrapper.aggregateForegroundStats( + eventLogWrapper.getForegroundStatsByTimestamps(startTime, endTime) + ) + ) + val viewTimeSpent = appContext.formattedTimeSpent(timeSpent) + screenTimeValue.postValue(viewTimeSpent) + prefs.screenTimeLastUpdated = endTime + } + + fun getPrivateSpaceAppList() { + viewModelScope.launch { + val handle = getPrivateSpaceUserHandle(appContext) + privateSpaceAvailable.value = handle != null + if (handle != null) { + privateSpaceLocked.value = isPrivateSpaceLocked(appContext, handle) + privateSpaceApps.value = getPrivateSpaceApps(appContext, prefs) + } else { + privateSpaceLocked.value = true + privateSpaceApps.value = emptyList() + } + } + } + + fun openPrivateSpaceSettings() { + try { + val intent = Intent("android.settings.PRIVATE_SPACE_SETTINGS") + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + appContext.startActivity(intent) + } catch (_: Exception) { + try { + val intent = Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + appContext.startActivity(intent) + } catch (_: Exception) { + appContext.showToast(appContext.getString(R.string.unable_to_open_app)) + } + } + } + + fun togglePrivateSpaceLock() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) return + val handle = getPrivateSpaceUserHandle(appContext) ?: return + try { + isPrivateSpaceToggling = true + val userManager = appContext.getSystemService(Context.USER_SERVICE) as UserManager + val currentlyLocked = userManager.isQuietModeEnabled(handle) + userManager.requestQuietModeEnabled(!currentlyLocked, handle) + } catch (e: Exception) { + isPrivateSpaceToggling = false + e.printStackTrace() + } + } + + fun setDefaultClockApp() { + viewModelScope.launch { + try { + Constants.CLOCK_APP_PACKAGES.firstOrNull { appContext.isPackageInstalled(it) }?.let { packageName -> + appContext.packageManager.getLaunchIntentForPackage(packageName)?.component?.className?.let { + prefs.clockAppPackage = packageName + prefs.clockAppClassName = it + prefs.clockAppUser = android.os.Process.myUserHandle().toString() + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/data/AppModel.kt b/app/src/main/java/app/sidephonelauncher/data/AppModel.kt new file mode 100644 index 0000000..8f64714 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/data/AppModel.kt @@ -0,0 +1,45 @@ +package app.sidephonelauncher.data + +import android.os.UserHandle +import java.text.CollationKey + +sealed class AppModel : Comparable { + abstract val appLabel: String + abstract val key: CollationKey? + abstract val appPackage: String + abstract val user: UserHandle + abstract val isNew: Boolean + + data class App( + override val appLabel: String, + override val key: CollationKey?, + override val appPackage: String, + val activityClassName: String?, + override val isNew: Boolean = false, + override val user: UserHandle, + ) : AppModel() + + data class PinnedShortcut( + override val appLabel: String, + override val key: CollationKey?, + override val appPackage: String, + val shortcutId: String, + override val isNew: Boolean = false, + override val user: UserHandle, + ) : AppModel() + + data class PrivateSpaceHeader( + val isLocked: Boolean = true, + override val user: UserHandle = android.os.Process.myUserHandle(), + ) : AppModel() { + override val appLabel: String = "" + override val key: CollationKey? = null + override val appPackage: String = "" + override val isNew: Boolean = false + } + + override fun compareTo(other: AppModel): Int = when { + key != null && other.key != null -> key!!.compareTo(other.key) + else -> appLabel.compareTo(other.appLabel, true) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/data/Constants.kt b/app/src/main/java/app/sidephonelauncher/data/Constants.kt new file mode 100644 index 0000000..2277b0d --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/data/Constants.kt @@ -0,0 +1,120 @@ +package app.sidephonelauncher.data + +object Constants { + + object Key { + const val FLAG = "flag" + const val RENAME = "rename" + } + + object Dialog { + const val ABOUT = "ABOUT" + const val WALLPAPER = "WALLPAPER" + const val REVIEW = "REVIEW" + const val RATE = "RATE" + const val SHARE = "SHARE" + const val HIDDEN = "HIDDEN" + const val KEYBOARD = "KEYBOARD" + const val DIGITAL_WELLBEING = "DIGITAL_WELLBEING" + const val PRO_MESSAGE = "PRO_MESSAGE" + } + + object UserState { + const val START = "START" + const val WALLPAPER = "WALLPAPER" + const val REVIEW = "REVIEW" + const val RATE = "RATE" + const val SHARE = "SHARE" + } + + object DateTime { + const val OFF = 0 + const val ON = 1 + const val DATE_ONLY = 2 + + fun isTimeVisible(dateTimeVisibility: Int): Boolean { + return dateTimeVisibility == ON + } + + fun isDateVisible(dateTimeVisibility: Int): Boolean { + return dateTimeVisibility == ON || dateTimeVisibility == DATE_ONLY + } + } + + object SwipeDownAction { + const val SEARCH = 1 + const val NOTIFICATIONS = 2 + } + + object CharacterIndicator { + const val SHOW = 102 + const val HIDE = 101 + } + + val CLOCK_APP_PACKAGES = arrayOf( + "com.google.android.deskclock", //Google Clock + "com.sec.android.app.clockpackage", //Samsung Clock + "com.oneplus.deskclock", //OnePlus Clock + "com.miui.clock", //Xiaomi Clock + ) + + const val WALL_TYPE_LIGHT = "light" + const val WALL_TYPE_DARK = "dark" + +// const val THEME_MODE_DARK = 0 +// const val THEME_MODE_LIGHT = 1 +// const val THEME_MODE_SYSTEM = 2 + + const val FLAG_LAUNCH_APP = 100 + const val FLAG_HIDDEN_APPS = 101 + + const val FLAG_SET_HOME_APP_1 = 1 + const val FLAG_SET_HOME_APP_2 = 2 + const val FLAG_SET_HOME_APP_3 = 3 + const val FLAG_SET_HOME_APP_4 = 4 + const val FLAG_SET_HOME_APP_5 = 5 + const val FLAG_SET_HOME_APP_6 = 6 + const val FLAG_SET_HOME_APP_7 = 7 + const val FLAG_SET_HOME_APP_8 = 8 + + const val FLAG_SET_SWIPE_LEFT_APP = 11 + const val FLAG_SET_SWIPE_RIGHT_APP = 12 + const val FLAG_SET_CLOCK_APP = 13 + const val FLAG_SET_CALENDAR_APP = 14 + const val FLAG_SET_SCREEN_TIME_APP = 15 + + const val REQUEST_CODE_ENABLE_ADMIN = 666 + const val REQUEST_CODE_LAUNCHER_SELECTOR = 678 + + const val HINT_RATE_US = 15 + + const val LONG_PRESS_DELAY_MS = 500L + const val ONE_DAY_IN_MILLIS = 86400000L + const val ONE_HOUR_IN_MILLIS = 3600000L + const val ONE_MINUTE_IN_MILLIS = 60000L + + const val MIN_ANIM_REFRESH_RATE = 30f + + const val URL_ABOUT_OLAUNCHER = "https://tanujnotes.substack.com/p/olauncher-minimal-af-launcher?utm_source=olauncher" + const val URL_OLAUNCHER_PRIVACY = "https://tanujnotes.notion.site/Olauncher-Privacy-Policy-dd6ac5101ddd4b3da9d27057889d44ab" + const val URL_DOUBLE_TAP = "https://tanujnotes.notion.site/Double-tap-to-lock-Olauncher-0f7fb103ec1f47d7a90cdfdcd7fb86ef" + const val URL_OLAUNCHER_GITHUB = "https://www.github.com/tanujnotes/Olauncher" + const val URL_OLAUNCHER_PLAY_STORE = "https://play.google.com/store/apps/details?id=app.sidephonelauncher" + const val URL_OLAUNCHER_PRO = "https://play.google.com/store/apps/details?id=app.prolauncher" + const val URL_PLAY_STORE_DEV = "https://play.google.com/store/apps/dev?id=7198807840081074933" + const val URL_TWITTER_TANUJ = "https://x.com/tanujnotes" + const val URL_INSTA_TANUJ = "https://instagram.com/tanuj_notes" + const val URL_WALLPAPERS = "https://gist.githubusercontent.com/tanujnotes/85e2d0343ace71e76615ac346fbff82b/raw" + const val URL_NTS = "https://play.google.com/store/apps/details?id=com.makenotetoself" + const val URL_PENTASTIC = "https://play.google.com/store/apps/details?id=app.pentastic" + const val URL_DEFAULT_DARK_WALLPAPER = "https://images.unsplash.com/photo-1512551980832-13df02babc9e" + const val URL_DEFAULT_LIGHT_WALLPAPER = "https://images.unsplash.com/photo-1515549832467-8783363e19b6" + const val URL_DUCK_SEARCH = "https://duck.co/?q=" + const val URL_DIGITAL_WELLBEING_LEARN_MORE = "https://tanujnotes.substack.com/p/digital-wellbeing-app-on-android?utm_source=olauncher" + + const val DIGITAL_WELLBEING_PACKAGE_NAME = "com.google.android.apps.wellbeing" + const val DIGITAL_WELLBEING_ACTIVITY = "com.google.android.apps.wellbeing.settings.TopLevelSettingsActivity" + const val DIGITAL_WELLBEING_SAMSUNG_PACKAGE_NAME = "com.samsung.android.forest" + const val DIGITAL_WELLBEING_SAMSUNG_ACTIVITY = "com.samsung.android.forest.launcher.LauncherActivity" + const val WALLPAPER_WORKER_NAME = "WALLPAPER_WORKER_NAME" +} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/data/DrawerCharacterModel.kt b/app/src/main/java/app/sidephonelauncher/data/DrawerCharacterModel.kt new file mode 100644 index 0000000..6652a18 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/data/DrawerCharacterModel.kt @@ -0,0 +1,6 @@ +package app.sidephonelauncher.data + +data class DrawerCharacterModel( + val character: String = "", + val inRange: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/data/Prefs.kt b/app/src/main/java/app/sidephonelauncher/data/Prefs.kt new file mode 100644 index 0000000..9e0daf5 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/data/Prefs.kt @@ -0,0 +1,647 @@ +package app.sidephonelauncher.data + +import android.content.Context +import android.content.SharedPreferences +import android.view.Gravity +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.edit + +class Prefs(context: Context) { + private val PREFS_FILENAME = "app.sidephonelauncher" + + private val FIRST_OPEN = "FIRST_OPEN" + private val FIRST_OPEN_TIME = "FIRST_OPEN_TIME" + private val FIRST_SETTINGS_OPEN = "FIRST_SETTINGS_OPEN" + private val FIRST_HIDE = "FIRST_HIDE" + private val USER_STATE = "USER_STATE" + private val LOCK_MODE = "LOCK_MODE" + private val HOME_APPS_NUM = "HOME_APPS_NUM" + private val AUTO_SHOW_KEYBOARD = "AUTO_SHOW_KEYBOARD" + private val KEYBOARD_MESSAGE = "KEYBOARD_MESSAGE" + private val DAILY_WALLPAPER = "DAILY_WALLPAPER" + private val DAILY_WALLPAPER_URL = "DAILY_WALLPAPER_URL" + private val HOME_ALIGNMENT = "HOME_ALIGNMENT" + private val HOME_BOTTOM_ALIGNMENT = "HOME_BOTTOM_ALIGNMENT" + private val APP_LABEL_ALIGNMENT = "APP_LABEL_ALIGNMENT" + private val STATUS_BAR = "STATUS_BAR" + private val DATE_TIME_VISIBILITY = "DATE_TIME_VISIBILITY" + private val SWIPE_LEFT_ENABLED = "SWIPE_LEFT_ENABLED" + private val SWIPE_RIGHT_ENABLED = "SWIPE_RIGHT_ENABLED" + private val HIDDEN_APPS = "HIDDEN_APPS" + private val HIDDEN_APPS_UPDATED = "HIDDEN_APPS_UPDATED" + private val SHOW_HINT_COUNTER = "SHOW_HINT_COUNTER" + private val APP_THEME = "APP_THEME" + private val ABOUT_CLICKED = "ABOUT_CLICKED" + private val RATE_CLICKED = "RATE_CLICKED" + private val WALLPAPER_MSG_SHOWN = "WALLPAPER_MSG_SHOWN" + private val SHARE_SHOWN_TIME = "SHARE_SHOWN_TIME" + private val SWIPE_DOWN_ACTION = "SWIPE_DOWN_ACTION" + private val TEXT_SIZE_SCALE = "TEXT_SIZE_SCALE" + private val PRO_MESSAGE_SHOWN = "PRO_MESSAGE_SHOWN" + private val HIDE_SET_DEFAULT_LAUNCHER = "HIDE_SET_DEFAULT_LAUNCHER" + private val SCREEN_TIME_LAST_UPDATED = "SCREEN_TIME_LAST_UPDATED" + private val LAUNCHER_RESTART_TIMESTAMP = "LAUNCHER_RECREATE_TIMESTAMP" + private val SHOWN_ON_DAY_OF_YEAR = "SHOWN_ON_DAY_OF_YEAR" + // Home button for recents feature disabled + // private val HOME_BUTTON_SHOW_RECENTS = "HOME_BUTTON_SHOW_RECENTS" + + private val APP_NAME_1 = "APP_NAME_1" + private val APP_NAME_2 = "APP_NAME_2" + private val APP_NAME_3 = "APP_NAME_3" + private val APP_NAME_4 = "APP_NAME_4" + private val APP_NAME_5 = "APP_NAME_5" + private val APP_NAME_6 = "APP_NAME_6" + private val APP_NAME_7 = "APP_NAME_7" + private val APP_NAME_8 = "APP_NAME_8" + private val APP_PACKAGE_1 = "APP_PACKAGE_1" + private val APP_PACKAGE_2 = "APP_PACKAGE_2" + private val APP_PACKAGE_3 = "APP_PACKAGE_3" + private val APP_PACKAGE_4 = "APP_PACKAGE_4" + private val APP_PACKAGE_5 = "APP_PACKAGE_5" + private val APP_PACKAGE_6 = "APP_PACKAGE_6" + private val APP_PACKAGE_7 = "APP_PACKAGE_7" + private val APP_PACKAGE_8 = "APP_PACKAGE_8" + private val APP_ACTIVITY_CLASS_NAME_1 = "APP_ACTIVITY_CLASS_NAME_1" + private val APP_ACTIVITY_CLASS_NAME_2 = "APP_ACTIVITY_CLASS_NAME_2" + private val APP_ACTIVITY_CLASS_NAME_3 = "APP_ACTIVITY_CLASS_NAME_3" + private val APP_ACTIVITY_CLASS_NAME_4 = "APP_ACTIVITY_CLASS_NAME_4" + private val APP_ACTIVITY_CLASS_NAME_5 = "APP_ACTIVITY_CLASS_NAME_5" + private val APP_ACTIVITY_CLASS_NAME_6 = "APP_ACTIVITY_CLASS_NAME_6" + private val APP_ACTIVITY_CLASS_NAME_7 = "APP_ACTIVITY_CLASS_NAME_7" + private val APP_ACTIVITY_CLASS_NAME_8 = "APP_ACTIVITY_CLASS_NAME_8" + private val APP_USER_1 = "APP_USER_1" + private val APP_USER_2 = "APP_USER_2" + private val APP_USER_3 = "APP_USER_3" + private val APP_USER_4 = "APP_USER_4" + private val APP_USER_5 = "APP_USER_5" + private val APP_USER_6 = "APP_USER_6" + private val APP_USER_7 = "APP_USER_7" + private val APP_USER_8 = "APP_USER_8" + + private val APP_NAME_SWIPE_LEFT = "APP_NAME_SWIPE_LEFT" + private val APP_NAME_SWIPE_RIGHT = "APP_NAME_SWIPE_RIGHT" + private val APP_PACKAGE_SWIPE_LEFT = "APP_PACKAGE_SWIPE_LEFT" + private val APP_PACKAGE_SWIPE_RIGHT = "APP_PACKAGE_SWIPE_RIGHT" + private val APP_ACTIVITY_CLASS_NAME_SWIPE_LEFT = "APP_ACTIVITY_CLASS_NAME_SWIPE_LEFT" + private val APP_ACTIVITY_CLASS_NAME_SWIPE_RIGHT = "APP_ACTIVITY_CLASS_NAME_SWIPE_RIGHT" + private val APP_USER_SWIPE_LEFT = "APP_USER_SWIPE_LEFT" + private val APP_USER_SWIPE_RIGHT = "APP_USER_SWIPE_RIGHT" + private val CLOCK_APP_PACKAGE = "CLOCK_APP_PACKAGE" + private val CLOCK_APP_USER = "CLOCK_APP_USER" + private val CLOCK_APP_CLASS_NAME = "CLOCK_APP_CLASS_NAME" + private val CALENDAR_APP_PACKAGE = "CALENDAR_APP_PACKAGE" + private val CALENDAR_APP_USER = "CALENDAR_APP_USER" + private val CALENDAR_APP_CLASS_NAME = "CALENDAR_APP_CLASS_NAME" + private val SCREEN_TIME_APP_PACKAGE = "SCREEN_TIME_APP_PACKAGE" + private val SCREEN_TIME_APP_USER = "SCREEN_TIME_APP_USER" + private val SCREEN_TIME_APP_CLASS_NAME = "SCREEN_TIME_APP_CLASS_NAME" + + private val IS_SHORTCUT_1 = "IS_SHORTCUT_1" + private val SHORTCUT_ID_1 = "SHORTCUT_ID_1" + private val IS_SHORTCUT_2 = "IS_SHORTCUT_2" + private val SHORTCUT_ID_2 = "SHORTCUT_ID_2" + private val IS_SHORTCUT_3 = "IS_SHORTCUT_3" + private val SHORTCUT_ID_3 = "SHORTCUT_ID_3" + private val IS_SHORTCUT_4 = "IS_SHORTCUT_4" + private val SHORTCUT_ID_4 = "SHORTCUT_ID_4" + private val IS_SHORTCUT_5 = "IS_SHORTCUT_5" + private val SHORTCUT_ID_5 = "SHORTCUT_ID_5" + private val IS_SHORTCUT_6 = "IS_SHORTCUT_6" + private val SHORTCUT_ID_6 = "SHORTCUT_ID_6" + private val IS_SHORTCUT_7 = "IS_SHORTCUT_7" + private val SHORTCUT_ID_7 = "SHORTCUT_ID_7" + private val IS_SHORTCUT_8 = "IS_SHORTCUT_8" + private val SHORTCUT_ID_8 = "SHORTCUT_ID_8" + + private val SHORTCUT_ID_SWIPE_LEFT = "SHORTCUT_ID_SWIPE_LEFT" + private val IS_SHORTCUT_SWIPE_LEFT = "IS_SHORTCUT_SWIPE_LEFT" + private val SHORTCUT_ID_SWIPE_RIGHT = "SHORTCUT_ID_SWIPE_RIGHT" + private val IS_SHORTCUT_SWIPE_RIGHT = "IS_SHORTCUT_SWIPE_RIGHT" + + private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_FILENAME, 0) + + var firstOpen: Boolean + get() = prefs.getBoolean(FIRST_OPEN, true) + set(value) = prefs.edit { putBoolean(FIRST_OPEN, value).apply() } + + var firstOpenTime: Long + get() = prefs.getLong(FIRST_OPEN_TIME, 0L) + set(value) = prefs.edit { putLong(FIRST_OPEN_TIME, value).apply() } + + var firstSettingsOpen: Boolean + get() = prefs.getBoolean(FIRST_SETTINGS_OPEN, true) + set(value) = prefs.edit { putBoolean(FIRST_SETTINGS_OPEN, value).apply() } + + var firstHide: Boolean + get() = prefs.getBoolean(FIRST_HIDE, true) + set(value) = prefs.edit { putBoolean(FIRST_HIDE, value).apply() } + + var userState: String + get() = prefs.getString(USER_STATE, Constants.UserState.START).toString() + set(value) = prefs.edit { putString(USER_STATE, value).apply() } + + var lockModeOn: Boolean + get() = prefs.getBoolean(LOCK_MODE, false) + set(value) = prefs.edit { putBoolean(LOCK_MODE, value).apply() } + + var autoShowKeyboard: Boolean + get() = prefs.getBoolean(AUTO_SHOW_KEYBOARD, true) + set(value) = prefs.edit { putBoolean(AUTO_SHOW_KEYBOARD, value).apply() } + + var keyboardMessageShown: Boolean + get() = prefs.getBoolean(KEYBOARD_MESSAGE, false) + set(value) = prefs.edit { putBoolean(KEYBOARD_MESSAGE, value).apply() } + + var dailyWallpaper: Boolean + get() = prefs.getBoolean(DAILY_WALLPAPER, false) + set(value) = prefs.edit { putBoolean(DAILY_WALLPAPER, value).apply() } + + var dailyWallpaperUrl: String + get() = prefs.getString(DAILY_WALLPAPER_URL, "").toString() + set(value) = prefs.edit { putString(DAILY_WALLPAPER_URL, value).apply() } + + var homeAppsNum: Int + get() = prefs.getInt(HOME_APPS_NUM, 4) + set(value) = prefs.edit { putInt(HOME_APPS_NUM, value).apply() } + + var homeAlignment: Int + get() = prefs.getInt(HOME_ALIGNMENT, Gravity.START) + set(value) = prefs.edit { putInt(HOME_ALIGNMENT, value).apply() } + + var homeBottomAlignment: Boolean + get() = prefs.getBoolean(HOME_BOTTOM_ALIGNMENT, false) + set(value) = prefs.edit { putBoolean(HOME_BOTTOM_ALIGNMENT, value).apply() } + + var appLabelAlignment: Int + get() = prefs.getInt(APP_LABEL_ALIGNMENT, Gravity.START) + set(value) = prefs.edit { putInt(APP_LABEL_ALIGNMENT, value).apply() } + + var showStatusBar: Boolean + get() = prefs.getBoolean(STATUS_BAR, false) + set(value) = prefs.edit { putBoolean(STATUS_BAR, value).apply() } + + var dateTimeVisibility: Int + get() = prefs.getInt(DATE_TIME_VISIBILITY, Constants.DateTime.ON) + set(value) = prefs.edit { putInt(DATE_TIME_VISIBILITY, value).apply() } + + var swipeLeftEnabled: Boolean + get() = prefs.getBoolean(SWIPE_LEFT_ENABLED, true) + set(value) = prefs.edit { putBoolean(SWIPE_LEFT_ENABLED, value).apply() } + + var swipeRightEnabled: Boolean + get() = prefs.getBoolean(SWIPE_RIGHT_ENABLED, true) + set(value) = prefs.edit { putBoolean(SWIPE_RIGHT_ENABLED, value).apply() } + + var appTheme: Int + get() = prefs.getInt(APP_THEME, AppCompatDelegate.MODE_NIGHT_YES) + set(value) = prefs.edit { putInt(APP_THEME, value).apply() } + + var textSizeScale: Float + get() = prefs.getFloat(TEXT_SIZE_SCALE, 1.0f) + set(value) = prefs.edit { putFloat(TEXT_SIZE_SCALE, value).apply() } + + var proMessageShown: Boolean + get() = prefs.getBoolean(PRO_MESSAGE_SHOWN, false) + set(value) = prefs.edit { putBoolean(PRO_MESSAGE_SHOWN, value).apply() } + + var hideSetDefaultLauncher: Boolean + get() = prefs.getBoolean(HIDE_SET_DEFAULT_LAUNCHER, false) + set(value) = prefs.edit { putBoolean(HIDE_SET_DEFAULT_LAUNCHER, value).apply() } + + var screenTimeLastUpdated: Long + get() = prefs.getLong(SCREEN_TIME_LAST_UPDATED, 0L) + set(value) = prefs.edit { putLong(SCREEN_TIME_LAST_UPDATED, value).apply() } + + var launcherRestartTimestamp: Long + get() = prefs.getLong(LAUNCHER_RESTART_TIMESTAMP, 0L) + set(value) = prefs.edit { putLong(LAUNCHER_RESTART_TIMESTAMP, value).apply() } + + var shownOnDayOfYear: Int + get() = prefs.getInt(SHOWN_ON_DAY_OF_YEAR, 0) + set(value) = prefs.edit { putInt(SHOWN_ON_DAY_OF_YEAR, value).apply() } + + // Home button for recents feature disabled + // var homeButtonShowRecents: Boolean + // get() = prefs.getBoolean(HOME_BUTTON_SHOW_RECENTS, false) + // set(value) = prefs.edit { putBoolean(HOME_BUTTON_SHOW_RECENTS, value).apply() } + + var hiddenApps: MutableSet + get() = prefs.getStringSet(HIDDEN_APPS, mutableSetOf()) as MutableSet + set(value) = prefs.edit { putStringSet(HIDDEN_APPS, value).apply() } + + var hiddenAppsUpdated: Boolean + get() = prefs.getBoolean(HIDDEN_APPS_UPDATED, false) + set(value) = prefs.edit { putBoolean(HIDDEN_APPS_UPDATED, value).apply() } + + var toShowHintCounter: Int + get() = prefs.getInt(SHOW_HINT_COUNTER, 1) + set(value) = prefs.edit { putInt(SHOW_HINT_COUNTER, value).apply() } + + var aboutClicked: Boolean + get() = prefs.getBoolean(ABOUT_CLICKED, false) + set(value) = prefs.edit { putBoolean(ABOUT_CLICKED, value).apply() } + + var rateClicked: Boolean + get() = prefs.getBoolean(RATE_CLICKED, false) + set(value) = prefs.edit { putBoolean(RATE_CLICKED, value).apply() } + + var wallpaperMsgShown: Boolean + get() = prefs.getBoolean(WALLPAPER_MSG_SHOWN, false) + set(value) = prefs.edit { putBoolean(WALLPAPER_MSG_SHOWN, value).apply() } + + var shareShownTime: Long + get() = prefs.getLong(SHARE_SHOWN_TIME, 0L) + set(value) = prefs.edit { putLong(SHARE_SHOWN_TIME, value).apply() } + + var swipeDownAction: Int + get() = prefs.getInt(SWIPE_DOWN_ACTION, Constants.SwipeDownAction.NOTIFICATIONS) + set(value) = prefs.edit { putInt(SWIPE_DOWN_ACTION, value).apply() } + + var appName1: String + get() = prefs.getString(APP_NAME_1, "").toString() + set(value) = prefs.edit { putString(APP_NAME_1, value).apply() } + + var appName2: String + get() = prefs.getString(APP_NAME_2, "").toString() + set(value) = prefs.edit { putString(APP_NAME_2, value).apply() } + + var appName3: String + get() = prefs.getString(APP_NAME_3, "").toString() + set(value) = prefs.edit { putString(APP_NAME_3, value).apply() } + + var appName4: String + get() = prefs.getString(APP_NAME_4, "").toString() + set(value) = prefs.edit { putString(APP_NAME_4, value).apply() } + + var appName5: String + get() = prefs.getString(APP_NAME_5, "").toString() + set(value) = prefs.edit { putString(APP_NAME_5, value).apply() } + + var appName6: String + get() = prefs.getString(APP_NAME_6, "").toString() + set(value) = prefs.edit { putString(APP_NAME_6, value).apply() } + + var appName7: String + get() = prefs.getString(APP_NAME_7, "").toString() + set(value) = prefs.edit { putString(APP_NAME_7, value).apply() } + + var appName8: String + get() = prefs.getString(APP_NAME_8, "").toString() + set(value) = prefs.edit { putString(APP_NAME_8, value).apply() } + + var appPackage1: String + get() = prefs.getString(APP_PACKAGE_1, "").toString() + set(value) = prefs.edit { putString(APP_PACKAGE_1, value).apply() } + + var appPackage2: String + get() = prefs.getString(APP_PACKAGE_2, "").toString() + set(value) = prefs.edit { putString(APP_PACKAGE_2, value).apply() } + + var appPackage3: String + get() = prefs.getString(APP_PACKAGE_3, "").toString() + set(value) = prefs.edit { putString(APP_PACKAGE_3, value).apply() } + + var appPackage4: String + get() = prefs.getString(APP_PACKAGE_4, "").toString() + set(value) = prefs.edit { putString(APP_PACKAGE_4, value).apply() } + + var appPackage5: String + get() = prefs.getString(APP_PACKAGE_5, "").toString() + set(value) = prefs.edit { putString(APP_PACKAGE_5, value).apply() } + + var appPackage6: String + get() = prefs.getString(APP_PACKAGE_6, "").toString() + set(value) = prefs.edit { putString(APP_PACKAGE_6, value).apply() } + + var appPackage7: String + get() = prefs.getString(APP_PACKAGE_7, "").toString() + set(value) = prefs.edit { putString(APP_PACKAGE_7, value).apply() } + + var appPackage8: String + get() = prefs.getString(APP_PACKAGE_8, "").toString() + set(value) = prefs.edit { putString(APP_PACKAGE_8, value).apply() } + + var appActivityClassName1: String? + get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_1, "").toString() + set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_1, value).apply() } + + var appActivityClassName2: String? + get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_2, "").toString() + set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_2, value).apply() } + + var appActivityClassName3: String? + get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_3, "").toString() + set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_3, value).apply() } + + var appActivityClassName4: String? + get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_4, "").toString() + set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_4, value).apply() } + + var appActivityClassName5: String? + get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_5, "").toString() + set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_5, value).apply() } + + var appActivityClassName6: String? + get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_6, "").toString() + set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_6, value).apply() } + + var appActivityClassName7: String? + get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_7, "").toString() + set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_7, value).apply() } + + var appActivityClassName8: String? + get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_8, "").toString() + set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_8, value).apply() } + + var appUser1: String + get() = prefs.getString(APP_USER_1, "").toString() + set(value) = prefs.edit { putString(APP_USER_1, value).apply() } + + var appUser2: String + get() = prefs.getString(APP_USER_2, "").toString() + set(value) = prefs.edit { putString(APP_USER_2, value).apply() } + + var appUser3: String + get() = prefs.getString(APP_USER_3, "").toString() + set(value) = prefs.edit { putString(APP_USER_3, value).apply() } + + var appUser4: String + get() = prefs.getString(APP_USER_4, "").toString() + set(value) = prefs.edit { putString(APP_USER_4, value).apply() } + + var appUser5: String + get() = prefs.getString(APP_USER_5, "").toString() + set(value) = prefs.edit { putString(APP_USER_5, value).apply() } + + var appUser6: String + get() = prefs.getString(APP_USER_6, "").toString() + set(value) = prefs.edit { putString(APP_USER_6, value).apply() } + + var appUser7: String + get() = prefs.getString(APP_USER_7, "").toString() + set(value) = prefs.edit { putString(APP_USER_7, value).apply() } + + var appUser8: String + get() = prefs.getString(APP_USER_8, "").toString() + set(value) = prefs.edit { putString(APP_USER_8, value).apply() } + + var appNameSwipeLeft: String + get() = prefs.getString(APP_NAME_SWIPE_LEFT, "Camera").toString() + set(value) = prefs.edit { putString(APP_NAME_SWIPE_LEFT, value).apply() } + + var appNameSwipeRight: String + get() = prefs.getString(APP_NAME_SWIPE_RIGHT, "Phone").toString() + set(value) = prefs.edit { putString(APP_NAME_SWIPE_RIGHT, value).apply() } + + var appPackageSwipeLeft: String + get() = prefs.getString(APP_PACKAGE_SWIPE_LEFT, "").toString() + set(value) = prefs.edit { putString(APP_PACKAGE_SWIPE_LEFT, value).apply() } + + var appActivityClassNameSwipeLeft: String? + get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_SWIPE_LEFT, "").toString() + set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_SWIPE_LEFT, value).apply() } + + var appPackageSwipeRight: String + get() = prefs.getString(APP_PACKAGE_SWIPE_RIGHT, "").toString() + set(value) = prefs.edit { putString(APP_PACKAGE_SWIPE_RIGHT, value).apply() } + + var appActivityClassNameRight: String? + get() = prefs.getString(APP_ACTIVITY_CLASS_NAME_SWIPE_RIGHT, "").toString() + set(value) = prefs.edit { putString(APP_ACTIVITY_CLASS_NAME_SWIPE_RIGHT, value).apply() } + + var appUserSwipeLeft: String + get() = prefs.getString(APP_USER_SWIPE_LEFT, "").toString() + set(value) = prefs.edit { putString(APP_USER_SWIPE_LEFT, value).apply() } + + var appUserSwipeRight: String + get() = prefs.getString(APP_USER_SWIPE_RIGHT, "").toString() + set(value) = prefs.edit { putString(APP_USER_SWIPE_RIGHT, value).apply() } + + var clockAppPackage: String + get() = prefs.getString(CLOCK_APP_PACKAGE, "").toString() + set(value) = prefs.edit { putString(CLOCK_APP_PACKAGE, value).apply() } + + var clockAppUser: String + get() = prefs.getString(CLOCK_APP_USER, "").toString() + set(value) = prefs.edit { putString(CLOCK_APP_USER, value).apply() } + + var clockAppClassName: String? + get() = prefs.getString(CLOCK_APP_CLASS_NAME, "").toString() + set(value) = prefs.edit { putString(CLOCK_APP_CLASS_NAME, value).apply() } + + var calendarAppPackage: String + get() = prefs.getString(CALENDAR_APP_PACKAGE, "").toString() + set(value) = prefs.edit { putString(CALENDAR_APP_PACKAGE, value).apply() } + + var calendarAppUser: String + get() = prefs.getString(CALENDAR_APP_USER, "").toString() + set(value) = prefs.edit { putString(CALENDAR_APP_USER, value).apply() } + + var calendarAppClassName: String? + get() = prefs.getString(CALENDAR_APP_CLASS_NAME, "").toString() + set(value) = prefs.edit { putString(CALENDAR_APP_CLASS_NAME, value).apply() } + + var screenTimeAppPackage: String + get() = prefs.getString(SCREEN_TIME_APP_PACKAGE, "").toString() + set(value) = prefs.edit { putString(SCREEN_TIME_APP_PACKAGE, value).apply() } + + var screenTimeAppUser: String + get() = prefs.getString(SCREEN_TIME_APP_USER, "").toString() + set(value) = prefs.edit { putString(SCREEN_TIME_APP_USER, value).apply() } + + var screenTimeAppClassName: String? + get() = prefs.getString(SCREEN_TIME_APP_CLASS_NAME, "").toString() + set(value) = prefs.edit { putString(SCREEN_TIME_APP_CLASS_NAME, value).apply() } + + var isShortcut1: Boolean + get() = prefs.getBoolean(IS_SHORTCUT_1, false) + set(value) = prefs.edit { putBoolean(IS_SHORTCUT_1, value) } + + var shortcutId1: String + get() = prefs.getString(SHORTCUT_ID_1, "").toString() + set(value) = prefs.edit { putString(SHORTCUT_ID_1, value) } + + var isShortcut2: Boolean + get() = prefs.getBoolean(IS_SHORTCUT_2, false) + set(value) = prefs.edit { putBoolean(IS_SHORTCUT_2, value) } + + var shortcutId2: String + get() = prefs.getString(SHORTCUT_ID_2, "").toString() + set(value) = prefs.edit { putString(SHORTCUT_ID_2, value) } + + var isShortcut3: Boolean + get() = prefs.getBoolean(IS_SHORTCUT_3, false) + set(value) = prefs.edit { putBoolean(IS_SHORTCUT_3, value) } + + var shortcutId3: String + get() = prefs.getString(SHORTCUT_ID_3, "").toString() + set(value) = prefs.edit { putString(SHORTCUT_ID_3, value) } + + var isShortcut4: Boolean + get() = prefs.getBoolean(IS_SHORTCUT_4, false) + set(value) = prefs.edit { putBoolean(IS_SHORTCUT_4, value) } + + var shortcutId4: String + get() = prefs.getString(SHORTCUT_ID_4, "").toString() + set(value) = prefs.edit { putString(SHORTCUT_ID_4, value) } + + var isShortcut5: Boolean + get() = prefs.getBoolean(IS_SHORTCUT_5, false) + set(value) = prefs.edit { putBoolean(IS_SHORTCUT_5, value) } + + var shortcutId5: String + get() = prefs.getString(SHORTCUT_ID_5, "").toString() + set(value) = prefs.edit { putString(SHORTCUT_ID_5, value) } + + var isShortcut6: Boolean + get() = prefs.getBoolean(IS_SHORTCUT_6, false) + set(value) = prefs.edit { putBoolean(IS_SHORTCUT_6, value) } + + var shortcutId6: String + get() = prefs.getString(SHORTCUT_ID_6, "").toString() + set(value) = prefs.edit { putString(SHORTCUT_ID_6, value) } + + var isShortcut7: Boolean + get() = prefs.getBoolean(IS_SHORTCUT_7, false) + set(value) = prefs.edit { putBoolean(IS_SHORTCUT_7, value) } + + var shortcutId7: String + get() = prefs.getString(SHORTCUT_ID_7, "").toString() + set(value) = prefs.edit { putString(SHORTCUT_ID_7, value) } + + var isShortcut8: Boolean + get() = prefs.getBoolean(IS_SHORTCUT_8, false) + set(value) = prefs.edit { putBoolean(IS_SHORTCUT_8, value) } + + var shortcutId8: String + get() = prefs.getString(SHORTCUT_ID_8, "").toString() + set(value) = prefs.edit { putString(SHORTCUT_ID_8, value) } + + var shortcutIdSwipeLeft: String + get() = prefs.getString(SHORTCUT_ID_SWIPE_LEFT, "").toString() + set(value) = prefs.edit { putString(SHORTCUT_ID_SWIPE_LEFT, value) } + + var isShortcutSwipeLeft: Boolean + get() = prefs.getBoolean(IS_SHORTCUT_SWIPE_LEFT, false) + set(value) = prefs.edit { putBoolean(IS_SHORTCUT_SWIPE_LEFT, value) } + + var shortcutIdSwipeRight: String + get() = prefs.getString(SHORTCUT_ID_SWIPE_RIGHT, "").toString() + set(value) = prefs.edit { putString(SHORTCUT_ID_SWIPE_RIGHT, value) } + + var isShortcutSwipeRight: Boolean + get() = prefs.getBoolean(IS_SHORTCUT_SWIPE_RIGHT, false) + set(value) = prefs.edit { putBoolean(IS_SHORTCUT_SWIPE_RIGHT, value) } + + fun getAppName(location: Int): String { + return when (location) { + 1 -> prefs.getString(APP_NAME_1, "").toString() + 2 -> prefs.getString(APP_NAME_2, "").toString() + 3 -> prefs.getString(APP_NAME_3, "").toString() + 4 -> prefs.getString(APP_NAME_4, "").toString() + 5 -> prefs.getString(APP_NAME_5, "").toString() + 6 -> prefs.getString(APP_NAME_6, "").toString() + 7 -> prefs.getString(APP_NAME_7, "").toString() + 8 -> prefs.getString(APP_NAME_8, "").toString() + else -> "" + } + } + + fun getAppPackage(location: Int): String { + return when (location) { + 1 -> prefs.getString(APP_PACKAGE_1, "").toString() + 2 -> prefs.getString(APP_PACKAGE_2, "").toString() + 3 -> prefs.getString(APP_PACKAGE_3, "").toString() + 4 -> prefs.getString(APP_PACKAGE_4, "").toString() + 5 -> prefs.getString(APP_PACKAGE_5, "").toString() + 6 -> prefs.getString(APP_PACKAGE_6, "").toString() + 7 -> prefs.getString(APP_PACKAGE_7, "").toString() + 8 -> prefs.getString(APP_PACKAGE_8, "").toString() + else -> "" + } + } + + fun getAppActivityClassName(location: Int): String { + return when (location) { + 1 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_1, "").toString() + 2 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_2, "").toString() + 3 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_3, "").toString() + 4 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_4, "").toString() + 5 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_5, "").toString() + 6 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_6, "").toString() + 7 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_7, "").toString() + 8 -> prefs.getString(APP_ACTIVITY_CLASS_NAME_8, "").toString() + else -> "" + } + } + + fun getAppUser(location: Int): String { + return when (location) { + 1 -> prefs.getString(APP_USER_1, "").toString() + 2 -> prefs.getString(APP_USER_2, "").toString() + 3 -> prefs.getString(APP_USER_3, "").toString() + 4 -> prefs.getString(APP_USER_4, "").toString() + 5 -> prefs.getString(APP_USER_5, "").toString() + 6 -> prefs.getString(APP_USER_6, "").toString() + 7 -> prefs.getString(APP_USER_7, "").toString() + 8 -> prefs.getString(APP_USER_8, "").toString() + else -> "" + } + } + + fun getShortcutId(location: Int): String { + return when (location) { + 1 -> shortcutId1 + 2 -> shortcutId2 + 3 -> shortcutId3 + 4 -> shortcutId4 + 5 -> shortcutId5 + 6 -> shortcutId6 + 7 -> shortcutId7 + 8 -> shortcutId8 + else -> "" + } + } + + fun getIsShortcut(location: Int): Boolean { + return when (location) { + 1 -> isShortcut1 + 2 -> isShortcut2 + 3 -> isShortcut3 + 4 -> isShortcut4 + 5 -> isShortcut5 + 6 -> isShortcut6 + 7 -> isShortcut7 + 8 -> isShortcut8 + else -> false + } + } + + fun setAppActivityClassName(location: Int, activityClassName: String) { + when (location) { + 1 -> appActivityClassName1 = activityClassName + 2 -> appActivityClassName2 = activityClassName + 3 -> appActivityClassName3 = activityClassName + 4 -> appActivityClassName4 = activityClassName + 5 -> appActivityClassName5 = activityClassName + 6 -> appActivityClassName6 = activityClassName + 7 -> appActivityClassName7 = activityClassName + 8 -> appActivityClassName8 = activityClassName + } + } + + fun updateAppActivityClassName(packageName: String, activityClassName: String) { + for (i in 1..8) { + if (getAppPackage(i) == packageName) setAppActivityClassName(i, activityClassName) + } + if (clockAppPackage == packageName) clockAppClassName = activityClassName + if (calendarAppPackage == packageName) calendarAppClassName = activityClassName + if (screenTimeAppPackage == packageName) screenTimeAppClassName = activityClassName + if (appPackageSwipeLeft == packageName) appActivityClassNameSwipeLeft = activityClassName + if (appPackageSwipeRight == packageName) appActivityClassNameRight = activityClassName + } + + fun getAppRenameLabel(appPackage: String): String = prefs.getString(appPackage, "").toString() + + fun setAppRenameLabel(appPackage: String, renameLabel: String) = prefs.edit { putString(appPackage, renameLabel) } +} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/helper/AppFilterHelper.kt b/app/src/main/java/app/sidephonelauncher/helper/AppFilterHelper.kt new file mode 100644 index 0000000..66c4904 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/helper/AppFilterHelper.kt @@ -0,0 +1,7 @@ +package app.sidephonelauncher.helper + +import app.sidephonelauncher.data.AppModel + +interface AppFilterHelper { + fun onAppFiltered(items:List) +} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/helper/AppUsageStats.kt b/app/src/main/java/app/sidephonelauncher/helper/AppUsageStats.kt new file mode 100644 index 0000000..5e19fa2 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/helper/AppUsageStats.kt @@ -0,0 +1,21 @@ +package app.sidephonelauncher.helper + +import android.os.Build +import androidx.annotation.RequiresApi + +class AppUsageStats( + val lastTimeUsedMillis: Long, + val totalTimeInForegroundMillis: Long, + @get:RequiresApi(Build.VERSION_CODES.Q) val lastTimeForegroundServiceUsedMillis: Long, + @get:RequiresApi(Build.VERSION_CODES.Q) val totalTimeForegroundServiceUsedMillis: Long, +) + +class AppUsageStatsBucket { + var startMillis: Long = 0L + var endMillis: Long = 0L + var totalTime: Long = 0L + + fun addTotalTime() { + this.totalTime += endMillis - startMillis + } +} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/helper/Extensions.kt b/app/src/main/java/app/sidephonelauncher/helper/Extensions.kt new file mode 100644 index 0000000..9c68b28 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/helper/Extensions.kt @@ -0,0 +1,192 @@ +package app.sidephonelauncher.helper + +import android.app.Activity +import android.app.AppOpsManager +import android.app.SearchManager +import android.app.role.RoleManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherApps +import android.content.pm.PackageManager +import android.content.res.Resources +import android.net.Uri +import android.os.Build +import android.os.UserHandle +import android.provider.Settings +import android.telephony.TelephonyManager +import android.view.View +import android.view.WindowManager +import android.view.inputmethod.InputMethodManager +import androidx.annotation.RequiresApi +import app.sidephonelauncher.BuildConfig +import app.sidephonelauncher.R +import app.sidephonelauncher.data.Constants +import java.util.Calendar +import java.util.Locale + +fun View.hideKeyboard() { + this.clearFocus() + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(windowToken, 0) +} + +fun View.showKeyboard(show: Boolean = true) { + if (show.not()) return + if (this.requestFocus()) + postDelayed({ + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) + }, 100) +} + + +@RequiresApi(Build.VERSION_CODES.Q) +fun Activity.showLauncherSelector(requestCode: Int) { + val roleManager = getSystemService(Context.ROLE_SERVICE) as RoleManager + if (roleManager.isRoleAvailable(RoleManager.ROLE_HOME)) { + val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_HOME) + startActivityForResult(intent, requestCode) + } else + resetDefaultLauncher() +} + +fun Context.resetDefaultLauncher() { + try { + val componentName = ComponentName(this, FakeHomeActivity::class.java) + packageManager.setComponentEnabledSetting( + componentName, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP + ) + val selector = Intent(Intent.ACTION_MAIN) + selector.addCategory(Intent.CATEGORY_HOME) + startActivity(selector) + packageManager.setComponentEnabledSetting( + componentName, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP + ) + } catch (e: Exception) { + e.printStackTrace() + } +} + +fun Context.isDefaultLauncher(): Boolean { + val launcherPackageName = getDefaultLauncherPackage(this) + return BuildConfig.APPLICATION_ID == launcherPackageName +} + +fun Context.resetLauncherViaFakeActivity() { + resetDefaultLauncher() + if (getDefaultLauncherPackage(this).contains(".")) + startActivity(Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)) +} + +fun Context.openSearch(query: String? = null) { + val intent = Intent(Intent.ACTION_WEB_SEARCH) + intent.putExtra(SearchManager.QUERY, query ?: "") + startActivity(intent) +} + +fun Context.isEinkDisplay(): Boolean { + return try { + val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager + windowManager.defaultDisplay.refreshRate <= Constants.MIN_ANIM_REFRESH_RATE + } catch (e: Exception) { + e.printStackTrace() + false + } +} + +fun Context.searchOnPlayStore(query: String? = null): Boolean { + return try { + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("https://play.google.com/store/search?q=$query&c=apps") + ).addFlags( + Intent.FLAG_ACTIVITY_NO_HISTORY or + Intent.FLAG_ACTIVITY_NEW_DOCUMENT or + Intent.FLAG_ACTIVITY_MULTIPLE_TASK + ) + ) + true + } catch (e: Exception) { + e.printStackTrace() + false + } +} + +fun Context.isPackageInstalled(packageName: String, userHandle: UserHandle = android.os.Process.myUserHandle()): Boolean { + val launcher = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + val activityInfo = launcher.getActivityList(packageName, userHandle) + return activityInfo.isNotEmpty() +} + +fun Context.isCountryIn(): Boolean { + val country = runCatching { + val telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager + telephonyManager?.simCountryIso?.takeIf { it.isNotBlank() } + ?: telephonyManager?.networkCountryIso?.takeIf { it.isNotBlank() } + }.getOrNull() ?: Locale.getDefault().country + return country.equals("IN", ignoreCase = true) +} + +@RequiresApi(Build.VERSION_CODES.Q) +fun Context.appUsagePermissionGranted(): Boolean { + val appOpsManager = getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager + return appOpsManager.unsafeCheckOpNoThrow( + "android:get_usage_stats", + android.os.Process.myUid(), + packageName + ) == AppOpsManager.MODE_ALLOWED +} + +fun Context.formattedTimeSpent(timeSpent: Long): String { + val seconds = timeSpent / 1000 + val minutes = seconds / 60 + val hours = minutes / 60 + val remainingMinutes = minutes % 60 + return when { + timeSpent == 0L -> "0m" + + hours > 0 -> getString( + R.string.time_spent_hour, + hours.toString(), + remainingMinutes.toString() + ) + + minutes > 0 -> { + getString(R.string.time_spent_min, minutes.toString()) + } + + else -> "<1m" + } +} + +fun Long.convertEpochToMidnight(): Long { + val calendar = Calendar.getInstance() + calendar.timeInMillis = this + calendar.set(Calendar.HOUR_OF_DAY, 0) + calendar.set(Calendar.MINUTE, 0) + calendar.set(Calendar.SECOND, 0) + calendar.set(Calendar.MILLISECOND, 0) + return calendar.timeInMillis +} + +fun Long.isDaySince(): Int = ((System.currentTimeMillis().convertEpochToMidnight() - this.convertEpochToMidnight()) + / Constants.ONE_DAY_IN_MILLIS).toInt() + +fun Long.hasBeenDays(days: Int): Boolean = + ((System.currentTimeMillis() - this) / Constants.ONE_DAY_IN_MILLIS) >= days + +fun Long.hasBeenHours(hours: Int): Boolean = + ((System.currentTimeMillis() - this) / Constants.ONE_HOUR_IN_MILLIS) >= hours + +fun Long.hasBeenMinutes(minutes: Int): Boolean = + ((System.currentTimeMillis() - this) / Constants.ONE_MINUTE_IN_MILLIS) >= minutes + +fun Int.dpToPx(): Int { + return (this * Resources.getSystem().displayMetrics.density).toInt() +} diff --git a/app/src/main/java/app/sidephonelauncher/helper/FakeHomeActivity.kt b/app/src/main/java/app/sidephonelauncher/helper/FakeHomeActivity.kt new file mode 100644 index 0000000..1a01634 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/helper/FakeHomeActivity.kt @@ -0,0 +1,12 @@ +package app.sidephonelauncher.helper + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import app.sidephonelauncher.R + +class FakeHomeActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_fake_home) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/helper/HomeNotificationListenerService.kt b/app/src/main/java/app/sidephonelauncher/helper/HomeNotificationListenerService.kt new file mode 100644 index 0000000..36de5d0 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/helper/HomeNotificationListenerService.kt @@ -0,0 +1,27 @@ +package app.sidephonelauncher.helper + +import android.service.notification.NotificationListenerService +import android.service.notification.StatusBarNotification + +class HomeNotificationListenerService : NotificationListenerService() { + + override fun onListenerConnected() { + super.onListenerConnected() + NotificationDotRepository.update(activeNotifications) + } + + override fun onNotificationPosted(sbn: StatusBarNotification?) { + super.onNotificationPosted(sbn) + NotificationDotRepository.update(activeNotifications) + } + + override fun onNotificationRemoved(sbn: StatusBarNotification?) { + super.onNotificationRemoved(sbn) + NotificationDotRepository.update(activeNotifications) + } + + override fun onListenerDisconnected() { + NotificationDotRepository.clear() + super.onListenerDisconnected() + } +} diff --git a/app/src/main/java/app/sidephonelauncher/helper/MyAccessibilityService.kt b/app/src/main/java/app/sidephonelauncher/helper/MyAccessibilityService.kt new file mode 100644 index 0000000..d9f6151 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/helper/MyAccessibilityService.kt @@ -0,0 +1,45 @@ +package app.sidephonelauncher.helper + +import android.accessibilityservice.AccessibilityService +import android.content.Intent +import android.os.Build +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import app.sidephonelauncher.R +import app.sidephonelauncher.data.Prefs + +class MyAccessibilityService : AccessibilityService() { + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + return START_STICKY + } + + override fun onServiceConnected() { + Prefs(applicationContext).lockModeOn = true + super.onServiceConnected() + } + + override fun onAccessibilityEvent(event: AccessibilityEvent) { + try { + val source: AccessibilityNodeInfo = event.source ?: return + if (source.className != "android.widget.FrameLayout") return + + when (source.contentDescription) { + getString(R.string.lock_layout_description) -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN) + } + // Home button for recents feature disabled + // getString(R.string.recents_layout_description) -> { + // performGlobalAction(GLOBAL_ACTION_RECENTS) + // } + } + } catch (e: Exception) { + return + } + } + + override fun onInterrupt() { + + } +} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/helper/NotificationDotRepository.kt b/app/src/main/java/app/sidephonelauncher/helper/NotificationDotRepository.kt new file mode 100644 index 0000000..a4825f4 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/helper/NotificationDotRepository.kt @@ -0,0 +1,58 @@ +package app.sidephonelauncher.helper + +import android.app.Notification +import android.service.notification.StatusBarNotification +import java.util.concurrent.CopyOnWriteArraySet + +object NotificationDotRepository { + private val listeners = CopyOnWriteArraySet<() -> Unit>() + + @Volatile + private var packagesWithNotifications: Set = emptySet() + + fun addListener(listener: () -> Unit) { + listeners += listener + } + + fun removeListener(listener: () -> Unit) { + listeners -= listener + } + + fun hasActiveNotification(packageName: String): Boolean { + if (packageName.isBlank()) return false + return packageName in packagesWithNotifications + } + + fun update(activeNotifications: Array?) { + val nextPackages = activeNotifications + ?.asSequence() + ?.filterNot(::shouldIgnore) + ?.map { it.packageName } + ?.filter { it.isNotBlank() } + ?.toSet() + ?: emptySet() + + if (nextPackages == packagesWithNotifications) return + packagesWithNotifications = nextPackages + listeners.forEach { it() } + } + + fun clear() { + if (packagesWithNotifications.isEmpty()) return + packagesWithNotifications = emptySet() + listeners.forEach { it() } + } + + private fun shouldIgnore(sbn: StatusBarNotification): Boolean { + val notification = sbn.notification + val flags = notification.flags + val category = notification.category + val isGroupSummary = flags and Notification.FLAG_GROUP_SUMMARY != 0 + val isForegroundService = flags and Notification.FLAG_FOREGROUND_SERVICE != 0 + val isOngoing = flags and Notification.FLAG_ONGOING_EVENT != 0 + val isPersistent = !sbn.isClearable + val isTransport = category == Notification.CATEGORY_TRANSPORT + val isService = category == Notification.CATEGORY_SERVICE + return isGroupSummary || isForegroundService || isOngoing || isPersistent || isTransport || isService + } +} diff --git a/app/src/main/java/app/sidephonelauncher/helper/PinItemActivity.kt b/app/src/main/java/app/sidephonelauncher/helper/PinItemActivity.kt new file mode 100644 index 0000000..1921cfe --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/helper/PinItemActivity.kt @@ -0,0 +1,51 @@ +package app.sidephonelauncher.helper + +import android.content.pm.LauncherApps +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class PinItemActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Set window to be transparent + window.setBackgroundDrawable(null) + + val launcherApps = getSystemService(LauncherApps::class.java) + val pinItemRequest = launcherApps.getPinItemRequest(intent) + + when (pinItemRequest != null) { + true -> handleRequestType(pinItemRequest) + false -> showToast("Invalid pin request") + } + + finish() + } + + private fun handleRequestType(pinItemRequest: LauncherApps.PinItemRequest) { + when (pinItemRequest.requestType) { + LauncherApps.PinItemRequest.REQUEST_TYPE_SHORTCUT -> + handleShortcutRequest(pinItemRequest) + + LauncherApps.PinItemRequest.REQUEST_TYPE_APPWIDGET -> + showToast("Widgets are not supported") + + else -> showToast("Unknown action not supported") + } + } + + private fun handleShortcutRequest(pinItemRequest: LauncherApps.PinItemRequest) { + val shortcutInfo = pinItemRequest.shortcutInfo + if (shortcutInfo != null) { + val success = pinItemRequest.accept() + val message = when (success) { + true -> "Shortcut pinned successfully" + false -> "Failed to pin shortcut" + } + showToast(message) + } else { + showToast("Invalid shortcut info") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/helper/SingleLiveEvent.kt b/app/src/main/java/app/sidephonelauncher/helper/SingleLiveEvent.kt new file mode 100644 index 0000000..84d66d3 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/helper/SingleLiveEvent.kt @@ -0,0 +1,36 @@ +package app.sidephonelauncher.helper + +import android.util.Log +import androidx.annotation.MainThread +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import java.util.concurrent.atomic.AtomicBoolean + +class SingleLiveEvent : MutableLiveData() { + private val pending = AtomicBoolean(false) + + @MainThread + override fun observe(owner: LifecycleOwner, observer: Observer) { + if (hasActiveObservers()) { + Log.d("SingleLiveEvent", "Only one observer will be notified of the changes") + } + + super.observe(owner) { t -> + if (pending.compareAndSet(true, false)) { + observer.onChanged(t) + } + } + } + + @MainThread + override fun setValue(t: T?) { + pending.set(true) + super.setValue(t) + } + + @MainThread + fun call() { + value = null + } +} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/helper/Utils.kt b/app/src/main/java/app/sidephonelauncher/helper/Utils.kt new file mode 100644 index 0000000..b9cf6c3 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/helper/Utils.kt @@ -0,0 +1,672 @@ +package app.sidephonelauncher.helper + +import android.annotation.SuppressLint +import android.app.SearchManager +import android.app.WallpaperManager +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.LauncherApps +import android.content.res.Configuration +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Point +import android.net.Uri +import android.os.Build +import android.os.UserHandle +import android.os.UserManager +import android.provider.AlarmClock +import android.provider.CalendarContract +import android.provider.MediaStore +import android.provider.Settings +import android.util.DisplayMetrics +import android.util.Log +import android.util.TypedValue +import android.view.View +import android.view.WindowManager +import android.view.animation.LinearInterpolator +import android.widget.Toast +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.graphics.createBitmap +import androidx.core.net.toUri +import app.sidephonelauncher.BuildConfig +import app.sidephonelauncher.R +import app.sidephonelauncher.data.AppModel +import app.sidephonelauncher.data.Constants +import app.sidephonelauncher.data.Prefs +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import java.io.InputStream +import java.net.HttpURLConnection +import java.net.URL +import java.text.Collator +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.Scanner +import kotlin.math.pow +import kotlin.math.sqrt + +fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) { + if (message.isNullOrBlank()) return + Toast.makeText(this, message, duration).show() +} + +fun Context.showToast(stringResource: Int, duration: Int = Toast.LENGTH_SHORT) { + Toast.makeText(this, getString(stringResource), duration).show() +} + +suspend fun getAppsList( + context: Context, + prefs: Prefs, + includeRegularApps: Boolean = true, + includeHiddenApps: Boolean = false, +): MutableList { + return withContext(Dispatchers.IO) { + val appList: MutableList = mutableListOf() + + try { + if (!Prefs(context).hiddenAppsUpdated) upgradeHiddenApps(Prefs(context)) + val hiddenApps = Prefs(context).hiddenApps + + val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager + val launcherApps = + context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + val collator = Collator.getInstance() + + for (profile in userManager.userProfiles) { + if (isPrivateSpaceProfile(context, profile)) continue + for (app in launcherApps.getActivityList(null, profile)) { + val appLabelShown = prefs.getAppRenameLabel(app.applicationInfo.packageName) + .ifBlank { app.label.toString() } + val appModel = AppModel.App( + appLabel = appLabelShown, + key = collator.getCollationKey(app.label.toString()), + appPackage = app.applicationInfo.packageName, + activityClassName = app.componentName.className, + isNew = (System.currentTimeMillis() - app.firstInstallTime) < Constants.ONE_HOUR_IN_MILLIS, + user = profile + ) + + // if the current app is not OLauncher + if (app.applicationInfo.packageName != BuildConfig.APPLICATION_ID) { + // is this a hidden app? + if (hiddenApps.contains(app.applicationInfo.packageName + "|" + profile.toString())) { + if (includeHiddenApps) { + appList.add(appModel) + } + } else { + // this is a regular app + if (includeRegularApps) { + appList.add(appModel) + } + } + } + } + } + + // Add shortcuts if we're getting regular apps + if (includeRegularApps && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val pinned = try { + getPinnedShortcuts(context, prefs, collator) + } catch (e: Exception) { + emptyList() + } + appList.addAll(pinned) + } + + appList.sortWith(compareBy(collator) { it.appLabel }) + } catch (e: Exception) { + e.printStackTrace() + } + appList + } +} + +@RequiresApi(Build.VERSION_CODES.O) +private suspend fun getPinnedShortcuts( + context: Context, + prefs: Prefs, + collator: Collator, +): List = + withContext(Dispatchers.IO) { + val pinnedShortcuts = mutableListOf() + val shortcuts = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as? LauncherApps + if (shortcuts?.hasShortcutHostPermission() == true) { + val query = LauncherApps.ShortcutQuery().apply { + setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED) + } + shortcuts.profiles.forEach { profile -> + if (isPrivateSpaceProfile(context, profile)) return@forEach + try { + shortcuts.getShortcuts(query, profile)?.forEach { shortcut -> + if (shortcut.isPinned && pinnedShortcuts.none { it.shortcutId == shortcut.id }) { + val label = prefs.getAppRenameLabel(shortcut.id) + .takeIf { it.isNotBlank() } + ?: shortcut.shortLabel?.toString() + ?: shortcut.longLabel?.toString().orEmpty() + pinnedShortcuts.add( + AppModel.PinnedShortcut( + appLabel = label, + key = collator.getCollationKey(label), + appPackage = shortcut.`package`, + shortcutId = shortcut.id, + isNew = false, + user = profile + ) + ) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + pinnedShortcuts + } + +// This is to ensure backward compatibility with older app versions +// which did not support multiple user profiles +private fun upgradeHiddenApps(prefs: Prefs) { + val hiddenAppsSet = prefs.hiddenApps + val newHiddenAppsSet = mutableSetOf() + for (hiddenPackage in hiddenAppsSet) { + if (hiddenPackage.contains("|")) newHiddenAppsSet.add(hiddenPackage) + else newHiddenAppsSet.add(hiddenPackage + android.os.Process.myUserHandle().toString()) + } + prefs.hiddenApps = newHiddenAppsSet + prefs.hiddenAppsUpdated = true +} + +fun isPackageInstalled(context: Context, packageName: String, userString: String): Boolean { + val launcher = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + val activityInfo = launcher.getActivityList(packageName, getUserHandleFromString(context, userString)) + if (activityInfo.isNotEmpty()) return true + return false +} + +fun isPrivateSpaceProfile(context: Context, userHandle: UserHandle): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) return false + return try { + val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + launcherApps.getLauncherUserInfo(userHandle)?.userType == "android.os.usertype.profile.PRIVATE" + } catch (e: Exception) { + false + } +} + +fun isPrivateSpaceLocked(context: Context, userHandle: UserHandle): Boolean { + return try { + val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager + userManager.isQuietModeEnabled(userHandle) + } catch (e: Exception) { + true + } +} + +fun getPrivateSpaceUserHandle(context: Context): UserHandle? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) return null + val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager + for (profile in userManager.userProfiles) { + if (isPrivateSpaceProfile(context, profile)) return profile + } + return null +} + +suspend fun getPrivateSpaceApps( + context: Context, + prefs: Prefs, +): MutableList { + return withContext(Dispatchers.IO) { + val appList: MutableList = mutableListOf() + try { + val privateSpaceHandle = getPrivateSpaceUserHandle(context) ?: return@withContext appList + val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + val collator = Collator.getInstance() + + for (app in launcherApps.getActivityList(null, privateSpaceHandle)) { + if (app.applicationInfo.packageName == BuildConfig.APPLICATION_ID) continue + val appLabelShown = prefs.getAppRenameLabel(app.applicationInfo.packageName) + .ifBlank { app.label.toString() } + appList.add( + AppModel.App( + appLabel = appLabelShown, + key = collator.getCollationKey(app.label.toString()), + appPackage = app.applicationInfo.packageName, + activityClassName = app.componentName.className, + isNew = false, + user = privateSpaceHandle + ) + ) + } + appList.sortWith(compareBy(collator) { it.appLabel }) + } catch (e: Exception) { + e.printStackTrace() + } + appList + } +} + +fun getUserHandleFromString(context: Context, userHandleString: String): UserHandle { + val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager + for (userHandle in userManager.userProfiles) { + if (userHandle.toString() == userHandleString) { + return userHandle + } + } + return android.os.Process.myUserHandle() +} + +fun isOlauncherDefault(context: Context): Boolean { + val launcherPackageName = getDefaultLauncherPackage(context) + return BuildConfig.APPLICATION_ID == launcherPackageName +} + +fun getDefaultLauncherPackage(context: Context): String { + val intent = Intent() + intent.action = Intent.ACTION_MAIN + intent.addCategory(Intent.CATEGORY_HOME) + val packageManager = context.packageManager + val result = packageManager.resolveActivity(intent, 0) + return if (result?.activityInfo != null) { + result.activityInfo.packageName + } else "android" +} + +fun setPlainWallpaperByTheme(context: Context, appTheme: Int) { + when (appTheme) { + AppCompatDelegate.MODE_NIGHT_YES -> setPlainWallpaper(context, android.R.color.black) + AppCompatDelegate.MODE_NIGHT_NO -> setPlainWallpaper(context, android.R.color.white) + else -> { + if (context.isDarkThemeOn()) + setPlainWallpaper(context, android.R.color.black) + else setPlainWallpaper(context, android.R.color.white) + } + } +} + +fun setPlainWallpaper(context: Context, color: Int) { + try { + val bitmap = createBitmap(1000, 2000) + bitmap.eraseColor(context.getColor(color)) + val manager = WallpaperManager.getInstance(context) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + manager.setBitmap(bitmap, null, false, WallpaperManager.FLAG_SYSTEM) + manager.setBitmap(bitmap, null, false, WallpaperManager.FLAG_LOCK) + } else + manager.setBitmap(bitmap) + bitmap.recycle() + } catch (e: Exception) { + e.printStackTrace() + } +} + +fun getChangedAppTheme(context: Context, currentAppTheme: Int): Int { + return when (currentAppTheme) { + AppCompatDelegate.MODE_NIGHT_YES -> AppCompatDelegate.MODE_NIGHT_NO + AppCompatDelegate.MODE_NIGHT_NO -> AppCompatDelegate.MODE_NIGHT_YES + else -> { + if (context.isDarkThemeOn()) + AppCompatDelegate.MODE_NIGHT_NO + else AppCompatDelegate.MODE_NIGHT_YES + } + } +} + +fun openAppInfo(context: Context, userHandle: UserHandle, packageName: String) { + val launcher = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + val component = launcher.getActivityList(packageName, userHandle).firstOrNull()?.componentName + if (component != null) + launcher.startAppDetailsActivity(component, userHandle, null, null) + else + context.showToast(context.getString(R.string.unable_to_open_app_info)) +} + +suspend fun getBitmapFromURL(src: String?): Bitmap? { + return withContext(Dispatchers.IO) { + var bitmap: Bitmap? = null + try { + val url = URL(src) + val connection: HttpURLConnection = url + .openConnection() as HttpURLConnection + connection.doInput = true + connection.connect() + val input: InputStream = connection.inputStream + bitmap = BitmapFactory.decodeStream(input) + } catch (e: Exception) { + e.printStackTrace() + } + bitmap + } +} + +suspend fun getWallpaperBitmap(originalImage: Bitmap, width: Int, height: Int): Bitmap { + return withContext(Dispatchers.IO) { + + val background = createBitmap(width, height) + + val originalWidth: Float = originalImage.width.toFloat() + val originalHeight: Float = originalImage.height.toFloat() + + val canvas = Canvas(background) + val heightScale: Float = height / originalHeight + val widthScale: Float = width / originalWidth + val scale = maxOf(heightScale, widthScale) + + val (xTranslation, yTranslation) = if (heightScale > widthScale) + Pair((width - originalWidth * heightScale) / 2.0f, 0f) + else + Pair(0f, (height - originalHeight * widthScale) / 2.0f) + + val transformation = Matrix() + transformation.postTranslate(xTranslation, yTranslation) + transformation.preScale(scale, scale) + + val paint = Paint() + paint.isFilterBitmap = true + canvas.drawBitmap(originalImage, transformation, paint) + + background + } +} + +suspend fun setWallpaper(appContext: Context, url: String): Boolean { + return withContext(Dispatchers.IO) { + val originalImageBitmap = getBitmapFromURL(url) ?: return@withContext false + if (appContext.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && isTablet(appContext).not()) + return@withContext false + + val wallpaperManager = WallpaperManager.getInstance(appContext) + val (width, height) = getScreenDimensions(appContext) + val scaledBitmap = getWallpaperBitmap(originalImageBitmap, width, height) + + try { + wallpaperManager.setBitmap(scaledBitmap, null, false, WallpaperManager.FLAG_SYSTEM) + wallpaperManager.setBitmap(scaledBitmap, null, false, WallpaperManager.FLAG_LOCK) + } catch (e: Exception) { + return@withContext false + } + + try { + originalImageBitmap.recycle() + scaledBitmap.recycle() + } catch (e: Exception) { + e.printStackTrace() + } + true + } +} + +fun getScreenDimensions(context: Context): Pair { + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val point = Point() + windowManager.defaultDisplay.getRealSize(point) + return Pair(point.x, point.y) +} + +suspend fun getTodaysWallpaper(wallType: String, firstOpenTime: Long): String { + return withContext(Dispatchers.IO) { + var wallpaperUrl: String + try { + val key = if (firstOpenTime.isDaySince() < 10) + String.format("0_%s", firstOpenTime.isDaySince().toString()) + else { + val month = SimpleDateFormat("M", Locale.ENGLISH).format(Date()) ?: "0" + val day = SimpleDateFormat("d", Locale.ENGLISH).format(Date()) ?: "0" + String.format("%s_%s", month, day) + } + + val url = URL(Constants.URL_WALLPAPERS) + val connection: HttpURLConnection = url.openConnection() as HttpURLConnection + connection.doInput = true + connection.connect() + + val inputStream = connection.inputStream + val scanner = Scanner(inputStream) + val stringBuffer = StringBuffer() + while (scanner.hasNext()) { + stringBuffer.append(scanner.nextLine()) + } + + val json = JSONObject(stringBuffer.toString()) + val wallpapers = json.getString(key) + val wallpapersJson = JSONObject(wallpapers) + wallpaperUrl = wallpapersJson.getString(wallType) + wallpaperUrl + + } catch (e: Exception) { + wallpaperUrl = getBackupWallpaper(wallType) + wallpaperUrl + } + } +} + +fun getBackupWallpaper(wallType: String): String { + return if (wallType == Constants.WALL_TYPE_LIGHT) + Constants.URL_DEFAULT_LIGHT_WALLPAPER + else Constants.URL_DEFAULT_DARK_WALLPAPER +} + +fun openSearch(context: Context) { + val intent = Intent(Intent.ACTION_WEB_SEARCH) + intent.putExtra(SearchManager.QUERY, "") + context.startActivity(intent) +} + +@SuppressLint("WrongConstant", "PrivateApi") +fun expandNotificationDrawer(context: Context) { + // Source: https://stackoverflow.com/a/51132142 + try { + val statusBarService = context.getSystemService("statusbar") + val statusBarManager = Class.forName("android.app.StatusBarManager") + val method = statusBarManager.getMethod("expandNotificationsPanel") + method.invoke(statusBarService) + } catch (e: Exception) { + e.printStackTrace() + } +} + +fun openDialerApp(context: Context) { + try { + val sendIntent = Intent(Intent.ACTION_DIAL) + context.startActivity(sendIntent) + } catch (e: Exception) { + e.printStackTrace() + } +} + +fun openCameraApp(context: Context) { + try { + val sendIntent = Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA) + context.startActivity(sendIntent) + } catch (e: Exception) { + e.printStackTrace() + } +} + +fun openAlarmApp(context: Context) { + try { + val intent = Intent(AlarmClock.ACTION_SHOW_ALARMS) + context.startActivity(intent) + } catch (e: Exception) { + Log.d("TAG", e.toString()) + } +} + +fun openCalendar(context: Context) { + try { + val calendarUri = CalendarContract.CONTENT_URI + .buildUpon() + .appendPath("time") + .build() + context.startActivity(Intent(Intent.ACTION_VIEW, calendarUri)) + } catch (e: Exception) { + try { + val intent = Intent(Intent.ACTION_MAIN) + intent.addCategory(Intent.CATEGORY_APP_CALENDAR) + context.startActivity(intent) + } catch (e: Exception) { + e.printStackTrace() + } + } +} + +fun isAccessServiceEnabled(context: Context): Boolean { + val enabled = try { + Settings.Secure.getInt(context.applicationContext.contentResolver, Settings.Secure.ACCESSIBILITY_ENABLED) + } catch (e: Exception) { + 0 + } + if (enabled == 1) { + val enabledServicesString: String? = Settings.Secure.getString(context.contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES) + return enabledServicesString?.contains(context.packageName + "/" + MyAccessibilityService::class.java.name) ?: false + } + return false +} + +fun isTablet(context: Context): Boolean { + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val metrics = DisplayMetrics() + windowManager.defaultDisplay.getMetrics(metrics) + val widthInches = metrics.widthPixels / metrics.xdpi + val heightInches = metrics.heightPixels / metrics.ydpi + val diagonalInches = sqrt(widthInches.toDouble().pow(2.0) + heightInches.toDouble().pow(2.0)) + if (diagonalInches >= 7.0) return true + return false +} + +fun Context.isDarkThemeOn(): Boolean { + return resources.configuration.uiMode and + Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES +} + +fun Context.copyToClipboard(text: String) { + val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText(getString(R.string.app_name), text) + clipboardManager.setPrimaryClip(clipData) + showToast("") +} + +fun Context.openUrl(url: String) { + if (url.isEmpty()) return + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse(url) + startActivity(intent) +} + +fun Context.isSystemApp(packageName: String, user: UserHandle? = null): Boolean { + if (packageName.isBlank()) return true + return try { + val launcherApps = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + val targetUser = user ?: android.os.Process.myUserHandle() + val activityList = launcherApps.getActivityList(packageName, targetUser) + if (activityList.isNotEmpty()) { + val applicationInfo = activityList.first().applicationInfo + ((applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) + || (applicationInfo.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP != 0)) + } else { + val applicationInfo = packageManager.getApplicationInfo(packageName, 0) + ((applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) + || (applicationInfo.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP != 0)) + } + } catch (e: Exception) { + e.printStackTrace() + false + } +} + +fun Context.uninstall(packageName: String) { + val intent = Intent(Intent.ACTION_DELETE) + intent.data = Uri.parse("package:$packageName") + startActivity(intent) +} + +@ColorInt +fun Context.getColorFromAttr( + @AttrRes attrColor: Int, + typedValue: TypedValue = TypedValue(), + resolveRefs: Boolean = true, +): Int { + theme.resolveAttribute(attrColor, typedValue, resolveRefs) + return typedValue.data +} + +fun View.animateAlpha(alpha: Float = 1.0f) { + this.animate().apply { + interpolator = LinearInterpolator() + duration = 200 + alpha(alpha) + start() + } +} + +fun Context.shareApp() { + val message = getString(R.string.are_you_using_your_phone_or_is_your_phone_using_you) + + "\n" + Constants.URL_OLAUNCHER_PLAY_STORE + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, message) + type = "text/plain" + } + + val shareIntent = Intent.createChooser(sendIntent, null) + startActivity(shareIntent) +} + +fun Context.rateApp() { + val intent = Intent( + Intent.ACTION_VIEW, + Constants.URL_OLAUNCHER_PLAY_STORE.toUri() + ) + var flags = Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_MULTIPLE_TASK + flags = flags or Intent.FLAG_ACTIVITY_NEW_DOCUMENT + intent.addFlags(flags) + startActivity(intent) +} + +@RequiresApi(Build.VERSION_CODES.N_MR1) +fun Context.deletePinnedShortcut(packageName: String, shortcutIdToDelete: String, user: UserHandle) { + val launcherApps = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + + // 1. Query for existing pinned shortcuts for the package + val query = LauncherApps.ShortcutQuery().apply { + setPackage(packageName) + // Query only for pinned shortcuts + setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED) + } + + try { + val pinnedShortcuts = launcherApps.getShortcuts(query, user) + + if (pinnedShortcuts != null) { + // 2. Filter out the shortcut to be deleted + val updatedPinnedIds = pinnedShortcuts + .filter { it.id != shortcutIdToDelete } + .map { it.id } + + // 3. Re-pin the remaining shortcuts + // This replaces the existing set of pinned shortcuts for this package + launcherApps.pinShortcuts(packageName, updatedPinnedIds, user) + } + } catch (e: SecurityException) { + // Handle cases where the app doesn't have permission + // (e.g., not the default launcher or active voice interaction service) + Log.e("ShortcutHelper", "Permission denied to modify pinned shortcuts for $packageName", e) + } catch (e: IllegalStateException) { + // Handle cases where the user profile is locked or not running + Log.e("ShortcutHelper", "User profile unavailable for modifying pinned shortcuts for $packageName", e) + } catch (e: Exception) { + // Handle other potential exceptions (like RemoteException wrapped) + Log.e("ShortcutHelper", "Failed to modify pinned shortcuts for $packageName", e) + } +} diff --git a/app/src/main/java/app/sidephonelauncher/helper/WallpaperWorker.kt b/app/src/main/java/app/sidephonelauncher/helper/WallpaperWorker.kt new file mode 100644 index 0000000..1f00826 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/helper/WallpaperWorker.kt @@ -0,0 +1,47 @@ +package app.sidephonelauncher.helper + +import android.content.Context +import androidx.appcompat.app.AppCompatDelegate +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import app.sidephonelauncher.data.Constants +import app.sidephonelauncher.data.Prefs +import kotlinx.coroutines.coroutineScope + +class WallpaperWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { + + private val prefs = Prefs(applicationContext) + + override suspend fun doWork(): Result = coroutineScope { + val success = + if (prefs.appTheme == AppCompatDelegate.MODE_NIGHT_YES && isOlauncherDefault(applicationContext).not()) + true + else if (prefs.dailyWallpaper) { + val wallType = checkWallpaperType() + val wallpaperUrl = getTodaysWallpaper(wallType, prefs.firstOpenTime) + if (prefs.dailyWallpaperUrl == wallpaperUrl) + true + else { + prefs.dailyWallpaperUrl = wallpaperUrl + setWallpaper(applicationContext, wallpaperUrl) + } + } else + true + + if (success) + Result.success() + else + Result.retry() + } + + private fun checkWallpaperType(): String { + return when (prefs.appTheme) { + AppCompatDelegate.MODE_NIGHT_YES -> Constants.WALL_TYPE_DARK + AppCompatDelegate.MODE_NIGHT_NO -> Constants.WALL_TYPE_LIGHT + else -> if (applicationContext.isDarkThemeOn()) + Constants.WALL_TYPE_DARK + else + Constants.WALL_TYPE_LIGHT + } + } +} diff --git a/app/src/main/java/app/sidephonelauncher/helper/usageStats/ComponentForegroundStat.kt b/app/src/main/java/app/sidephonelauncher/helper/usageStats/ComponentForegroundStat.kt new file mode 100644 index 0000000..0d587d6 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/helper/usageStats/ComponentForegroundStat.kt @@ -0,0 +1,28 @@ +package app.sidephonelauncher.helper.usageStats + +import java.util.Date + +/** + * Object representing a timespan that an application was in the foreground. + * + * @property beginTime The start of the timespan, in milliseconds since the epoch. + * @property endTime The end of the timespan, in milliseconds since the epoch. + * @property packageName The package name of the application. + */ +data class ComponentForegroundStat( + val beginTime: Long, + val endTime: Long, + val packageName: String +) { + /** + * Overriding toString for a more readable log output, similar to the original Java class. + * The default data class toString() would also work, but this one is more explicit + * about formatting the timestamps as Instants. + */ + override fun toString(): String { + return "ComponentForegroundStat(" + + "beginTime=${Date(beginTime)}, " + + "endTime=${Date(endTime)}, " + + "packageName='$packageName')" + } +} diff --git a/app/src/main/java/app/sidephonelauncher/helper/usageStats/EventLogWrapper.kt b/app/src/main/java/app/sidephonelauncher/helper/usageStats/EventLogWrapper.kt new file mode 100644 index 0000000..3578e35 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/helper/usageStats/EventLogWrapper.kt @@ -0,0 +1,391 @@ +package app.sidephonelauncher.helper.usageStats + +import android.app.ActivityManager +import android.app.usage.UsageEvents +import android.app.usage.UsageStatsManager +import android.content.Context +import android.icu.util.Calendar +import android.util.Log +import java.util.concurrent.TimeUnit +import java.util.function.BiConsumer +import kotlin.math.max +import kotlin.math.min + +class EventLogWrapper(private val context: Context) { + private val usageStatsManager by lazy { context.getSystemService("usagestats") as UsageStatsManager } + private val guardian = UnmatchedCloseEventGuardian(usageStatsManager) + + + /** + * Collects event information from system to calculate and aggregate precise + * foreground time statistics for the specified period. + * + * Comments refer to the cases from + * [the documentation.](https://codeberg.org/fynngodau/usageDirect/wiki/Event-log-wrapper-scenarios) + * + * @param start First point in time to include in results + * @param end Last point in time to include in results + * @return A list of foreground stats for the specified period + */ + fun getForegroundStatsByTimestamps(start: Long, end: Long): List { + var queryStart = start // Can be mutated by DEVICE_STARTUP event + + /* + * Because sometimes, open events do not have close events when they should, as a hack / workaround, + * we query the apps currently in the foreground and match them against the apps that are currently + * in the foreground if the query start date is very recent or in the future. Thus, we are using this + * to tell apart True from Faulty unmatched open events. + * + * We query processes in the beginning of this method call in case querying the event log takes a + * little longer. + */ + val foregroundProcesses = mutableListOf() + if (end >= System.currentTimeMillis() - 1500) { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager + activityManager?.runningAppProcesses?.forEach { appProcess -> + if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND || + appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE + ) { + if (context.packageName != appProcess.processName) { + foregroundProcesses.add(appProcess.processName) + } + } + } + } + + // Assumption: events are ordered chronologically + val events = usageStatsManager.queryEvents(queryStart, end) + + /* …except that sometimes, the events that are close to each other are swapped in a way that + * breaks the assumption that all end times which do not have a matching start time have + * started before start. We handle those as Duplicate close event and Duplicate open event. + * Therefore, we keep null entries in our moveToForegroundMap instead of removing the entries + * to prevent apps that had been opened previously in a period from being counted as "opened + * before start" (as they are not a True unmatched close event). + */ + // Map components to the last moveToForeground event. Null value means it was closed. + val moveToForegroundMap = mutableMapOf() + val componentForegroundStats = mutableListOf() + + // Iterate over events + val event = UsageEvents.Event() + + while (events.hasNextEvent()) { + events.getNextEvent(event) + if (context.packageName == event.packageName) continue + + val appClass = AppClass(event.packageName, event.className) + + when (event.eventType) { + /* + * "An event type denoting that an android.app.Activity moved to the foreground." + * (old definition: "An event type denoting that a component moved to the foreground.") + */ + UsageEvents.Event.ACTIVITY_RESUMED, + /* + * public static final int android.app.usage.UsageEvents.Event.CONTINUE_PREVIOUS_DAY = 4; + * (annotated as @hide) + * "An event type denoting that a component was in the foreground the previous day. + * This is effectively treated as a MOVE_TO_FOREGROUND." + */ + 4 -> { + // Store open timestamp in map, overwriting earlier timestamps in case of Duplicate open event + moveToForegroundMap[appClass] = event.timeStamp + } + + /* + * "An event type denoting that an android.app.Activity moved to the background." + * (old definition: "An event type denoting that a component moved to the background.") + */ + UsageEvents.Event.ACTIVITY_PAUSED, + /* + * "An activity becomes invisible on the UI, corresponding to Activity.onStop() + * of the activity's lifecycle." + */ + UsageEvents.Event.ACTIVITY_STOPPED, + /* + * public static final int android.app.usage.UsageEvents.Event.END_OF_DAY = 3; + * (annotated as @hide) + * "An event type denoting that a component was in the foreground when the stats + * rolled-over. This is effectively treated as a {@link #MOVE_TO_BACKGROUND}." + */ + 3 -> { + val eventBeginTime: Long? = moveToForegroundMap[appClass]?.also { + // Open and close events in order. Mark as closed. + moveToForegroundMap[appClass] = null + } ?: if ( + // App has not been in this query yet (test for Duplicate close event) + moveToForegroundMap.keys.none { it.packageName == event.packageName } && + // Test if this unmatched close event is True by asking the Guardian to scan for it + guardian.test(event, queryStart) + ) { + // Identified as True unmatched close event. Take start as a starting timestamp. + queryStart + } else { + null // Ignore Faulty unmatched close event + } + + if (eventBeginTime != null) { + // Check if another of the app's components have moved to the foreground in the meantime + val endTime = moveToForegroundMap.entries + .filter { (key, value) -> key.packageName == event.packageName && value != null } + .mapNotNull { it.value } + .minOrNull() ?: event.timeStamp + + componentForegroundStats.add( + ComponentForegroundStat(eventBeginTime, endTime, event.packageName) + ) + } + } + + /* + * "An event type denoting that the Android runtime underwent a shutdown process..." + */ + UsageEvents.Event.DEVICE_SHUTDOWN -> { + // Per docs: iterate over remaining start events and treat them as closed + moveToForegroundMap.forEach { (key, value) -> + if (value != null) { // If it's a remaining start event + componentForegroundStats.add( + ComponentForegroundStat(value, event.timeStamp, key.packageName) + ) + // Set entire app to closed + moveToForegroundMap.keys + .filter { it.packageName == key.packageName } + .forEach { samePackageKey -> moveToForegroundMap[samePackageKey] = null } + } + } + } + + /* + * "An event type denoting that the Android runtime started up..." + */ + UsageEvents.Event.DEVICE_STARTUP -> { + // Per docs: remove pending open events + moveToForegroundMap.clear() + + /* No package could be open longer than a reboot. Thus, we set the `start` + * timestamp to the boot event's timestamp in case we later assume that a + * package has been open "since the start of the period". It is not logical + * that this would happen but we can never know with this API. + */ + queryStart = event.timeStamp + } + } + } + + // Iterate over remaining start events + moveToForegroundMap.forEach { (key, value) -> + if (value != null) { // If it's a remaining start event + // Test if it's a foreground app (True unmatched open event) + if (foregroundProcesses.any { it.contains(key.packageName) }) { + componentForegroundStats.add( + ComponentForegroundStat( + value, + min(System.currentTimeMillis(), end), + key.packageName + ) + ) + } + // If app is not in foreground, drop event (Assume Faulty unmatched open event) + } + } + + /* If nothing happened during the timespan but there is an app in the foreground, + * then this app was used the whole period time and there was No event for it. + * Because the foreground applications API call is documented as not to be used + * for purposes like this, we first query whether the process name is a valid + * package name and if not, we drop it. + */ + if (moveToForegroundMap.isEmpty()) { + val packageManager = context.packageManager + foregroundProcesses.forEach { foregroundProcess -> + if (packageManager.getLaunchIntentForPackage(foregroundProcess) != null) { + componentForegroundStats.add( + ComponentForegroundStat( + queryStart, + min(System.currentTimeMillis(), end), + foregroundProcess + ) + ) + Log.d("EventLogWrapper", "Assuming that application $foregroundProcess has been used the whole query time") + } + } + } + + return componentForegroundStats + } + + /** + * Collects event information from system to calculate and aggregate precise + * foreground time statistics for the specified relative day. + * + * @param offset Day to query back in time relative to today + */ + fun getForegroundStatsByRelativeDay(offset: Int): List { + val cal = Calendar.getInstance() + cal.add(Calendar.DAY_OF_YEAR, -offset) + // Set to start of the day + cal.set(Calendar.HOUR_OF_DAY, 0) + cal.set(Calendar.MINUTE, 0) + cal.set(Calendar.SECOND, 0) + cal.set(Calendar.MILLISECOND, 0) + val beginTime = cal.timeInMillis + + // Set to start of the next day + cal.add(Calendar.DAY_OF_YEAR, 1) + val endTime = cal.timeInMillis + + return getForegroundStatsByTimestamps(beginTime, endTime) + } + + /** + * Collects event information from system to calculate and aggregate precise + * foreground time statistics starting at `start` and ending at + * the end of the day that contains `start`. + * + * @param start Starting time of query and point in time in day to query + */ + fun getForegroundStatsByPartialDay(start: Long): List { + val cal = Calendar.getInstance() + cal.timeInMillis = start + // Set to start of the next day + cal.add(Calendar.DAY_OF_YEAR, 1) + cal.set(Calendar.HOUR_OF_DAY, 0) + cal.set(Calendar.MINUTE, 0) + cal.set(Calendar.SECOND, 0) + cal.set(Calendar.MILLISECOND, 0) + val endTime = cal.timeInMillis + return getForegroundStatsByTimestamps(start, endTime) + } + + /** + * Takes a list of foreground stats and aggregates them to usage stats. + * Assumes all provided usage stats to be on the same day. + * + * @param endConsumer Consumer that accepts ending times of component + * foreground stats with their package name + */ + @JvmOverloads + fun aggregateForegroundStats( + foregroundStats: List, + endConsumer: BiConsumer? = null + ): List { + if (foregroundStats.isEmpty()) return emptyList() + + // Group by package name, merge overlapping intervals, then sum durations + val applicationTotalTime = foregroundStats + .groupBy { it.packageName } + .mapValues { (_, stats) -> + mergeOverlappingIntervals(stats).sumOf { it.endTime - it.beginTime } + } + + val firstBeginTime = foregroundStats.first().beginTime + val timeZoneOffset = Calendar.getInstance().timeZone.getOffset(firstBeginTime) + val day = TimeUnit.MILLISECONDS.toDays(firstBeginTime + timeZoneOffset) + + // Optionally consume end times + endConsumer?.let { consumer -> + foregroundStats.forEach { consumer.accept(it.packageName, it.endTime) } + } + + // Map the aggregated times to SimpleUsageStat objects + return applicationTotalTime.map { (packageName, totalTime) -> + SimpleUsageStat(day, totalTime, packageName) + } + } + + /** + * Collects all event information from system to calculate and aggregate precise + * foreground time statistics for the provided day and presents this information as + * [SimpleUsageStat]s. + * + * @param day Day since epoch + */ + private fun getForegroundStatsByDay(day: Long): List { + val cal = Calendar.getInstance() + val timeZoneOffset = cal.timeZone.getOffset(System.currentTimeMillis()) + + // To convert epoch day to milliseconds, we first get the UTC millis and then adjust for timezone + val start = TimeUnit.DAYS.toMillis(day) - timeZoneOffset + val end = TimeUnit.DAYS.toMillis(day + 1) - timeZoneOffset + return getForegroundStatsByTimestamps(start, end) + } + + /** + * Collects all event information from system to calculate and aggregate precise + * foreground time statistics and presents this information as [SimpleUsageStat]s. + * **This method call causes lag** if called with a low since value. + * + * @param daySince Return data from this day on + * @param endConsumer Consumer that accepts ending times of component + * foreground stats with their package name + */ + fun getAllSimpleUsageStats(daySince: Long, endConsumer: BiConsumer): List { + val usageStats = mutableListOf() + var currentDay = daySince + + val timeZoneOffset = Calendar.getInstance().timeZone.getOffset(System.currentTimeMillis()) + val today = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() + timeZoneOffset) + + // Maximum event log size + currentDay = max(today - 10, currentDay) + + while (currentDay <= today) { + val foregroundStats = getForegroundStatsByDay(currentDay) + usageStats.addAll(aggregateForegroundStats(foregroundStats, endConsumer)) + currentDay++ + } + + return usageStats + } + + /** + * Returns only usage statistics that have not been counted yet for + * only the day that contains `timestamp` + * + * @param endConsumer Consumer that accepts ending times of component + * foreground stats with their package name + */ + fun getIncrementalSimpleUsageStats(timestamp: Long, endConsumer: BiConsumer): List { + val foregroundStats = getForegroundStatsByPartialDay(timestamp) + return aggregateForegroundStats(foregroundStats, endConsumer) + } + + fun aggregateSimpleUsageStats(usageStats: List): Long { + return usageStats.sumOf { it.timeUsed } + } + /** + * Merges overlapping or adjacent time intervals to prevent double-counting + * when multiple components of the same package have overlapping foreground times. + */ + private fun mergeOverlappingIntervals( + stats: List + ): List { + if (stats.size <= 1) return stats + + val sorted = stats.sortedBy { it.beginTime } + val merged = mutableListOf() + var current = sorted[0] + + for (i in 1 until sorted.size) { + val next = sorted[i] + if (next.beginTime <= current.endTime) { + current = ComponentForegroundStat( + current.beginTime, + max(current.endTime, next.endTime), + current.packageName + ) + } else { + merged.add(current) + current = next + } + } + merged.add(current) + return merged + } + + /** + * Stores a class name and its corresponding package. + */ + private data class AppClass(val packageName: String, val className: String?) +} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/helper/usageStats/SimpleUsageStat.kt b/app/src/main/java/app/sidephonelauncher/helper/usageStats/SimpleUsageStat.kt new file mode 100644 index 0000000..7c8681d --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/helper/usageStats/SimpleUsageStat.kt @@ -0,0 +1,48 @@ +package app.sidephonelauncher.helper.usageStats + +import android.app.usage.UsageStats +import android.icu.util.Calendar +import java.util.concurrent.TimeUnit + +/** + * A data class to hold simplified usage statistics. + * + * @property day The day since epoch that this object concerns. + * @property timeUsed The time that the application has been in the foreground in milliseconds on this day. + * @property applicationId The package name of the application that this object concerns. + */ +data class SimpleUsageStat( + val day: Long, + val timeUsed: Long, + val applicationId: String +) { + /** + * Secondary constructor to create a SimpleUsageStat from the system's [UsageStats]. + */ + constructor(systemUsageStat: UsageStats) : this( + day = getEpochDay(systemUsageStat.lastTimeUsed), + timeUsed = systemUsageStat.totalTimeInForeground, + applicationId = systemUsageStat.packageName + ) + + companion object { + /** + * Converts a list of system [UsageStats] to a list of [SimpleUsageStat]. + * This function is kept for direct compatibility with the original Java static method. + */ + @JvmStatic + fun asSimpleStats(usageStats: List): List { + return usageStats.map { SimpleUsageStat(it) } + } + + /** + * Calculates the epoch day from a timestamp in milliseconds, compatible with API 24+. + * It manually calculates the day by using integer division on the milliseconds. + */ + private fun getEpochDay(lastTimeUsed: Long): Long { + val timeZoneOffset = Calendar.getInstance().timeZone.getOffset(lastTimeUsed) + return TimeUnit.MILLISECONDS.toDays(lastTimeUsed + timeZoneOffset) + } + } +} + diff --git a/app/src/main/java/app/sidephonelauncher/helper/usageStats/UnmatchedCloseEventGuardian.kt b/app/src/main/java/app/sidephonelauncher/helper/usageStats/UnmatchedCloseEventGuardian.kt new file mode 100644 index 0000000..3470109 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/helper/usageStats/UnmatchedCloseEventGuardian.kt @@ -0,0 +1,65 @@ +package app.sidephonelauncher.helper.usageStats + +import android.app.usage.UsageEvents +import android.app.usage.UsageStatsManager +import android.util.Log + +/** + * “…a diminutive Guardian who traveled backward through time…” + * + * Guards [EventLogWrapper] against Faulty unmatched close events (per + * [the documentation](https://codeberg.org/fynngodau/usageDirect/wiki/Event-log-wrapper-scenarios)) + * by seeking backwards through time and scanning for the open event. + */ +class UnmatchedCloseEventGuardian(private val usageStatsManager: UsageStatsManager) { + + companion object { + private const val SCAN_INTERVAL = 1000L * 60 * 60 * 24 // 24 hours + } + + /** + * @param event Event to validate + * @param queryStart Timestamp at which original query o + * @return True if the event is valid, false otherwise + */ + fun test(event: UsageEvents.Event, queryStart: Long): Boolean { + val events = usageStatsManager.queryEvents(queryStart - SCAN_INTERVAL, queryStart) + + // Reusable event object for iteration + val e = UsageEvents.Event() + + // Track whether the package is currently in foreground or background + var open = false // Not open until opened + + while (events.hasNextEvent()) { + events.getNextEvent(e) + + if (e.eventType == UsageEvents.Event.DEVICE_STARTUP) { + // Consider all apps closed after startup according to docs + open = false + } + + // Only consider events concerning our package otherwise + if (event.packageName == e.packageName) { + when (e.eventType) { + // see EventLogWrapper + UsageEvents.Event.ACTIVITY_RESUMED, 4 -> { + open = true + } + UsageEvents.Event.ACTIVITY_PAUSED, 3 -> { + if (e.timeStamp != event.timeStamp) { + // Don't flip to 'false' if we're looking at the original event itself + open = false + } + } + } + } + } + + val result = if (open) "True" else "Faulty" + Log.d("Guardian", "Scanned for package ${event.packageName} and determined event to be $result") + + // Event is valid if it was previously opened (within SCAN_INTERVAL) + return open + } +} diff --git a/app/src/main/java/app/sidephonelauncher/listener/DeviceAdmin.kt b/app/src/main/java/app/sidephonelauncher/listener/DeviceAdmin.kt new file mode 100644 index 0000000..4a0f360 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/listener/DeviceAdmin.kt @@ -0,0 +1,19 @@ +package app.sidephonelauncher.listener + +import android.app.admin.DeviceAdminReceiver +import android.content.Context +import android.content.Intent +import android.widget.Toast +import app.sidephonelauncher.R + +class DeviceAdmin : DeviceAdminReceiver() { + fun onEnabled(intent: Intent?, context: Context) { + super.onEnabled(context, intent!!) + Toast.makeText(context, context.getString(R.string.enabled), Toast.LENGTH_SHORT).show() + } + + fun onDisabled(intent: Intent?, context: Context) { + super.onDisabled(context, intent!!) + Toast.makeText(context, context.getString(R.string.enabled), Toast.LENGTH_SHORT).show() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/listener/OnSwipeTouchListener.kt b/app/src/main/java/app/sidephonelauncher/listener/OnSwipeTouchListener.kt new file mode 100644 index 0000000..09e2aa8 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/listener/OnSwipeTouchListener.kt @@ -0,0 +1,113 @@ +package app.sidephonelauncher.listener + +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 app.sidephonelauncher.data.Constants +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.math.abs + +/* +Swipe, double tap and long press touch listener for a view +Source: https://www.tutorialspoint.com/how-to-handle-swipe-gestures-in-kotlin +*/ + +internal open class OnSwipeTouchListener(c: Context?) : OnTouchListener { + private var longPressOn = false + + // private var doubleTapOn = false + private val gestureDetector: GestureDetector + + override fun onTouch(view: View, motionEvent: MotionEvent): Boolean { + if (motionEvent.action == MotionEvent.ACTION_UP) + longPressOn = false + return gestureDetector.onTouchEvent(motionEvent) + } + + private inner class GestureListener : SimpleOnGestureListener() { + private val SWIPE_THRESHOLD: Int = 100 + private val SWIPE_VELOCITY_THRESHOLD: Int = 100 + + override fun onDown(e: MotionEvent): Boolean { + return true + } + + override fun onSingleTapUp(e: MotionEvent): Boolean { +// if (doubleTapOn) { +// doubleTapOn = false +// onTripleClick() +// } + onClick() + return super.onSingleTapUp(e) + } + + override fun onDoubleTap(e: MotionEvent): Boolean { +// doubleTapOn = true +// Timer().schedule(Constants.TRIPLE_TAP_DELAY_MS) { +// if (doubleTapOn) { +// doubleTapOn = false +// onDoubleClick() +// } +// } + onDoubleClick() + return super.onDoubleTap(e) + } + + override fun onLongPress(e: MotionEvent) { + longPressOn = true + GlobalScope.launch { + delay(Constants.LONG_PRESS_DELAY_MS) + withContext(Dispatchers.Main) { + if (isActive && longPressOn) + onLongClick() + } + } + super.onLongPress(e) + } + + override fun onFling( + event1: MotionEvent?, + event2: MotionEvent, + velocityX: Float, + velocityY: Float, + ): Boolean { + try { + val diffY = event2.y - (event1?.y ?: 0F) + val diffX = event2.x - (event1?.x ?: 0F) + if (abs(diffX) > abs(diffY)) { + if (abs(diffX) > SWIPE_THRESHOLD && abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { + if (diffX > 0) onSwipeRight() else onSwipeLeft() + } + } else { + if (abs(diffY) > SWIPE_THRESHOLD && abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) { + if (diffY < 0) onSwipeUp() else onSwipeDown() + } + } + } catch (exception: Exception) { + exception.printStackTrace() + } + return false + } + } + + open fun onSwipeRight() {} + open fun onSwipeLeft() {} + open fun onSwipeUp() {} + open fun onSwipeDown() {} + open fun onLongClick() {} + open fun onDoubleClick() {} + open fun onTripleClick() {} + open fun onClick() {} + + init { + gestureDetector = GestureDetector(c, GestureListener()) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/listener/ViewSwipeTouchListener.kt b/app/src/main/java/app/sidephonelauncher/listener/ViewSwipeTouchListener.kt new file mode 100644 index 0000000..daee737 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/listener/ViewSwipeTouchListener.kt @@ -0,0 +1,96 @@ +package app.sidephonelauncher.listener + +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 app.sidephonelauncher.data.Constants +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.math.abs + +internal open class ViewSwipeTouchListener(c: Context?, v: View) : OnTouchListener { + private var longPressOn = false + private val gestureDetector: GestureDetector + + override fun onTouch(view: View, motionEvent: MotionEvent): Boolean { + when (motionEvent.action) { + MotionEvent.ACTION_DOWN -> view.isPressed = true + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> view.isPressed = false + } + return gestureDetector.onTouchEvent(motionEvent) + } + + private inner class GestureListener(private val view: View) : SimpleOnGestureListener() { + private val SWIPE_THRESHOLD: Int = 100 + private val SWIPE_VELOCITY_THRESHOLD: Int = 100 + + override fun onDown(e: MotionEvent): Boolean { + return true + } + + override fun onSingleTapUp(e: MotionEvent): Boolean { + view.performClick() + return true + } + + override fun onDoubleTap(e: MotionEvent): Boolean { + onDoubleClick() + return super.onDoubleTap(e) + } + + override fun onLongPress(e: MotionEvent) { + longPressOn = true + GlobalScope.launch { + delay(Constants.LONG_PRESS_DELAY_MS) + withContext(Dispatchers.Main) { + if (isActive && longPressOn) + view.performLongClick() + } + } + super.onLongPress(e) + } + + override fun onFling( + event1: MotionEvent?, + event2: MotionEvent, + velocityX: Float, + velocityY: Float, + ): Boolean { + try { + val diffY = event2.y - (event1?.y ?: 0F) + val diffX = event2.x - (event1?.x ?: 0F) + if (abs(diffX) > abs(diffY)) { + if (abs(diffX) > SWIPE_THRESHOLD && abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { + if (diffX > 0) onSwipeRight() else onSwipeLeft() + } + } else { + if (abs(diffY) > SWIPE_THRESHOLD && abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) { + if (diffY < 0) onSwipeUp() else onSwipeDown() + } + } + } catch (exception: Exception) { + exception.printStackTrace() + } + return false + } + } + + open fun onSwipeRight() {} + open fun onSwipeLeft() {} + open fun onSwipeUp() {} + open fun onSwipeDown() {} + open fun onLongClick(view: View) {} + private fun onDoubleClick() {} + open fun onClick(view: View) {} + + init { + gestureDetector = GestureDetector(c, GestureListener(v)) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/sidephonelauncher/ui/AppDrawerAdapter.kt b/app/src/main/java/app/sidephonelauncher/ui/AppDrawerAdapter.kt new file mode 100644 index 0000000..063b519 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/ui/AppDrawerAdapter.kt @@ -0,0 +1,378 @@ +package app.sidephonelauncher.ui + +import android.content.Context +import android.content.pm.LauncherApps +import android.os.UserHandle +import android.text.Editable +import android.text.TextWatcher +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.Filter +import android.widget.Filterable +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import app.sidephonelauncher.R +import app.sidephonelauncher.data.AppModel +import app.sidephonelauncher.data.Constants +import app.sidephonelauncher.databinding.AdapterAppDrawerBinding +import app.sidephonelauncher.databinding.AdapterPrivateSpaceHeaderBinding +import app.sidephonelauncher.helper.hideKeyboard +import app.sidephonelauncher.helper.isSystemApp +import app.sidephonelauncher.helper.showKeyboard +import java.text.Normalizer + +class AppDrawerAdapter( + private var flag: Int, + private val appLabelGravity: Int, + private val appClickListener: (AppModel) -> Unit, + private val appInfoListener: (AppModel) -> Unit, + private val appDeleteListener: (AppModel) -> Unit, + private val appHideListener: (AppModel, Int) -> Unit, + private val appRenameListener: (AppModel, String) -> Unit, + private val privateSpaceToggleListener: () -> Unit = {}, + private val privateSpaceSettingsListener: () -> Unit = {}, +) : ListAdapter(DIFF_CALLBACK), Filterable { + + companion object { + const val VIEW_TYPE_APP = 0 + const val VIEW_TYPE_PRIVATE_HEADER = 1 + + val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: AppModel, newItem: AppModel): Boolean = when { + oldItem is AppModel.App && newItem is AppModel.App -> + oldItem.appPackage == newItem.appPackage && oldItem.user == newItem.user + + oldItem is AppModel.PinnedShortcut && newItem is AppModel.PinnedShortcut -> + oldItem.shortcutId == newItem.shortcutId && oldItem.user == newItem.user + + oldItem is AppModel.PrivateSpaceHeader && newItem is AppModel.PrivateSpaceHeader -> true + + else -> false + } + + override fun areContentsTheSame(oldItem: AppModel, newItem: AppModel): Boolean = + oldItem == newItem + } + } + + private var autoLaunch = true + private var isBangSearch = false + var allowAutoLaunch = true + private val diacriticsRegex = Regex("\\p{InCombiningDiacriticalMarks}+") + private val separatorsRegex = Regex("[-_+,.`'\\s\\p{Z}]") + private val appFilter = createAppFilter() + private val myUserHandle = android.os.Process.myUserHandle() + + var appsList: MutableList = mutableListOf() + var appFilteredList: MutableList = mutableListOf() + + override fun getItemViewType(position: Int): Int { + return when (appFilteredList.getOrNull(position)) { + is AppModel.PrivateSpaceHeader -> VIEW_TYPE_PRIVATE_HEADER + else -> VIEW_TYPE_APP + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + VIEW_TYPE_PRIVATE_HEADER -> PrivateSpaceHeaderViewHolder( + AdapterPrivateSpaceHeaderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + else -> ViewHolder( + AdapterAppDrawerBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + try { + if (appFilteredList.isEmpty() || position == RecyclerView.NO_POSITION) return + val appModel = appFilteredList[holder.bindingAdapterPosition] + when (holder) { + is PrivateSpaceHeaderViewHolder -> { + holder.bind( + appLabelGravity, + privateSpaceToggleListener, + privateSpaceSettingsListener, + ) + } + is ViewHolder -> holder.bind( + flag, + appLabelGravity, + myUserHandle, + appModel, + appClickListener, + appDeleteListener, + appInfoListener, + appHideListener, + appRenameListener + ) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + override fun getFilter(): Filter = this.appFilter + + private fun createAppFilter(): Filter { + return object : Filter() { + override fun performFiltering(charSearch: CharSequence?): FilterResults { + isBangSearch = charSearch?.startsWith("!") ?: false + autoLaunch = allowAutoLaunch && (charSearch?.startsWith(" ")?.not() ?: true) + + val appFilteredList = (if (charSearch.isNullOrBlank()) appsList + else appsList.filter { app -> + app !is AppModel.PrivateSpaceHeader && appLabelMatches(app.appLabel, charSearch) + } as MutableList) + + val filterResults = FilterResults() + filterResults.values = appFilteredList + return filterResults + } + + @Suppress("UNCHECKED_CAST") + override fun publishResults(constraint: CharSequence?, results: FilterResults?) { + results?.values?.let { + val items = it as MutableList + appFilteredList = items + submitList(appFilteredList) { + autoLaunch() + } + } + } + } + } + + private fun autoLaunch() { + try { + if (itemCount == 1 + && autoLaunch + && isBangSearch.not() + && flag == Constants.FLAG_LAUNCH_APP + && appFilteredList.isNotEmpty() + && appFilteredList[0] !is AppModel.PrivateSpaceHeader + ) appClickListener(appFilteredList[0]) + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun appLabelMatches(appLabel: String, charSearch: CharSequence): Boolean { + if (appLabel.contains(charSearch.trim(), true)) return true + val query = charSearch.normalizeForSearch() + return query.isNotEmpty() && appLabel.normalizeForSearch().contains(query, true) + } + + private fun CharSequence.normalizeForSearch(): String = + Normalizer.normalize(this, Normalizer.Form.NFD) + .replace(diacriticsRegex, "") + .replace(separatorsRegex, "") + + fun setAppList(appsList: MutableList) { + appsList.add( + AppModel.App( + appLabel = "", + key = null, + appPackage = "", + activityClassName = "", + isNew = false, + user = android.os.Process.myUserHandle() + ) + ) + this.appsList = appsList + this.appFilteredList = appsList + submitList(appsList) + } + + fun launchFirstInList() { + val first = appFilteredList.firstOrNull { it !is AppModel.PrivateSpaceHeader } + if (first != null) appClickListener(first) + } + + class PrivateSpaceHeaderViewHolder(private val binding: AdapterPrivateSpaceHeaderBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind( + appLabelGravity: Int, + toggleListener: () -> Unit, + settingsListener: () -> Unit, + ) = with(binding) { + privateSpaceTitle.gravity = appLabelGravity + privateSpaceTitle.setOnClickListener { toggleListener() } + privateSpaceTitle.setOnLongClickListener { + settingsListener() + true + } + } + } + + class ViewHolder(private val binding: AdapterAppDrawerBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind( + flag: Int, + appLabelGravity: Int, + myUserHandle: UserHandle, + appModel: AppModel, + clickListener: (AppModel) -> Unit, + appDeleteListener: (AppModel) -> Unit, + appInfoListener: (AppModel) -> Unit, + appHideListener: (AppModel, Int) -> Unit, + appRenameListener: (AppModel, String) -> Unit, + ) = with(binding) { + appHideLayout.visibility = View.GONE + renameLayout.visibility = View.GONE + appTitle.visibility = View.VISIBLE + + appTitle.text = buildString { + append(appModel.appLabel) + if (appModel.isNew) append(" ✦") + } + appTitle.gravity = appLabelGravity + otherProfileIndicator.isVisible = appModel.user != myUserHandle + + appRow.setOnClickListener { clickListener(appModel) } + appRow.setOnLongClickListener { + if (appModel.appPackage.isNotEmpty()) { + appDelete.alpha = when ( + appModel is AppModel.PinnedShortcut || !root.context.isSystemApp(appModel.appPackage, appModel.user) + ) { + true -> 1.0f + false -> 0.5f + } + appHide.text = if (flag == Constants.FLAG_HIDDEN_APPS) + root.context.getString(R.string.adapter_show) + else + root.context.getString(R.string.adapter_hide) + appTitle.visibility = View.INVISIBLE + appHide.alpha = when (appModel is AppModel.PinnedShortcut) { + true -> 0.5f + false -> 1.0f + } + appHideLayout.visibility = View.VISIBLE + appRename.isVisible = flag != Constants.FLAG_HIDDEN_APPS + } + true + } + appRow.setOnKeyListener { _, keyCode, event -> + when (keyCode) { + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_DPAD_CENTER, + KeyEvent.KEYCODE_NUMPAD_ENTER -> { + when (event.action) { + KeyEvent.ACTION_DOWN -> { + if (event.repeatCount > 0) { + appRow.performLongClick() + } + true + } + KeyEvent.ACTION_UP -> { + if (event.repeatCount == 0) appRow.performClick() + true + } + else -> false + } + } + else -> false + } + } + + appRename.setOnClickListener { + if (appModel.appPackage.isNotEmpty()) { + etAppRename.hint = getAppName(etAppRename.context, appModel.appPackage, appModel.user) + etAppRename.setText(appModel.appLabel) + etAppRename.setSelectAllOnFocus(true) + renameLayout.visibility = View.VISIBLE + appHideLayout.visibility = View.GONE + etAppRename.showKeyboard() + etAppRename.imeOptions = EditorInfo.IME_ACTION_DONE + } + } + etAppRename.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> + appTitle.visibility = if (hasFocus) View.INVISIBLE else View.VISIBLE + } + etAppRename.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + etAppRename.hint = getAppName(etAppRename.context, appModel.appPackage, appModel.user) + } + + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + after: Int, + ) { + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + etAppRename.hint = "" + } + }) + etAppRename.setOnEditorActionListener { _, actionCode, _ -> + if (actionCode == EditorInfo.IME_ACTION_DONE) { + val renameLabel = etAppRename.text.toString().trim() + if (renameLabel.isNotBlank() && appModel.appPackage.isNotBlank()) { + appRenameListener(appModel, renameLabel) + renameLayout.visibility = View.GONE + } + true + } + false + } + tvSaveRename.setOnClickListener { + etAppRename.hideKeyboard() + val renameLabel = etAppRename.text.toString().trim() + if (renameLabel.isNotBlank() && appModel.appPackage.isNotBlank()) { + appRenameListener(appModel, renameLabel) + renameLayout.visibility = View.GONE + } else { + appRenameListener( + appModel, + getAppName(etAppRename.context, appModel.appPackage, appModel.user) + ) + renameLayout.visibility = View.GONE + } + } + appInfo.setOnClickListener { appInfoListener(appModel) } + appDelete.setOnClickListener { appDeleteListener(appModel) } + appMenuClose.setOnClickListener { + appHideLayout.visibility = View.GONE + appTitle.visibility = View.VISIBLE + } + appRenameClose.setOnClickListener { + renameLayout.visibility = View.GONE + appTitle.visibility = View.VISIBLE + } + appHide.setOnClickListener { appHideListener(appModel, bindingAdapterPosition) } + } + + private fun getAppName(context: Context, appPackage: String, user: UserHandle): String { + val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + return try { + val activityList = launcherApps.getActivityList(appPackage, user) + if (activityList.isNotEmpty()) { + activityList.first().label.toString() + } else { + val packageManager = context.packageManager + packageManager.getApplicationLabel( + packageManager.getApplicationInfo(appPackage, 0) + ).toString() + } + } catch (_: Exception) { + "" + } + } + } +} diff --git a/app/src/main/java/app/sidephonelauncher/ui/AppDrawerFragment.kt b/app/src/main/java/app/sidephonelauncher/ui/AppDrawerFragment.kt new file mode 100644 index 0000000..a60795c --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/ui/AppDrawerFragment.kt @@ -0,0 +1,461 @@ +package app.sidephonelauncher.ui + +import android.content.Context +import android.graphics.Typeface +import android.os.Build +import android.os.Bundle +import android.os.Process +import android.text.InputType +import android.text.Spannable +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.AnimationUtils +import android.view.inputmethod.BaseInputConnection +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.TextView +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.Recycler +import app.sidephonelauncher.MainViewModel +import app.sidephonelauncher.R +import app.sidephonelauncher.data.AppModel +import app.sidephonelauncher.data.Constants +import app.sidephonelauncher.data.Prefs +import app.sidephonelauncher.databinding.FragmentAppDrawerBinding +import app.sidephonelauncher.helper.deletePinnedShortcut +import app.sidephonelauncher.helper.hideKeyboard +import app.sidephonelauncher.helper.isEinkDisplay +import app.sidephonelauncher.helper.isSystemApp +import app.sidephonelauncher.helper.openAppInfo +import app.sidephonelauncher.helper.openSearch +import app.sidephonelauncher.helper.openUrl +import app.sidephonelauncher.helper.showKeyboard +import app.sidephonelauncher.helper.showToast +import app.sidephonelauncher.helper.uninstall + +class AppDrawerFragment : Fragment() { + + private lateinit var prefs: Prefs + private lateinit var adapter: AppDrawerAdapter + private lateinit var linearLayoutManager: LinearLayoutManager + private var searchTextView: TextView? = null + private var cachedIsCjkKeyboard: Boolean? = null + + private var flag = Constants.FLAG_LAUNCH_APP + private var canRename = false + private var currentAppList: List? = null + private var currentPrivateSpaceApps: List? = null + private var currentPrivateSpaceLocked: Boolean = true + private var currentPrivateSpaceAvailable: Boolean = false + + private val viewModel: MainViewModel by activityViewModels() + private var _binding: FragmentAppDrawerBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentAppDrawerBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + prefs = Prefs(requireContext()) + arguments?.let { + flag = it.getInt(Constants.Key.FLAG, Constants.FLAG_LAUNCH_APP) + canRename = it.getBoolean(Constants.Key.RENAME, false) + } + + initViews() + initSearch() + initAdapter() + initObservers() + initClickListeners() + initKeyNavigation() + } + + private fun initKeyNavigation() { + binding.recyclerView.setOnKeyListener { _, keyCode, event -> + if (event.action != KeyEvent.ACTION_DOWN) return@setOnKeyListener false + when (keyCode) { + KeyEvent.KEYCODE_DPAD_UP -> { + if (!binding.recyclerView.canScrollVertically(-1)) { + val firstVisible = (binding.recyclerView.layoutManager as? LinearLayoutManager) + ?.findFirstVisibleItemPosition() ?: -1 + if (firstVisible <= 0) { + binding.search.requestFocus() + return@setOnKeyListener true + } + } + false + } + KeyEvent.KEYCODE_DEL -> { + val query = binding.search.query + if (query.isNullOrEmpty()) { + findNavController().popBackStack() + true + } else false + } + else -> false + } + } + + binding.search.setOnKeyListener { _, keyCode, event -> + if (event.action != KeyEvent.ACTION_DOWN) return@setOnKeyListener false + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + focusFirstLaunchableItem() + } else { + false + } + } + + searchTextView?.setOnKeyListener { _, keyCode, event -> + if (event.action != KeyEvent.ACTION_DOWN) return@setOnKeyListener false + when (keyCode) { + KeyEvent.KEYCODE_DPAD_DOWN -> focusFirstLaunchableItem() + KeyEvent.KEYCODE_DPAD_LEFT -> { + val textView = searchTextView + val selectionStart = textView?.selectionStart ?: -1 + val selectionEnd = textView?.selectionEnd ?: -1 + if (selectionStart <= 0 && selectionEnd <= 0) { + findNavController().popBackStack() + true + } else { + false + } + } + else -> false + } + } + } + + private fun initViews() { + if (flag == Constants.FLAG_HIDDEN_APPS) + binding.search.queryHint = getString(R.string.hidden_apps) + else if (flag in Constants.FLAG_SET_HOME_APP_1..Constants.FLAG_SET_CALENDAR_APP) + binding.search.queryHint = "Please select an app" + try { + searchTextView = binding.search.findViewById(R.id.search_src_text) + searchTextView?.gravity = prefs.appLabelAlignment + searchTextView?.inputType = + InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + searchTextView?.typeface = Typeface.DEFAULT + searchTextView?.imeOptions = + EditorInfo.IME_ACTION_SEARCH or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun initSearch() { + binding.search.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + if (query?.startsWith("!") == true) + requireContext().openUrl(Constants.URL_DUCK_SEARCH + query.replace(" ", "%20")) + else if (adapter.itemCount == 0) + requireContext().openSearch(query?.trim()) + else + adapter.launchFirstInList() + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + try { + adapter.allowAutoLaunch = !isSearchComposing() + adapter.filter.filter(newText) + binding.appRename.visibility = + if (canRename && newText.isNotBlank()) View.VISIBLE else View.GONE + return true + } catch (e: Exception) { + e.printStackTrace() + } + return false + } + }) + } + + private fun isSearchComposing(): Boolean { + val text = searchTextView?.text + if (text !is Spannable) return false + val start = BaseInputConnection.getComposingSpanStart(text) + val end = BaseInputConnection.getComposingSpanEnd(text) + if (start !in 0 until end) return false + return isCjkKeyboard() + } + + private fun isCjkKeyboard(): Boolean { + cachedIsCjkKeyboard?.let { return it } + val result = try { + val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + val subtype = imm.currentInputMethodSubtype + val language = when { + subtype == null -> "" + subtype.languageTag.isNotEmpty() -> subtype.languageTag + else -> subtype.locale + } + language.startsWith("zh") || language.startsWith("ja") || language.startsWith("ko") + } catch (e: Exception) { + false + } + cachedIsCjkKeyboard = result + return result + } + + private fun initAdapter() { + adapter = AppDrawerAdapter( + flag, + prefs.appLabelAlignment, + appClickListener = { appModel -> + viewModel.selectedApp(appModel, flag) + if (flag == Constants.FLAG_LAUNCH_APP || flag == Constants.FLAG_HIDDEN_APPS) + findNavController().popBackStack(R.id.mainFragment, false) + else + findNavController().popBackStack() + }, + appInfoListener = { + openAppInfo( + requireContext(), + it.user, + it.appPackage + ) + findNavController().popBackStack(R.id.mainFragment, false) + }, + appDeleteListener = { appModel -> + when (appModel) { + is AppModel.PrivateSpaceHeader -> {} + is AppModel.PinnedShortcut -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + requireContext().deletePinnedShortcut( + packageName = appModel.appPackage, + shortcutIdToDelete = appModel.shortcutId, + user = appModel.user, + ) + } + + is AppModel.App -> { + if (appModel.user != Process.myUserHandle()) { + openAppInfo(requireContext(), appModel.user, appModel.appPackage) + } else if (requireContext().isSystemApp(appModel.appPackage, appModel.user)) { + requireContext().showToast(getString(R.string.system_app_cannot_delete)) + openAppInfo(requireContext(), appModel.user, appModel.appPackage) + } else { + requireContext().uninstall(appModel.appPackage) + } + } + } + viewModel.getAppList() + }, + appHideListener = { appModel, position -> + if (appModel is AppModel.PinnedShortcut) { + requireContext().showToast("Hiding pinned shortcuts is not supported") + return@AppDrawerAdapter + } + adapter.appFilteredList.removeAt(position) + adapter.notifyItemRemoved(position) + adapter.appsList.remove(appModel) + + val newSet = mutableSetOf() + newSet.addAll(prefs.hiddenApps) + if (flag == Constants.FLAG_HIDDEN_APPS) + newSet.remove(appModel.appPackage + "|" + appModel.user.toString()) + else + newSet.add(appModel.appPackage + "|" + appModel.user.toString()) + + prefs.hiddenApps = newSet + if (newSet.isEmpty()) + findNavController().popBackStack() + if (prefs.firstHide) { + binding.search.hideKeyboard() + prefs.firstHide = false + viewModel.showDialog.postValue(Constants.Dialog.HIDDEN) + findNavController().navigate(R.id.action_appListFragment_to_settingsFragment2) + } + viewModel.getAppList() + viewModel.getHiddenApps() + }, + appRenameListener = { appModel, renameLabel -> + val identifier = when (appModel) { + is AppModel.PinnedShortcut -> appModel.shortcutId + is AppModel.App -> appModel.appPackage + else -> return@AppDrawerAdapter + } + prefs.setAppRenameLabel(identifier, renameLabel) + viewModel.getAppList() + }, + privateSpaceToggleListener = { + viewModel.togglePrivateSpaceLock() + }, + privateSpaceSettingsListener = { + viewModel.openPrivateSpaceSettings() + findNavController().popBackStack(R.id.mainFragment, false) + } + ) + + linearLayoutManager = object : LinearLayoutManager(requireContext()) { + override fun scrollVerticallyBy( + dx: Int, + recycler: Recycler, + state: RecyclerView.State, + ): Int { + val scrollRange = super.scrollVerticallyBy(dx, recycler, state) + val overScroll = dx - scrollRange + if (overScroll < -10 && binding.recyclerView.scrollState == RecyclerView.SCROLL_STATE_DRAGGING) + checkMessageAndExit() + return scrollRange + } + } + + binding.recyclerView.layoutManager = linearLayoutManager + binding.recyclerView.adapter = adapter + binding.recyclerView.addOnScrollListener(getRecyclerViewOnScrollListener()) + binding.recyclerView.itemAnimator = null + if (requireContext().isEinkDisplay().not()) + binding.recyclerView.layoutAnimation = + AnimationUtils.loadLayoutAnimation(requireContext(), R.anim.layout_anim_from_bottom) + } + + private fun initObservers() { + viewModel.firstOpen.observe(viewLifecycleOwner) {} + if (flag == Constants.FLAG_HIDDEN_APPS) { + viewModel.hiddenApps.observe(viewLifecycleOwner) { + it?.let { + adapter.setAppList(it.toMutableList()) + } + } + } else { + viewModel.appList.observe(viewLifecycleOwner) { + currentAppList = it + updateCombinedAppList() + } + if (flag == Constants.FLAG_LAUNCH_APP) { + viewModel.privateSpaceAvailable.observe(viewLifecycleOwner) { + currentPrivateSpaceAvailable = it + updateCombinedAppList() + } + viewModel.privateSpaceLocked.observe(viewLifecycleOwner) { + currentPrivateSpaceLocked = it + updateCombinedAppList() + } + viewModel.privateSpaceApps.observe(viewLifecycleOwner) { + currentPrivateSpaceApps = it + updateCombinedAppList() + } + } + } + } + + private fun updateCombinedAppList() { + val apps = currentAppList ?: return + val combined = apps.toMutableList() + + if (flag == Constants.FLAG_LAUNCH_APP && currentPrivateSpaceAvailable) { + combined.add(AppModel.PrivateSpaceHeader(isLocked = currentPrivateSpaceLocked)) + if (!currentPrivateSpaceLocked) { + currentPrivateSpaceApps?.let { combined.addAll(it) } + } + } + + adapter.setAppList(combined) + adapter.filter.filter(binding.search.query) + } + + private fun findFirstLaunchablePosition(): Int { + return adapter.appFilteredList.indexOfFirst { appModel -> + appModel !is AppModel.PrivateSpaceHeader && appModel.appPackage.isNotBlank() + }.takeIf { it >= 0 } ?: RecyclerView.NO_POSITION + } + + private fun focusFirstLaunchableItem(): Boolean { + val position = findFirstLaunchablePosition() + if (position == RecyclerView.NO_POSITION) return false + + binding.recyclerView.requestFocus() + binding.recyclerView.scrollToPosition(position) + binding.recyclerView.post { + val holder = binding.recyclerView.findViewHolderForAdapterPosition(position) + val itemView = holder?.itemView?.findViewById(R.id.appRow) ?: holder?.itemView + itemView?.requestFocus() + } + return true + } + + private fun initClickListeners() { + binding.appRename.setOnClickListener { + val name = binding.search.query.toString().trim() + if (name.isEmpty()) { + requireContext().showToast(getString(R.string.type_a_new_app_name_first)) + binding.search.showKeyboard() + return@setOnClickListener + } + + when (flag) { + Constants.FLAG_SET_HOME_APP_1 -> prefs.appName1 = name + Constants.FLAG_SET_HOME_APP_2 -> prefs.appName2 = name + Constants.FLAG_SET_HOME_APP_3 -> prefs.appName3 = name + Constants.FLAG_SET_HOME_APP_4 -> prefs.appName4 = name + Constants.FLAG_SET_HOME_APP_5 -> prefs.appName5 = name + Constants.FLAG_SET_HOME_APP_6 -> prefs.appName6 = name + Constants.FLAG_SET_HOME_APP_7 -> prefs.appName7 = name + Constants.FLAG_SET_HOME_APP_8 -> prefs.appName8 = name + } + findNavController().popBackStack() + } + } + + private fun getRecyclerViewOnScrollListener(): RecyclerView.OnScrollListener { + return object : RecyclerView.OnScrollListener() { + var onTop = false + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + when (newState) { + RecyclerView.SCROLL_STATE_DRAGGING -> { + onTop = !recyclerView.canScrollVertically(-1) + if (onTop) + binding.search.hideKeyboard() + } + RecyclerView.SCROLL_STATE_IDLE -> { + if (!recyclerView.canScrollVertically(1)) + binding.search.hideKeyboard() + else if (!recyclerView.canScrollVertically(-1)) + if (!onTop && isRemoving.not()) + binding.search.showKeyboard(prefs.autoShowKeyboard) + } + } + } + } + } + + private fun checkMessageAndExit() { + findNavController().popBackStack() + if (flag == Constants.FLAG_LAUNCH_APP) + viewModel.checkForMessages.call() + } + + override fun onStart() { + super.onStart() + cachedIsCjkKeyboard = null + binding.search.showKeyboard(prefs.autoShowKeyboard) + } + + override fun onStop() { + binding.search.hideKeyboard() + super.onStop() + } + + override fun onDestroyView() { + super.onDestroyView() + searchTextView = null + _binding = null + } +} diff --git a/app/src/main/java/app/sidephonelauncher/ui/HomeAppTextView.kt b/app/src/main/java/app/sidephonelauncher/ui/HomeAppTextView.kt new file mode 100644 index 0000000..ffce91a --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/ui/HomeAppTextView.kt @@ -0,0 +1,51 @@ +package app.sidephonelauncher.ui + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import android.widget.TextView +import app.sidephonelauncher.helper.dpToPx + +class HomeAppTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = android.R.attr.textViewStyle, +) : TextView(context, attrs, defStyleAttr) { + + private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + alpha = 220 + style = Paint.Style.FILL + } + + private val dotRadiusPx = 3.dpToPx().toFloat() + private val reservedSideSpacePx = 14.dpToPx() + + private var showNotificationDot = false + + init { + setPaddingRelative( + paddingStart + reservedSideSpacePx, + paddingTop, + paddingEnd + reservedSideSpacePx, + paddingBottom, + ) + } + + fun setShowNotificationDot(show: Boolean) { + if (showNotificationDot == show) return + showNotificationDot = show + invalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + if (!showNotificationDot) return + + val cx = width - reservedSideSpacePx / 2f + val cy = height / 2f + canvas.drawCircle(cx, cy, dotRadiusPx, dotPaint) + } +} diff --git a/app/src/main/java/app/sidephonelauncher/ui/HomeFragment.kt b/app/src/main/java/app/sidephonelauncher/ui/HomeFragment.kt new file mode 100644 index 0000000..0161c36 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/ui/HomeFragment.kt @@ -0,0 +1,905 @@ +package app.sidephonelauncher.ui + +import android.app.admin.DevicePolicyManager +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherApps +import android.content.res.Configuration +import android.os.BatteryManager +import android.os.Build +import android.os.Bundle +import android.view.Gravity +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowInsets +import android.widget.FrameLayout +import android.widget.TextView +import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.os.bundleOf +import androidx.core.view.isVisible +import androidx.core.view.setPadding +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import app.sidephonelauncher.MainViewModel +import app.sidephonelauncher.R +import app.sidephonelauncher.data.AppModel +import app.sidephonelauncher.data.Constants +import app.sidephonelauncher.data.Prefs +import app.sidephonelauncher.databinding.FragmentHomeBinding +import app.sidephonelauncher.helper.NotificationDotRepository +import app.sidephonelauncher.helper.appUsagePermissionGranted +import app.sidephonelauncher.helper.dpToPx +import app.sidephonelauncher.helper.expandNotificationDrawer +import app.sidephonelauncher.helper.getChangedAppTheme +import app.sidephonelauncher.helper.getUserHandleFromString +import app.sidephonelauncher.helper.isPackageInstalled +import app.sidephonelauncher.helper.openAlarmApp +import app.sidephonelauncher.helper.openCalendar +import app.sidephonelauncher.helper.openCameraApp +import app.sidephonelauncher.helper.openDialerApp +import app.sidephonelauncher.helper.openSearch +import app.sidephonelauncher.helper.setPlainWallpaperByTheme +import app.sidephonelauncher.helper.showToast +import app.sidephonelauncher.listener.OnSwipeTouchListener +import app.sidephonelauncher.listener.ViewSwipeTouchListener +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class HomeFragment : Fragment(), View.OnClickListener, View.OnLongClickListener { + + private lateinit var prefs: Prefs + private lateinit var viewModel: MainViewModel + private lateinit var deviceManager: DevicePolicyManager + + private var _binding: FragmentHomeBinding? = null + private val binding get() = _binding!! + + private var dpadLeftKeyDownTime = 0L + private val notificationDotListener: () -> Unit = { + activity?.runOnUiThread { + if (_binding != null) updateHomeNotificationDots() + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentHomeBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + prefs = Prefs(requireContext()) + viewModel = activity?.run { + ViewModelProvider(this)[MainViewModel::class.java] + } ?: throw Exception("Invalid Activity") + + deviceManager = context?.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager + + initObservers() + setHomeAlignment(prefs.homeAlignment) + initSwipeTouchListener() + initClickListeners() + initKeyNavigation() + } + + override fun onResume() { + super.onResume() + NotificationDotRepository.addListener(notificationDotListener) + dpadLeftKeyDownTime = 0L + populateHomeScreen(false) + viewModel.isOlauncherDefault() + if (prefs.showStatusBar) showStatusBar() + else hideStatusBar() + requestInitialFocus() + } + + override fun onClick(view: View) { + when (view.id) { + R.id.lock -> {} + R.id.clock -> openClockApp() + R.id.date -> openCalendarApp() + R.id.setDefaultLauncher -> viewModel.resetLauncherLiveData.call() + R.id.tvScreenTime -> openScreenTimeDigitalWellbeing() + else -> { + try { + val appLocation = view.tag.toString().toInt() + homeAppClicked(appLocation) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + } + + private fun openClockApp() { + if (prefs.clockAppPackage.isBlank()) + openAlarmApp(requireContext()) + else + launchApp( + "Clock", + prefs.clockAppPackage, + prefs.clockAppClassName, + prefs.clockAppUser + ) + } + + private fun openCalendarApp() { + if (prefs.calendarAppPackage.isBlank()) + openCalendar(requireContext()) + else + launchApp( + "Calendar", + prefs.calendarAppPackage, + prefs.calendarAppClassName, + prefs.calendarAppUser + ) + } + + override fun onLongClick(view: View): Boolean { + when (view.id) { + R.id.homeApp1 -> showAppList(Constants.FLAG_SET_HOME_APP_1, prefs.appName1.isNotEmpty(), true) + R.id.homeApp2 -> showAppList(Constants.FLAG_SET_HOME_APP_2, prefs.appName2.isNotEmpty(), true) + R.id.homeApp3 -> showAppList(Constants.FLAG_SET_HOME_APP_3, prefs.appName3.isNotEmpty(), true) + R.id.homeApp4 -> showAppList(Constants.FLAG_SET_HOME_APP_4, prefs.appName4.isNotEmpty(), true) + R.id.homeApp5 -> showAppList(Constants.FLAG_SET_HOME_APP_5, prefs.appName5.isNotEmpty(), true) + R.id.homeApp6 -> showAppList(Constants.FLAG_SET_HOME_APP_6, prefs.appName6.isNotEmpty(), true) + R.id.homeApp7 -> showAppList(Constants.FLAG_SET_HOME_APP_7, prefs.appName7.isNotEmpty(), true) + R.id.homeApp8 -> showAppList(Constants.FLAG_SET_HOME_APP_8, prefs.appName8.isNotEmpty(), true) + R.id.clock -> { + showAppList(Constants.FLAG_SET_CLOCK_APP) + prefs.clockAppPackage = "" + prefs.clockAppClassName = "" + prefs.clockAppUser = "" + } + R.id.date -> { + showAppList(Constants.FLAG_SET_CALENDAR_APP) + prefs.calendarAppPackage = "" + prefs.calendarAppClassName = "" + prefs.calendarAppUser = "" + } + R.id.tvScreenTime -> { + showAppList(Constants.FLAG_SET_SCREEN_TIME_APP) + prefs.screenTimeAppPackage = "" + prefs.screenTimeAppClassName = "" + prefs.screenTimeAppUser = "" + } + R.id.setDefaultLauncher -> { + prefs.hideSetDefaultLauncher = true + binding.setDefaultLauncher.visibility = View.GONE + if (viewModel.isOlauncherDefault.value != true) { + requireContext().showToast(R.string.set_as_default_launcher) + findNavController().navigate(R.id.action_mainFragment_to_settingsFragment) + } + } + } + return true + } + + private fun initObservers() { + if (prefs.firstSettingsOpen) { + binding.firstRunTips.visibility = View.VISIBLE + binding.setDefaultLauncher.visibility = View.GONE + } else binding.firstRunTips.visibility = View.GONE + + viewModel.refreshHome.observe(viewLifecycleOwner) { + populateHomeScreen(it) + } + viewModel.isOlauncherDefault.observe(viewLifecycleOwner, Observer { + if (it != true) { + if (prefs.dailyWallpaper && prefs.appTheme == AppCompatDelegate.MODE_NIGHT_YES) { + prefs.dailyWallpaper = false + viewModel.cancelWallpaperWorker() + } + prefs.homeBottomAlignment = false + setHomeAlignment() + } + if (binding.firstRunTips.isVisible) return@Observer + binding.setDefaultLauncher.isVisible = it.not() && prefs.hideSetDefaultLauncher.not() + }) + viewModel.homeAppAlignment.observe(viewLifecycleOwner) { + setHomeAlignment(it) + } + viewModel.toggleDateTime.observe(viewLifecycleOwner) { + populateDateTime() + } + viewModel.screenTimeValue.observe(viewLifecycleOwner) { + it?.let { binding.tvScreenTime.text = it } + } + } + + private fun initKeyNavigation() { + binding.mainLayout.isFocusable = true + binding.mainLayout.setOnKeyListener { _, keyCode, event -> + handleHomeKeyEvent(keyCode, event) + } + } + + private fun handleHomeKeyEvent(keyCode: Int, event: KeyEvent): Boolean { + val visibleApps = getVisibleHomeApps() + if (visibleApps.isEmpty()) return false + + val focusedIndex = visibleApps.indexOf(activity?.currentFocus) + + when (event.action) { + KeyEvent.ACTION_DOWN -> { + when (keyCode) { + KeyEvent.KEYCODE_DPAD_UP -> { + if (event.repeatCount > 0) return true + if (focusedIndex > 0) { + visibleApps[focusedIndex - 1].requestFocus() + } else { + swipeDownAction() + } + return true + } + KeyEvent.KEYCODE_DPAD_DOWN -> { + if (event.repeatCount > 0) return true + if (focusedIndex < visibleApps.size - 1) { + visibleApps[focusedIndex + 1].requestFocus() + } + return true + } + KeyEvent.KEYCODE_DPAD_LEFT -> { + if (dpadLeftKeyDownTime == 0L) dpadLeftKeyDownTime = System.currentTimeMillis() + if (event.repeatCount > 0) { + val elapsed = System.currentTimeMillis() - dpadLeftKeyDownTime + if (elapsed >= Constants.LONG_PRESS_DELAY_MS) { + dpadLeftKeyDownTime = 0L + openSwipeLeftApp() + } + } + return true + } + KeyEvent.KEYCODE_DPAD_RIGHT -> { + if (event.repeatCount > 0) return true + showAppList(Constants.FLAG_LAUNCH_APP) + return true + } + KeyEvent.KEYCODE_DPAD_CENTER, + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_NUMPAD_ENTER -> return false + } + } + KeyEvent.ACTION_UP -> { + when (keyCode) { + KeyEvent.KEYCODE_DPAD_DOWN -> return true + KeyEvent.KEYCODE_DPAD_LEFT -> { + dpadLeftKeyDownTime = 0L + return true + } + KeyEvent.KEYCODE_DPAD_RIGHT -> return true + KeyEvent.KEYCODE_DPAD_CENTER, + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_NUMPAD_ENTER -> return false + } + } + } + return false + } + + private fun requestInitialFocus() { + if (binding.mainLayout.isInTouchMode) return + getVisibleHomeApps().firstOrNull()?.requestFocus() + } + + private fun getVisibleHomeApps(): List { + return listOf( + binding.homeApp1, + binding.homeApp2, + binding.homeApp3, + binding.homeApp4, + binding.homeApp5, + binding.homeApp6, + binding.homeApp7, + binding.homeApp8, + ).filter { it.visibility == View.VISIBLE } + } + + private fun initSwipeTouchListener() { + val context = requireContext() + binding.mainLayout.setOnTouchListener(getSwipeGestureListener(context)) + binding.homeApp1.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp1)) + binding.homeApp2.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp2)) + binding.homeApp3.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp3)) + binding.homeApp4.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp4)) + binding.homeApp5.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp5)) + binding.homeApp6.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp6)) + binding.homeApp7.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp7)) + binding.homeApp8.setOnTouchListener(getViewSwipeTouchListener(context, binding.homeApp8)) + } + + private fun initClickListeners() { + binding.lock.setOnClickListener(this) + binding.clock.setOnClickListener(this) + binding.date.setOnClickListener(this) + binding.clock.setOnLongClickListener(this) + binding.date.setOnLongClickListener(this) + binding.setDefaultLauncher.setOnClickListener(this) + binding.setDefaultLauncher.setOnLongClickListener(this) + binding.tvScreenTime.setOnClickListener(this) + binding.tvScreenTime.setOnLongClickListener(this) + + listOf( + binding.homeApp1, + binding.homeApp2, + binding.homeApp3, + binding.homeApp4, + binding.homeApp5, + binding.homeApp6, + binding.homeApp7, + binding.homeApp8, + ).forEach { homeApp -> + homeApp.setOnClickListener(this) + homeApp.setOnLongClickListener(this) + homeApp.setOnKeyListener { view, keyCode, event -> + when (keyCode) { + KeyEvent.KEYCODE_DPAD_DOWN -> { + if (event.action != KeyEvent.ACTION_DOWN) return@setOnKeyListener true + if (event.repeatCount > 0) return@setOnKeyListener true + val visibleApps = getVisibleHomeApps() + val focusedIndex = visibleApps.indexOf(view) + if (focusedIndex < 0) return@setOnKeyListener true + if (focusedIndex < visibleApps.size - 1) { + visibleApps[focusedIndex + 1].requestFocus() + } + true + } + KeyEvent.KEYCODE_DPAD_RIGHT -> { + if (event.action != KeyEvent.ACTION_DOWN) return@setOnKeyListener true + if (event.repeatCount > 0) return@setOnKeyListener true + showAppList(Constants.FLAG_LAUNCH_APP) + true + } + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_DPAD_CENTER, + KeyEvent.KEYCODE_NUMPAD_ENTER -> { + when (event.action) { + KeyEvent.ACTION_DOWN -> { + if (event.repeatCount > 0) { + view.performLongClick() + } + true + } + KeyEvent.ACTION_UP -> { + if (event.repeatCount == 0) view.performClick() + true + } + else -> false + } + } + else -> false + } + } + } + } + + private fun setHomeAlignment(horizontalGravity: Int = prefs.homeAlignment) { + val verticalGravity = if (prefs.homeBottomAlignment) Gravity.BOTTOM else Gravity.CENTER_VERTICAL + binding.homeAppsLayout.gravity = horizontalGravity or verticalGravity + binding.dateTimeLayout.gravity = horizontalGravity + homeAppTextViews().forEach { it.gravity = horizontalGravity } + } + + private fun populateDateTime() { + binding.dateTimeLayout.isVisible = prefs.dateTimeVisibility != Constants.DateTime.OFF + binding.clock.isVisible = Constants.DateTime.isTimeVisible(prefs.dateTimeVisibility) + binding.date.isVisible = Constants.DateTime.isDateVisible(prefs.dateTimeVisibility) + + val dateFormat = SimpleDateFormat("EEE, d MMM", Locale.getDefault()) + var dateText = dateFormat.format(Date()) + + if (!prefs.showStatusBar) { + val battery = (requireContext().getSystemService(Context.BATTERY_SERVICE) as BatteryManager) + .getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) + if (battery > 0) + dateText = getString(R.string.day_battery, dateText, battery) + } + binding.date.text = dateText.replace(".,", ",") + } + + @RequiresApi(Build.VERSION_CODES.Q) + private fun populateScreenTime() { + if (requireContext().appUsagePermissionGranted().not()) return + + viewModel.getTodaysScreenTime() + binding.tvScreenTime.visibility = View.VISIBLE + + val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + val horizontalMargin = if (isLandscape) 64.dpToPx() else 10.dpToPx() + val marginTop = if (isLandscape) { + if (prefs.dateTimeVisibility == Constants.DateTime.DATE_ONLY) 36.dpToPx() else 56.dpToPx() + } else { + if (prefs.dateTimeVisibility == Constants.DateTime.DATE_ONLY) 45.dpToPx() else 72.dpToPx() + } + val params = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT + ).apply { + topMargin = marginTop + marginStart = horizontalMargin + marginEnd = horizontalMargin + gravity = if (prefs.homeAlignment == Gravity.END) Gravity.START else Gravity.END + } + binding.tvScreenTime.layoutParams = params + binding.tvScreenTime.setPadding(10.dpToPx()) + } + + private fun populateHomeScreen(appCountUpdated: Boolean) { + if (appCountUpdated) hideHomeApps() + restoreHomeAppFocusability() + populateDateTime() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + populateScreenTime() + + val homeAppsNum = prefs.homeAppsNum + if (homeAppsNum == 0) return + + binding.homeApp1.visibility = View.VISIBLE + if (!setHomeAppText(binding.homeApp1 as TextView, prefs.appName1, prefs.appPackage1, prefs.appUser1, prefs.isShortcut1, prefs.shortcutId1)) { + prefs.appName1 = "" + prefs.appPackage1 = "" + } + if (homeAppsNum == 1) return + + binding.homeApp2.visibility = View.VISIBLE + if (!setHomeAppText(binding.homeApp2 as TextView, prefs.appName2, prefs.appPackage2, prefs.appUser2, prefs.isShortcut2, prefs.shortcutId2)) { + prefs.appName2 = "" + prefs.appPackage2 = "" + } + if (homeAppsNum == 2) return + + binding.homeApp3.visibility = View.VISIBLE + if (!setHomeAppText(binding.homeApp3 as TextView, prefs.appName3, prefs.appPackage3, prefs.appUser3, prefs.isShortcut3, prefs.shortcutId3)) { + prefs.appName3 = "" + prefs.appPackage3 = "" + } + if (homeAppsNum == 3) return + + binding.homeApp4.visibility = View.VISIBLE + if (!setHomeAppText(binding.homeApp4 as TextView, prefs.appName4, prefs.appPackage4, prefs.appUser4, prefs.isShortcut4, prefs.shortcutId4)) { + prefs.appName4 = "" + prefs.appPackage4 = "" + } + if (homeAppsNum == 4) return + + binding.homeApp5.visibility = View.VISIBLE + if (!setHomeAppText(binding.homeApp5 as TextView, prefs.appName5, prefs.appPackage5, prefs.appUser5, prefs.isShortcut5, prefs.shortcutId5)) { + prefs.appName5 = "" + prefs.appPackage5 = "" + } + if (homeAppsNum == 5) return + + binding.homeApp6.visibility = View.VISIBLE + if (!setHomeAppText(binding.homeApp6 as TextView, prefs.appName6, prefs.appPackage6, prefs.appUser6, prefs.isShortcut6, prefs.shortcutId6)) { + prefs.appName6 = "" + prefs.appPackage6 = "" + } + if (homeAppsNum == 6) return + + binding.homeApp7.visibility = View.VISIBLE + if (!setHomeAppText(binding.homeApp7 as TextView, prefs.appName7, prefs.appPackage7, prefs.appUser7, prefs.isShortcut7, prefs.shortcutId7)) { + prefs.appName7 = "" + prefs.appPackage7 = "" + } + if (homeAppsNum == 7) return + + binding.homeApp8.visibility = View.VISIBLE + if (!setHomeAppText(binding.homeApp8 as TextView, prefs.appName8, prefs.appPackage8, prefs.appUser8, prefs.isShortcut8, prefs.shortcutId8)) { + prefs.appName8 = "" + prefs.appPackage8 = "" + } + } + + private fun setHomeAppText( + textView: TextView, + appName: String, + packageName: String, + userString: String, + isShortcut: Boolean, + shortcutId: String?, + ): Boolean { + val userHandle = getUserHandleFromString(requireContext(), userString) + + if (isShortcut) { + val launcherApps = requireContext().getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + val query = LauncherApps.ShortcutQuery().apply { + setPackage(packageName) + setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED) + } + + try { + val shortcuts = launcherApps.getShortcuts(query, userHandle) + if (shortcuts?.any { it.id == shortcutId } == true) { + textView.text = appName + updateHomeAppDot(textView, packageName) + return true + } + textView.text = "" + updateHomeAppDot(textView, null) + return false + } catch (e: Exception) { + e.printStackTrace() + textView.text = "" + updateHomeAppDot(textView, null) + return false + } + } + + if (isPackageInstalled(requireContext(), packageName, userString)) { + textView.text = appName + updateHomeAppDot(textView, packageName) + return true + } + textView.text = "" + updateHomeAppDot(textView, null) + return false + } + + private fun updateHomeNotificationDots() { + updateHomeAppDot(binding.homeApp1 as TextView, prefs.appPackage1) + updateHomeAppDot(binding.homeApp2 as TextView, prefs.appPackage2) + updateHomeAppDot(binding.homeApp3 as TextView, prefs.appPackage3) + updateHomeAppDot(binding.homeApp4 as TextView, prefs.appPackage4) + updateHomeAppDot(binding.homeApp5 as TextView, prefs.appPackage5) + updateHomeAppDot(binding.homeApp6 as TextView, prefs.appPackage6) + updateHomeAppDot(binding.homeApp7 as TextView, prefs.appPackage7) + updateHomeAppDot(binding.homeApp8 as TextView, prefs.appPackage8) + } + + private fun homeAppTextViews(): List { + return listOf( + binding.homeApp1 as TextView, + binding.homeApp2 as TextView, + binding.homeApp3 as TextView, + binding.homeApp4 as TextView, + binding.homeApp5 as TextView, + binding.homeApp6 as TextView, + binding.homeApp7 as TextView, + binding.homeApp8 as TextView, + ) + } + + private fun updateHomeAppDot(textView: TextView, packageName: String?) { + (textView as? HomeAppTextView)?.setShowNotificationDot( + packageName.isNullOrBlank().not() && NotificationDotRepository.hasActiveNotification(packageName) + ) + } + + private fun restoreHomeAppFocusability() { + listOf( + binding.homeApp1, + binding.homeApp2, + binding.homeApp3, + binding.homeApp4, + binding.homeApp5, + binding.homeApp6, + binding.homeApp7, + binding.homeApp8, + ).forEach { it.isFocusable = true } + } + + private fun hideHomeApps() { + listOf( + binding.homeApp1, + binding.homeApp2, + binding.homeApp3, + binding.homeApp4, + binding.homeApp5, + binding.homeApp6, + binding.homeApp7, + binding.homeApp8, + ).forEach { + it.visibility = View.GONE + it.isFocusable = false + } + } + + private fun launchAppOrShortcut( + appName: String, + packageName: String, + activityClassName: String?, + shortcutId: String?, + isShortcut: Boolean, + userString: String, + fallback: (() -> Unit)? = null, + ) { + if (appName.isEmpty()) { + showLongPressToast() + return + } + if (isShortcut && !shortcutId.isNullOrEmpty()) { + launchShortcut( + packageName = packageName, + shortcutId = shortcutId, + shortcutLabel = appName, + userString = userString + ) + } else if (packageName.isNotEmpty()) { + launchApp( + appName = appName, + packageName = packageName, + activityClassName = activityClassName, + userString = userString + ) + } else { + fallback?.invoke() + } + } + + private fun launchShortcut(shortcutId: String, packageName: String, shortcutLabel: String, userString: String) { + viewModel.selectedApp( + AppModel.PinnedShortcut( + shortcutId = shortcutId, + appLabel = shortcutLabel, + user = getUserHandleFromString(requireContext(), userString), + key = null, + appPackage = packageName, + isNew = false, + ), + Constants.FLAG_LAUNCH_APP + ) + } + + private fun launchApp(appName: String, packageName: String, activityClassName: String?, userString: String) { + viewModel.selectedApp( + AppModel.App( + appLabel = appName, + key = null, + appPackage = packageName, + activityClassName = activityClassName, + isNew = false, + user = getUserHandleFromString(requireContext(), userString) + ), + Constants.FLAG_LAUNCH_APP + ) + } + + private fun homeAppClicked(location: Int) { + launchAppOrShortcut( + appName = prefs.getAppName(location), + packageName = prefs.getAppPackage(location), + activityClassName = prefs.getAppActivityClassName(location), + shortcutId = prefs.getShortcutId(location), + isShortcut = prefs.getIsShortcut(location), + userString = prefs.getAppUser(location) + ) + } + + private fun openSwipeRightApp() { + if (!prefs.swipeRightEnabled) return + launchAppOrShortcut( + appName = prefs.appNameSwipeRight, + packageName = prefs.appPackageSwipeRight, + activityClassName = prefs.appActivityClassNameRight, + shortcutId = prefs.shortcutIdSwipeRight, + isShortcut = prefs.isShortcutSwipeRight, + userString = prefs.appUserSwipeRight, + fallback = { openDialerApp(requireContext()) } + ) + } + + private fun openSwipeLeftApp() { + if (!prefs.swipeLeftEnabled) return + launchAppOrShortcut( + appName = prefs.appNameSwipeLeft, + packageName = prefs.appPackageSwipeLeft, + activityClassName = prefs.appActivityClassNameSwipeLeft, + shortcutId = prefs.shortcutIdSwipeLeft, + isShortcut = prefs.isShortcutSwipeLeft, + userString = prefs.appUserSwipeLeft, + fallback = { openCameraApp(requireContext()) } + ) + } + + private fun showAppList(flag: Int, rename: Boolean = false, includeHiddenApps: Boolean = false) { + viewModel.getAppList(includeHiddenApps) + try { + findNavController().navigate( + R.id.action_mainFragment_to_appListFragment, + bundleOf( + Constants.Key.FLAG to flag, + Constants.Key.RENAME to rename + ) + ) + } catch (e: Exception) { + findNavController().navigate( + R.id.appListFragment, + bundleOf( + Constants.Key.FLAG to flag, + Constants.Key.RENAME to rename + ) + ) + e.printStackTrace() + } + } + + private fun swipeDownAction() { + when (prefs.swipeDownAction) { + Constants.SwipeDownAction.SEARCH -> openSearch(requireContext()) + else -> expandNotificationDrawer(requireContext()) + } + } + + private fun lockPhone() { + requireActivity().runOnUiThread { + try { + deviceManager.lockNow() + } catch (e: SecurityException) { + requireContext().showToast(getString(R.string.please_turn_on_double_tap_to_unlock), Toast.LENGTH_LONG) + findNavController().navigate(R.id.action_mainFragment_to_settingsFragment) + } catch (e: Exception) { + requireContext().showToast(getString(R.string.launcher_failed_to_lock_device), Toast.LENGTH_LONG) + prefs.lockModeOn = false + } + } + } + + private fun showStatusBar() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + requireActivity().window.insetsController?.show(WindowInsets.Type.statusBars()) + else + @Suppress("DEPRECATION", "InlinedApi") + requireActivity().window.decorView.apply { + systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + } + } + + private fun hideStatusBar() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + requireActivity().window.insetsController?.hide(WindowInsets.Type.statusBars()) + else { + @Suppress("DEPRECATION") + requireActivity().window.decorView.apply { + systemUiVisibility = View.SYSTEM_UI_FLAG_IMMERSIVE or View.SYSTEM_UI_FLAG_FULLSCREEN + } + } + } + + private fun changeAppTheme() { + if (prefs.dailyWallpaper.not()) return + val changedAppTheme = getChangedAppTheme(requireContext(), prefs.appTheme) + prefs.appTheme = changedAppTheme + if (prefs.dailyWallpaper) { + setPlainWallpaperByTheme(requireContext(), changedAppTheme) + viewModel.setWallpaperWorker() + } + requireActivity().recreate() + } + + private fun openScreenTimeDigitalWellbeing() { + if (prefs.screenTimeAppPackage.isNotBlank()) { + launchApp( + "Screen Time", + prefs.screenTimeAppPackage, + prefs.screenTimeAppClassName, + prefs.screenTimeAppUser + ) + return + } + val intent = Intent() + try { + intent.setClassName( + Constants.DIGITAL_WELLBEING_PACKAGE_NAME, + Constants.DIGITAL_WELLBEING_ACTIVITY + ) + startActivity(intent) + } catch (e: Exception) { + e.printStackTrace() + try { + intent.setClassName( + Constants.DIGITAL_WELLBEING_SAMSUNG_PACKAGE_NAME, + Constants.DIGITAL_WELLBEING_SAMSUNG_ACTIVITY + ) + startActivity(intent) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private fun showLongPressToast() = requireContext().showToast(getString(R.string.long_press_to_select_app)) + + private fun textOnClick(view: View) = onClick(view) + + private fun textOnLongClick(view: View) = onLongClick(view) + + private fun getSwipeGestureListener(context: Context): View.OnTouchListener { + return object : OnSwipeTouchListener(context) { + override fun onSwipeLeft() { + super.onSwipeLeft() + openSwipeLeftApp() + } + + override fun onSwipeRight() { + super.onSwipeRight() + openSwipeRightApp() + } + + override fun onSwipeUp() { + super.onSwipeUp() + showAppList(Constants.FLAG_LAUNCH_APP) + } + + override fun onSwipeDown() { + super.onSwipeDown() + swipeDownAction() + } + + override fun onLongClick() { + super.onLongClick() + try { + findNavController().navigate(R.id.action_mainFragment_to_settingsFragment) + viewModel.firstOpen(false) + } catch (e: Exception) { + e.printStackTrace() + } + } + + override fun onDoubleClick() { + super.onDoubleClick() + if (!prefs.lockModeOn) return + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + binding.lock.performClick() + else + lockPhone() + } + + override fun onClick() { + super.onClick() + viewModel.checkForMessages.call() + } + } + } + + private fun getViewSwipeTouchListener(context: Context, view: View): View.OnTouchListener { + return object : ViewSwipeTouchListener(context, view) { + override fun onSwipeLeft() { + super.onSwipeLeft() + openSwipeLeftApp() + } + + override fun onSwipeRight() { + super.onSwipeRight() + openSwipeRightApp() + } + + override fun onSwipeUp() { + super.onSwipeUp() + showAppList(Constants.FLAG_LAUNCH_APP) + } + + override fun onSwipeDown() { + super.onSwipeDown() + swipeDownAction() + } + + override fun onLongClick(view: View) { + super.onLongClick(view) + textOnLongClick(view) + } + + override fun onClick(view: View) { + super.onClick(view) + textOnClick(view) + } + } + } + + override fun onPause() { + NotificationDotRepository.removeListener(notificationDotListener) + super.onPause() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/app/sidephonelauncher/ui/SettingsFragment.kt b/app/src/main/java/app/sidephonelauncher/ui/SettingsFragment.kt new file mode 100644 index 0000000..3b46513 --- /dev/null +++ b/app/src/main/java/app/sidephonelauncher/ui/SettingsFragment.kt @@ -0,0 +1,695 @@ +package app.sidephonelauncher.ui + +import android.app.admin.DevicePolicyManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.os.Process +import android.provider.Settings +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowInsets +import android.widget.Toast +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.os.bundleOf +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import app.sidephonelauncher.BuildConfig +import app.sidephonelauncher.MainViewModel +import app.sidephonelauncher.R +import app.sidephonelauncher.data.Constants +import app.sidephonelauncher.data.Prefs +import app.sidephonelauncher.databinding.FragmentSettingsBinding +import app.sidephonelauncher.helper.animateAlpha +import app.sidephonelauncher.helper.appUsagePermissionGranted +import app.sidephonelauncher.helper.getColorFromAttr +import app.sidephonelauncher.helper.isAccessServiceEnabled +import app.sidephonelauncher.helper.isDarkThemeOn +import app.sidephonelauncher.helper.isEinkDisplay +import app.sidephonelauncher.helper.isCountryIn +import app.sidephonelauncher.helper.isOlauncherDefault +import app.sidephonelauncher.helper.isTablet +import app.sidephonelauncher.helper.openAppInfo +import app.sidephonelauncher.helper.openUrl +import app.sidephonelauncher.helper.rateApp +import app.sidephonelauncher.helper.setPlainWallpaper +import app.sidephonelauncher.helper.shareApp +import app.sidephonelauncher.helper.showToast +import app.sidephonelauncher.listener.DeviceAdmin + +class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongClickListener { + + private lateinit var prefs: Prefs + private lateinit var viewModel: MainViewModel + private lateinit var deviceManager: DevicePolicyManager + private lateinit var componentName: ComponentName + + private var _binding: FragmentSettingsBinding? = null + private val binding get() = _binding!! + private val showPentastic = System.currentTimeMillis() % 2 == 0L + private var showInstagram = false + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentSettingsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + prefs = Prefs(requireContext()) + viewModel = activity?.run { + ViewModelProvider(this)[MainViewModel::class.java] + } ?: throw Exception("Invalid Activity") + viewModel.isOlauncherDefault() + + deviceManager = requireContext().getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager + componentName = ComponentName(requireContext(), DeviceAdmin::class.java) + checkAdminPermission() + + binding.homeAppsNum.text = prefs.homeAppsNum.toString() + populateProMessage() + populateKeyboardText() + populateScreenTimeOnOff() + populateLockSettings() + // Home button for recents feature disabled + // populateHomeButtonRecents() + populateWallpaperText() + populateAppThemeText() + populateTextSize() + populateAlignment() + populateStatusBar() + populateDateTime() + populateSwipeApps() + populateSwipeDownAction() + populateActionHints() + showInstagram = requireContext().isCountryIn() + if (showInstagram) binding.twitter.text = getString(R.string.instagram) + initClickListeners() + initObservers() + + if (showPentastic) + binding.footer.text = getText(R.string.new_app_minimal_todo_lists) + } + + override fun onClick(view: View) { + binding.appsNumSelectLayout.visibility = View.GONE + binding.dateTimeSelectLayout.visibility = View.GONE + binding.appThemeSelectLayout.visibility = View.GONE + binding.swipeDownSelectLayout.visibility = View.GONE + if (view.id != R.id.textSizeMinus && view.id != R.id.textSizePlus) { + if (binding.textSizesLayout.isVisible) { + binding.textSizesLayout.visibility = View.GONE + applyTextSizeScale() + } + } + if (view.id != R.id.alignmentBottom) + binding.alignmentSelectLayout.visibility = View.GONE + + when (view.id) { + R.id.olauncherHiddenApps -> showHiddenApps() + R.id.moreFeatures -> viewModel.showDialog.postValue(Constants.Dialog.PRO_MESSAGE) + R.id.screenTimeOnOff -> viewModel.showDialog.postValue(Constants.Dialog.DIGITAL_WELLBEING) + R.id.appInfo -> openAppInfo(requireContext(), Process.myUserHandle(), BuildConfig.APPLICATION_ID) + R.id.setLauncher -> viewModel.resetLauncherLiveData.call() + R.id.toggleLock -> toggleLockMode() + // Home button for recents feature disabled + // R.id.homeButtonRecents -> toggleHomeButtonRecents() + R.id.autoShowKeyboard -> toggleKeyboardText() + R.id.homeAppsNum -> binding.appsNumSelectLayout.visibility = View.VISIBLE + R.id.dailyWallpaperUrl -> requireContext().openUrl(prefs.dailyWallpaperUrl) + R.id.dailyWallpaper -> toggleDailyWallpaperUpdate() + R.id.alignment -> binding.alignmentSelectLayout.visibility = View.VISIBLE + R.id.alignmentLeft -> viewModel.updateHomeAlignment(Gravity.START) + R.id.alignmentCenter -> viewModel.updateHomeAlignment(Gravity.CENTER) + R.id.alignmentRight -> viewModel.updateHomeAlignment(Gravity.END) + R.id.alignmentBottom -> updateHomeBottomAlignment() + R.id.statusBar -> toggleStatusBar() + R.id.dateTime -> binding.dateTimeSelectLayout.visibility = View.VISIBLE + R.id.dateTimeOn -> toggleDateTime(Constants.DateTime.ON) + R.id.dateTimeOff -> toggleDateTime(Constants.DateTime.OFF) + R.id.dateOnly -> toggleDateTime(Constants.DateTime.DATE_ONLY) + R.id.appThemeText -> binding.appThemeSelectLayout.visibility = View.VISIBLE + R.id.themeLight -> updateTheme(AppCompatDelegate.MODE_NIGHT_NO) + R.id.themeDark -> updateTheme(AppCompatDelegate.MODE_NIGHT_YES) + R.id.themeSystem -> updateTheme(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + R.id.textSizeValue -> binding.textSizesLayout.visibility = View.VISIBLE + R.id.actionAccessibility -> openAccessibilityService() + R.id.closeAccessibility -> toggleAccessibilityVisibility(false) + R.id.notWorking -> requireContext().openUrl(Constants.URL_DOUBLE_TAP) + + R.id.tvGestures -> binding.flSwipeDown.visibility = View.VISIBLE + + R.id.maxApps0 -> updateHomeAppsNum(0) + R.id.maxApps1 -> updateHomeAppsNum(1) + R.id.maxApps2 -> updateHomeAppsNum(2) + R.id.maxApps3 -> updateHomeAppsNum(3) + R.id.maxApps4 -> updateHomeAppsNum(4) + R.id.maxApps5 -> updateHomeAppsNum(5) + R.id.maxApps6 -> updateHomeAppsNum(6) + R.id.maxApps7 -> updateHomeAppsNum(7) + R.id.maxApps8 -> updateHomeAppsNum(8) + + R.id.textSizeMinus -> adjustTextSizePreview(-0.1f) + R.id.textSizePlus -> adjustTextSizePreview(0.1f) + + R.id.swipeLeftApp -> showAppListIfEnabled(Constants.FLAG_SET_SWIPE_LEFT_APP) + R.id.swipeRightApp -> showAppListIfEnabled(Constants.FLAG_SET_SWIPE_RIGHT_APP) + R.id.swipeDownAction -> binding.swipeDownSelectLayout.visibility = View.VISIBLE + R.id.notifications -> updateSwipeDownAction(Constants.SwipeDownAction.NOTIFICATIONS) + R.id.search -> updateSwipeDownAction(Constants.SwipeDownAction.SEARCH) + + R.id.aboutOlauncher -> { + prefs.aboutClicked = true + requireContext().openUrl(Constants.URL_ABOUT_OLAUNCHER) + } + + R.id.share -> requireActivity().shareApp() + R.id.rate -> { + prefs.rateClicked = true + requireActivity().rateApp() + } + + R.id.twitter -> requireContext().openUrl( + if (showInstagram) Constants.URL_INSTA_TANUJ else Constants.URL_TWITTER_TANUJ + ) + R.id.github -> requireContext().openUrl(Constants.URL_OLAUNCHER_GITHUB) + R.id.privacy -> requireContext().openUrl(Constants.URL_OLAUNCHER_PRIVACY) + R.id.footer -> { + requireContext().openUrl( + if (showPentastic) Constants.URL_PENTASTIC else Constants.URL_NTS + ) + } + } + } + + override fun onLongClick(view: View): Boolean { + when (view.id) { + R.id.alignment -> { + prefs.appLabelAlignment = prefs.homeAlignment + findNavController().navigate(R.id.action_settingsFragment_to_appListFragment) + requireContext().showToast(getString(R.string.alignment_changed)) + } + + R.id.dailyWallpaper -> removeWallpaper() + R.id.appThemeText -> { + binding.appThemeSelectLayout.visibility = View.VISIBLE + binding.themeSystem.visibility = View.VISIBLE + } + + R.id.swipeLeftApp -> toggleSwipeLeft() + R.id.swipeRightApp -> toggleSwipeRight() + R.id.toggleLock -> startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)) + } + return true + } + + private fun initClickListeners() { + binding.olauncherHiddenApps.setOnClickListener(this) + binding.scrollLayout.setOnClickListener(this) + binding.appInfo.setOnClickListener(this) + binding.setLauncher.setOnClickListener(this) + binding.aboutOlauncher.setOnClickListener(this) + binding.moreFeatures.setOnClickListener(this) + binding.autoShowKeyboard.setOnClickListener(this) + binding.toggleLock.setOnClickListener(this) + // Home button for recents feature disabled + // binding.homeButtonRecents.setOnClickListener(this) + binding.homeAppsNum.setOnClickListener(this) + binding.screenTimeOnOff.setOnClickListener(this) + binding.dailyWallpaperUrl.setOnClickListener(this) + binding.dailyWallpaper.setOnClickListener(this) + binding.alignment.setOnClickListener(this) + binding.alignmentLeft.setOnClickListener(this) + binding.alignmentCenter.setOnClickListener(this) + binding.alignmentRight.setOnClickListener(this) + binding.alignmentBottom.setOnClickListener(this) + binding.statusBar.setOnClickListener(this) + binding.dateTime.setOnClickListener(this) + binding.dateTimeOn.setOnClickListener(this) + binding.dateTimeOff.setOnClickListener(this) + binding.dateOnly.setOnClickListener(this) + binding.swipeLeftApp.setOnClickListener(this) + binding.swipeRightApp.setOnClickListener(this) + binding.swipeDownAction.setOnClickListener(this) + binding.search.setOnClickListener(this) + binding.notifications.setOnClickListener(this) + binding.appThemeText.setOnClickListener(this) + binding.themeLight.setOnClickListener(this) + binding.themeDark.setOnClickListener(this) + binding.themeSystem.setOnClickListener(this) + binding.textSizeValue.setOnClickListener(this) + binding.actionAccessibility.setOnClickListener(this) + binding.closeAccessibility.setOnClickListener(this) + binding.notWorking.setOnClickListener(this) + + binding.share.setOnClickListener(this) + binding.rate.setOnClickListener(this) + binding.twitter.setOnClickListener(this) + binding.github.setOnClickListener(this) + binding.privacy.setOnClickListener(this) + binding.footer.setOnClickListener(this) + + binding.maxApps0.setOnClickListener(this) + binding.maxApps1.setOnClickListener(this) + binding.maxApps2.setOnClickListener(this) + binding.maxApps3.setOnClickListener(this) + binding.maxApps4.setOnClickListener(this) + binding.maxApps5.setOnClickListener(this) + binding.maxApps6.setOnClickListener(this) + binding.maxApps7.setOnClickListener(this) + binding.maxApps8.setOnClickListener(this) + + binding.textSizeMinus.setOnClickListener(this) + binding.textSizePlus.setOnClickListener(this) + + binding.dailyWallpaper.setOnLongClickListener(this) + binding.alignment.setOnLongClickListener(this) + binding.appThemeText.setOnLongClickListener(this) + binding.swipeLeftApp.setOnLongClickListener(this) + binding.swipeRightApp.setOnLongClickListener(this) + binding.toggleLock.setOnLongClickListener(this) + } + + private fun initObservers() { + if (prefs.firstSettingsOpen) { + viewModel.showDialog.postValue(Constants.Dialog.ABOUT) + prefs.firstSettingsOpen = false + } + viewModel.isOlauncherDefault.observe(viewLifecycleOwner) { + if (it) { + binding.setLauncher.text = getString(R.string.change_default_launcher) + prefs.toShowHintCounter += 1 + } + } + viewModel.homeAppAlignment.observe(viewLifecycleOwner) { + populateAlignment() + } + viewModel.updateSwipeApps.observe(viewLifecycleOwner) { + populateSwipeApps() + } + } + + private fun toggleSwipeLeft() { + prefs.swipeLeftEnabled = !prefs.swipeLeftEnabled + if (prefs.swipeLeftEnabled) { + binding.swipeLeftApp.setTextColor(requireContext().getColorFromAttr(R.attr.primaryColor)) + requireContext().showToast(getString(R.string.swipe_left_app_enabled)) + } else { + binding.swipeLeftApp.setTextColor(requireContext().getColorFromAttr(R.attr.primaryColorTrans50)) + requireContext().showToast(getString(R.string.swipe_left_app_disabled)) + } + } + + private fun toggleSwipeRight() { + prefs.swipeRightEnabled = !prefs.swipeRightEnabled + if (prefs.swipeRightEnabled) { + binding.swipeRightApp.setTextColor(requireContext().getColorFromAttr(R.attr.primaryColor)) + requireContext().showToast(getString(R.string.swipe_right_app_enabled)) + } else { + binding.swipeRightApp.setTextColor(requireContext().getColorFromAttr(R.attr.primaryColorTrans50)) + requireContext().showToast(getString(R.string.swipe_right_app_disabled)) + } + } + + private fun toggleStatusBar() { + prefs.showStatusBar = !prefs.showStatusBar + populateStatusBar() + } + + private fun populateStatusBar() { + if (prefs.showStatusBar) { + showStatusBar() + binding.statusBar.text = getString(R.string.on) + } else { + hideStatusBar() + binding.statusBar.text = getString(R.string.off) + } + } + + private fun toggleDateTime(selected: Int) { + prefs.dateTimeVisibility = selected + populateDateTime() + viewModel.toggleDateTime() + } + + private fun populateDateTime() { + binding.dateTime.text = getString( + when (prefs.dateTimeVisibility) { + Constants.DateTime.DATE_ONLY -> R.string.date + Constants.DateTime.ON -> R.string.on + else -> R.string.off + } + ) + } + + private fun showStatusBar() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + requireActivity().window.insetsController?.show(WindowInsets.Type.statusBars()) + else + @Suppress("DEPRECATION", "InlinedApi") + requireActivity().window.decorView.apply { + systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + } + } + + private fun hideStatusBar() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + requireActivity().window.insetsController?.hide(WindowInsets.Type.statusBars()) + else { + @Suppress("DEPRECATION") + requireActivity().window.decorView.apply { + systemUiVisibility = View.SYSTEM_UI_FLAG_IMMERSIVE or View.SYSTEM_UI_FLAG_FULLSCREEN + } + } + } + + private fun showHiddenApps() { + if (prefs.hiddenApps.isEmpty()) { + requireContext().showToast(getString(R.string.no_hidden_apps)) + return + } + viewModel.getHiddenApps() + findNavController().navigate( + R.id.action_settingsFragment_to_appListFragment, + bundleOf(Constants.Key.FLAG to Constants.FLAG_HIDDEN_APPS) + ) + } + + private fun checkAdminPermission() { + val isAdmin: Boolean = deviceManager.isAdminActive(componentName) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) + prefs.lockModeOn = isAdmin + } + + private fun toggleAccessibilityVisibility(show: Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + binding.notWorking.visibility = View.VISIBLE + if (isAccessServiceEnabled(requireContext())) + binding.actionAccessibility.text = getString(R.string.disable) + binding.accessibilityLayout.isVisible = show + binding.scrollView.animateAlpha(if (show) 0.5f else 1f) + } + + private fun openAccessibilityService() { + toggleAccessibilityVisibility(false) + // prefs.lockModeOn = true + populateLockSettings() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)) + } + + private fun toggleLockMode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + if (!prefs.lockModeOn && !isAccessServiceEnabled(requireContext())) { + toggleAccessibilityVisibility(true) + return + } + prefs.lockModeOn = !prefs.lockModeOn + } else { + val isAdmin: Boolean = deviceManager.isAdminActive(componentName) + if (isAdmin) { + removeActiveAdmin("Admin permission removed.") + prefs.lockModeOn = false + } else { + val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN) + intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, componentName) + intent.putExtra( + DevicePolicyManager.EXTRA_ADD_EXPLANATION, + getString(R.string.admin_permission_message) + ) + requireActivity().startActivityForResult(intent, Constants.REQUEST_CODE_ENABLE_ADMIN) + } + } + populateLockSettings() + } + + private fun removeActiveAdmin(toastMessage: String? = null) { + try { + deviceManager.removeActiveAdmin(componentName) // for backward compatibility + requireContext().showToast(toastMessage) + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun removeWallpaper() { + if (requireContext().isEinkDisplay()) { + prefs.appTheme = AppCompatDelegate.MODE_NIGHT_NO + setPlainWallpaper(requireContext(), android.R.color.white) + } else { + prefs.appTheme = AppCompatDelegate.MODE_NIGHT_YES + setPlainWallpaper(requireContext(), android.R.color.black) + } + if (!prefs.dailyWallpaper) return + prefs.dailyWallpaper = false + populateWallpaperText() + viewModel.cancelWallpaperWorker() + } + + private fun toggleDailyWallpaperUpdate() { + if (prefs.dailyWallpaper.not() && prefs.appTheme == AppCompatDelegate.MODE_NIGHT_YES && viewModel.isOlauncherDefault.value == false) { + requireContext().showToast(R.string.set_as_default_launcher_first) + return + } + prefs.dailyWallpaper = !prefs.dailyWallpaper + populateWallpaperText() + if (prefs.dailyWallpaper) { + viewModel.setWallpaperWorker() + showWallpaperToasts() + } else viewModel.cancelWallpaperWorker() + } + + private fun showWallpaperToasts() { + if (isOlauncherDefault(requireContext())) + requireContext().showToast(getString(R.string.your_wallpaper_will_update_shortly)) + else + requireContext().showToast(getString(R.string.olauncher_is_not_default_launcher), Toast.LENGTH_LONG) + } + + private fun updateHomeAppsNum(num: Int) { + binding.homeAppsNum.text = num.toString() + binding.appsNumSelectLayout.visibility = View.GONE + prefs.homeAppsNum = num + viewModel.refreshHome(true) + } + + private var pendingTextSizeScale: Float = -1f + + private fun adjustTextSizePreview(delta: Float) { + val maxScale = if (isTablet(requireContext())) 2.0f else 1.5f + val current = if (pendingTextSizeScale > 0) pendingTextSizeScale else prefs.textSizeScale + val newScale = Math.round((current + delta) * 10f) / 10f + val clamped = newScale.coerceIn(0.5f, maxScale) + if (clamped == current) return + pendingTextSizeScale = clamped + val formatted = String.format("%.1f", clamped) + binding.textSizeValue.text = formatted + binding.textSizeCurrent.text = formatted + } + + private fun applyTextSizeScale() { + if (pendingTextSizeScale < 0 || prefs.textSizeScale == pendingTextSizeScale) { + pendingTextSizeScale = -1f + return + } + prefs.textSizeScale = pendingTextSizeScale + pendingTextSizeScale = -1f + requireActivity().recreate() + } + + private fun toggleKeyboardText() { + if (prefs.autoShowKeyboard && prefs.keyboardMessageShown.not()) { + viewModel.showDialog.postValue(Constants.Dialog.KEYBOARD) + prefs.keyboardMessageShown = true + } else { + prefs.autoShowKeyboard = !prefs.autoShowKeyboard + populateKeyboardText() + } + } + + private fun updateTheme(appTheme: Int) { + if (AppCompatDelegate.getDefaultNightMode() == appTheme) return + prefs.appTheme = appTheme + populateAppThemeText(appTheme) + setAppTheme(appTheme) + } + + private fun setAppTheme(theme: Int) { + if (AppCompatDelegate.getDefaultNightMode() == theme) return + if (prefs.dailyWallpaper) { + setPlainWallpaper(theme) + viewModel.setWallpaperWorker() + } + requireActivity().recreate() + } + + private fun setPlainWallpaper(appTheme: Int) { + when (appTheme) { + AppCompatDelegate.MODE_NIGHT_YES -> setPlainWallpaper(requireContext(), android.R.color.black) + AppCompatDelegate.MODE_NIGHT_NO -> setPlainWallpaper(requireContext(), android.R.color.white) + else -> { + if (requireContext().isDarkThemeOn()) + setPlainWallpaper(requireContext(), android.R.color.black) + else setPlainWallpaper(requireContext(), android.R.color.white) + } + } + } + + private fun populateAppThemeText(appTheme: Int = prefs.appTheme) { + when (appTheme) { + AppCompatDelegate.MODE_NIGHT_YES -> binding.appThemeText.text = getString(R.string.dark) + AppCompatDelegate.MODE_NIGHT_NO -> binding.appThemeText.text = getString(R.string.light) + else -> binding.appThemeText.text = getString(R.string.system_default) + } + } + + private fun populateTextSize() { + val formatted = String.format("%.1f", prefs.textSizeScale) + binding.textSizeValue.text = formatted + binding.textSizeCurrent.text = formatted + } + + private fun populateScreenTimeOnOff() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (requireContext().appUsagePermissionGranted()) binding.screenTimeOnOff.text = getString(R.string.on) + else binding.screenTimeOnOff.text = getString(R.string.off) + } else binding.screenTimeLayout.visibility = View.GONE + } + + private fun populateKeyboardText() { + if (prefs.autoShowKeyboard) binding.autoShowKeyboard.text = getString(R.string.on) + else binding.autoShowKeyboard.text = getString(R.string.off) + } + + private fun populateWallpaperText() { + if (prefs.dailyWallpaper) binding.dailyWallpaper.text = getString(R.string.on) + else binding.dailyWallpaper.text = getString(R.string.off) + } + + private fun updateHomeBottomAlignment() { + if (viewModel.isOlauncherDefault.value != true) { + requireContext().showToast(getString(R.string.please_set_olauncher_as_default_first), Toast.LENGTH_LONG) + return + } + prefs.homeBottomAlignment = !prefs.homeBottomAlignment + populateAlignment() + viewModel.updateHomeAlignment(prefs.homeAlignment) + } + + private fun populateAlignment() { + when (prefs.homeAlignment) { + Gravity.START -> binding.alignment.text = getString(R.string.left) + Gravity.CENTER -> binding.alignment.text = getString(R.string.center) + Gravity.END -> binding.alignment.text = getString(R.string.right) + } + binding.alignmentBottom.text = if (prefs.homeBottomAlignment) + getString(R.string.bottom_on) + else getString(R.string.bottom_off) + } + + // Home button for recents feature disabled + // private fun toggleHomeButtonRecents() { + // if (!prefs.homeButtonShowRecents && !isAccessServiceEnabled(requireContext())) { + // toggleAccessibilityVisibility(true) + // return + // } + // prefs.homeButtonShowRecents = !prefs.homeButtonShowRecents + // populateHomeButtonRecents() + // } + + // private fun populateHomeButtonRecents() { + // binding.homeButtonRecents.text = getString( + // if (prefs.homeButtonShowRecents && isAccessServiceEnabled(requireContext())) R.string.on + // else R.string.off + // ) + // } + + private fun populateLockSettings() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + binding.toggleLock.text = getString( + if (prefs.lockModeOn && isAccessServiceEnabled(requireContext())) R.string.on + else R.string.off + ) + } else { + binding.toggleLock.text = getString( + if (prefs.lockModeOn) R.string.on + else R.string.off + ) + } + } + + private fun populateSwipeDownAction() { + binding.swipeDownAction.text = when (prefs.swipeDownAction) { + Constants.SwipeDownAction.NOTIFICATIONS -> getString(R.string.notifications) + else -> getString(R.string.search) + } + } + + private fun updateSwipeDownAction(swipeDownFor: Int) { + if (prefs.swipeDownAction == swipeDownFor) return + prefs.swipeDownAction = swipeDownFor + populateSwipeDownAction() + } + + private fun populateSwipeApps() { + binding.swipeLeftApp.text = prefs.appNameSwipeLeft + binding.swipeRightApp.text = prefs.appNameSwipeRight + if (!prefs.swipeLeftEnabled) + binding.swipeLeftApp.setTextColor(requireContext().getColorFromAttr(R.attr.primaryColorTrans50)) + if (!prefs.swipeRightEnabled) + binding.swipeRightApp.setTextColor(requireContext().getColorFromAttr(R.attr.primaryColorTrans50)) + } + +// private fun populateDigitalWellbeing() { +// binding.digitalWellbeing.isVisible = requireContext().isPackageInstalled(Constants.DIGITAL_WELLBEING_PACKAGE_NAME).not() +// && requireContext().isPackageInstalled(Constants.DIGITAL_WELLBEING_SAMSUNG_PACKAGE_NAME).not() +// && prefs.hideDigitalWellbeing.not() +// } + + private fun showAppListIfEnabled(flag: Int) { + if ((flag == Constants.FLAG_SET_SWIPE_LEFT_APP) and !prefs.swipeLeftEnabled) { + requireContext().showToast(getString(R.string.long_press_to_enable)) + return + } + if ((flag == Constants.FLAG_SET_SWIPE_RIGHT_APP) and !prefs.swipeRightEnabled) { + requireContext().showToast(getString(R.string.long_press_to_enable)) + return + } + viewModel.getAppList(true) + findNavController().navigate( + R.id.action_settingsFragment_to_appListFragment, + bundleOf(Constants.Key.FLAG to flag) + ) + } + + private fun populateActionHints() { + if (prefs.aboutClicked.not()) + binding.aboutOlauncher.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_info, 0) + if (viewModel.isOlauncherDefault.value != true) return + if (prefs.rateClicked.not() && prefs.toShowHintCounter > Constants.HINT_RATE_US && prefs.toShowHintCounter < Constants.HINT_RATE_US + 100) + binding.rate.setCompoundDrawablesWithIntrinsicBounds(0, android.R.drawable.arrow_down_float, 0, 0) + } + + private fun populateProMessage() { + if (prefs.proMessageShown.not() && prefs.userState == Constants.UserState.SHARE) { + prefs.proMessageShown = true + viewModel.showDialog.postValue(Constants.Dialog.PRO_MESSAGE) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onDestroy() { + viewModel.checkForMessages.call() + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 0f474c2..3787274 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -74,7 +74,7 @@ android:paddingTop="112dp" android:paddingBottom="48dp"> - - - - - - - -