@@ -29,10 +29,10 @@ import android.webkit.WebResourceResponse
2929import android.webkit.WebView
3030import android.webkit.WebViewClient
3131import androidx.activity.OnBackPressedCallback
32+ import androidx.annotation.VisibleForTesting
3233import androidx.core.os.bundleOf
3334import androidx.fragment.app.commit
3435import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_INDEFINITE
35- import com.ichi2.anki.common.annotations.NeedsTest
3636import com.ichi2.anki.databinding.ActivitySharedDecksBinding
3737import com.ichi2.anki.snackbar.showSnackbar
3838import com.ichi2.anki.workarounds.SafeWebViewLayout
@@ -68,143 +68,148 @@ class SharedDecksActivity : AnkiActivity(R.layout.activity_shared_decks) {
6868 * History should not be cleared before the page finishes loading otherwise there would be
6969 * an extra entry in the history since the previous page would not get cleared.
7070 */
71- private val webViewClient =
72- object : WebViewClient () {
73- private var redirectTimes = 0
74-
75- override fun doUpdateVisitedHistory (
76- view : WebView ? ,
77- url : String? ,
78- isReload : Boolean ,
79- ) {
80- super .doUpdateVisitedHistory(view, url, isReload)
81- onBackPressedCallback.isEnabled = binding.webView.canGoBack()
82- }
71+ @VisibleForTesting(otherwise = VisibleForTesting . PRIVATE )
72+ internal inner class SharedDeckWebViewClient : WebViewClient () {
73+ private var redirectTimes = 0
74+
75+ override fun doUpdateVisitedHistory (
76+ view : WebView ? ,
77+ url : String? ,
78+ isReload : Boolean ,
79+ ) {
80+ super .doUpdateVisitedHistory(view, url, isReload)
81+ onBackPressedCallback.isEnabled = binding.webView.canGoBack()
82+ }
8383
84- override fun onPageFinished (
85- view : WebView ? ,
86- url : String? ,
87- ) {
88- // Clear history if mShouldHistoryBeCleared is true and set it to false
89- if (shouldHistoryBeCleared) {
90- binding.webView.clearHistory()
91- shouldHistoryBeCleared = false
92- }
93- super .onPageFinished(view, url)
84+ override fun onPageFinished (
85+ view : WebView ? ,
86+ url : String? ,
87+ ) {
88+ // Clear history if mShouldHistoryBeCleared is true and set it to false
89+ if (shouldHistoryBeCleared) {
90+ binding.webView.clearHistory()
91+ shouldHistoryBeCleared = false
9492 }
93+ super .onPageFinished(view, url)
94+ }
9595
96- /* *
97- * Prevent the WebView from loading urls which arent needed for importing shared decks.
98- * This is to prevent potential misuse, such as bypassing content restrictions or
99- * using the AnkiDroid WebView as a regular browser to bypass browser blocks,
100- * which could lead to procrastination.
101- */
102- override fun shouldOverrideUrlLoading (
103- view : WebView ? ,
104- request : WebResourceRequest ? ,
105- ): Boolean {
106- val host = request?.url?.host
107- if (host != null ) {
108- if (allowedHosts.any { regex -> regex.matches(host) }) {
109- return super .shouldOverrideUrlLoading(view, request)
110- }
96+ /* *
97+ * Prevent the WebView from loading urls which arent needed for importing shared decks.
98+ * This is to prevent potential misuse, such as bypassing content restrictions or
99+ * using the AnkiDroid WebView as a regular browser to bypass browser blocks,
100+ * which could lead to procrastination.
101+ */
102+ override fun shouldOverrideUrlLoading (
103+ view : WebView ? ,
104+ request : WebResourceRequest ? ,
105+ ): Boolean {
106+ val host = request?.url?.host
107+ if (host != null ) {
108+ if (allowedHosts.any { regex -> regex.matches(host) }) {
109+ return super .shouldOverrideUrlLoading(view, request)
111110 }
111+ }
112112
113- request?.url?.let { super @SharedDecksActivity.openUrl(it) }
113+ request?.url?.let { super @SharedDecksActivity.openUrl(it) }
114114
115- return true
116- }
115+ return true
116+ }
117117
118- private val cookieManager: CookieManager by lazy {
119- CookieManager .getInstance()
120- }
118+ private val cookieManager: CookieManager by lazy {
119+ CookieManager .getInstance()
120+ }
121121
122- private val isLoggedInToAnkiWeb : Boolean
123- get() {
124- try {
125- // cookies are null after the user logs out, or if the site is first visited
126- val cookies = cookieManager.getCookie( " https://ankiweb.net " ) ? : return false
127- // ankiweb currently (2024-09-25) sets two cookies:
128- // * ` ankiweb`, which is base64-encoded JSON
129- // * `has_auth `, which is 1
130- return cookies.contains( " has_auth=1 " )
131- } catch (e : Exception ) {
132- Timber .w(e, " Could not determine login status " )
133- return false
134- }
122+ @VisibleForTesting(otherwise = VisibleForTesting . PRIVATE )
123+ internal val isLoggedInToAnkiWeb : Boolean
124+ get() {
125+ try {
126+ // cookies are null after the user logs out, or if the site is first visited
127+ val cookies = cookieManager.getCookie( " https:// ankiweb.net " ) ? : return false
128+ // ankiweb currently (2024-09-25) sets two cookies:
129+ // * `ankiweb `, which is base64-encoded JSON
130+ // * `has_auth`, which is 1
131+ return cookies.contains( " has_auth=1 " )
132+ } catch (e : Exception ) {
133+ Timber .w(e, " Could not determine login status " )
134+ return false
135135 }
136+ }
136137
137- @NeedsTest(" A user is not redirected to login/signup if they are logged in to AnkiWeb" )
138- override fun onReceivedHttpError (
139- view : WebView ? ,
140- request : WebResourceRequest ? ,
141- errorResponse : WebResourceResponse ? ,
142- ) {
143- super .onReceivedHttpError(view, request, errorResponse)
138+ override fun onReceivedHttpError (
139+ view : WebView ? ,
140+ request : WebResourceRequest ? ,
141+ errorResponse : WebResourceResponse ? ,
142+ ) {
143+ super .onReceivedHttpError(view, request, errorResponse)
144144
145- if (errorResponse?.statusCode != HTTP_STATUS_TOO_MANY_REQUESTS ) return
145+ if (errorResponse?.statusCode != HTTP_STATUS_TOO_MANY_REQUESTS ) return
146146
147- // If a user is logged in, they see: "Daily limit exceeded; please try again tomorrow."
148- // We have nothing we can do here
149- if (isLoggedInToAnkiWeb) return
147+ // If a user is logged in, they see: "Daily limit exceeded; please try again tomorrow."
148+ // We have nothing we can do here
149+ if (isLoggedInToAnkiWeb) return
150150
151- // The following cases are handled below:
152- // "Please log in to download more decks." - on clicking "Download"
153- // "Please log in to perform more searches" - on searching
154- redirectUserToSignUpOrLogin()
155- }
151+ // The following cases are handled below:
152+ // "Please log in to download more decks." - on clicking "Download"
153+ // "Please log in to perform more searches" - on searching
154+ redirectUserToSignUpOrLogin()
155+ }
156156
157- override fun onReceivedError (
158- view : WebView ? ,
159- request : WebResourceRequest ? ,
160- error : WebResourceError ? ,
161- ) {
162- // Set mShouldHistoryBeCleared to false if error occurs since it might have been true
163- shouldHistoryBeCleared = false
164- super .onReceivedError(view, request, error)
165- }
157+ override fun onReceivedError (
158+ view : WebView ? ,
159+ request : WebResourceRequest ? ,
160+ error : WebResourceError ? ,
161+ ) {
162+ // Set mShouldHistoryBeCleared to false if error occurs since it might have been true
163+ shouldHistoryBeCleared = false
164+ super .onReceivedError(view, request, error)
165+ }
166166
167- /* *
168- * Redirects the user to a login page
169- *
170- * A message is shown informing the user they need to log in to download more decks
171- *
172- * If the user has not logged in **inside AnkiDroid** then the message provides
173- * the user with an action to sign up
174- *
175- * The redirect is not performed if [redirectTimes] is 3 or more
176- */
177- private fun redirectUserToSignUpOrLogin () {
178- // inform the user they need to log in as they've hit a rate limit
179- showSnackbar(R .string.shared_decks_login_required, LENGTH_INDEFINITE ) {
180- if (isLoggedIn()) return @showSnackbar
181-
182- // If a user is not logged in inside AnkiDroid, assume they have no AnkiWeb account
183- // and give them the option to sign up
184- setAction(R .string.sign_up) {
185- binding.webView.loadUrl(getString(R .string.shared_decks_sign_up_url))
186- }
167+ /* *
168+ * Redirects the user to a login page
169+ *
170+ * A message is shown informing the user they need to log in to download more decks
171+ *
172+ * If the user has not logged in **inside AnkiDroid** then the message provides
173+ * the user with an action to sign up
174+ *
175+ * The redirect is not performed if [redirectTimes] is 3 or more
176+ */
177+ private fun redirectUserToSignUpOrLogin () {
178+ // inform the user they need to log in as they've hit a rate limit
179+ showSnackbar(R .string.shared_decks_login_required, LENGTH_INDEFINITE ) {
180+ if (isLoggedIn()) return @showSnackbar
181+
182+ // If a user is not logged in inside AnkiDroid, assume they have no AnkiWeb account
183+ // and give them the option to sign up
184+ setAction(R .string.sign_up) {
185+ binding.webView.loadUrl(getString(R .string.shared_decks_sign_up_url))
187186 }
187+ }
188188
189- // redirect user to /account/login
190- // TODO: the result of login is typically redirecting the user to their decks
191- // this should be improved
192-
193- if (redirectTimes++ < 3 ) {
194- val url = getString(R .string.shared_decks_login_url)
195- Timber .i(" HTTP 429, redirecting to login: '$url '" )
196- binding.webView.loadUrl(url)
197- } else {
198- // Ensure that we do not have an infinite redirect
199- Timber .w(" HTTP 429 redirect limit exceeded, only displaying message" )
200- }
189+ // redirect user to /account/login
190+ // TODO: the result of login is typically redirecting the user to their decks
191+ // this should be improved
192+
193+ if (redirectTimes++ < 3 ) {
194+ val url = getString(R .string.shared_decks_login_url)
195+ Timber .i(" HTTP 429, redirecting to login: '$url '" )
196+ binding.webView.loadUrl(url)
197+ } else {
198+ // Ensure that we do not have an infinite redirect
199+ Timber .w(" HTTP 429 redirect limit exceeded, only displaying message" )
201200 }
202201 }
202+ }
203+
204+ @VisibleForTesting(otherwise = VisibleForTesting .PRIVATE )
205+ internal val webViewClient = SharedDeckWebViewClient ()
203206
204207 companion object {
205208 const val SHARED_DECKS_DOWNLOAD_FRAGMENT = " SharedDecksDownloadFragment"
206209 const val DOWNLOAD_FILE = " DownloadFile"
207- private const val HTTP_STATUS_TOO_MANY_REQUESTS = 429
210+
211+ @VisibleForTesting(otherwise = VisibleForTesting .PRIVATE )
212+ const val HTTP_STATUS_TOO_MANY_REQUESTS = 429
208213 }
209214
210215 // Show WebView with AnkiWeb shared decks with the functionality to capture downloads and import decks.
0 commit comments