Skip to content

Commit df0089e

Browse files
committed
PassCodeManager: add some logic to avoid counting the same activity twice
This is very specific to the case where an activity extending from AuthenticatorActivity _and_ running as `singleTask` gets restarted, in which case it calls `PassCodeManager.onActivityResumed` twice (once on `onResume` and once on `onNewIntent`). Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>
1 parent cd8ed82 commit df0089e

File tree

3 files changed

+202
-15
lines changed

3 files changed

+202
-15
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Nextcloud Android client application
3+
*
4+
* @author Álvaro Brey
5+
* Copyright (C) 2023 Álvaro Brey
6+
* Copyright (C) 2023 Nextcloud GmbH
7+
*
8+
* This program is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
10+
* License as published by the Free Software Foundation; either
11+
* version 3 of the License, or any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public
19+
* License along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
23+
package com.owncloud.android.authentication
24+
25+
import android.app.Activity
26+
import android.os.PowerManager
27+
import com.nextcloud.client.core.Clock
28+
import com.nextcloud.client.preferences.AppPreferences
29+
import com.owncloud.android.ui.activity.SettingsActivity
30+
import io.mockk.MockKAnnotations
31+
import io.mockk.every
32+
import io.mockk.impl.annotations.MockK
33+
import io.mockk.just
34+
import io.mockk.mockk
35+
import io.mockk.runs
36+
import org.junit.Assert.assertTrue
37+
import org.junit.Before
38+
import org.junit.Test
39+
40+
/**
41+
* This class should really be unit tests, but PassCodeManager needs a refactor
42+
* to decouple the locking logic from the platform classes
43+
*/
44+
class PassCodeManagerIT {
45+
@MockK
46+
lateinit var appPreferences: AppPreferences
47+
48+
@MockK
49+
lateinit var clockImpl: Clock
50+
51+
lateinit var sut: PassCodeManager
52+
53+
@Before
54+
fun before() {
55+
MockKAnnotations.init(this, relaxed = true)
56+
sut = PassCodeManager(appPreferences, clockImpl)
57+
}
58+
59+
@Test
60+
fun testResumeDuplicateActivity() {
61+
// mock activity instead of using real one to avoid dealing with activity transitions
62+
val activity: Activity = mockk()
63+
val powerManager: PowerManager = mockk()
64+
every { powerManager.isScreenOn } returns true
65+
every { activity.getSystemService(Activity.POWER_SERVICE) } returns powerManager
66+
every { activity.window } returns null
67+
every { activity.startActivityForResult(any(), any()) } just runs
68+
every { activity.moveTaskToBack(any()) } returns true
69+
70+
// set locked state
71+
every { appPreferences.lockPreference } returns SettingsActivity.LOCK_PASSCODE
72+
every { appPreferences.lockTimestamp } returns 200
73+
every { clockImpl.millisSinceBoot } returns 10000
74+
75+
// resume activity twice
76+
var askedForPin = sut.onActivityResumed(activity)
77+
assertTrue("Passcode not requested on first launch", askedForPin)
78+
sut.onActivityResumed(activity)
79+
80+
// stop it once
81+
sut.onActivityStopped(activity)
82+
83+
// resume again. should ask for passcode
84+
askedForPin = sut.onActivityResumed(activity)
85+
assertTrue("Passcode not requested on subsequent launch after stop", askedForPin)
86+
}
87+
}

app/src/main/java/com/owncloud/android/authentication/PassCodeManager.kt

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import android.content.Intent
2525
import android.os.PowerManager
2626
import android.view.View
2727
import android.view.WindowManager
28+
import androidx.annotation.VisibleForTesting
2829
import com.nextcloud.client.core.Clock
2930
import com.nextcloud.client.preferences.AppPreferences
3031
import com.owncloud.android.MainApp
@@ -48,9 +49,12 @@ class PassCodeManager(private val preferences: AppPreferences, private val clock
4849
* the pass code being requested on screen rotations.
4950
*/
5051
private const val PASS_CODE_TIMEOUT = 5000
52+
53+
private const val TAG = "PassCodeManager"
5154
}
5255

5356
private var visibleActivitiesCounter = 0
57+
private var lastResumedActivity: Activity? = null
5458

5559
private fun isExemptActivity(activity: Activity): Boolean {
5660
return exemptOfPasscodeActivities.contains(activity.javaClass)
@@ -64,20 +68,17 @@ class PassCodeManager(private val preferences: AppPreferences, private val clock
6468
if (!isExemptActivity(activity)) {
6569
val passcodeRequested = passCodeShouldBeRequested(timestamp)
6670
val credentialsRequested = deviceCredentialsShouldBeRequested(timestamp, activity)
67-
if (passcodeRequested || credentialsRequested) {
68-
getActivityRootView(activity)?.visibility = View.GONE
69-
} else {
70-
getActivityRootView(activity)?.visibility = View.VISIBLE
71-
}
71+
val shouldHideView = passcodeRequested || credentialsRequested
72+
toggleActivityVisibility(shouldHideView, activity)
73+
askedForPin = shouldHideView
74+
7275
if (passcodeRequested) {
73-
askedForPin = true
74-
preferences.lockTimestamp = 0
7576
requestPasscode(activity)
77+
} else if (credentialsRequested) {
78+
requestCredentials(activity)
7679
}
77-
if (credentialsRequested) {
78-
askedForPin = true
80+
if (askedForPin) {
7981
preferences.lockTimestamp = 0
80-
requestCredentials(activity)
8182
}
8283
}
8384

@@ -86,12 +87,39 @@ class PassCodeManager(private val preferences: AppPreferences, private val clock
8687
}
8788

8889
if (!isExemptActivity(activity)) {
89-
visibleActivitiesCounter++ // keep it AFTER passCodeShouldBeRequested was checked
90+
addVisibleActivity(activity) // keep it AFTER passCodeShouldBeRequested was checked
9091
}
9192

9293
return askedForPin
9394
}
9495

96+
/**
97+
* Used to hide root view while transitioning to passcode activity
98+
*/
99+
private fun toggleActivityVisibility(
100+
hide: Boolean,
101+
activity: Activity
102+
) {
103+
if (hide) {
104+
getActivityRootView(activity)?.visibility = View.GONE
105+
} else {
106+
getActivityRootView(activity)?.visibility = View.VISIBLE
107+
}
108+
}
109+
110+
private fun addVisibleActivity(activity: Activity) {
111+
// don't count the same activity twice
112+
if (lastResumedActivity != activity) {
113+
visibleActivitiesCounter++
114+
lastResumedActivity = activity
115+
}
116+
}
117+
118+
private fun removeVisibleActivity() {
119+
visibleActivitiesCounter--
120+
lastResumedActivity = null
121+
}
122+
95123
private fun setSecureFlag(activity: Activity) {
96124
val window = activity.window
97125
if (window != null) {
@@ -118,7 +146,7 @@ class PassCodeManager(private val preferences: AppPreferences, private val clock
118146

119147
fun onActivityStopped(activity: Activity) {
120148
if (visibleActivitiesCounter > 0 && !isExemptActivity(activity)) {
121-
visibleActivitiesCounter--
149+
removeVisibleActivity()
122150
}
123151
val powerMgr = activity.getSystemService(Context.POWER_SERVICE) as PowerManager
124152
if ((isPassCodeEnabled() || deviceCredentialsAreEnabled(activity)) && !powerMgr.isScreenOn) {
@@ -137,7 +165,8 @@ class PassCodeManager(private val preferences: AppPreferences, private val clock
137165
abs(clock.millisSinceBoot - timestamp) > PASS_CODE_TIMEOUT &&
138166
visibleActivitiesCounter <= 0
139167

140-
private fun passCodeShouldBeRequested(timestamp: Long): Boolean {
168+
@VisibleForTesting
169+
fun passCodeShouldBeRequested(timestamp: Long): Boolean {
141170
return shouldBeLocked(timestamp) && isPassCodeEnabled()
142171
}
143172

@@ -153,7 +182,7 @@ class PassCodeManager(private val preferences: AppPreferences, private val clock
153182
}
154183

155184
private fun getActivityRootView(activity: Activity): View? {
156-
return activity.window.findViewById(android.R.id.content)
157-
?: activity.window.decorView.findViewById(android.R.id.content)
185+
return activity.window?.findViewById(android.R.id.content)
186+
?: activity.window?.decorView?.findViewById(android.R.id.content)
158187
}
159188
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Nextcloud Android client application
3+
*
4+
* @author Álvaro Brey
5+
* Copyright (C) 2023 Álvaro Brey
6+
* Copyright (C) 2023 Nextcloud GmbH
7+
*
8+
* This program is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
10+
* License as published by the Free Software Foundation; either
11+
* version 3 of the License, or any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public
19+
* License along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
23+
package com.owncloud.android.authentication
24+
25+
import com.nextcloud.client.core.Clock
26+
import com.nextcloud.client.preferences.AppPreferences
27+
import com.owncloud.android.ui.activity.SettingsActivity
28+
import io.mockk.MockKAnnotations
29+
import io.mockk.every
30+
import io.mockk.impl.annotations.MockK
31+
import org.junit.Assert.assertFalse
32+
import org.junit.Assert.assertTrue
33+
import org.junit.Before
34+
import org.junit.Test
35+
36+
class PassCodeManagerTest {
37+
@MockK
38+
lateinit var appPreferences: AppPreferences
39+
40+
@MockK
41+
lateinit var clockImpl: Clock
42+
43+
lateinit var sut: PassCodeManager
44+
45+
@Before
46+
fun before() {
47+
MockKAnnotations.init(this, relaxed = true)
48+
sut = PassCodeManager(appPreferences, clockImpl)
49+
}
50+
51+
@Test
52+
fun testLocked() {
53+
every { appPreferences.lockPreference } returns SettingsActivity.LOCK_PASSCODE
54+
every { clockImpl.millisSinceBoot } returns 10000
55+
assertTrue("Passcode not requested", sut.passCodeShouldBeRequested(200))
56+
}
57+
58+
@Test
59+
fun testPasscodeNotRequested_notEnabled() {
60+
every { appPreferences.lockPreference } returns ""
61+
every { clockImpl.millisSinceBoot } returns 10000
62+
assertFalse("Passcode requested but it shouldn't have been", sut.passCodeShouldBeRequested(200))
63+
}
64+
65+
@Test
66+
fun testPasscodeNotRequested_unlockedRecently() {
67+
every { appPreferences.lockPreference } returns SettingsActivity.LOCK_PASSCODE
68+
every { clockImpl.millisSinceBoot } returns 210
69+
assertFalse("Passcode requested but it shouldn't have been", sut.passCodeShouldBeRequested(200))
70+
}
71+
}

0 commit comments

Comments
 (0)