That fuck shit the fascists are using
1package org.tm.archive.components
2
3import android.content.Context
4import android.os.Build
5import android.util.AttributeSet
6import android.util.DisplayMetrics
7import android.view.Surface
8import androidx.constraintlayout.widget.ConstraintLayout
9import androidx.constraintlayout.widget.Guideline
10import androidx.core.content.withStyledAttributes
11import androidx.core.graphics.Insets
12import androidx.core.view.ViewCompat
13import androidx.core.view.WindowInsetsAnimationCompat
14import androidx.core.view.WindowInsetsCompat
15import org.signal.core.util.logging.Log
16import org.tm.archive.R
17import org.tm.archive.keyvalue.SignalStore
18import org.tm.archive.util.ServiceUtil
19import org.tm.archive.util.ViewUtil
20
21/**
22 * A specialized [ConstraintLayout] that sets guidelines based on the window insets provided
23 * by the system. For improved backwards-compatibility we must use [ViewCompat] for configuring
24 * the inset change callbacks.
25 *
26 * In portrait mode these are how the guidelines will be configured:
27 *
28 * - [R.id.status_bar_guideline] is set to the bottom of the status bar
29 * - [R.id.navigation_bar_guideline] is set to the top of the navigation bar
30 * - [R.id.parent_start_guideline] is set to the start of the parent
31 * - [R.id.parent_end_guideline] is set to the end of the parent
32 * - [R.id.keyboard_guideline] will be set to the top of the keyboard and will
33 * change as the keyboard is shown or hidden
34 *
35 * In landscape, the spirit of the guidelines are maintained but their names may not
36 * correlated exactly to the inset they are providing.
37 *
38 * These guidelines will only be updated if present in your layout, you can use
39 * `<include layout="@layout/system_ui_guidelines" />` to quickly include them.
40 */
41@Suppress("LeakingThis")
42open class InsetAwareConstraintLayout @JvmOverloads constructor(
43 context: Context,
44 attrs: AttributeSet? = null,
45 defStyleAttr: Int = 0
46) : ConstraintLayout(context, attrs, defStyleAttr) {
47
48 companion object {
49 private val TAG = Log.tag(InsetAwareConstraintLayout::class.java)
50 private val keyboardType = WindowInsetsCompat.Type.ime()
51 private val windowTypes = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
52 }
53
54 protected val statusBarGuideline: Guideline? by lazy { findViewById(R.id.status_bar_guideline) }
55 private val navigationBarGuideline: Guideline? by lazy { findViewById(R.id.navigation_bar_guideline) }
56 private val parentStartGuideline: Guideline? by lazy { findViewById(R.id.parent_start_guideline) }
57 private val parentEndGuideline: Guideline? by lazy { findViewById(R.id.parent_end_guideline) }
58 private val keyboardGuideline: Guideline? by lazy { findViewById(R.id.keyboard_guideline) }
59
60 private val windowInsetsListeners: MutableSet<WindowInsetsListener> = mutableSetOf()
61 private val keyboardStateListeners: MutableSet<KeyboardStateListener> = mutableSetOf()
62 private val keyboardAnimator = KeyboardInsetAnimator()
63 private val displayMetrics = DisplayMetrics()
64 private var overridingKeyboard: Boolean = false
65 private var previousKeyboardHeight: Int = 0
66
67 val isKeyboardShowing: Boolean
68 get() = previousKeyboardHeight > 0
69
70 init {
71 ViewCompat.setOnApplyWindowInsetsListener(this) { _, windowInsetsCompat ->
72 applyInsets(windowInsets = windowInsetsCompat.getInsets(windowTypes), keyboardInsets = windowInsetsCompat.getInsets(keyboardType))
73 windowInsetsCompat
74 }
75
76 if (attrs != null) {
77 context.withStyledAttributes(attrs, R.styleable.InsetAwareConstraintLayout) {
78 if (getBoolean(R.styleable.InsetAwareConstraintLayout_animateKeyboardChanges, false)) {
79 ViewCompat.setWindowInsetsAnimationCallback(this@InsetAwareConstraintLayout, keyboardAnimator)
80 }
81 }
82 }
83 }
84
85 fun addKeyboardStateListener(listener: KeyboardStateListener) {
86 keyboardStateListeners += listener
87 }
88
89 fun removeKeyboardStateListener(listener: KeyboardStateListener) {
90 keyboardStateListeners.remove(listener)
91 }
92
93 fun addWindowInsetsListener(listener: WindowInsetsListener) {
94 windowInsetsListeners += listener
95 }
96
97 fun removeWindowInsetsListener(listener: WindowInsetsListener) {
98 windowInsetsListeners.remove(listener)
99 }
100
101 private fun applyInsets(windowInsets: Insets, keyboardInsets: Insets) {
102 val isLtr = ViewUtil.isLtr(this)
103
104 val statusBar = windowInsets.top
105 val navigationBar = windowInsets.bottom
106 val parentStart = if (isLtr) windowInsets.left else windowInsets.right
107 val parentEnd = if (isLtr) windowInsets.right else windowInsets.left
108
109 statusBarGuideline?.setGuidelineBegin(statusBar)
110 navigationBarGuideline?.setGuidelineEnd(navigationBar)
111 parentStartGuideline?.setGuidelineBegin(parentStart)
112 parentEndGuideline?.setGuidelineEnd(parentEnd)
113
114 windowInsetsListeners.forEach { it.onApplyWindowInsets(statusBar, navigationBar, parentStart, parentEnd) }
115
116 if (keyboardInsets.bottom > 0) {
117 setKeyboardHeight(keyboardInsets.bottom)
118 if (!keyboardAnimator.animating) {
119 keyboardGuideline?.setGuidelineEnd(keyboardInsets.bottom)
120 } else {
121 keyboardAnimator.endingGuidelineEnd = keyboardInsets.bottom
122 }
123 } else if (!overridingKeyboard) {
124 if (!keyboardAnimator.animating) {
125 keyboardGuideline?.setGuidelineEnd(windowInsets.bottom)
126 } else {
127 keyboardAnimator.endingGuidelineEnd = windowInsets.bottom
128 }
129 }
130
131 if (previousKeyboardHeight != keyboardInsets.bottom) {
132 keyboardStateListeners.forEach {
133 if (previousKeyboardHeight <= 0 && keyboardInsets.bottom > 0) {
134 it.onKeyboardShown()
135 } else if (previousKeyboardHeight > 0 && keyboardInsets.bottom <= 0) {
136 it.onKeyboardHidden()
137 }
138 }
139 }
140
141 previousKeyboardHeight = keyboardInsets.bottom
142 }
143
144 protected fun overrideKeyboardGuidelineWithPreviousHeight() {
145 overridingKeyboard = true
146 keyboardGuideline?.setGuidelineEnd(getKeyboardHeight())
147 }
148
149 protected fun clearKeyboardGuidelineOverride() {
150 overridingKeyboard = false
151 }
152
153 protected fun resetKeyboardGuideline() {
154 clearKeyboardGuidelineOverride()
155 keyboardGuideline?.setGuidelineEnd(navigationBarGuideline.guidelineEnd)
156 }
157
158 private fun getKeyboardHeight(): Int {
159 val height = if (isLandscape()) {
160 SignalStore.misc().keyboardLandscapeHeight
161 } else {
162 SignalStore.misc().keyboardPortraitHeight
163 }
164
165 val minHeight = resources.getDimensionPixelSize(R.dimen.default_custom_keyboard_size)
166 return if (height > minHeight) {
167 height
168 } else {
169 Log.w(TAG, "Saved keyboard height ($height) is too low, using default size ($minHeight)")
170 minHeight
171 }
172 }
173
174 private fun setKeyboardHeight(height: Int) {
175 if (isLandscape()) {
176 SignalStore.misc().keyboardLandscapeHeight = height
177 } else {
178 SignalStore.misc().keyboardPortraitHeight = height
179 }
180 }
181
182 private fun isLandscape(): Boolean {
183 val rotation = getDeviceRotation()
184 return rotation == Surface.ROTATION_90
185 }
186
187 @Suppress("DEPRECATION")
188 private fun getDeviceRotation(): Int {
189 if (isInEditMode) {
190 return Surface.ROTATION_0
191 }
192
193 if (Build.VERSION.SDK_INT >= 30) {
194 context.display?.getRealMetrics(displayMetrics)
195 } else {
196 ServiceUtil.getWindowManager(context).defaultDisplay.getRealMetrics(displayMetrics)
197 }
198
199 return if (displayMetrics.widthPixels > displayMetrics.heightPixels) Surface.ROTATION_90 else Surface.ROTATION_0
200 }
201
202 private val Guideline?.guidelineEnd: Int
203 get() = if (this == null) 0 else (layoutParams as LayoutParams).guideEnd
204
205 interface KeyboardStateListener {
206 fun onKeyboardShown()
207 fun onKeyboardHidden()
208 }
209
210 interface WindowInsetsListener {
211 fun onApplyWindowInsets(statusBar: Int, navigationBar: Int, parentStart: Int, parentEnd: Int)
212 }
213
214 /**
215 * Adjusts the [keyboardGuideline] to move with the IME keyboard opening or closing.
216 */
217 private inner class KeyboardInsetAnimator : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
218
219 var animating = false
220 private set
221
222 private var startingGuidelineEnd: Int = 0
223 var endingGuidelineEnd: Int = 0
224 set(value) {
225 field = value
226 growing = value > startingGuidelineEnd
227 }
228 private var growing: Boolean = false
229
230 override fun onPrepare(animation: WindowInsetsAnimationCompat) {
231 if (overridingKeyboard) {
232 return
233 }
234
235 animating = true
236 startingGuidelineEnd = keyboardGuideline.guidelineEnd
237 }
238
239 override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {
240 if (overridingKeyboard) {
241 return insets
242 }
243
244 val imeAnimation = runningAnimations.find { it.typeMask and WindowInsetsCompat.Type.ime() != 0 }
245 if (imeAnimation == null) {
246 return insets
247 }
248
249 val estimatedKeyboardHeight: Int = if (growing) {
250 endingGuidelineEnd * imeAnimation.interpolatedFraction
251 } else {
252 startingGuidelineEnd * (1f - imeAnimation.interpolatedFraction)
253 }.toInt()
254
255 if (growing) {
256 keyboardGuideline?.setGuidelineEnd(estimatedKeyboardHeight.coerceAtLeast(startingGuidelineEnd))
257 } else {
258 keyboardGuideline?.setGuidelineEnd(estimatedKeyboardHeight.coerceAtLeast(endingGuidelineEnd))
259 }
260
261 return insets
262 }
263
264 override fun onEnd(animation: WindowInsetsAnimationCompat) {
265 if (overridingKeyboard) {
266 return
267 }
268
269 keyboardGuideline?.setGuidelineEnd(endingGuidelineEnd)
270 animating = false
271 }
272 }
273}