Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Full support for Android 15, edge-to-edge mode on Android 15 #4897

Merged
merged 13 commits into from
Feb 6, 2025
Merged
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ final def CUSTOM_INSTANCE = ""
final def SUPPORT_ACCOUNT_URL = "https://mastodon.social/@Tusky"

android {
compileSdk 34
compileSdk 35
namespace "com.keylesspalace.tusky"
defaultConfig {
applicationId APP_ID
namespace "com.keylesspalace.tusky"
minSdk 24
targetSdk 34
targetSdk 35
versionCode 129
versionName "27.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand Down
5 changes: 2 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,7 @@
<activity
android:name=".components.compose.ComposeActivity"
android:theme="@style/TuskyDialogActivityTheme"
android:alwaysRetainTaskState="true"
android:windowSoftInputMode="stateVisible|adjustResize" />
android:alwaysRetainTaskState="true" />
<activity
android:name=".components.viewthread.ViewThreadActivity"
android:configChanges="orientation|screenSize" />
Expand All @@ -117,7 +116,7 @@
android:theme="@style/TuskyBaseTheme"
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
<activity android:name=".components.account.AccountActivity" />
<activity android:name=".EditProfileActivity" />
<activity android:name=".EditProfileActivity"/>
<activity android:name=".components.preference.PreferencesActivity" />
<activity android:name=".StatusListActivity" />
<activity android:name=".components.accountlist.AccountListActivity" />
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import android.text.style.URLSpan
import android.text.util.Linkify
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
import com.keylesspalace.tusky.databinding.ActivityAboutBinding
Expand Down Expand Up @@ -41,6 +46,15 @@ class AboutActivity : BottomSheetActivity() {

setTitle(R.string.about_title_activity)

ViewCompat.setOnApplyWindowInsetsListener(binding.scrollView) { scrollView, insets ->
val systemBarInsets = insets.getInsets(systemBars())
scrollView.updatePadding(bottom = systemBarInsets.bottom)

WindowInsetsCompat.Builder(insets)
.setInsets(systemBars(), Insets.of(systemBarInsets.left, systemBarInsets.top, systemBarInsets.right, 0))
.build()
}

binding.versionTextView.text = getString(R.string.about_app_version, getString(R.string.app_name), BuildConfig.VERSION_NAME)

binding.deviceInfo.text = getString(
Expand Down
53 changes: 49 additions & 4 deletions app/src/main/java/com/keylesspalace/tusky/BaseActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,22 @@ import android.content.Intent
import android.content.SharedPreferences
import android.graphics.BitmapFactory
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.displayCutout
import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.core.view.updatePadding
import androidx.lifecycle.ViewModelProvider.Factory
import androidx.lifecycle.lifecycleScope
import com.google.android.material.R as materialR
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.keylesspalace.tusky.MainActivity.Companion.redirectIntent
Expand Down Expand Up @@ -88,16 +98,28 @@ abstract class BaseActivity : AppCompatActivity() {
setTheme(R.style.TuskyTheme)
}

/* set the taskdescription programmatically, the theme would turn it blue */
/* Set the taskdescription programmatically - by default the primary color is used.
* On newer Android versions (or launchers?) this doesn't seem to have an effect. */
val appName = getString(R.string.app_name)
val appIcon = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
val recentsBackgroundColor = MaterialColors.getColor(
this,
com.google.android.material.R.attr.colorSurface,
materialR.attr.colorSurface,
Color.BLACK
)

setTaskDescription(TaskDescription(appName, appIcon, recentsBackgroundColor))
val taskDescription = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
TaskDescription.Builder()
.setLabel(appName)
.setIcon(R.mipmap.ic_launcher)
.setPrimaryColor(recentsBackgroundColor)
.build()
} else {
val appIcon = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
@Suppress("DEPRECATION")
TaskDescription(appName, appIcon, recentsBackgroundColor)
}

setTaskDescription(taskDescription)

val style = textStyle(preferences.getString(PrefKeys.STATUS_TEXT_SIZE, "medium"))
getTheme().applyStyle(style, true)
Expand All @@ -107,6 +129,29 @@ abstract class BaseActivity : AppCompatActivity() {
}
}

override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
window.decorView.setBackgroundColor(Color.BLACK)

val contentView: View = findViewById(android.R.id.content)
contentView.setBackgroundColor(MaterialColors.getColor(contentView, android.R.attr.colorBackground))

// handle left/right insets. This is relevant for edge-to-edge mode in landscape orientation
ViewCompat.setOnApplyWindowInsetsListener(contentView) { _, insets ->
val systemBarInsets = insets.getInsets(systemBars())
val displayCutoutInsets = insets.getInsets(displayCutout())
// use padding for system bar insets so they get our background color and margin for cutout insets to turn them black
contentView.updatePadding(left = systemBarInsets.left, right = systemBarInsets.right)
(contentView.layoutParams as ViewGroup.MarginLayoutParams).leftMargin = displayCutoutInsets.left
connyduck marked this conversation as resolved.
Show resolved Hide resolved
(contentView.layoutParams as ViewGroup.MarginLayoutParams).rightMargin = displayCutoutInsets.right

WindowInsetsCompat.Builder(insets)
connyduck marked this conversation as resolved.
Show resolved Hide resolved
.setInsets(systemBars(), Insets.of(0, systemBarInsets.top, 0, systemBarInsets.bottom))
.setInsets(displayCutout(), Insets.of(0, displayCutoutInsets.top, 0, displayCutoutInsets.bottom))
.build()
}
}

private fun activityTransitionWasRequested(): Boolean {
return intent.getBooleanExtra(OPEN_WITH_SLIDE_IN, false)
}
Expand Down
25 changes: 25 additions & 0 deletions app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ import android.widget.ImageView
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.ime
import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
Expand Down Expand Up @@ -119,6 +125,25 @@ class EditProfileActivity : BaseActivity() {
setDisplayShowHomeEnabled(true)
}

ViewCompat.setOnApplyWindowInsetsListener(binding.root) { scrollView, insets ->
// if keyboard visible -> set inset on the root to push the scrollview up
// if keyboard hidden -> set inset on the scrollview so last element does not get obscured by navigation bar
// scrollview has clipToPadding set to false so it draws behind the navigation bar in edge-to-edge mode
val imeInsets = insets.getInsets(ime())
val systemBarsInsets = insets.getInsets(systemBars())
binding.root.updatePadding(bottom = imeInsets.bottom)
val scrollViewPadding = if (imeInsets.bottom == 0) {
systemBarsInsets.bottom
} else {
0
}
binding.scrollView.updatePadding(bottom = scrollViewPadding)
WindowInsetsCompat.Builder(insets)
.setInsets(ime(), Insets.of(imeInsets.left, imeInsets.top, imeInsets.right, 0))
.setInsets(systemBars(), Insets.of(systemBarsInsets.left, systemBarsInsets.top, imeInsets.right, 0))
.build()
}

binding.avatarButton.setOnClickListener { pickMedia(PickType.AVATAR) }
binding.headerButton.setOnClickListener { pickMedia(PickType.HEADER) }

Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import android.os.Bundle
import android.util.Log
import android.widget.TextView
import androidx.annotation.RawRes
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
import dagger.hilt.android.AndroidEntryPoint
Expand All @@ -45,6 +50,15 @@ class LicenseActivity : BaseActivity() {

setTitle(R.string.title_licenses)

ViewCompat.setOnApplyWindowInsetsListener(binding.scrollView) { scrollView, insets ->
val systemBarInsets = insets.getInsets(systemBars())
scrollView.updatePadding(bottom = systemBarInsets.bottom)

WindowInsetsCompat.Builder(insets)
.setInsets(systemBars(), Insets.of(systemBarInsets.left, systemBarInsets.top, systemBarInsets.right, 0))
.build()
}

loadFileIntoTextView(R.raw.apache, binding.licenseApacheTextView)
}

Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import com.keylesspalace.tusky.databinding.DialogListBinding
import com.keylesspalace.tusky.databinding.ItemListBinding
import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.ensureBottomMargin
import com.keylesspalace.tusky.util.ensureBottomPadding
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
Expand Down Expand Up @@ -78,6 +80,9 @@ class ListsActivity : BaseActivity() {
setDisplayShowHomeEnabled(true)
}

binding.addListButton.ensureBottomMargin()
binding.listsRecycler.ensureBottomPadding(fab = true)

binding.listsRecycler.adapter = adapter
binding.listsRecycler.layoutManager = LinearLayoutManager(this)
binding.listsRecycler.addItemDecoration(
Expand Down
53 changes: 45 additions & 8 deletions app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.MenuItem.SHOW_AS_ACTION_NEVER
import android.view.View
import android.view.ViewGroup.LayoutParams
import android.widget.ImageView
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
Expand All @@ -47,10 +48,15 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.Insets
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.MenuProvider
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.MarginPageTransformer
import com.bumptech.glide.Glide
Expand Down Expand Up @@ -91,7 +97,6 @@ import com.keylesspalace.tusky.usecase.DeveloperToolsUseCase
import com.keylesspalace.tusky.usecase.LogoutUsecase
import com.keylesspalace.tusky.util.ActivityConstants
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.getDimension
import com.keylesspalace.tusky.util.getParcelableExtraCompat
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.overrideActivityTransitionCompat
Expand Down Expand Up @@ -236,9 +241,43 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
}
}

window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own
setContentView(binding.root)

val bottomBarHeight = if (preferences.getString(PrefKeys.MAIN_NAV_POSITION, "top") == "bottom") {
resources.getDimensionPixelSize(R.dimen.bottomAppBarHeight)
} else {
0
}

val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
ViewCompat.setOnApplyWindowInsetsListener(binding.viewPager) { _, insets ->
val systemBarsInsets = insets.getInsets(systemBars())
val bottomInsets = systemBarsInsets.bottom

(binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = bottomBarHeight + fabMargin + bottomInsets
binding.mainDrawer.recyclerView.updatePadding(bottom = bottomInsets)

if (preferences.getString(PrefKeys.MAIN_NAV_POSITION, "top") == "top") {
insets
} else {
binding.viewPager.updatePadding(bottom = bottomBarHeight + bottomInsets)
WindowInsetsCompat.Builder(insets)
.setInsets(systemBars(), Insets.of(systemBarsInsets.left, systemBarsInsets.top, systemBarsInsets.right, 0))
.build()
}
}
} else {
// don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own
// on Vanilla Ice Cream (API 35) and up there is no status bar color because of edge-to-edge mode
@Suppress("DEPRECATION")
window.statusBarColor = Color.TRANSPARENT

(binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = bottomBarHeight + fabMargin
binding.viewPager.updatePadding(bottom = bottomBarHeight)
}

binding.composeButton.setOnClickListener {
val composeIntent = Intent(applicationContext, ComposeActivity::class.java)
startActivity(composeIntent)
Expand All @@ -252,11 +291,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
"top" -> setSupportActionBar(binding.topNav)
"bottom" -> setSupportActionBar(binding.bottomNav)
}
binding.mainToolbar.hide()
// this is a bit hacky, but when the mainToolbar is GONE, the toolbar size gets messed up for some reason
binding.mainToolbar.layoutParams.height = 0
binding.mainToolbar.visibility = View.INVISIBLE
// There's not enough space in the top/bottom bars to show the title as well.
supportActionBar?.setDisplayShowTitleEnabled(false)
} else {
setSupportActionBar(binding.mainToolbar)
binding.mainToolbar.layoutParams.height = LayoutParams.WRAP_CONTENT
binding.mainToolbar.show()
}

Expand Down Expand Up @@ -793,15 +835,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {

private fun setupTabs(tabs: List<TabData>) {
val activeTabLayout = if (preferences.getString(PrefKeys.MAIN_NAV_POSITION, "top") == "bottom") {
val actionBarSize = getDimension(this, androidx.appcompat.R.attr.actionBarSize)
val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin)
(binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin
binding.topNav.hide()
binding.bottomTabLayout
} else {
binding.bottomNav.hide()
(binding.viewPager.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = 0
(binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).anchorId = R.id.viewPager
binding.tabLayout
}

Expand Down
23 changes: 17 additions & 6 deletions app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ package com.keylesspalace.tusky
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
Expand All @@ -34,7 +39,7 @@ import com.keylesspalace.tusky.components.account.list.ListSelectionFragment
import com.keylesspalace.tusky.databinding.ActivityTabPreferenceBinding
import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.hashtagPattern
import com.keylesspalace.tusky.util.ensureBottomPadding
import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
Expand Down Expand Up @@ -82,6 +87,17 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener, ListSelec
setDisplayShowHomeEnabled(true)
}

binding.currentTabsRecyclerView.ensureBottomPadding(fab = true)
ViewCompat.setOnApplyWindowInsetsListener(binding.actionButton) { _, insets ->
val systemBarInsets = insets.getInsets(systemBars())
val actionButtonMargin = resources.getDimensionPixelSize(R.dimen.fabMargin)
(binding.actionButton.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = systemBarInsets.bottom + actionButtonMargin
(binding.sheet.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = systemBarInsets.bottom + actionButtonMargin
WindowInsetsCompat.Builder(insets)
.setInsets(systemBars(), Insets.of(systemBarInsets.left, systemBarInsets.top, systemBarInsets.right, 0))
.build()
}

currentTabs = accountManager.activeAccount?.tabPreferences.orEmpty().toMutableList()
currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT)
binding.currentTabsRecyclerView.adapter = currentTabsAdapter
Expand Down Expand Up @@ -253,11 +269,6 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener, ListSelec
saveTabs()
}

private fun validateHashtag(input: CharSequence?): Boolean {
val trimmedInput = input?.trim() ?: ""
return trimmedInput.isNotEmpty() && hashtagPattern.matcher(trimmedInput).matches()
}

private fun updateAvailableTabs() {
val addableTabs: MutableList<TabData> = mutableListOf()

Expand Down
Loading
Loading