diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index b61de7b..f275581 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -58,8 +58,16 @@ jobs: :app:checkDebugAarMetadata :app:assembleDebug :app:assembleDebugAndroidTest :app:lintDebug :app:testDebugUnitTest - api27: - name: API 27 package smoke + devices: + name: API ${{ matrix.api-level }} legacy compatibility + strategy: + fail-fast: false + matrix: + include: + - api-level: 27 + target: default + - api-level: 36 + target: google_apis runs-on: ubuntu-24.04 timeout-minutes: 30 steps: @@ -99,13 +107,22 @@ jobs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Package instrumentation on clean API 27 + - name: Legacy instrumentation on clean emulator uses: ReactiveCircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2 with: - api-level: 27 - target: default + api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} arch: x86_64 - avd-name: foundation-api27 + avd-name: foundation-api${{ matrix.api-level }} force-avd-creation: true emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -wipe-data - script: ./gradlew --no-daemon --stacktrace :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.acetimetracker.ExampleInstrumentedTest + script: ./gradlew --no-daemon --stacktrace :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.acetimetracker.ExampleInstrumentedTest,com.example.acetimetracker.LegacyWindowInsetsTest,com.example.acetimetracker.MainActivityBackTest,com.example.acetimetracker.LegacyWindowRuntimeTest + + - name: Retain device reports + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: legacy-api${{ matrix.api-level }} + path: | + app/build/reports/androidTests/connected/ + app/build/outputs/androidTest-results/connected/ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 18dc479..d8fa3a9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,7 @@ android { defaultConfig { applicationId = "com.example.acetimetracker" minSdk = 27 - // Task 2 owns the target-36 system-bar and Back compatibility change. - targetSdk = 34 + targetSdk = 36 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/androidTest/java/com/example/acetimetracker/LegacyWindowInsetsTest.kt b/app/src/androidTest/java/com/example/acetimetracker/LegacyWindowInsetsTest.kt new file mode 100644 index 0000000..16989e0 --- /dev/null +++ b/app/src/androidTest/java/com/example/acetimetracker/LegacyWindowInsetsTest.kt @@ -0,0 +1,111 @@ +package com.example.acetimetracker + +import android.graphics.Rect +import android.os.Build +import android.widget.FrameLayout +import androidx.core.graphics.Insets +import androidx.core.view.DisplayCutoutCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SdkSuppress +import androidx.test.platform.app.InstrumentationRegistry +import com.example.acetimetracker.core.ui.installLegacyWindowInsets +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class LegacyWindowInsetsTest { + private val instrumentation = InstrumentationRegistry.getInstrumentation() + + // Removing the listener, adding to current padding, summing IME and bars, + // or consuming unrelated inset types must break these real View assertions. + @Test + @SdkSuppress(minSdkVersion = 30) + fun appliesSystemBarsAndCutoutOnce() = withContent { view -> + ViewCompat.dispatchApplyWindowInsets(view, fixture()) + assertPadding(view, 33, 29, 7, 27) + } + + @Test + @SdkSuppress(minSdkVersion = 30) + fun imeUsesMaximumBottomInsetAndResets() = withContent { view -> + ViewCompat.dispatchApplyWindowInsets(view, fixture(imeBottom = 250)) + assertEquals(261, view.paddingBottom) + ViewCompat.dispatchApplyWindowInsets(view, fixture()) + assertEquals(27, view.paddingBottom) + } + + @Test + @SdkSuppress(minSdkVersion = 30) + fun redispatchAndReinstallDoNotAccumulate() = withContent { view -> + ViewCompat.dispatchApplyWindowInsets(view, fixture()) + ViewCompat.dispatchApplyWindowInsets(view, fixture()) + installLegacyWindowInsets(view) + ViewCompat.dispatchApplyWindowInsets(view, fixture()) + assertPadding(view, 33, 29, 7, 27) + } + + @Test + fun hiddenBarsRestoreInitialPadding() = withContent { view -> + ViewCompat.dispatchApplyWindowInsets(view, if (Build.VERSION.SDK_INT >= 30) fixture(250) else bars()) + ViewCompat.dispatchApplyWindowInsets(view, WindowInsetsCompat.Builder().build()) + assertPadding(view, 3, 5, 7, 11) + } + + @Test + fun systemBarsRedispatchAndReinstallDoNotAccumulate() = withContent { view -> + ViewCompat.dispatchApplyWindowInsets(view, bars()) + assertPadding(view, 3, 29, 7, 27) + installLegacyWindowInsets(view) + ViewCompat.dispatchApplyWindowInsets(view, bars()) + ViewCompat.dispatchApplyWindowInsets(view, bars()) + assertPadding(view, 3, 29, 7, 27) + } + + @Test + @SdkSuppress(minSdkVersion = 30) + fun forwardsUnhandledInsetsToChildren() = withContent { view -> + val child = FrameLayout(view.context) + view.addView(child) + var delivered: WindowInsetsCompat? = null + ViewCompat.setOnApplyWindowInsetsListener(child) { _, insets -> + delivered = insets + insets + } + val input = WindowInsetsCompat.Builder(fixture(imeBottom = 250)) + .setInsets(WindowInsetsCompat.Type.systemGestures(), Insets.of(12, 0, 13, 16)) + .build() + ViewCompat.dispatchApplyWindowInsets(view, input) + assertEquals(Insets.NONE, delivered!!.getInsets(WindowInsetsCompat.Type.systemBars())) + assertEquals(Insets.NONE, delivered!!.getInsets(WindowInsetsCompat.Type.displayCutout())) + assertEquals(Insets.NONE, delivered!!.getInsets(WindowInsetsCompat.Type.ime())) + assertEquals(Insets.of(12, 0, 13, 16), delivered!!.getInsets(WindowInsetsCompat.Type.systemGestures())) + } + + private fun withContent(block: (FrameLayout) -> Unit) { + instrumentation.runOnMainSync { + val view = FrameLayout(instrumentation.targetContext) + view.setPadding(3, 5, 7, 11) + installLegacyWindowInsets(view) + block(view) + } + } + + private fun bars() = WindowInsetsCompat.Builder() + .setInsets(WindowInsetsCompat.Type.systemBars(), Insets.of(0, 24, 0, 16)) + .build() + + private fun fixture(imeBottom: Int = 0) = WindowInsetsCompat.Builder(bars()) + .setDisplayCutout(DisplayCutoutCompat(Rect(30, 0, 0, 0), emptyList())) + .setInsets(WindowInsetsCompat.Type.ime(), Insets.of(0, 0, 0, imeBottom)) + .build() + + private fun assertPadding(view: FrameLayout, left: Int, top: Int, right: Int, bottom: Int) { + assertEquals(left, view.paddingLeft) + assertEquals(top, view.paddingTop) + assertEquals(right, view.paddingRight) + assertEquals(bottom, view.paddingBottom) + } +} diff --git a/app/src/androidTest/java/com/example/acetimetracker/LegacyWindowRuntimeTest.kt b/app/src/androidTest/java/com/example/acetimetracker/LegacyWindowRuntimeTest.kt new file mode 100644 index 0000000..e5be823 --- /dev/null +++ b/app/src/androidTest/java/com/example/acetimetracker/LegacyWindowRuntimeTest.kt @@ -0,0 +1,172 @@ +package com.example.acetimetracker + +import android.os.SystemClock +import android.util.Log +import android.view.ViewGroup +import android.widget.EditText +import android.view.inputmethod.InputMethodManager +import androidx.core.view.GravityCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.appcompat.app.AppCompatActivity +import androidx.drawerlayout.widget.DrawerLayout +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.closeSoftKeyboard +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +/** Device coverage supplements synthetic fixtures, including pre-30 IME delivery. */ +@RunWith(AndroidJUnit4::class) +class LegacyWindowRuntimeTest { + @Test + fun focusedPasswordRemainsAboveKeyboard() = verifyFocusedField(Login::class.java, R.id.etPassword) + + @Test + fun focusedConfirmationRemainsAboveKeyboard() = verifyFocusedField(Registration::class.java, R.id.etConfirmPassword) + + private fun verifyFocusedField(activityClass: Class, fieldId: Int) { + ActivityScenario.launch(activityClass).use { scenario -> + closeSoftKeyboard() + onView(withId(fieldId)).check(matches(isDisplayed())) + var windowBottom = 0 + scenario.onActivity { + windowBottom = bottomOnScreen(it.window.decorView) + val password = it.findViewById(fieldId) + password.requestFocus() + (it.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as InputMethodManager) + .showSoftInput(password, InputMethodManager.SHOW_IMPLICIT) + } + await("focused password fully visible above keyboard") { + var ready = false + scenario.onActivity { + val password = it.findViewById(fieldId) + val insets = ViewCompat.getRootWindowInsets(it.window.decorView)!! + val ime = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom + val bottom = bottomOnScreen(password) + ready = insets.isVisible(WindowInsetsCompat.Type.ime()) && password.hasFocus() && + bottom <= windowBottom - ime + Log.i("P1Runtime", "Focused password: bottom=$bottom, keyboardTop=${windowBottom - ime}, focus=${password.hasFocus()}") + } + ready + } + } + } + + @Test + fun realKeyboardShowHideRestoresContentPadding() { + ActivityScenario.launch(Login::class.java).use { scenario -> + var initialBottom = 0 + var windowBottom = 0 + var initialUsableBottom = 0 + closeSoftKeyboard() + await("initial layout and bar insets settled") { + var ready = false + scenario.onActivity { + val decor = it.window.decorView + val content = it.findViewById(android.R.id.content) + val insets = ViewCompat.getRootWindowInsets(decor) + ready = decor.hasWindowFocus() && content.isLaidOut && insets != null && + !insets.isVisible(WindowInsetsCompat.Type.ime()) && + content.paddingBottom == insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom + if (ready) { + initialBottom = content.paddingBottom + windowBottom = bottomOnScreen(decor) + initialUsableBottom = bottomOnScreen(content.getChildAt(0)) + } + } + ready + } + onView(withId(R.id.etEmail)).perform(click()) + await("keyboard visible and content above it") { + var ready = false + scenario.onActivity { + val content = it.findViewById(android.R.id.content) + val insets = ViewCompat.getRootWindowInsets(it.window.decorView)!! + val ime = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom + val usableBottom = bottomOnScreen(content.getChildAt(0)) + ready = insets.isVisible(WindowInsetsCompat.Type.ime()) && ime > initialBottom && + content.paddingBottom == ime && usableBottom == windowBottom - ime + Log.i("P1Runtime", "IME check: bottom=$ime, contentHeight=${content.height}, usableBottom=$usableBottom, expected=${windowBottom - ime}") + } + ready + } + systemBack() + await("keyboard hidden and original bar padding restored") { + var ready = false + scenario.onActivity { + val content = it.findViewById(android.R.id.content) + val insets = ViewCompat.getRootWindowInsets(it.window.decorView)!! + ready = !insets.isVisible(WindowInsetsCompat.Type.ime()) && content.paddingBottom == initialBottom && + bottomOnScreen(content.getChildAt(0)) == initialUsableBottom + if (ready) Log.i("P1Runtime", "IME hidden: bottom=${content.paddingBottom}, contentHeight=${content.height}") + } + ready + } + assertEquals(Lifecycle.State.RESUMED, scenario.state) + } + } + + @Test + fun registrationLoginLinkAndSystemBackPreserveFormNavigation() { + ActivityScenario.launch(Registration::class.java).use { scenario -> + onView(withId(R.id.etUsername)).check(matches(isDisplayed())) + onView(withId(R.id.tvLoginLink)).perform(click()) + closeSoftKeyboard() + onView(withId(R.id.btnLogin)).check(matches(isDisplayed())) + systemBack() + await("Registration resumed after system Back transition") { + var ready = false + if (scenario.state == Lifecycle.State.RESUMED) { + scenario.onActivity { ready = it.window.decorView.hasWindowFocus() } + } + ready + } + onView(withId(R.id.etUsername)).check(matches(isDisplayed())) + } + } + + @Test + fun visibleBackReturnsFromAboutToDrawerHost() { + ActivityScenario.launch(MainActivity::class.java).use { scenario -> + scenario.onActivity { + it.findViewById(R.id.drawer_layout).openDrawer(GravityCompat.START, false) + } + onView(withId(R.id.nav_about)).perform(click()) + onView(withId(R.id.btnBack)).check(matches(isDisplayed())).perform(click()) + await("drawer host resumed") { scenario.state == Lifecycle.State.RESUMED } + scenario.onActivity { assertTrue(it.findViewById(R.id.drawer_layout).isShown) } + } + } + + private fun systemBack() { + InstrumentationRegistry.getInstrumentation().uiAutomation + .executeShellCommand("input keyevent KEYCODE_BACK").use { descriptor -> + java.io.FileInputStream(descriptor.fileDescriptor).use { it.readBytes() } + } + } + + private fun bottomOnScreen(view: android.view.View): Int { + val location = IntArray(2) + view.getLocationOnScreen(location) + return location[1] + view.height + } + + private fun await(description: String, condition: () -> Boolean) { + val deadline = SystemClock.uptimeMillis() + 10000 + while (SystemClock.uptimeMillis() < deadline) { + if (condition()) return + SystemClock.sleep(50) + } + assertTrue("Timed out waiting for $description", condition()) + } +} diff --git a/app/src/androidTest/java/com/example/acetimetracker/MainActivityBackTest.kt b/app/src/androidTest/java/com/example/acetimetracker/MainActivityBackTest.kt new file mode 100644 index 0000000..9c22e85 --- /dev/null +++ b/app/src/androidTest/java/com/example/acetimetracker/MainActivityBackTest.kt @@ -0,0 +1,56 @@ +package com.example.acetimetracker + +import android.os.SystemClock +import androidx.core.view.GravityCompat +import androidx.drawerlayout.widget.DrawerLayout +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MainActivityBackTest { + @Test + fun backClosesDrawerBeforeFinishing() = verifyBack { scenario -> + scenario.onActivity { it.onBackPressedDispatcher.onBackPressed() } + } + + @Test + fun systemBackClosesDrawerBeforeFinishing() = verifyBack { + InstrumentationRegistry.getInstrumentation().uiAutomation + .executeShellCommand("input keyevent KEYCODE_BACK").use { descriptor -> + java.io.FileInputStream(descriptor.fileDescriptor).use { it.readBytes() } + } + } + + private fun verifyBack(back: (ActivityScenario) -> Unit) { + ActivityScenario.launch(MainActivity::class.java).use { scenario -> + lateinit var activity: MainActivity + scenario.onActivity { + activity = it + it.findViewById(R.id.drawer_layout).openDrawer(GravityCompat.START, false) + } + back(scenario) + val deadline = SystemClock.uptimeMillis() + 5000 + var drawerOpen = true + while (drawerOpen && SystemClock.uptimeMillis() < deadline) { + scenario.onActivity { + drawerOpen = it.findViewById(R.id.drawer_layout).isDrawerOpen(GravityCompat.START) + assertFalse(it.isFinishing) + } + SystemClock.sleep(20) + } + assertFalse(drawerOpen) + back(scenario) + while (scenario.state != Lifecycle.State.DESTROYED && SystemClock.uptimeMillis() < deadline) { + SystemClock.sleep(20) + } + assertEquals(Lifecycle.State.DESTROYED, scenario.state) + assertFalse(activity.isChangingConfigurations) + } + } +} diff --git a/app/src/main/java/com/example/acetimetracker/About.kt b/app/src/main/java/com/example/acetimetracker/About.kt index a7896ae..bb579fb 100644 --- a/app/src/main/java/com/example/acetimetracker/About.kt +++ b/app/src/main/java/com/example/acetimetracker/About.kt @@ -2,16 +2,15 @@ package com.example.acetimetracker import android.os.Bundle import android.widget.ImageButton -import androidx.appcompat.app.AppCompatActivity -class About : AppCompatActivity() { +class About : LegacyActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_about) val btnBack = findViewById(R.id.btnBack) btnBack.setOnClickListener { - onBackPressed() // This will simulate the back button press + onBackPressedDispatcher.onBackPressed() } } } \ No newline at end of file diff --git a/app/src/main/java/com/example/acetimetracker/Categories.kt b/app/src/main/java/com/example/acetimetracker/Categories.kt index 9f920ae..7dd1439 100644 --- a/app/src/main/java/com/example/acetimetracker/Categories.kt +++ b/app/src/main/java/com/example/acetimetracker/Categories.kt @@ -6,13 +6,12 @@ import android.widget.Button import android.widget.EditText import android.widget.ImageButton import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore -class Categories : AppCompatActivity() { +class Categories : LegacyActivity() { private lateinit var recyclerView: RecyclerView private lateinit var categoryAdapter: CategoryAdapter private lateinit var firestore: FirebaseFirestore @@ -45,7 +44,7 @@ class Categories : AppCompatActivity() { // Back button listener btnBack.setOnClickListener { - onBackPressed() + onBackPressedDispatcher.onBackPressed() } // Add Category button listener diff --git a/app/src/main/java/com/example/acetimetracker/EditProfileActivity.kt b/app/src/main/java/com/example/acetimetracker/EditProfileActivity.kt index 42831dc..78d82f2 100644 --- a/app/src/main/java/com/example/acetimetracker/EditProfileActivity.kt +++ b/app/src/main/java/com/example/acetimetracker/EditProfileActivity.kt @@ -12,7 +12,6 @@ import android.widget.EditText import android.widget.ImageView import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieDrawable import com.bumptech.glide.Glide @@ -23,7 +22,7 @@ import com.google.firebase.firestore.SetOptions import com.google.firebase.storage.FirebaseStorage import java.util.UUID -class EditProfileActivity : AppCompatActivity() { +class EditProfileActivity : LegacyActivity() { private lateinit var auth: FirebaseAuth private lateinit var firestore: FirebaseFirestore diff --git a/app/src/main/java/com/example/acetimetracker/GraphActivity.kt b/app/src/main/java/com/example/acetimetracker/GraphActivity.kt index faad6b7..7e2c8af 100644 --- a/app/src/main/java/com/example/acetimetracker/GraphActivity.kt +++ b/app/src/main/java/com/example/acetimetracker/GraphActivity.kt @@ -3,7 +3,6 @@ package com.example.acetimetracker import android.graphics.Color import android.os.Bundle import android.util.Log -import androidx.appcompat.app.AppCompatActivity import com.github.mikephil.charting.charts.PieChart import com.github.mikephil.charting.data.PieData import com.github.mikephil.charting.data.PieDataSet @@ -13,7 +12,7 @@ import com.github.mikephil.charting.utils.ColorTemplate import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore -class GraphActivity : AppCompatActivity() { +class GraphActivity : LegacyActivity() { private lateinit var chart: PieChart private lateinit var firestore: FirebaseFirestore diff --git a/app/src/main/java/com/example/acetimetracker/LegacyActivity.kt b/app/src/main/java/com/example/acetimetracker/LegacyActivity.kt new file mode 100644 index 0000000..c5d8f6b --- /dev/null +++ b/app/src/main/java/com/example/acetimetracker/LegacyActivity.kt @@ -0,0 +1,23 @@ +package com.example.acetimetracker + +import android.os.Bundle +import android.view.ViewGroup +import android.view.WindowManager +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import com.example.acetimetracker.core.ui.installLegacyWindowInsets + +/** Temporary inset owner for XML screens. The future Compose shell owns its own insets. */ +abstract class LegacyActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + // Required for IME inset delivery on pre-Android 11 windows. + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + } + + override fun setContentView(layoutResID: Int) { + super.setContentView(layoutResID) + installLegacyWindowInsets(findViewById(android.R.id.content)) + } +} diff --git a/app/src/main/java/com/example/acetimetracker/Login.kt b/app/src/main/java/com/example/acetimetracker/Login.kt index bf53d62..ec946f9 100644 --- a/app/src/main/java/com/example/acetimetracker/Login.kt +++ b/app/src/main/java/com/example/acetimetracker/Login.kt @@ -9,12 +9,11 @@ import android.widget.Button import android.widget.EditText import android.widget.Toast import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.auth import com.google.firebase.Firebase -class Login : AppCompatActivity() { +class Login : LegacyActivity() { private lateinit var auth: FirebaseAuth private lateinit var etEmail: EditText diff --git a/app/src/main/java/com/example/acetimetracker/MainActivity.kt b/app/src/main/java/com/example/acetimetracker/MainActivity.kt index 1ab0935..d37bd6d 100644 --- a/app/src/main/java/com/example/acetimetracker/MainActivity.kt +++ b/app/src/main/java/com/example/acetimetracker/MainActivity.kt @@ -7,8 +7,8 @@ import android.view.MenuItem import android.widget.ImageView import android.widget.TextView import android.widget.Toast +import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.ActionBarDrawerToggle -import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout @@ -21,7 +21,7 @@ import com.google.android.material.navigation.NavigationView import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore -class MainActivity : AppCompatActivity() { +class MainActivity : LegacyActivity() { private lateinit var drawerLayout: DrawerLayout private lateinit var navigationView: NavigationView @@ -40,6 +40,20 @@ class MainActivity : AppCompatActivity() { drawerLayout = findViewById(R.id.drawer_layout) navigationView = findViewById(R.id.nav_view) + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (drawerLayout.isDrawerOpen(GravityCompat.START)) { + drawerLayout.closeDrawer(GravityCompat.START) + } else { + isEnabled = false + try { + onBackPressedDispatcher.onBackPressed() + } finally { + isEnabled = true + } + } + } + }) auth = FirebaseAuth.getInstance() firestore = FirebaseFirestore.getInstance() @@ -172,11 +186,4 @@ class MainActivity : AppCompatActivity() { return super.onOptionsItemSelected(item) } - override fun onBackPressed() { - if (drawerLayout.isDrawerOpen(navigationView)) { - drawerLayout.closeDrawer(navigationView) - } else { - super.onBackPressed() - } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/acetimetracker/Myprogress.kt b/app/src/main/java/com/example/acetimetracker/Myprogress.kt index 60f121e..1dff359 100644 --- a/app/src/main/java/com/example/acetimetracker/Myprogress.kt +++ b/app/src/main/java/com/example/acetimetracker/Myprogress.kt @@ -10,7 +10,6 @@ import android.widget.ArrayAdapter import android.widget.Spinner import android.widget.TextView import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -23,7 +22,7 @@ import com.github.mikephil.charting.formatter.IndexAxisValueFormatter import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore -class Myprogress : AppCompatActivity() { +class Myprogress : LegacyActivity() { private lateinit var categorySpinner: Spinner private lateinit var recyclerView: RecyclerView private lateinit var timesheetAdapter: TimesheetAdapter diff --git a/app/src/main/java/com/example/acetimetracker/Registration.kt b/app/src/main/java/com/example/acetimetracker/Registration.kt index 486e5a8..2949910 100644 --- a/app/src/main/java/com/example/acetimetracker/Registration.kt +++ b/app/src/main/java/com/example/acetimetracker/Registration.kt @@ -1,6 +1,5 @@ package com.example.acetimetracker -import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.widget.Button import android.widget.EditText @@ -14,7 +13,7 @@ import android.text.style.UnderlineSpan import android.widget.TextView import com.google.firebase.auth.FirebaseAuth -class Registration : AppCompatActivity() { +class Registration : LegacyActivity() { private lateinit var auth: FirebaseAuth // Declare Firebase Authentication instance diff --git a/app/src/main/java/com/example/acetimetracker/Splash.kt b/app/src/main/java/com/example/acetimetracker/Splash.kt index a27d491..11fed51 100644 --- a/app/src/main/java/com/example/acetimetracker/Splash.kt +++ b/app/src/main/java/com/example/acetimetracker/Splash.kt @@ -5,9 +5,8 @@ import android.content.Intent import android.os.Bundle import android.os.Handler import android.os.Looper -import androidx.appcompat.app.AppCompatActivity -class Splash : AppCompatActivity() { +class Splash : LegacyActivity() { private val SPLASH_DELAY: Long = 4000 // 4 seconds private lateinit var handler: Handler diff --git a/app/src/main/java/com/example/acetimetracker/Timer.kt b/app/src/main/java/com/example/acetimetracker/Timer.kt index 0df805f..b75acc8 100644 --- a/app/src/main/java/com/example/acetimetracker/Timer.kt +++ b/app/src/main/java/com/example/acetimetracker/Timer.kt @@ -14,12 +14,11 @@ import android.widget.ImageButton import android.widget.Spinner import android.widget.TextView import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore -class Timer : AppCompatActivity() { +class Timer : LegacyActivity() { private lateinit var timerTextView: TextView private lateinit var startButton: Button @@ -121,7 +120,7 @@ class Timer : AppCompatActivity() { feedbackTextView = findViewById(R.id.feedbackTextView) btnBack.setOnClickListener { - onBackPressed() + onBackPressedDispatcher.onBackPressed() } fetchCategories() diff --git a/app/src/main/java/com/example/acetimetracker/Timesheet.kt b/app/src/main/java/com/example/acetimetracker/Timesheet.kt index 37eab2e..0376908 100644 --- a/app/src/main/java/com/example/acetimetracker/Timesheet.kt +++ b/app/src/main/java/com/example/acetimetracker/Timesheet.kt @@ -15,7 +15,6 @@ import android.widget.ImageView import android.widget.Spinner import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity import com.bumptech.glide.Glide import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FieldValue @@ -26,7 +25,7 @@ import java.util.Calendar import java.util.Locale import java.util.UUID -class Timesheet : AppCompatActivity() { +class Timesheet : LegacyActivity() { private lateinit var firestore: FirebaseFirestore private lateinit var auth: FirebaseAuth diff --git a/app/src/main/java/com/example/acetimetracker/core/ui/LegacyWindowInsets.kt b/app/src/main/java/com/example/acetimetracker/core/ui/LegacyWindowInsets.kt new file mode 100644 index 0000000..4c7e3ff --- /dev/null +++ b/app/src/main/java/com/example/acetimetracker/core/ui/LegacyWindowInsets.kt @@ -0,0 +1,34 @@ +package com.example.acetimetracker.core.ui + +import android.view.ViewGroup +import androidx.core.graphics.Insets +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import com.example.acetimetracker.R + +private data class InitialPadding(val left: Int, val top: Int, val right: Int, val bottom: Int) + +internal fun installLegacyWindowInsets(content: ViewGroup) { + val initial = (content.getTag(R.id.legacy_initial_padding) as? InitialPadding) + ?: InitialPadding(content.paddingLeft, content.paddingTop, content.paddingRight, content.paddingBottom) + .also { content.setTag(R.id.legacy_initial_padding, it) } + val barsAndCutout = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + val handled = barsAndCutout or WindowInsetsCompat.Type.ime() + ViewCompat.setOnApplyWindowInsetsListener(content) { view, insets -> + val bars = insets.getInsets(barsAndCutout) + val ime = insets.getInsets(WindowInsetsCompat.Type.ime()) + view.setPadding( + initial.left + bars.left, + initial.top + bars.top, + initial.right + bars.right, + initial.bottom + maxOf(bars.bottom, ime.bottom) + ) + // Children retain gesture/tappable-element insets, but must not apply these twice. + WindowInsetsCompat.Builder(insets) + .setInsets(handled, Insets.NONE) + .setInsetsIgnoringVisibility(barsAndCutout, Insets.NONE) + .setDisplayCutout(null) + .build() + } + ViewCompat.requestApplyInsets(content) +} diff --git a/app/src/main/java/com/example/acetimetracker/profile.kt b/app/src/main/java/com/example/acetimetracker/profile.kt index 73edbfa..69ca3e9 100644 --- a/app/src/main/java/com/example/acetimetracker/profile.kt +++ b/app/src/main/java/com/example/acetimetracker/profile.kt @@ -7,13 +7,12 @@ import android.widget.Button import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView -import androidx.appcompat.app.AppCompatActivity import com.bumptech.glide.Glide import com.bumptech.glide.load.resource.bitmap.CircleCrop import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore -class profile : AppCompatActivity() { +class profile : LegacyActivity() { private lateinit var auth: FirebaseAuth private lateinit var firestore: FirebaseFirestore @@ -27,7 +26,7 @@ class profile : AppCompatActivity() { val btnBack = findViewById(R.id.btnBack) btnBack.setOnClickListener { - onBackPressed() + onBackPressedDispatcher.onBackPressed() } val editProfileButton = findViewById