Compare commits

7 Commits

Author SHA1 Message Date
c972125fef release build v0.3.5
Some checks failed
Android Main Branch CI / Build, Sign & Upload (push) Has been cancelled
Update CHANGELOG.md / changelog (push) Has been cancelled
Validate Gradle Wrapper / Validation (push) Has been cancelled
Android Release CI / Build, Sign & Release (push) Has been cancelled
2026-08-19 11:51:36 +02:00
cd94721fc8 docs: add CLAUDE.md with build, signing, and on-device testing notes 2026-08-19 11:51:21 +02:00
8cb9166f41 fix: widen weather glyph/temperature gap; drop debug logs 2026-08-19 11:51:21 +02:00
010ff8a0c8 feat: current weather on home screen (MET/Yr API, no key)
The Yr app (no.nrk.yr) exposes no data to third parties; its backend,
api.met.no locationforecast 2.0, works with no API key (User-Agent header
only). New toggle 'Show Current Weather' under Display, right after 'Show
App Icons'. The home element is formatted exactly like the daily word and
shows a nerd-font weather glyph (nf-weather-*, from the installed
JetBrainsMonoNerdFont) plus the temperature in Celsius, e.g. '\uE312 16°'.

A 95KB subset of JetBrainsMonoNerdFont (ASCII + weather glyphs, OFL) is
bundled as R.font.jetbrains_mono_nf_weather so the glyphs render even if
the launcher font setting changes.

Uses the launcher's existing saved location (lat/lon prefs).
2026-08-19 11:15:21 +02:00
5288c980d0 feat: notification dots on app icons
New NotificationBadgeService (NotificationListenerService) tracks pending,
dismissible notifications per app. Home, favorite, and drawer icons get a
red dot in the top-right corner when the app has active notifications.

Settings: 'Notification Dots' toggle under Display features; enabling it
opens the system notification-access screen when permission is missing.
Dots update live via a StateFlow observed by the fragments.
2026-08-19 10:53:39 +02:00
0aaf00988d feat: scroll app list from anywhere on screen; gate search item animation
DrawFragment: app-list touch area now forwards vertical drags/flings to the
drawer RecyclerView, so the list scrolls even when grabbing empty space
right of the app names. OnSwipeTouchListener gained no-op vertical scroll
and fling hooks.

When animations are disabled, drop the RecyclerView item animator so
DiffUtil search updates no longer pan entries up into place.
2026-08-19 10:39:40 +02:00
e69796535f fix: next alarm time follows system 24-hour format setting
getNextAlarm hardcoded 'hh:mm a' (12-hour). Now uses
DateFormat.is24HourFormat to pick HH:mm vs hh:mm a.
2026-08-19 10:18:57 +02:00
31 changed files with 871 additions and 29 deletions

131
CLAUDE.md Normal file
View File

@@ -0,0 +1,131 @@
# Working with this EasyLauncher fork
Quick reference for agents working on this fork (Gitea: `jonas/EasyLauncher`,
installed on the Unihertz Titan 2 Elite as `app.easy.launcher`).
**PROJECT.md in ~/titan2-elite is the source of truth for the phone itself.**
## Golden rules
1. The installed app id is **`app.easy.launcher`** — NOT upstream's
`com.github.droidworksstudio.launcher`. Every fork build must keep this id.
2. Release APKs are signed with `~/android-keystores/easylauncher-release.jks`
(alias `easylauncher`, pass in `easylauncher-release.pass`). Never commit an
unsigned or differently-signed APK to `dist/`.
3. `adb install -r dist/...-Signed.apk` updates the phone in place (same
signature). A debug build cannot be installed over it (signature mismatch).
4. After a phone reboot the phone is CE-locked until the user enters the PIN;
app prefs/location are not visible to the app until then. Ask the user to
unlock instead of debugging it.
5. Never fetch weather without a real location (lat/lon 0,0 must be treated as
"no location").
## Build
```bash
export JAVA_HOME=~/jdk21 # Gradle 8.13, compileSdk/targetSdk 36, minSdk 24
./gradlew :app:compileWithInternetReleaseKotlin :app:compileWithoutInternetReleaseKotlin --offline
./gradlew :app:assembleWithInternetRelease :app:assembleWithoutInternetRelease --offline
```
- Two flavors: `withInternet` (INTERNET + location permissions, "Easy Launcher")
and `withoutInternet`.
- Outputs: `app/build/outputs/apk/{withInternet,withoutInternet}/release/`.
- `weather.properties` (OpenWeatherMap key) is optional and absent here — the
OWM weather widget is dead without it. The home-screen current-weather
element uses the MET/Yr API instead (no key needed, see below).
## Release flow (see git history for the v0.3.x pattern)
1. Bump `versionCode`/`versionName` in `app/build.gradle.kts`
(currently 35 / 0.3.5).
2. Build both flavors, then sign each:
```bash
PASS=$(cat ~/android-keystores/easylauncher-release.pass)
~/android-sdk/build-tools/36.0.0/apksigner sign --v4-signing-enabled true \
--ks ~/android-keystores/easylauncher-release.jks --ks-key-alias easylauncher \
--ks-pass "pass:$PASS" --key-pass "pass:$PASS" \
--out dist/EasyLauncher-Internet-v0.3.5-Signed.apk \
app/build/outputs/apk/withInternet/release/app.easy.launcher_v0.3.5-Release.apk
# repeat for withoutInternet -> dist/EasyLauncher-v0.3.5-Signed.apk
```
3. `apksigner verify --print-certs` should show the keystore SHA-256.
4. Commit as `release build vX.Y.Z` (includes the dist APKs + .idsig files),
annotated tag `vX.Y.Z`, push `main` + tag to `origin` (Gitea), create a
Gitea release with both APKs as assets.
## On-device testing (Titan 2 Elite via adb)
- Wake: `input keyevent KEYCODE_WAKEUP`; go home: `input keyevent KEYCODE_HOME`.
- Open the app drawer: `input keyevent KEYCODE_SPACE` (the physical-keyboard
"key press -> app list" trigger; requires MainActivity focused).
- Scroll the drawer from the right edge: `input swipe 1000 850 1000 250 250`.
- Screenshot: `adb exec-out screencap -p > s.png`; view text via
`uiautomator dump`.
- Inspect the launcher's own state with `su -c '...'` (root via Magisk).
### Editing app prefs from the host (fragile — read carefully)
Prefs live in `/data/data/app.easy.launcher/shared_prefs/`. To change them:
1. Pull the file, edit locally, push back via stdin:
`adb shell "su -c 'cat > <path>'" < localfile`
2. **The file name must end in `.xml`** (`EasyLauncher.pref.xml`,
`EasyWeather.pref.xml`) — `getSharedPreferences("name")` appends `.xml`.
A missing suffix silently reads as an EMPTY map.
3. **Run `restorecon -F <file>`** — files created via `su cat` get the wrong
SELinux context (`s0` instead of `s0:c18,c257,c512,c768`) and the app can't
read them (silent empty map).
4. `am force-stop app.easy.launcher` then press HOME so the process restarts
and re-reads the file.
### Notification dots
- Service: `.service.NotificationBadgeService` (NotificationListenerService).
Users must grant notification access; the "Notification Dots" settings toggle
opens `ACTION_NOTIFICATION_LISTENER_SETTINGS` when missing.
- Grant from adb:
`cmd notification allow_listener app.easy.launcher/com.github.droidworksstudio.launcher.service.NotificationBadgeService`
- Test notification: `cmd notification post -t 'Title' tag 'body'` — posts as
`com.android.shell`, which has no launcher icon, so no dot is visible; verify
visually with a real app notification instead.
- Counts live in `NotificationBadgeService.notificationCounts`
(StateFlow, key `"userId/packageName"`); fragments rebind visible rows.
### Home current-weather element (MET/Yr)
- Source: `https://api.met.no/weatherapi/locationforecast/2.0/compact` — **no API
key**, but REQUIRES a descriptive `User-Agent` header (see `MetApiService`).
Verified working from PC and phone network.
- `AppHelper.fetchMetWeather(context, lat, lon)` returns Celsius (rounded) +
MET `symbol_code`; 15-minute cache in `met_weather_prefs`.
- The icon is a **Nerd Font weather glyph** (`nf-weather-*`, U+E300+ PUA),
mapped from the symbol code in `MetSymbolMapper`. It renders with the bundled
`R.font.jetbrains_mono_nf_weather` — a ~95KB fontTools subset of
JetBrainsMonoNerdFont (ASCII + 38 weather glyphs + °). The phone's system
font IS `JetBrainsMonoNerdFont` (fonts.xml default), but the bundled subset
guarantees rendering even if the launcher-font setting changes. Regenerate
the subset with fontTools if more glyphs are ever needed (source:
`/system/fonts/JetBrainsMonoNerdFont-Regular.ttf`).
- Display mirrors the daily word (uses the daily-word color/size/alignment
prefs); text = `"$glyph $temp°"` (two spaces).
- Needs a real saved location: `EasyWeather.pref.xml` keys `LATITUDE`/
`LONGITUDE` (floats). MainActivity saves the real fix there; for testing you
can seed e.g. Haugesund 59.4138 / 5.2680 (remember the `.xml` suffix +
`restorecon`!).
## Known behavior quirks
- The accessibility-service dialog ("Please turn on accessibility service to
use double tap to lock") pops on home when `ActionService` isn't running.
Enable the service in system settings to suppress it during testing.
- `Application.setCustomFont` patches the `Typeface` DEFAULT/MONO/SERIF/SANS
static fields when a non-System launcher font is selected.
- With "Disable Animations" on, the drawer RecyclerView item animator is null
and navigation transitions are skipped.
- Drawer whole-screen scrolling: `appListTouchArea` forwards vertical drags/
flings through `OnSwipeTouchListener` hooks (`onVerticalScroll`/`onVerticalFling`).
Signs were verified against AOSP source: `scrollBy(0, distanceY)` is
pass-through; fling takes the NEGATED pointer velocity.

View File

@@ -19,8 +19,8 @@ android {
applicationId = "app.easy.launcher"
minSdk = 24
targetSdk = 36
versionCode = 34
versionName = "0.3.4"
versionCode = 35
versionName = "0.3.5"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders["internetPermission"] = "android.permission.INTERNET"

View File

@@ -103,6 +103,15 @@
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
</service>
<service
android:name=".service.NotificationBadgeService"
android:exported="true"
android:label="@string/notification_badge_service_label"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"

View File

@@ -0,0 +1 @@
This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: https://scripts.sil.org/OFL

View File

@@ -33,14 +33,18 @@ import com.github.droidworksstudio.launcher.R
import com.github.droidworksstudio.launcher.accessibility.ActionService
import com.github.droidworksstudio.launcher.data.dao.AppInfoDAO
import com.github.droidworksstudio.launcher.data.entities.AppInfo
import com.github.droidworksstudio.launcher.helper.weather.MetForecastResponse
import com.github.droidworksstudio.launcher.helper.weather.WeatherResponse
import com.github.droidworksstudio.launcher.utils.Constants
import com.github.droidworksstudio.launcher.utils.MetApiService
import com.github.droidworksstudio.launcher.utils.MetSymbolMapper
import com.github.droidworksstudio.launcher.utils.WeatherApiService
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import kotlinx.coroutines.flow.first
import kotlin.math.roundToInt
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.net.UnknownHostException
@@ -264,7 +268,9 @@ class AppHelper @Inject constructor() {
if (nextAlarmClock == null) return "No alarm is set."
val alarmTime = nextAlarmClock.triggerTime
val formattedTime = SimpleDateFormat("EEE, MMM d hh:mm a", Locale.getDefault()).format(alarmTime)
val is24Hour = android.text.format.DateFormat.is24HourFormat(context)
val timePattern = if (is24Hour) "EEE, MMM d HH:mm" else "EEE, MMM d hh:mm a"
val formattedTime = SimpleDateFormat(timePattern, Locale.getDefault()).format(alarmTime)
val drawable = AppCompatResources.getDrawable(context, R.drawable.ic_alarm_clock)
val fontSize = TypedValue.applyDimension(
@@ -550,4 +556,91 @@ class AppHelper @Inject constructor() {
// Data class to hold cached weather data along with timestamp
data class CachedWeatherData(val timestamp: Long, val weatherResponse: WeatherResponse)
sealed class MetWeatherResult {
data class Success(val temperature: Int, val symbolCode: String) : MetWeatherResult()
data class Failure(val errorMessage: String) : MetWeatherResult()
}
/**
* Fetches the current temperature and symbol from the MET/Yr API
* (api.met.no locationforecast). No API key needed - only a
* descriptive User-Agent header. Result is cached for 15 minutes.
*/
fun fetchMetWeather(
context: Context,
latitude: Float,
longitude: Float,
): MetWeatherResult {
if (latitude == 0f && longitude == 0f) {
return MetWeatherResult.Failure("No location available")
}
val cached = context.getMetWeatherFromCache()
if (cached?.let {
System.currentTimeMillis() - it.first < TimeUnit.MINUTES.toMillis(15)
} == true
) {
return cached.second
}
try {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.met.no/weatherapi/locationforecast/2.0/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(MetApiService::class.java)
val response = service.getCompact(latitude.toDouble(), longitude.toDouble()).execute()
if (response.isSuccessful) {
val body = response.body()
val timeseries = body?.properties?.timeseries
val first = timeseries?.firstOrNull()
if (first != null) {
val temperature = first.data.instant.details.airTemperature.roundToInt()
val symbolCode = first.data.next1Hours?.summary?.symbolCode
?: first.data.next6Hours?.summary?.symbolCode
?: "cloudy"
val result = MetWeatherResult.Success(temperature, symbolCode)
context.cacheMetWeather(result)
return result
}
return MetWeatherResult.Failure("Empty forecast")
} else {
return MetWeatherResult.Failure("MET API error: ${response.code()}")
}
} catch (e: UnknownHostException) {
Log.e("AppHelper", "MET fetch: unknown host api.met.no", e)
return MetWeatherResult.Failure("Unknown host: api.met.no")
} catch (e: Exception) {
Log.e("AppHelper", "MET fetch failed", e)
return MetWeatherResult.Failure(e.message ?: "MET fetch failed")
}
}
/**
* Builds the home-screen weather text: nerd-font weather glyph followed
* by the temperature in Celsius, e.g. "\uE312 16°".
*/
fun buildCurrentWeatherText(temperature: Int, symbolCode: String): String {
return "${MetSymbolMapper.glyphFor(symbolCode)} $temperature°"
}
private fun Context.cacheMetWeather(result: MetWeatherResult.Success) {
val sharedPreferences = getSharedPreferences(Constants.MET_WEATHER_PREFS, Context.MODE_PRIVATE)
sharedPreferences.edit()
.putLong(Constants.MET_WEATHER_TIMESTAMP, System.currentTimeMillis())
.putInt(Constants.MET_WEATHER_TEMPERATURE, result.temperature)
.putString(Constants.MET_WEATHER_SYMBOL, result.symbolCode)
.apply()
}
private fun Context.getMetWeatherFromCache(): Pair<Long, MetWeatherResult>? {
val sharedPreferences = getSharedPreferences(Constants.MET_WEATHER_PREFS, Context.MODE_PRIVATE)
val timestamp = sharedPreferences.getLong(Constants.MET_WEATHER_TIMESTAMP, -1L)
if (timestamp == -1L) return null
val temperature = sharedPreferences.getInt(Constants.MET_WEATHER_TEMPERATURE, 0)
val symbol = sharedPreferences.getString(Constants.MET_WEATHER_SYMBOL, "cloudy") ?: "cloudy"
return timestamp to MetWeatherResult.Success(temperature, symbol)
}
}

View File

@@ -198,6 +198,14 @@ class PreferenceHelper @Inject constructor(@ApplicationContext context: Context)
get() = prefs.getBoolean(Constants.DISABLE_ANIMATIONS, false)
set(value) = prefs.edit().putBoolean(Constants.DISABLE_ANIMATIONS, value).apply()
var showNotificationDots: Boolean
get() = prefs.getBoolean(Constants.SHOW_NOTIFICATION_DOTS, false)
set(value) = prefs.edit().putBoolean(Constants.SHOW_NOTIFICATION_DOTS, value).apply()
var showCurrentWeather: Boolean
get() = prefs.getBoolean(Constants.SHOW_CURRENT_WEATHER, false)
set(value) = prefs.edit().putBoolean(Constants.SHOW_CURRENT_WEATHER, value).apply()
var searchEngines: Constants.SearchEngines
get() {
return try {

View File

@@ -0,0 +1,46 @@
package com.github.droidworksstudio.launcher.helper.weather
import com.google.gson.annotations.SerializedName
/**
* Locationforecast 2.0 compact response (api.met.no) - only the fields the
* home-screen current-weather element needs.
*/
data class MetForecastResponse(
val properties: MetProperties
)
data class MetProperties(
val timeseries: List<MetTimeSeries>
)
data class MetTimeSeries(
val time: String,
val data: MetData
)
data class MetData(
val instant: MetInstant,
@SerializedName("next_1_hours")
val next1Hours: MetNextHours? = null,
@SerializedName("next_6_hours")
val next6Hours: MetNextHours? = null
)
data class MetInstant(
val details: MetDetails
)
data class MetDetails(
@SerializedName("air_temperature")
val airTemperature: Double = 0.0
)
data class MetNextHours(
val summary: MetSummary
)
data class MetSummary(
@SerializedName("symbol_code")
val symbolCode: String = ""
)

View File

@@ -90,6 +90,7 @@ internal open class OnSwipeTouchListener(context: Context?, private val preferen
}
// Vertical swipe
else {
onVerticalScroll(distanceY)
if (abs(diffY) > swipeThreshold && abs(distanceY) > swipeScrollThreshold) {
if (diffY > 0) onSwipeDown() else onSwipeUp()
}
@@ -115,6 +116,7 @@ internal open class OnSwipeTouchListener(context: Context?, private val preferen
}
} else {
if (abs(diffY) > swipeThreshold && abs(velocityY) > swipeVelocityThreshold) {
onVerticalFling(velocityY)
if (diffY < 0) onSwipeUp() else onSwipeDown()
}
}
@@ -129,6 +131,21 @@ internal open class OnSwipeTouchListener(context: Context?, private val preferen
open fun onSwipeLeft() {}
open fun onSwipeUp() {}
open fun onSwipeDown() {}
/**
* Called for every vertical drag movement (per-event delta, same sign
* convention as RecyclerView's own touch handling: positive when the
* finger moves up). Default is a no-op.
*/
open fun onVerticalScroll(distanceY: Float) {}
/**
* Called on a vertical fling with the raw pointer velocity in pixels per
* second (positive when the finger moved down, same convention as
* VelocityTracker.getYVelocity()). Default is a no-op.
*/
open fun onVerticalFling(velocityY: Float) {}
open fun onLongClick() {}
open fun onDoubleClick() {}
open fun onTripleClick() {}

View File

@@ -0,0 +1,76 @@
package com.github.droidworksstudio.launcher.service
import android.app.Notification
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* Tracks pending (dismissible) notifications per app so the launcher can show
* a red dot on app icons.
*
* The user must grant notification access in the system settings
* (Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS) before this service
* receives any events. Android rebinds it automatically after every boot.
*/
class NotificationBadgeService : NotificationListenerService() {
/** Notification key -> "userId/packageName". Keyed to dedupe updates. */
private val keyToPackage = HashMap<String, String>()
override fun onListenerConnected() {
super.onListenerConnected()
synchronized(keyToPackage) {
keyToPackage.clear()
activeNotifications.forEach { sbn ->
if (isCounted(sbn)) keyToPackage[sbn.key] = userIdKey(sbn)
}
publish()
}
}
override fun onNotificationPosted(sbn: StatusBarNotification) {
if (!isCounted(sbn)) return
synchronized(keyToPackage) {
// A posted notification with an existing key is just an update.
if (keyToPackage.containsKey(sbn.key)) return
keyToPackage[sbn.key] = userIdKey(sbn)
publish()
}
}
override fun onNotificationRemoved(sbn: StatusBarNotification) {
synchronized(keyToPackage) {
if (keyToPackage.remove(sbn.key) != null) publish()
}
}
/** Only swipe-away, non-ongoing notifications get a dot. */
private fun isCounted(sbn: StatusBarNotification): Boolean {
val notification = sbn.notification
if (!sbn.isClearable) return false
if (notification.flags and Notification.FLAG_ONGOING_EVENT != 0) return false
return true
}
private fun userIdKey(sbn: StatusBarNotification) =
"${sbn.userId}/" + sbn.packageName
private fun publish() {
val counts = HashMap<String, Int>()
keyToPackage.values.forEach { key ->
counts[key] = (counts[key] ?: 0) + 1
}
notificationCounts.value = counts
}
companion object {
/** Active notification count per app, keyed by "userId/packageName". */
val notificationCounts: MutableStateFlow<Map<String, Int>> =
MutableStateFlow(emptyMap())
fun hasNotifications(userId: Int, packageName: String): Boolean =
notificationCounts.value["$userId/$packageName"]?.let { it > 0 } ?: false
}
}

View File

@@ -40,6 +40,8 @@ import com.github.droidworksstudio.launcher.helper.PreferenceHelper
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
import com.github.droidworksstudio.launcher.listener.OnSwipeTouchListener
import com.github.droidworksstudio.launcher.listener.ScrollEventListener
import com.github.droidworksstudio.launcher.service.NotificationBadgeService
import kotlin.math.roundToInt
import com.github.droidworksstudio.launcher.ui.bottomsheetdialog.AppInfoBottomSheetFragment
import com.github.droidworksstudio.launcher.utils.Constants
import com.github.droidworksstudio.launcher.viewmodel.AppViewModel
@@ -110,6 +112,7 @@ class DrawFragment : Fragment(),
observeClickListener()
observeSwipeTouchListener()
observeScrollTouchListener()
observeNotificationBadges()
// Initialize observation of drawer apps
observeDrawerApps()
@@ -157,6 +160,30 @@ class DrawFragment : Fragment(),
setHasFixedSize(false)
layoutManager = StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL)
isNestedScrollingEnabled = false
// Search results update the list with DiffUtil. When animations
// are disabled, drop the item animator so entries don't "pan-up"
// into place while filtering.
if (preferenceHelper.disableAnimations) itemAnimator = null
}
}
private fun observeNotificationBadges() {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
NotificationBadgeService.notificationCounts.collect {
val recyclerView = binding.drawAdapter
for (i in 0 until recyclerView.childCount) {
val holder = recyclerView.getChildViewHolder(recyclerView.getChildAt(i))
if (holder is DrawViewHolder) {
val position = holder.bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
holder.bind(drawAdapter.currentList[position])
}
}
}
}
}
}
}
@@ -237,7 +264,7 @@ class DrawFragment : Fragment(),
binding.apply {
mainView.setOnTouchListener(getSwipeGestureListener(context))
touchArea.setOnTouchListener(getSwipeGestureListener(context))
appListTouchArea.setOnTouchListener(getSwipeGestureListener(context))
appListTouchArea.setOnTouchListener(getAppListSwipeGestureListener(context))
}
}
@@ -302,36 +329,65 @@ class DrawFragment : Fragment(),
return object : OnSwipeTouchListener(context, preferenceHelper) {
override fun onSwipeLeft() {
super.onSwipeLeft()
val actionTypeNavOptions: NavOptions? =
if (preferenceHelper.disableAnimations) null
else appHelper.getActionType(Constants.Swipe.Left)
Handler(Looper.getMainLooper()).post {
findNavController().navigate(
R.id.HomeFragment,
null,
actionTypeNavOptions
)
}
navigateToHome(Constants.Swipe.Left)
}
override fun onSwipeRight() {
super.onSwipeRight()
val actionTypeNavOptions: NavOptions? =
if (preferenceHelper.disableAnimations) null
else appHelper.getActionType(Constants.Swipe.Right)
Handler(Looper.getMainLooper()).post {
findNavController().navigate(
R.id.HomeFragment,
null,
actionTypeNavOptions
)
}
navigateToHome(Constants.Swipe.Right)
}
}
}
/**
* Swipe listener for the app list area.
*
* Behaves like [getSwipeGestureListener] (left/right swipes navigate
* home), but vertical drags and flings over the empty space around the
* list (e.g. right of the app names) scroll the list itself. This makes
* the drawer scrollable from the whole screen, not just the app rows.
*/
private fun getAppListSwipeGestureListener(context: Context): View.OnTouchListener {
return object : OnSwipeTouchListener(context, preferenceHelper) {
override fun onSwipeLeft() {
super.onSwipeLeft()
navigateToHome(Constants.Swipe.Left)
}
override fun onSwipeRight() {
super.onSwipeRight()
navigateToHome(Constants.Swipe.Right)
}
override fun onVerticalScroll(distanceY: Float) {
super.onVerticalScroll(distanceY)
binding.drawAdapter.scrollBy(0, distanceY.roundToInt())
}
override fun onVerticalFling(velocityY: Float) {
super.onVerticalFling(velocityY)
// RecyclerView.fling expects the negated pointer velocity
// (the same negation RecyclerView applies to its own
// VelocityTracker value in onTouchEvent).
binding.drawAdapter.fling(0, -velocityY.roundToInt())
}
}
}
private fun navigateToHome(swipe: Constants.Swipe) {
val actionTypeNavOptions: NavOptions? =
if (preferenceHelper.disableAnimations) null
else appHelper.getActionType(swipe)
Handler(Looper.getMainLooper()).post {
findNavController().navigate(
R.id.HomeFragment,
null,
actionTypeNavOptions
)
}
}
/**
* Runs the fuzzy search synchronously over the in-memory index.
*

View File

@@ -14,7 +14,9 @@ import com.github.droidworksstudio.launcher.data.entities.AppInfo
import com.github.droidworksstudio.launcher.databinding.ItemDrawBinding
import com.github.droidworksstudio.launcher.helper.PreferenceHelper
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
import com.github.droidworksstudio.launcher.service.NotificationBadgeService
import com.github.droidworksstudio.launcher.utils.Constants
import com.github.droidworksstudio.launcher.utils.NotificationDotHelper
class DrawViewHolder(
private val binding: ItemDrawBinding,
@@ -70,7 +72,20 @@ class DrawViewHolder(
}
appDrawIcon.layoutParams = layoutParams
appDrawIcon.setImageDrawable(appNewIcon ?: nonNullDrawable)
val baseIcon: Drawable = appNewIcon ?: nonNullDrawable
appDrawIcon.setImageDrawable(
if (preferenceHelper.showNotificationDots &&
NotificationBadgeService.hasNotifications(
appInfo.userHandle,
appInfo.packageName
)
) {
NotificationDotHelper.withDot(itemView.context, baseIcon)
} else {
baseIcon
}
)
appDrawIcon.visibility = View.VISIBLE
val parentLayout = appDrawName.parent as LinearLayoutCompat

View File

@@ -11,7 +11,9 @@ import android.view.ViewGroup
import androidx.annotation.RequiresApi
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
@@ -28,6 +30,8 @@ import com.github.droidworksstudio.launcher.helper.PreferenceHelper
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
import com.github.droidworksstudio.launcher.listener.OnItemMoveListener
import com.github.droidworksstudio.launcher.listener.OnSwipeTouchListener
import com.github.droidworksstudio.launcher.service.NotificationBadgeService
import com.github.droidworksstudio.launcher.ui.favorite.FavoriteViewHolder
import com.github.droidworksstudio.launcher.viewmodel.AppViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@@ -82,6 +86,7 @@ class FavoriteFragment : Fragment(),
setupRecyclerView()
observeFavorite()
observeHomeAppOrder()
observeNotificationBadges()
observeSwipeTouchListener()
}
@@ -128,6 +133,25 @@ class FavoriteFragment : Fragment(),
}
}
private fun observeNotificationBadges() {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
NotificationBadgeService.notificationCounts.collect {
val recyclerView = binding.favoriteAdapter
for (i in 0 until recyclerView.childCount) {
val holder = recyclerView.getChildViewHolder(recyclerView.getChildAt(i))
if (holder is FavoriteViewHolder) {
val position = holder.bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
holder.bind(favoriteAdapter.currentList[position])
}
}
}
}
}
}
}
private fun observeHomeAppOrder() {
binding.favoriteAdapter.adapter = favoriteAdapter
val listener: OnItemMoveListener.OnItemActionListener = favoriteAdapter

View File

@@ -11,6 +11,8 @@ import com.github.droidworksstudio.launcher.data.entities.AppInfo
import com.github.droidworksstudio.launcher.databinding.ItemFavoriteBinding
import com.github.droidworksstudio.launcher.helper.PreferenceHelper
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
import com.github.droidworksstudio.launcher.service.NotificationBadgeService
import com.github.droidworksstudio.launcher.utils.NotificationDotHelper
@SuppressLint("ClickableViewAccessibility")
class FavoriteViewHolder(
@@ -48,7 +50,18 @@ class FavoriteViewHolder(
if (preferenceHelper.showAppIcon) {
val appIcon =
binding.root.context.packageManager.getApplicationIcon(appInfo.packageName)
appFavoriteLeftIcon.setImageDrawable(appIcon)
val baseIcon: android.graphics.drawable.Drawable =
if (preferenceHelper.showNotificationDots &&
NotificationBadgeService.hasNotifications(
appInfo.userHandle,
appInfo.packageName
)
) {
NotificationDotHelper.withDot(binding.root.context, appIcon)
} else {
appIcon
}
appFavoriteLeftIcon.setImageDrawable(baseIcon)
appFavoriteLeftIcon.layoutParams.width =
preferenceHelper.appTextSize.toInt() * 3
appFavoriteLeftIcon.layoutParams.height =

View File

@@ -22,9 +22,12 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.widget.AppCompatTextView
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavOptions
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.StaggeredGridLayoutManager
@@ -46,6 +49,7 @@ import com.github.droidworksstudio.launcher.helper.PreferenceHelper
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
import com.github.droidworksstudio.launcher.listener.OnSwipeTouchListener
import com.github.droidworksstudio.launcher.listener.ScrollEventListener
import com.github.droidworksstudio.launcher.service.NotificationBadgeService
import com.github.droidworksstudio.launcher.ui.bottomsheetdialog.AppInfoBottomSheetFragment
import com.github.droidworksstudio.launcher.utils.Constants
import com.github.droidworksstudio.launcher.viewmodel.AppViewModel
@@ -54,6 +58,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@@ -111,6 +116,12 @@ class HomeFragment : Fragment(),
setupRecyclerView()
observeSwipeTouchListener()
observeUserInterfaceSettings()
// Nerd-font weather glyphs (nf-weather-*) live in the bundled
// subset font; the system font here is JetBrainsMonoNerdFont but
// this guarantees rendering even if the launcher font is changed.
binding.currentWeather.typeface =
ResourcesCompat.getFont(requireContext(), R.font.jetbrains_mono_nf_weather)
}
@SuppressLint("ClickableViewAccessibility")
@@ -232,6 +243,25 @@ class HomeFragment : Fragment(),
}
}
private fun observeNotificationBadges() {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
NotificationBadgeService.notificationCounts.collect {
val recyclerView = binding.appListAdapter
for (i in 0 until recyclerView.childCount) {
val holder = recyclerView.getChildViewHolder(recyclerView.getChildAt(i))
if (holder is HomeViewHolder) {
val position = holder.bindingAdapterPosition
if (position != androidx.recyclerview.widget.RecyclerView.NO_POSITION) {
holder.bind(homeAdapter.currentList[position])
}
}
}
}
}
}
}
@SuppressLint("ClickableViewAccessibility", "InflateParams")
private fun observeSwipeTouchListener() {
binding.apply {
@@ -250,6 +280,7 @@ class HomeFragment : Fragment(),
preferenceViewModel.setShowDate(preferenceHelper.showDate)
preferenceViewModel.setShowAlarmClock(preferenceHelper.showAlarmClock)
preferenceViewModel.setShowDailyWord(preferenceHelper.showDailyWord)
preferenceViewModel.setShowCurrentWeather(preferenceHelper.showCurrentWeather)
preferenceViewModel.setShowBattery(preferenceHelper.showBattery)
preferenceViewModel.showTimeLiveData.observe(viewLifecycleOwner) {
@@ -301,6 +332,17 @@ class HomeFragment : Fragment(),
)
}
preferenceViewModel.showCurrentWeatherLiveData.observe(viewLifecycleOwner) {
appHelper.updateUI(
binding.currentWeather,
preferenceHelper.homeDailyWordAlignment,
preferenceHelper.dailyWordColor,
preferenceHelper.dailyWordTextSize,
it
)
if (it) loadCurrentWeather()
}
binding.apply {
mainView.hideKeyboard()
@@ -321,6 +363,41 @@ class HomeFragment : Fragment(),
}
}
/**
* Fetches the current temperature from the MET/Yr API and shows it as a
* nerd-font weather glyph plus Celsius value, e.g. "\uE312 16°".
* Formatted like the daily word (same alignment/color/size settings).
*/
private fun loadCurrentWeather() {
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
val sharedPreferences =
requireContext().getSharedPreferences(Constants.WEATHER_PREFS, Context.MODE_PRIVATE)
val latitude = sharedPreferences.getFloat(Constants.LATITUDE, 0f)
val longitude = sharedPreferences.getFloat(Constants.LONGITUDE, 0f)
val result = appHelper.fetchMetWeather(requireContext(), latitude, longitude)
withContext(Dispatchers.Main) {
when (result) {
is AppHelper.MetWeatherResult.Success -> {
binding.currentWeather.text = appHelper.buildCurrentWeatherText(
result.temperature,
result.symbolCode
)
binding.currentWeather.visibility = View.VISIBLE
}
is AppHelper.MetWeatherResult.Failure -> {
// No data (offline, no location): keep any cached text,
// otherwise hide the element.
if (binding.currentWeather.text.isNullOrEmpty()) {
binding.currentWeather.visibility = View.GONE
}
}
}
}
}
}
private fun observeBioAuthCheck(appInfo: AppInfo) {
if (!appInfo.lock)
context.launchApp(appInfo)
@@ -584,6 +661,8 @@ class HomeFragment : Fragment(),
binding.mainView.hideKeyboard()
observeUserInterfaceSettings()
observeFavoriteAppList()
observeNotificationBadges()
if (preferenceHelper.showCurrentWeather) loadCurrentWeather()
}
override fun onAppClicked(appInfo: AppInfo) {

View File

@@ -15,7 +15,9 @@ import com.github.droidworksstudio.launcher.data.entities.AppInfo
import com.github.droidworksstudio.launcher.databinding.ItemHomeBinding
import com.github.droidworksstudio.launcher.helper.PreferenceHelper
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
import com.github.droidworksstudio.launcher.service.NotificationBadgeService
import com.github.droidworksstudio.launcher.utils.Constants
import com.github.droidworksstudio.launcher.utils.NotificationDotHelper
import javax.inject.Inject
class HomeViewHolder @Inject constructor(
@@ -70,7 +72,20 @@ class HomeViewHolder @Inject constructor(
}
appHomeIcon.layoutParams = layoutParams
appHomeIcon.setImageDrawable(appNewIcon ?: nonNullDrawable)
val baseIcon: Drawable = appNewIcon ?: nonNullDrawable
appHomeIcon.setImageDrawable(
if (preferenceHelper.showNotificationDots &&
NotificationBadgeService.hasNotifications(
appInfo.userHandle,
appInfo.packageName
)
) {
NotificationDotHelper.withDot(itemView.context, baseIcon)
} else {
baseIcon
}
)
appHomeIcon.visibility = View.VISIBLE
val parentLayout = appHomeName.parent as LinearLayoutCompat

View File

@@ -1,8 +1,11 @@
package com.github.droidworksstudio.launcher.ui.settings
import android.content.Context
import android.content.ComponentName
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
@@ -18,6 +21,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import com.github.droidworksstudio.common.getAppNameFromPackageName
import com.github.droidworksstudio.common.showLongToast
import com.github.droidworksstudio.launcher.R
import com.github.droidworksstudio.launcher.databinding.FragmentSettingsFeaturesBinding
import com.github.droidworksstudio.launcher.helper.AppHelper
@@ -25,6 +29,7 @@ import com.github.droidworksstudio.launcher.helper.AppReloader
import com.github.droidworksstudio.launcher.helper.PreferenceHelper
import com.github.droidworksstudio.launcher.listener.ScrollEventListener
import com.github.droidworksstudio.launcher.repository.AppInfoRepository
import com.github.droidworksstudio.launcher.service.NotificationBadgeService
import com.github.droidworksstudio.launcher.utils.Constants
import com.github.droidworksstudio.launcher.viewmodel.PreferenceViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -129,6 +134,7 @@ class SettingsFeaturesFragment : Fragment(),
homeAlignmentBottomSwitchCompat.isChecked = preferenceHelper.homeAlignmentBottom
lockSettingsSwitchCompat.isChecked = preferenceHelper.settingsLock
disableAnimationsSwitchCompat.isChecked = preferenceHelper.disableAnimations
showNotificationDotsSwitchCompat.isChecked = preferenceHelper.showNotificationDots
}
}
@@ -450,10 +456,37 @@ class SettingsFeaturesFragment : Fragment(),
val feedbackType = if (isChecked) "on" else "off"
appHelper.triggerHapticFeedback(context, feedbackType)
}
showNotificationDotsSwitchCompat.setOnCheckedChangeListener { _, isChecked ->
preferenceViewModel.setShowNotificationDots(isChecked)
val feedbackType = if (isChecked) "on" else "off"
appHelper.triggerHapticFeedback(context, feedbackType)
if (isChecked && !isNotificationAccessGranted()) {
openNotificationAccessSettings()
}
}
}
}
private fun isNotificationAccessGranted(): Boolean {
val componentName = ComponentName(requireContext(), NotificationBadgeService::class.java)
val notificationManager =
requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
return notificationManager.isNotificationListenerAccessGranted(componentName)
}
private fun openNotificationAccessSettings() {
try {
startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
} catch (_: Exception) {
requireContext().showLongToast(
getString(R.string.toast_cannot_open_notification_access_settings)
)
}
}
private var swipeActionDialog: AlertDialog? = null
@RequiresApi(Build.VERSION_CODES.Q)

View File

@@ -95,6 +95,7 @@ class SettingsLookFeelFragment : Fragment(),
alarmClockSwitchCompat.isChecked = preferenceHelper.showAlarmClock
dailyWordSwitchCompat.isChecked = preferenceHelper.showDailyWord
appIconsSwitchCompat.isChecked = preferenceHelper.showAppIcon
currentWeatherSwitchCompat.isChecked = preferenceHelper.showCurrentWeather
}
}
@@ -188,6 +189,12 @@ class SettingsLookFeelFragment : Fragment(),
val feedbackType = if (isChecked) "on" else "off"
appHelper.triggerHapticFeedback(context, feedbackType)
}
currentWeatherSwitchCompat.setOnCheckedChangeListener { _, isChecked ->
preferenceViewModel.setShowCurrentWeather(isChecked)
val feedbackType = if (isChecked) "on" else "off"
appHelper.triggerHapticFeedback(context, feedbackType)
}
}
}

View File

@@ -20,6 +20,10 @@ object Constants {
const val WIDGET_BATTERY = "WIDGET_BATTERY"
const val WEATHER_PREFS = "EasyWeather.pref"
const val MET_WEATHER_PREFS = "met_weather_prefs"
const val MET_WEATHER_TIMESTAMP = "MET_WEATHER_TIMESTAMP"
const val MET_WEATHER_TEMPERATURE = "MET_WEATHER_TEMPERATURE"
const val MET_WEATHER_SYMBOL = "MET_WEATHER_SYMBOL"
const val WEATHER_RESPONSE = "WEATHER_RESPONSE"
const val WEATHER_UNITS = "WEATHER_UNITS"
const val LATITUDE = "LATITUDE"
@@ -70,6 +74,8 @@ object Constants {
const val HOME_ALLIGNMENT_BOTTOM = "HOME_ALLIGNMENT_BOTTOM"
const val TOGGLE_SETTING_LOCK = "TOGGLE_SETTING_LOCK"
const val DISABLE_ANIMATIONS = "DISABLE_ANIMATIONS"
const val SHOW_NOTIFICATION_DOTS = "SHOW_NOTIFICATION_DOTS"
const val SHOW_CURRENT_WEATHER = "SHOW_CURRENT_WEATHER"
const val HOME_DATE_ALIGNMENT = "HOME_DATE_ALIGNMENT"
const val HOME_TIME_ALIGNMENT = "HOME_TIME_ALIGNMENT"

View File

@@ -0,0 +1,19 @@
package com.github.droidworksstudio.launcher.utils
import com.github.droidworksstudio.launcher.helper.weather.MetForecastResponse
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Query
interface MetApiService {
@Headers(
"User-Agent: app.easy.launcher (https://gitea.haugesenspil.dk/jonas/EasyLauncher)"
)
@GET("compact")
fun getCompact(
@Query("lat") latitude: Double,
@Query("lon") longitude: Double
): Call<MetForecastResponse>
}

View File

@@ -0,0 +1,57 @@
package com.github.droidworksstudio.launcher.utils
/**
* Maps MET/Yr symbol codes to Nerd Font weather glyphs
* (nf-weather-* codepoints from the Weather Icons font embedded in
* JetBrainsMonoNerdFont). Rendered with R.font.jetbrains_mono_nf_weather.
*/
object MetSymbolMapper {
/**
* Returns the nf-weather glyph char for a MET `symbol_code`
* (e.g. "partlycloudy_day"). Unknown codes fall back to a cloud.
*/
fun glyphFor(symbolCode: String): Char {
val normalized = symbolCode.removeSuffix("_polartwilight")
val night = normalized.endsWith("_night")
val base = normalized
.removeSuffix("_day")
.removeSuffix("_night")
return when (base) {
"clearsky", "fair" -> if (night) '\uE32B' else '\uE30D'
"partlycloudy" -> if (night) '\uE379' else '\uE302'
"cloudy" -> '\uE312'
"fog" -> '\uE313'
"lightrain" -> '\uE31B'
"rain" -> '\uE318'
"heavyrain" -> '\uE319'
"lightrainshowers" -> if (night) '\uE328' else '\uE30B'
"rainshowers" -> if (night) '\uE326' else '\uE309'
"heavyrainshowers" -> if (night) '\uE324' else '\uE307'
"lightrainandthunder" -> '\uE315'
"rainandthunder" -> '\uE31D'
"heavyrainandthunder" -> '\uE31C'
"lightrainshowersandthunder" -> if (night) '\uE322' else '\uE305'
"rainshowersandthunder" -> if (night) '\uE32A' else '\uE30F'
"heavyrainshowersandthunder" -> if (night) '\uE329' else '\uE30E'
"sleet" -> '\uE316'
"lightssleetshowers", "sleetshowers" -> if (night) '\uE323' else '\uE306'
"heavysleetshowers" -> if (night) '\uE364' else '\uE362'
"sleetandthunder",
"lightsleetshowersandthunder",
"sleetshowersandthunder",
-> if (night) '\uE364' else '\uE362'
"lightssnow", "lightsnow", "snow" -> '\uE31A'
"heavysnow" -> '\uE35E'
"lightssnowshowers", "snowshowers" -> if (night) '\uE327' else '\uE30A'
"heavysnowshowers" -> if (night) '\uE361' else '\uE35F'
"snowandthunder",
"snowshowersandthunder",
-> if (night) '\uE367' else '\uE365'
"thunder" -> '\uE31D'
"wind" -> '\uE34B'
else -> '\uE312'
}
}
}

View File

@@ -0,0 +1,51 @@
package com.github.droidworksstudio.launcher.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import com.github.droidworksstudio.common.ColorIconsExtensions
object NotificationDotHelper {
/**
* Returns a copy of [icon] with a red dot drawn in its top-right corner,
* surrounded by a thin white border so it stays visible on busy icons.
*/
fun withDot(context: Context, icon: Drawable): Drawable {
val source = ColorIconsExtensions.drawableToBitmap(icon)
val width = source.width
val height = source.height
if (width <= 0 || height <= 0) return icon
val dotRadius = (minOf(width, height) * 0.22f).coerceAtLeast(4f)
val centerX = width - dotRadius * 0.75f
val centerY = dotRadius * 0.75f
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawBitmap(source, 0f, 0f, null)
val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.RED
}
canvas.drawCircle(centerX, centerY, dotRadius, dotPaint)
val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = (dotRadius * 0.28f).coerceAtLeast(1.5f)
}
canvas.drawCircle(
centerX,
centerY,
dotRadius - borderPaint.strokeWidth / 2f,
borderPaint
)
return BitmapDrawable(context.resources, bitmap)
}
}

View File

@@ -49,6 +49,8 @@ class PreferenceViewModel @Inject constructor(
private val autoKeyboardLiveData: MutableLiveData<Boolean> = MutableLiveData()
private val lockSettingsLiveData: MutableLiveData<Boolean> = MutableLiveData()
private val disableAnimationsLiveData: MutableLiveData<Boolean> = MutableLiveData()
val showNotificationDotsLiveData: MutableLiveData<Boolean> = MutableLiveData()
val showCurrentWeatherLiveData: MutableLiveData<Boolean> = MutableLiveData()
private val appGroupPaddingSizeLiveData: MutableLiveData<Float> = MutableLiveData()
private val appPaddingSizeLiveData: MutableLiveData<Float> = MutableLiveData()
@@ -297,6 +299,16 @@ class PreferenceViewModel @Inject constructor(
disableAnimationsLiveData.postValue((preferenceHelper.disableAnimations))
}
fun setShowNotificationDots(showNotificationDots: Boolean) {
preferenceHelper.showNotificationDots = showNotificationDots
showNotificationDotsLiveData.postValue((preferenceHelper.showNotificationDots))
}
fun setShowCurrentWeather(showCurrentWeather: Boolean) {
preferenceHelper.showCurrentWeather = showCurrentWeather
showCurrentWeatherLiveData.postValue((preferenceHelper.showCurrentWeather))
}
fun setAppLanguage(appLanguage: Constants.Language) {
preferenceHelper.appLanguage = appLanguage
appLanguageLiveData.postValue((preferenceHelper.appLanguage))

Binary file not shown.

View File

@@ -78,6 +78,14 @@
android:textSize="32sp"
android:visibility="gone" />
<TextView
android:id="@+id/currentWeather"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="32sp"
android:visibility="gone" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat

View File

@@ -238,6 +238,37 @@
tools:ignore="TouchTargetSizeCheck" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:ignore="MissingConstraints">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/showNotificationDots_text"
style="@style/TextDefaultStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="left|center"
android:text="@string/settings_display_notification_dots"
android:textSize="@dimen/text_large"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="RtlHardcoded" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/showNotificationDots_switchCompat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleX="0.7"
android:scaleY="0.8"
android:thumb="@drawable/shape_switch_thumb"
app:track="@drawable/selector_switch"
tools:ignore="TouchTargetSizeCheck" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat

View File

@@ -300,6 +300,37 @@
tools:ignore="TouchTargetSizeCheck" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:ignore="MissingConstraints">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/currentWeather_text"
style="@style/TextDefaultStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="left|center"
android:text="@string/settings_display_current_weather"
android:textSize="@dimen/text_large"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="RtlHardcoded" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/currentWeather_switchCompat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleX="0.7"
android:scaleY="0.8"
android:thumb="@drawable/shape_switch_thumb"
app:track="@drawable/selector_switch"
tools:ignore="TouchTargetSizeCheck" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat

View File

@@ -104,12 +104,15 @@
<string name="settings_display_alarm_clock">Show Alarm Clock</string>
<string name="settings_display_daily_word">Show Daily Word</string>
<string name="settings_display_app_icons">Show App Icons</string>
<string name="settings_display_current_weather">Show Current Weather</string>
<string name="settings_display_automatic_keyboard">Auto Show Keyboard</string>
<string name="settings_display_auto_open_apps">Auto Open Last App</string>
<string name="settings_home_alignment_bottom">Home Alignment Bottom</string>
<string name="settings_search_from_start">Search From Start</string>
<string name="settings_display_lock_settings">Lock Settings</string>
<string name="settings_display_disable_animations">Disable Animations</string>
<string name="settings_display_notification_dots">Notification Dots</string>
<string name="toast_cannot_open_notification_access_settings">Cannot open notification access settings.</string>
<string name="settings_appearance_text_size_title">Size</string>
<string name="settings_appearance_color_title">Color</string>
@@ -200,6 +203,7 @@
<string name="accessibility_settings_disable">Disable</string>
<string name="accessibility_service_name">Easy Launcher Actions Service</string>
<string name="notification_badge_service_label">Easy Launcher - notification dots</string>
<string name="accessibility_service_desc">Please turn on accessibility service to use double tap to lock feature in Easy Launcher.\n\nThis permission is used only to turn off your screen. Our
accessibility service does not collect or share any data.</string>

Binary file not shown.

Binary file not shown.

BIN
dist/EasyLauncher-v0.3.5-Signed.apk vendored Normal file

Binary file not shown.

Binary file not shown.