Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cfa2fdc84 | ||
|
|
d0d1c031d0 | ||
|
|
494364ffa2 | ||
|
|
80a56c01c9 |
No files matched your search
@@ -8,6 +8,12 @@
|
||||
<dimen name="flexera_drawer_row_icon_size">34dp</dimen>
|
||||
<dimen name="flexera_drawer_row_icon_gap">16dp</dimen>
|
||||
<dimen name="flexera_drawer_row_text_size">15sp</dimen>
|
||||
<!-- The "comfy" row size the owner can choose instead (Settings, App drawer): a little
|
||||
larger all round, between compact and the first version. -->
|
||||
<dimen name="flexera_drawer_row_height_comfy">58dp</dimen>
|
||||
<dimen name="flexera_drawer_row_icon_size_comfy">40dp</dimen>
|
||||
<dimen name="flexera_drawer_row_icon_gap_comfy">18dp</dimen>
|
||||
<dimen name="flexera_drawer_row_text_size_comfy">16sp</dimen>
|
||||
<!-- Row padding on the edge without the wave alphabet. The search bar lines up with it. -->
|
||||
<dimen name="flexera_drawer_row_padding_start">24dp</dimen>
|
||||
<!-- Row padding on the wave alphabet's edge, which is the end edge unless the owner moves it
|
||||
@@ -15,10 +21,15 @@
|
||||
<dimen name="flexera_drawer_row_padding_end">56dp</dimen>
|
||||
|
||||
<!-- Wave alphabet (PRD AD-4 to AD-6). -->
|
||||
<!-- How wide a band along the edge starts a scrub when touched. -->
|
||||
<dimen name="flexera_wave_touch_width">40dp</dimen>
|
||||
<!-- Where the letters rest, measured from the edge. -->
|
||||
<dimen name="flexera_wave_edge_inset">16dp</dimen>
|
||||
<!-- How wide a band along the edge starts a scrub when touched in the open drawer. -->
|
||||
<dimen name="flexera_wave_touch_width">60dp</dimen>
|
||||
<!-- The same band on the home screen, where nothing shows it: narrower, so that fewer
|
||||
swipes that only pass near the edge are taken for a scrub (PRD AD-10). -->
|
||||
<dimen name="flexera_wave_home_touch_width">40dp</dimen>
|
||||
<!-- Where the letters rest, measured from the edge. Inward of the strip of edge where
|
||||
Android starts its Back swipe (about 24dp), so a thumb on the letters is not on it.
|
||||
(First version: 16dp, with the whole edge reserved, which left no Back swipe.) -->
|
||||
<dimen name="flexera_wave_edge_inset">30dp</dimen>
|
||||
<dimen name="flexera_wave_letter_size">12sp</dimen>
|
||||
<!-- Letters never sit further apart than this, however few there are. Close together, a
|
||||
little closer than Niagara Launcher's, so a whole alphabet is about 83 mm tall and fits
|
||||
@@ -63,4 +74,7 @@
|
||||
<dimen name="flexera_drawer_header_height">34dp</dimen>
|
||||
<dimen name="flexera_drawer_header_padding_bottom">4dp</dimen>
|
||||
<dimen name="flexera_drawer_header_text_size">12sp</dimen>
|
||||
<dimen name="flexera_drawer_header_height_comfy">38dp</dimen>
|
||||
<dimen name="flexera_drawer_header_padding_bottom_comfy">5dp</dimen>
|
||||
<dimen name="flexera_drawer_header_text_size_comfy">13sp</dimen>
|
||||
</resources>
|
||||
@@ -74,6 +74,9 @@
|
||||
<!-- The drawer's wave alphabet -->
|
||||
<!-- Read by screen readers as the name of the alphabet strip, followed by the current letter -->
|
||||
<string name="flexera_alphabet_description">Alphabet</string>
|
||||
<string name="flexera_row_size">Row size</string>
|
||||
<string name="flexera_row_size_compact">Compact</string>
|
||||
<string name="flexera_row_size_comfy">Comfy</string>
|
||||
<string name="flexera_alphabet_search">Search</string>
|
||||
<!-- App drawer settings (app.flexera.ui.FlexeraDrawerSettings) -->
|
||||
<string name="flexera_notification_previews">Notification previews</string>
|
||||
@@ -90,7 +93,15 @@
|
||||
<string name="flexera_update_check_idle">Flexera Launcher also checks on its own, once a day</string>
|
||||
<string name="flexera_update_checking">Checking for updates…</string>
|
||||
<string name="flexera_update_up_to_date">You have the latest version</string>
|
||||
<string name="flexera_update_check_failed">Couldn\'t reach the update server. Check your connection, then tap to try again.</string>
|
||||
<!-- Why a check for updates failed. %1$s is the update server's name, such as git.example.org -->
|
||||
<string name="flexera_update_failed_no_permission">Android does not let Flexera Launcher use the network on this phone. Allow it in the phone\'s settings for this app, under Permissions, Network, then tap to try again.</string>
|
||||
<string name="flexera_update_failed_not_found">This phone could not find %1$s. Check the connection and any VPN or private DNS, then tap to try again.</string>
|
||||
<string name="flexera_update_failed_no_answer">%1$s did not answer. Check the connection and any VPN, then tap to try again.</string>
|
||||
<string name="flexera_update_failed_not_secure">A secure connection to %1$s could not be made. Check the phone\'s date and time and any VPN, then tap to try again.</string>
|
||||
<string name="flexera_update_failed_server_error">%1$s answered with an error. Tap to try again later.</string>
|
||||
<string name="flexera_update_failed_bad_answer">The answer from %1$s could not be read. Tap to try again later.</string>
|
||||
<!-- %1$s is the error in the system\'s own words, for whoever looks for the cause -->
|
||||
<string name="flexera_update_failed_detail">Details: %1$s</string>
|
||||
<!-- %1$s is a version number such as 0.2.0 -->
|
||||
<string name="flexera_update_download">Download version %1$s</string>
|
||||
<!-- %1$s is a file size such as 24 MB -->
|
||||
|
||||
@@ -47,9 +47,54 @@ object FlexeraDrawer {
|
||||
@JvmStatic
|
||||
fun sectionHeader(sectionName: String): AdapterItem = SectionHeaderItem(sectionName)
|
||||
|
||||
/** The sizes the owner can give the list's rows (Settings, App drawer). */
|
||||
enum class RowSize(val key: String) { COMPACT("compact"), COMFY("comfy") }
|
||||
|
||||
/** The row size in use. Changing the setting recreates the launcher. */
|
||||
@JvmStatic
|
||||
fun createSectionHeader(inflater: LayoutInflater, parent: ViewGroup): View =
|
||||
inflater.inflate(R.layout.flexera_all_apps_section_header, parent, false).also(::applySidePadding)
|
||||
fun rowSize(context: Context): RowSize {
|
||||
val key = PreferenceManager.getInstance(context).flexeraDrawerRowSize.get()
|
||||
return RowSize.entries.firstOrNull { it.key == key } ?: RowSize.COMPACT
|
||||
}
|
||||
|
||||
/** Picks the dimension of the row size in use. */
|
||||
private fun sized(context: Context, compact: Int, comfy: Int): Int =
|
||||
if (rowSize(context) == RowSize.COMFY) comfy else compact
|
||||
|
||||
/** How tall a letter header is. */
|
||||
@JvmStatic
|
||||
fun headerHeight(context: Context): Int = context.resources.getDimensionPixelSize(
|
||||
sized(context, R.dimen.flexera_drawer_header_height, R.dimen.flexera_drawer_header_height_comfy),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createSectionHeader(inflater: LayoutInflater, parent: ViewGroup): View {
|
||||
val header = inflater.inflate(R.layout.flexera_all_apps_section_header, parent, false) as TextView
|
||||
val context = header.context
|
||||
val res = context.resources
|
||||
// The layout carries the compact sizes; the row size in use has the last word.
|
||||
header.layoutParams.height = headerHeight(context)
|
||||
header.setTextSize(
|
||||
TypedValue.COMPLEX_UNIT_PX,
|
||||
res.getDimension(
|
||||
sized(context, R.dimen.flexera_drawer_header_text_size, R.dimen.flexera_drawer_header_text_size_comfy),
|
||||
),
|
||||
)
|
||||
header.setPadding(
|
||||
header.paddingLeft,
|
||||
header.paddingTop,
|
||||
header.paddingRight,
|
||||
res.getDimensionPixelSize(
|
||||
sized(
|
||||
context,
|
||||
R.dimen.flexera_drawer_header_padding_bottom,
|
||||
R.dimen.flexera_drawer_header_padding_bottom_comfy,
|
||||
),
|
||||
),
|
||||
)
|
||||
applySidePadding(header)
|
||||
return header
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the owner has moved the wave alphabet to the left edge (PRD AD-15). Changing the
|
||||
@@ -93,10 +138,17 @@ object FlexeraDrawer {
|
||||
// Other icons share the "all apps" display type (search results, for one); this tag
|
||||
// tells list rows apart from them.
|
||||
icon.setTag(R.id.flexera_drawer_row, true)
|
||||
val textSize = res.getDimension(R.dimen.flexera_drawer_row_text_size)
|
||||
icon.layoutParams.height = rowHeight(icon.context)
|
||||
icon.compoundDrawablePadding = res.getDimensionPixelSize(R.dimen.flexera_drawer_row_icon_gap)
|
||||
icon.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
val context = icon.context
|
||||
icon.layoutParams.height = rowHeight(context)
|
||||
icon.setIconSizeOverride(
|
||||
res.getDimensionPixelSize(
|
||||
sized(context, R.dimen.flexera_drawer_row_icon_size, R.dimen.flexera_drawer_row_icon_size_comfy),
|
||||
),
|
||||
)
|
||||
icon.compoundDrawablePadding = res.getDimensionPixelSize(
|
||||
sized(context, R.dimen.flexera_drawer_row_icon_gap, R.dimen.flexera_drawer_row_icon_gap_comfy),
|
||||
)
|
||||
icon.setTextSize(TypedValue.COMPLEX_UNIT_PX, rowTextSize(context))
|
||||
applySidePadding(icon)
|
||||
// The drawer has no background, so names sit directly on the dimmed wallpaper: white,
|
||||
// with a soft shadow to stay readable over light areas.
|
||||
@@ -118,13 +170,17 @@ object FlexeraDrawer {
|
||||
* the row grows instead.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun rowHeight(context: Context): Int {
|
||||
val res = context.resources
|
||||
return max(
|
||||
res.getDimensionPixelSize(R.dimen.flexera_drawer_row_height),
|
||||
(res.getDimension(R.dimen.flexera_drawer_row_text_size) * ROW_HEIGHT_IN_TEXT_SIZES).toInt(),
|
||||
)
|
||||
}
|
||||
fun rowHeight(context: Context): Int = max(
|
||||
context.resources.getDimensionPixelSize(
|
||||
sized(context, R.dimen.flexera_drawer_row_height, R.dimen.flexera_drawer_row_height_comfy),
|
||||
),
|
||||
(rowTextSize(context) * ROW_HEIGHT_IN_TEXT_SIZES).toInt(),
|
||||
)
|
||||
|
||||
/** The size of an app name in a row, in pixels. */
|
||||
private fun rowTextSize(context: Context): Float = context.resources.getDimension(
|
||||
sized(context, R.dimen.flexera_drawer_row_text_size, R.dimen.flexera_drawer_row_text_size_comfy),
|
||||
)
|
||||
|
||||
/** The least a row's height may be, in name text sizes: two lines of text and some air. */
|
||||
private const val ROW_HEIGHT_IN_TEXT_SIZES = 2.4f
|
||||
@@ -304,7 +360,7 @@ object FlexeraDrawer {
|
||||
): Boolean {
|
||||
val wave = waveAlphabet(appsView) ?: return false
|
||||
val offset = layoutOffset(wave, dragLayer)
|
||||
return wave.isOnStripBand(ev.x - offset.x, ev.y - offset.y)
|
||||
return wave.isOnHomeBand(ev.x - offset.x, ev.y - offset.y)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,7 @@ import android.os.Bundle
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.SeekBar
|
||||
@@ -50,7 +51,9 @@ import kotlin.math.roundToInt
|
||||
* as the last.
|
||||
*
|
||||
* The view covers the whole drawer so it has room to draw the wave, but it only takes touches
|
||||
* that start on the strip; every other touch falls through to the list underneath.
|
||||
* that start on the strip; every other touch falls through to the list underneath. The very
|
||||
* edge is also where Android starts its Back swipe. The strip leaves that to Android: a touch
|
||||
* there becomes a scrub only once it moves up or down or rests, and a sideways swipe goes back.
|
||||
*
|
||||
* [beginScrub], [moveScrub] and [endScrub] are public so that another gesture, such as swipe up
|
||||
* and hold from the home screen (PRD AD-17), can hand its touch over to the alphabet.
|
||||
@@ -79,7 +82,9 @@ class WaveAlphabetView(
|
||||
private val waveWidth = res.getDimension(R.dimen.flexera_wave_width)
|
||||
private val farMargin = res.getDimension(R.dimen.flexera_wave_far_margin)
|
||||
private val letterSize = res.getDimension(R.dimen.flexera_wave_letter_size)
|
||||
private val headerHeight = res.getDimensionPixelSize(R.dimen.flexera_drawer_header_height)
|
||||
private val headerHeight = FlexeraDrawer.headerHeight(context)
|
||||
private val homeTouchWidth = res.getDimension(R.dimen.flexera_wave_home_touch_width)
|
||||
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||
|
||||
/** The section name Launcher3 gives the work profile's info card, an "i" in a circle. */
|
||||
private val workInfoSection = res.getString(R.string.work_profile_edu_section)
|
||||
@@ -153,11 +158,25 @@ class WaveAlphabetView(
|
||||
private var fingerY = 0f
|
||||
private var selected = -1
|
||||
|
||||
// A touch that began on the edge strip Android uses for Back, before it is known whether
|
||||
// it is a scrub: where it went down, where it is now, and the timer that makes a touch
|
||||
// at rest a scrub.
|
||||
private var waitingForBack = false
|
||||
private var downX = 0f
|
||||
private var downY = 0f
|
||||
private var lastX = 0f
|
||||
private var lastY = 0f
|
||||
private val scrubAfterRest = Runnable {
|
||||
if (waitingForBack) {
|
||||
waitingForBack = false
|
||||
beginScrub(lastX, lastY)
|
||||
}
|
||||
}
|
||||
|
||||
/** 0 when the strip is at rest, 1 when the wave is fully out. Animated on touch down and up. */
|
||||
private var press = 0f
|
||||
private var pressAnimator: ValueAnimator? = null
|
||||
|
||||
private val exclusionRect = Rect()
|
||||
private val location = IntArray(2)
|
||||
private val otherLocation = IntArray(2)
|
||||
|
||||
@@ -250,16 +269,9 @@ class WaveAlphabetView(
|
||||
restingLetterSize = min(letterSize, spacing * MAX_LETTER_SHARE_OF_SLOT)
|
||||
}
|
||||
|
||||
// The strip sits where Android's back swipe starts. A home app may reserve the whole
|
||||
// edge, so the scrub never turns into Back.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
if (onLeft) {
|
||||
exclusionRect.set(0, stripTop.toInt(), touchWidth.toInt(), stripBottom.toInt())
|
||||
} else {
|
||||
exclusionRect.set((width - touchWidth).toInt(), stripTop.toInt(), width, stripBottom.toInt())
|
||||
}
|
||||
systemGestureExclusionRects = listOf(exclusionRect)
|
||||
}
|
||||
// The strip does not reserve the edge for itself: Android's Back swipe starts there,
|
||||
// and the owner closes the drawer with it. The two share the edge by direction, see
|
||||
// onTouchEvent.
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
@@ -275,6 +287,7 @@ class WaveAlphabetView(
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
|
||||
stopWaitingForBack()
|
||||
pressAnimator?.cancel()
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
@@ -384,13 +397,29 @@ class WaveAlphabetView(
|
||||
* True when this point lies on the band along the edge that the letters occupy, whether or
|
||||
* not the drawer is open. Scrub from home (PRD AD-10) asks this while the drawer is closed.
|
||||
*/
|
||||
fun isOnStripBand(x: Float, y: Float): Boolean {
|
||||
fun isOnStripBand(x: Float, y: Float): Boolean = isOnBand(x, y, touchWidth)
|
||||
|
||||
/** The same for the home screen, where the band is narrower because nothing shows it. */
|
||||
fun isOnHomeBand(x: Float, y: Float): Boolean = isOnBand(x, y, homeTouchWidth)
|
||||
|
||||
private fun isOnBand(x: Float, y: Float, bandWidth: Float): Boolean {
|
||||
if (layoutDirty) computeLayout()
|
||||
if (letters.isEmpty()) return false
|
||||
val onEdge = if (onLeft) x <= touchWidth else x >= width - touchWidth
|
||||
val onEdge = if (onLeft) x <= bandWidth else x >= width - bandWidth
|
||||
return onEdge && y >= stripTop - verticalMargin && y <= stripBottom + verticalMargin
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a touch at [x] starts on the strip of edge where Android may begin its Back
|
||||
* swipe. With three-button navigation there is no such strip.
|
||||
*/
|
||||
private fun isInBackSwipeZone(x: Float): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return false
|
||||
@Suppress("DEPRECATION")
|
||||
val insets = rootWindowInsets?.systemGestureInsets ?: return false
|
||||
return if (onLeft) x < insets.left else x > width - insets.right
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
when (event.actionMasked) {
|
||||
@@ -398,16 +427,50 @@ class WaveAlphabetView(
|
||||
if (!isOnStrip(event.x, event.y)) return false
|
||||
// Keep the drawer from scrolling or closing under the thumb.
|
||||
parent?.requestDisallowInterceptTouchEvent(true)
|
||||
beginScrub(event.x, event.y)
|
||||
downX = event.x
|
||||
downY = event.y
|
||||
lastX = event.x
|
||||
lastY = event.y
|
||||
if (isInBackSwipeZone(event.x)) {
|
||||
// This may be the start of Android's Back swipe, which moves sideways.
|
||||
// Show nothing until the touch says which it is: a move up or down or a
|
||||
// short rest is a scrub, a move sideways is left to Android.
|
||||
waitingForBack = true
|
||||
postDelayed(scrubAfterRest, REST_BEFORE_SCRUB_MS)
|
||||
} else {
|
||||
beginScrub(event.x, event.y)
|
||||
}
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (!scrubbing) return false
|
||||
moveScrub(event.x, event.y)
|
||||
lastX = event.x
|
||||
lastY = event.y
|
||||
if (waitingForBack) {
|
||||
val dx = abs(event.x - downX)
|
||||
val dy = abs(event.y - downY)
|
||||
if (dy > touchSlop && dy >= dx) {
|
||||
stopWaitingForBack()
|
||||
beginScrub(event.x, event.y)
|
||||
} else if (dx > touchSlop) {
|
||||
// Sideways: Android takes the touch from here on, and with it Back.
|
||||
stopWaitingForBack()
|
||||
}
|
||||
} else if (scrubbing) {
|
||||
moveScrub(event.x, event.y)
|
||||
}
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
if (!scrubbing) return false
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (waitingForBack) {
|
||||
// Down and up without moving: a tap on a letter.
|
||||
stopWaitingForBack()
|
||||
beginScrub(event.x, event.y)
|
||||
}
|
||||
endScrub()
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
stopWaitingForBack()
|
||||
endScrub()
|
||||
return true
|
||||
}
|
||||
@@ -415,6 +478,11 @@ class WaveAlphabetView(
|
||||
return scrubbing
|
||||
}
|
||||
|
||||
private fun stopWaitingForBack() {
|
||||
waitingForBack = false
|
||||
removeCallbacks(scrubAfterRest)
|
||||
}
|
||||
|
||||
/** Starts a scrub with the finger at ([x], [y]) in this view's coordinates. */
|
||||
fun beginScrub(x: Float, y: Float) {
|
||||
refreshIfChanged()
|
||||
@@ -643,6 +711,12 @@ class WaveAlphabetView(
|
||||
/** The tallest a resting letter may be, as a share of the vertical room it has. */
|
||||
const val MAX_LETTER_SHARE_OF_SLOT = 0.85f
|
||||
|
||||
/**
|
||||
* How long a touch on the Back strip may rest before it counts as a scrub. Long
|
||||
* enough for a Back swipe to have started moving, short enough not to be felt.
|
||||
*/
|
||||
const val REST_BEFORE_SCRUB_MS = 130L
|
||||
|
||||
const val PRESS_IN_MS = 110L
|
||||
const val PRESS_OUT_MS = 220L
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.flexera.update.CheckProblem
|
||||
import app.flexera.update.FlexeraRelease
|
||||
import app.flexera.update.FlexeraUpdater
|
||||
import app.flexera.update.UpdateFailure
|
||||
@@ -294,7 +295,7 @@ private fun UpdateStatusRow(state: UpdateState) {
|
||||
if (release == null) {
|
||||
ClickablePreference(
|
||||
label = stringResource(id = R.string.flexera_update_check),
|
||||
subtitle = stringResource(id = R.string.flexera_update_check_failed),
|
||||
subtitle = checkFailureText(state),
|
||||
onClick = { FlexeraUpdater.check(context) },
|
||||
)
|
||||
} else {
|
||||
@@ -379,6 +380,25 @@ private fun QrCode(text: String, description: String, modifier: Modifier = Modif
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Says why a check for updates failed and what to do about it, then the error's own words, which
|
||||
* are what someone looking for the cause will ask for.
|
||||
*/
|
||||
@Composable
|
||||
private fun checkFailureText(state: UpdateState.Failed): String {
|
||||
val host = FlexeraUpdater.SERVER_HOST
|
||||
val reason = when (state.problem) {
|
||||
CheckProblem.NO_PERMISSION -> stringResource(id = R.string.flexera_update_failed_no_permission)
|
||||
CheckProblem.SERVER_NOT_FOUND -> stringResource(id = R.string.flexera_update_failed_not_found, host)
|
||||
CheckProblem.NOT_SECURE -> stringResource(id = R.string.flexera_update_failed_not_secure, host)
|
||||
CheckProblem.SERVER_ERROR -> stringResource(id = R.string.flexera_update_failed_server_error, host)
|
||||
CheckProblem.BAD_ANSWER -> stringResource(id = R.string.flexera_update_failed_bad_answer, host)
|
||||
CheckProblem.NO_ANSWER, null -> stringResource(id = R.string.flexera_update_failed_no_answer, host)
|
||||
}
|
||||
val detail = state.detail ?: return reason
|
||||
return reason + "\n" + stringResource(id = R.string.flexera_update_failed_detail, detail)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun downloadSizeText(context: Context, release: FlexeraRelease): String =
|
||||
if (release.apkSize > 0) {
|
||||
|
||||
@@ -4,11 +4,14 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.flexera.drawer.FlexeraDrawer
|
||||
import app.lawnchair.preferences.getAdapter
|
||||
import app.lawnchair.preferences.preferenceManager
|
||||
import app.lawnchair.preferences2.preferenceManager2
|
||||
import app.lawnchair.ui.preferences.LocalIsExpandedScreen
|
||||
import app.lawnchair.ui.preferences.components.NavigationActionPreference
|
||||
import app.lawnchair.ui.preferences.components.controls.ListPreference
|
||||
import app.lawnchair.ui.preferences.components.controls.ListPreferenceEntry
|
||||
import app.lawnchair.ui.preferences.components.controls.SwitchPreference
|
||||
import app.lawnchair.ui.preferences.components.layout.PreferenceGroup
|
||||
import app.lawnchair.ui.preferences.components.layout.PreferenceLayout
|
||||
@@ -75,6 +78,18 @@ fun FlexeraDrawerSettings(modifier: Modifier = Modifier) {
|
||||
// No background colour or opacity here: the drawer is transparent with white text, and a
|
||||
// light panel behind white text would make the list unreadable.
|
||||
PreferenceGroup(heading = stringResource(id = R.string.style)) {
|
||||
ListPreference(
|
||||
adapter = prefs.flexeraDrawerRowSize.getAdapter(),
|
||||
entries = listOf(
|
||||
ListPreferenceEntry(FlexeraDrawer.RowSize.COMPACT.key) {
|
||||
stringResource(id = R.string.flexera_row_size_compact)
|
||||
},
|
||||
ListPreferenceEntry(FlexeraDrawer.RowSize.COMFY.key) {
|
||||
stringResource(id = R.string.flexera_row_size_comfy)
|
||||
},
|
||||
),
|
||||
label = stringResource(id = R.string.flexera_row_size),
|
||||
)
|
||||
SwitchPreference(
|
||||
label = stringResource(id = R.string.pref_all_apps_search_bar_background),
|
||||
adapter = prefs2.appDrawerSearchBarBackground.getAdapter(),
|
||||
|
||||
@@ -24,6 +24,8 @@ import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.net.UnknownHostException
|
||||
import javax.net.ssl.SSLException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -31,6 +33,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
/** One published version of Flexera Launcher. */
|
||||
@@ -56,6 +59,31 @@ enum class UpdateFailure {
|
||||
WRONG_FILE,
|
||||
}
|
||||
|
||||
/**
|
||||
* Why a check for updates failed, as far as the phone can tell. The About screen says it in
|
||||
* words, because "could not reach the server" alone leaves the owner guessing between the
|
||||
* connection, a VPN, the server and the phone's own settings.
|
||||
*/
|
||||
enum class CheckProblem {
|
||||
/** Android does not let this app use the network (a per-app switch on some systems). */
|
||||
NO_PERMISSION,
|
||||
|
||||
/** The server's name could not be looked up. */
|
||||
SERVER_NOT_FOUND,
|
||||
|
||||
/** The server was found but did not answer in time, or refused the connection. */
|
||||
NO_ANSWER,
|
||||
|
||||
/** The encrypted connection could not be set up. */
|
||||
NOT_SECURE,
|
||||
|
||||
/** The server answered, with an error. */
|
||||
SERVER_ERROR,
|
||||
|
||||
/** The server answered with something that is not a release. */
|
||||
BAD_ANSWER,
|
||||
}
|
||||
|
||||
/** Where the updater currently stands. The About screen shows one row per state. */
|
||||
sealed interface UpdateState {
|
||||
/** Nothing has been checked since the launcher started. */
|
||||
@@ -72,8 +100,17 @@ sealed interface UpdateState {
|
||||
|
||||
data class ReadyToInstall(val release: FlexeraRelease, val file: File) : UpdateState
|
||||
|
||||
/** [release] is set when the failure happened after a newer version had been found. */
|
||||
data class Failed(val failure: UpdateFailure, val release: FlexeraRelease? = null) : UpdateState
|
||||
/**
|
||||
* [release] is set when the failure happened after a newer version had been found. For a
|
||||
* failed check, [problem] says why and [detail] carries the error's own words, for
|
||||
* whoever has to find the cause.
|
||||
*/
|
||||
data class Failed(
|
||||
val failure: UpdateFailure,
|
||||
val release: FlexeraRelease? = null,
|
||||
val problem: CheckProblem? = null,
|
||||
val detail: String? = null,
|
||||
) : UpdateState
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,7 +227,7 @@ object FlexeraUpdater {
|
||||
Log.w(TAG, "Update check failed", error)
|
||||
// A background check that fails stays quiet: the About screen keeps what it showed
|
||||
// before, including an update that is already waiting. It tries again in an hour.
|
||||
mutableState.value = if (automatic) before else UpdateState.Failed(UpdateFailure.CHECK)
|
||||
mutableState.value = if (automatic) before else failedCheck(error)
|
||||
return@launch
|
||||
}
|
||||
|
||||
@@ -302,13 +339,41 @@ object FlexeraUpdater {
|
||||
private fun isBusy(): Boolean =
|
||||
mutableState.value.let { it is UpdateState.Checking || it is UpdateState.Downloading }
|
||||
|
||||
/** The server's name without the scheme, for messages. */
|
||||
@JvmField
|
||||
val SERVER_HOST: String = SERVER.substringAfter("://")
|
||||
|
||||
private class NoNetworkPermissionException : IOException("This app has no network permission")
|
||||
private class ServerErrorException(status: Int) : IOException("The server answered HTTP $status")
|
||||
private class BadAnswerException(message: String) : IOException(message)
|
||||
|
||||
/** Turns the error of a failed check into a state that says what kind of failure it was. */
|
||||
private fun failedCheck(error: Throwable): UpdateState.Failed {
|
||||
val problem = when (error) {
|
||||
is NoNetworkPermissionException, is SecurityException -> CheckProblem.NO_PERMISSION
|
||||
is UnknownHostException -> CheckProblem.SERVER_NOT_FOUND
|
||||
is SSLException -> CheckProblem.NOT_SECURE
|
||||
is ServerErrorException -> CheckProblem.SERVER_ERROR
|
||||
is BadAnswerException, is JSONException -> CheckProblem.BAD_ANSWER
|
||||
else -> CheckProblem.NO_ANSWER
|
||||
}
|
||||
// Android's own error classes keep their names in a release build; this file's are
|
||||
// renamed by the shrinker, and their messages say everything anyway.
|
||||
val name = error.javaClass.name.takeIf { it.startsWith("java") || it.startsWith("android") || it.startsWith("org.json") }
|
||||
?.substringAfterLast('.')
|
||||
val detail = listOfNotNull(name, error.message).joinToString(": ").take(MAX_DETAIL_LENGTH)
|
||||
return UpdateState.Failed(UpdateFailure.CHECK, problem = problem, detail = detail.ifEmpty { null })
|
||||
}
|
||||
|
||||
private const val MAX_DETAIL_LENGTH = 200
|
||||
|
||||
/** Returns the latest published release, or null when nothing has been published yet. */
|
||||
private fun fetchLatestRelease(context: Context): FlexeraRelease? {
|
||||
val connection = open(context, LATEST_RELEASE_URL)
|
||||
try {
|
||||
val status = connection.responseCode
|
||||
if (status == HttpURLConnection.HTTP_NOT_FOUND) return null
|
||||
if (status != HttpURLConnection.HTTP_OK) throw IOException("Update server answered HTTP $status")
|
||||
if (status != HttpURLConnection.HTTP_OK) throw ServerErrorException(status)
|
||||
|
||||
val json = JSONObject(connection.inputStream.bufferedReader().use { it.readText() })
|
||||
val tag = json.getString("tag_name")
|
||||
@@ -326,7 +391,7 @@ object FlexeraUpdater {
|
||||
apkSize = asset.optLong("size", 0L),
|
||||
)
|
||||
}
|
||||
throw IOException("Release $tag has no APK attached")
|
||||
throw BadAnswerException("Release $tag has no APK attached")
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
@@ -373,7 +438,7 @@ object FlexeraUpdater {
|
||||
}
|
||||
|
||||
private fun open(context: Context, url: String): HttpURLConnection {
|
||||
if (!FlexeraNetwork.hasInternetPermission(context)) throw IOException("No INTERNET permission")
|
||||
if (!FlexeraNetwork.hasInternetPermission(context)) throw NoNetworkPermissionException()
|
||||
return (URL(url).openConnection() as HttpURLConnection).apply {
|
||||
connectTimeout = CONNECT_TIMEOUT_MS
|
||||
readTimeout = READ_TIMEOUT_MS
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
# build.gradle reads this and derives the Android version code from it
|
||||
# (major * 10000 + minor * 100 + patch), so the name and the code can never disagree.
|
||||
# flexera/release.ps1 raises it for every release.
|
||||
versionName=0.6.0
|
||||
versionName=0.7.0
|
||||
@@ -224,6 +224,9 @@ class PreferenceManager @Inject constructor(
|
||||
// Flexera Launcher: whether drawer rows show the app's latest notification under its name (PRD AD-11).
|
||||
val flexeraNotificationPreviews = BoolPref("pref_flexeraNotificationPreviews", true, recreate)
|
||||
|
||||
// Flexera Launcher: how large the rows of the drawer list are, "compact" or "comfy" (FlexeraDrawer.RowSize).
|
||||
val flexeraDrawerRowSize = StringPref("pref_flexeraDrawerRowSize", "compact", recreate)
|
||||
|
||||
override fun close() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@@ -195,7 +195,8 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
|
||||
|
||||
private boolean mLayoutHorizontal;
|
||||
private final boolean mIsRtl;
|
||||
private final int mIconSize;
|
||||
// Flexera Launcher: not final, see setIconSizeOverride.
|
||||
private int mIconSize;
|
||||
|
||||
@ViewDebug.ExportedProperty(category = "launcher")
|
||||
private boolean mHideBadge = false;
|
||||
@@ -1563,6 +1564,15 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
|
||||
return mIconSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flexera Launcher: the rows of the drawer list come in more than one size, chosen by
|
||||
* the owner at run time, which the layout attribute cannot follow. Call before an icon
|
||||
* is applied.
|
||||
*/
|
||||
public void setIconSizeOverride(int iconSize) {
|
||||
mIconSize = iconSize;
|
||||
}
|
||||
|
||||
public boolean isDisplaySearchResult() {
|
||||
return mDisplay == DISPLAY_SEARCH_RESULT
|
||||
|| mDisplay == DISPLAY_SEARCH_RESULT_SMALL
|
||||
|
||||
Reference in new issue
Block a user