Add ability to manually refresh google captcha

dev
k1rakishou 6 years ago
parent 6fdc7b74c2
commit 2e10056546
  1. 9
      Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
  2. 2
      Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
  3. 45
      Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java
  4. 267
      Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java
  5. 3
      Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java
  6. 35
      Clover/app/src/main/res/layout/layout_captcha_nojs_v2.xml
  7. 3
      Clover/app/src/main/res/values/strings.xml

@ -37,6 +37,7 @@ import org.floens.chan.core.site.http.Reply;
import org.floens.chan.core.site.http.ReplyResponse;
import org.floens.chan.ui.captcha.AuthenticationLayoutCallback;
import org.floens.chan.ui.captcha.AuthenticationLayoutInterface;
import org.floens.chan.ui.captcha.v2.CaptchaNoJsLayoutV2;
import org.floens.chan.ui.helper.ImagePickDelegate;
import org.floens.chan.utils.Logger;
@ -280,8 +281,13 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
draft.captchaChallenge = challenge;
draft.captchaResponse = response;
// we don't need this to be called for new captcha window.
// Otherwise "Request captcha request is already in progress" message will be shown
if (!(authenticationLayout instanceof CaptchaNoJsLayoutV2)) {
// should this be called here?
authenticationLayout.reset();
}
makeSubmitCall();
}
@ -405,8 +411,7 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
}
public void switchPage(Page page, boolean animate) {
// by default try to use the new nojs captcha view
switchPage(page, animate, true);
switchPage(page, animate, ChanSettings.useNewCaptchaWindow.get());
}
public void switchPage(Page page, boolean animate, boolean useV2NoJsCaptcha) {

@ -162,6 +162,7 @@ public class ChanSettings {
public static final BooleanSetting crashReporting;
public static final BooleanSetting useNewCaptchaWindow;
public static final BooleanSetting useRealGoogleCookies;
public static final StringSetting googleCookie;
static {
SettingProvider p = new SharedPreferencesSettingProvider(AndroidUtils.getPreferences());
@ -254,6 +255,7 @@ public class ChanSettings {
crashReporting = new BooleanSetting(p, "preference_crash_reporting", true);
useNewCaptchaWindow = new BooleanSetting(p, "use_new_captcha_window", true);
useRealGoogleCookies = new BooleanSetting(p, "use_real_google_cookies", false);
googleCookie = new StringSetting(p, "google_cookie", "");
// Old (but possibly still in some users phone)
// preference_board_view_mode default "list"

@ -36,6 +36,7 @@ import android.widget.ScrollView;
import android.widget.Toast;
import org.floens.chan.R;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.ui.captcha.AuthenticationLayoutCallback;
@ -55,6 +56,7 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout
private AppCompatButton captchaVerifyButton;
private AppCompatButton useOldCaptchaButton;
private AppCompatButton reloadCaptchaButton;
private AppCompatButton refreshCookiesButton;
private ConstraintLayout buttonsHolder;
private ScrollView background;
@ -90,6 +92,7 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout
captchaVerifyButton = view.findViewById(R.id.captcha_layout_v2_verify_button);
useOldCaptchaButton = view.findViewById(R.id.captcha_layout_v2_use_old_captcha_button);
reloadCaptchaButton = view.findViewById(R.id.captcha_layout_v2_reload_button);
refreshCookiesButton = view.findViewById(R.id.captcha_layout_v2_refresh_cookies);
buttonsHolder = view.findViewById(R.id.captcha_layout_v2_buttons_holder);
background = view.findViewById(R.id.captcha_layout_v2_background);
@ -100,11 +103,17 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout
captchaVerifyButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary));
useOldCaptchaButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary));
reloadCaptchaButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary));
refreshCookiesButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary));
captchaVerifyButton.setOnClickListener(this);
useOldCaptchaButton.setOnClickListener(this);
reloadCaptchaButton.setOnClickListener(this);
if (ChanSettings.useRealGoogleCookies.get()) {
refreshCookiesButton.setVisibility(View.VISIBLE);
refreshCookiesButton.setOnClickListener(this);
}
captchaVerifyButton.setEnabled(false);
}
@ -148,6 +157,9 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout
showToast(getContext().getString(
R.string.captcha_layout_v2_captcha_request_is_already_in_progress));
break;
case AlreadyShutdown:
// do nothing
break;
}
}
@ -171,10 +183,35 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout
});
}
@Override
public void onGoogleCookiesRefreshed() {
// called on a background thread
AndroidUtils.runOnUiThread(() -> {
showToast("Google cookies successfully refreshed");
// refresh the captcha as well
reset();
});
}
// Called when we could not get google cookies
@Override
public void onGetGoogleCookieError(boolean shouldFallback, Throwable error) {
// called on a background thread
handleError(shouldFallback, error);
}
// Called when we got response from re-captcha but could not parse some part of it
@Override
public void onCaptchaInfoParseError(Throwable error) {
// called on a background thread
handleError(true, error);
}
private void handleError(boolean shouldFallback, Throwable error) {
AndroidUtils.runOnUiThread(() -> {
Logger.e(TAG, "CaptchaV2 error", error);
@ -182,7 +219,10 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout
showToast(message);
captchaVerifyButton.setEnabled(true);
if (shouldFallback) {
callback.onFallbackToV1CaptchaView();
}
});
}
@ -200,6 +240,8 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout
callback.onFallbackToV1CaptchaView();
} else if (v == reloadCaptchaButton) {
reset();
} else if (v == refreshCookiesButton) {
presenter.refreshCookies();
}
}
@ -269,6 +311,9 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout
case AlreadyInProgress:
showToast(getContext().getString(R.string.captcha_layout_v2_verification_already_in_progress));
break;
case AlreadyShutdown:
// do nothing
break;
}
} catch (Throwable error) {
onCaptchaInfoParseError(error);

@ -18,8 +18,11 @@
package org.floens.chan.ui.captcha.v2;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.utils.BackgroundUtils;
import org.floens.chan.utils.Logger;
import java.io.IOException;
@ -30,8 +33,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
@ -42,21 +44,23 @@ import okhttp3.ResponseBody;
public class CaptchaNoJsPresenterV2 {
private static final String TAG = "CaptchaNoJsPresenterV2";
// TODO: change useragent?
private static final String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36";
private static final String userAgentHeader = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36";
private static final String acceptHeader = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3";
private static final String acceptEncodingHeader = "deflate, br";
private static final String acceptLanguageHeader = "en-US";
private static final String recaptchaUrlBase = "https://www.google.com/recaptcha/api/fallback?k=";
private static final String googleBaseUrl = "https://www.google.com/";
private static final String encoding = "UTF-8";
private static final String mediaType = "application/x-www-form-urlencoded";
private static final String recaptchaChallengeString = "reCAPTCHA challenge";
private static final String verificationTokenString = "fbc-verification-token";
private static final String setCookieHeaderName = "set-cookie";
private static final int SUCCESS_STATUS_CODE = 200;
private static final long CAPTCHA_REQUEST_THROTTLE_MS = 3000L;
private static final String googleCookies =
"SID=gjaHjfFJPAN5HO3MVVZpjHFKa_249dsfjHa9klsiaflsd99.asHqjsM2lAS; " +
"HSID=j7m0aFJ82lPF7Hd9d; " +
"SSID=nJKpa81jOskq7Jsps; " +
"NID=87=gkOAkg09AKnvJosKq82kgnDnHj8Om2pLskKhdna02msog8HkdHDlasDf";
// this cookie is taken from dashchan
private static final String defaultGoogleCookies = "NID=87=gkOAkg09AKnvJosKq82kgnDnHj8Om2pLskKhdna02msog8HkdHDlasDf";
// TODO: inject this in the future when https://github.com/Floens/Clover/pull/678 is merged
private final OkHttpClient okHttpClient = new OkHttpClient();
@ -68,16 +72,22 @@ public class CaptchaNoJsPresenterV2 {
private AuthenticationCallbacks callbacks;
@Nullable
private CaptchaInfo prevCaptchaInfo = null;
@NonNull
// either the default cookie or a real cookie
private volatile String googleCookie;
private AtomicBoolean verificationInProgress = new AtomicBoolean(false);
private AtomicBoolean captchaRequestInProgress = new AtomicBoolean(false);
private AtomicBoolean refreshCookiesRequestInProgress = new AtomicBoolean(false);
private String siteKey;
private String baseUrl;
private long lastTimeCaptchRequest = 0L;
private long lastTimeCaptchaRequest = 0L;
public CaptchaNoJsPresenterV2(@Nullable AuthenticationCallbacks callbacks, Context context) {
this.callbacks = callbacks;
this.parser = new CaptchaNoJsHtmlParser(context, okHttpClient);
this.googleCookie = ChanSettings.googleCookie.get();
}
public void init(String siteKey, String baseUrl) {
@ -86,15 +96,22 @@ public class CaptchaNoJsPresenterV2 {
}
/**
* Send challenge solution back
* Send challenge solution back to the recaptcha
*/
public VerifyError verify(
List<Integer> selectedIds
) throws CaptchaNoJsV2Error, UnsupportedEncodingException {
) throws CaptchaNoJsV2Error {
if (!verificationInProgress.compareAndSet(false, true)) {
Logger.d(TAG, "Verify captcha request is already in progress");
return VerifyError.AlreadyInProgress;
}
if (executor.isShutdown()) {
verificationInProgress.set(false);
Logger.d(TAG, "Cannot verify, executor is already shutdown");
return VerifyError.AlreadyShutdown;
}
try {
if (selectedIds.isEmpty()) {
verificationInProgress.set(false);
@ -109,43 +126,40 @@ public class CaptchaNoJsPresenterV2 {
throw new CaptchaNoJsV2Error("C parameter is null");
}
if (googleCookie.isEmpty()) {
throw new IllegalStateException("Google cookies are not supposed to be empty here");
}
executor.submit(() -> {
try {
String recaptchaUrl = recaptchaUrlBase + siteKey;
RequestBody body = createResponseBody(prevCaptchaInfo, selectedIds);
Request request = new Request.Builder()
.url(recaptchaUrl)
.post(body)
.header("User-Agent", userAgent)
.header("Referer", recaptchaUrl)
.header("Accept-Language", "en-US")
.header("Cookie", googleCookies)
.header("User-Agent", userAgentHeader)
.header("Accept", acceptHeader)
.header("Accept-Encoding", acceptEncodingHeader)
.header("Accept-Language", acceptLanguageHeader)
.header("Cookie", googleCookie)
.build();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
if (callbacks != null) {
try {
prevCaptchaInfo = null;
callbacks.onCaptchaInfoParseError(e);
try (Response response = okHttpClient.newCall(request).execute()) {
prevCaptchaInfo = handleGetRecaptchaResponse(response);
} finally {
verificationInProgress.set(false);
}
}
}
@Override
public void onResponse(Call call, Response response) {
executor.execute(() -> {
// to avoid okhttp's threads to hang
} catch (Throwable error) {
if (callbacks != null) {
try {
prevCaptchaInfo = handleGetRecaptchaResponse(response);
prevCaptchaInfo = null;
callbacks.onCaptchaInfoParseError(error);
} finally {
verificationInProgress.set(false);
response.close();
}
});
}
}
});
@ -156,57 +170,89 @@ public class CaptchaNoJsPresenterV2 {
}
}
/**
* Manually refreshes the google cookie
* */
public void refreshCookies() {
if (!refreshCookiesRequestInProgress.compareAndSet(false, true)) {
return;
}
if (executor.isShutdown()) {
refreshCookiesRequestInProgress.set(false);
return;
}
executor.submit(() -> {
try {
googleCookie = getGoogleCookies(true);
if (callbacks != null) {
callbacks.onGoogleCookiesRefreshed();
}
} catch (IOException e) {
if (callbacks != null) {
callbacks.onGetGoogleCookieError(false, e);
}
} finally {
refreshCookiesRequestInProgress.set(false);
}
});
}
/**
* Requests captcha data, parses it and then passes it to the render function
*/
public RequestCaptchaInfoError requestCaptchaInfo() {
if (!captchaRequestInProgress.compareAndSet(false, true)) {
Logger.d(TAG, "Request captcha request is already in progress");
return RequestCaptchaInfoError.AlreadyInProgress;
}
try {
// recaptcha may become very angry at you if your are fetching it too fast
if (System.currentTimeMillis() - lastTimeCaptchRequest < CAPTCHA_REQUEST_THROTTLE_MS) {
if (System.currentTimeMillis() - lastTimeCaptchaRequest < CAPTCHA_REQUEST_THROTTLE_MS) {
captchaRequestInProgress.set(false);
Logger.d(TAG, "Requesting captcha info too fast");
return RequestCaptchaInfoError.HoldYourHorses;
}
lastTimeCaptchRequest = System.currentTimeMillis();
String recaptchaUrl = recaptchaUrlBase + siteKey;
if (executor.isShutdown()) {
captchaRequestInProgress.set(false);
Logger.d(TAG, "Cannot request captcha info, executor is already shutdown");
return RequestCaptchaInfoError.AlreadyShutdown;
}
Request request = new Request.Builder()
.url(recaptchaUrl)
.header("User-Agent", userAgent)
.header("Referer", baseUrl)
.header("Accept-Language", "en-US")
.header("Cookie", googleCookies)
.build();
lastTimeCaptchaRequest = System.currentTimeMillis();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
if (callbacks != null) {
executor.submit(() -> {
try {
prevCaptchaInfo = null;
callbacks.onCaptchaInfoParseError(e);
} finally {
captchaRequestInProgress.set(false);
try {
googleCookie = getGoogleCookies(false);
} catch (Throwable error) {
if (callbacks != null) {
callbacks.onGetGoogleCookieError(true, error);
}
throw error;
}
try {
prevCaptchaInfo = getCaptchaInfo();
} catch (Throwable error) {
if (callbacks != null) {
callbacks.onCaptchaInfoParseError(error);
}
@Override
public void onResponse(Call call, Response response) {
executor.execute(() -> {
// to avoid okhttp's threads to hang
throw error;
}
} catch (Throwable error) {
Logger.e(TAG, "Error while executing captcha requests", error);
try {
prevCaptchaInfo = handleGetRecaptchaResponse(response);
prevCaptchaInfo = null;
googleCookie = defaultGoogleCookies;
} finally {
captchaRequestInProgress.set(false);
response.close();
}
});
}
});
@ -223,6 +269,66 @@ public class CaptchaNoJsPresenterV2 {
}
}
@NonNull
private String getGoogleCookies(boolean forced) throws IOException {
if (BackgroundUtils.isMainThread()) {
throw new RuntimeException("Must not be executed on the main thread");
}
if (!ChanSettings.useRealGoogleCookies.get()) {
Logger.d(TAG, "Google cookies request is disabled in the settings, using the default ones");
return defaultGoogleCookies;
}
if (!forced && !googleCookie.isEmpty()) {
Logger.d(TAG, "We already have google cookies");
return googleCookie;
}
Request request = new Request.Builder()
.url(googleBaseUrl)
.header("User-Agent", userAgentHeader)
.header("Accept", acceptHeader)
.header("Accept-Encoding", acceptEncodingHeader)
.header("Accept-Language", acceptLanguageHeader)
.build();
try (Response response = okHttpClient.newCall(request).execute()) {
String newCookie = handleGetGoogleCookiesResponse(response);
ChanSettings.googleCookie.set(newCookie);
Logger.d(TAG, "Successfully refreshed google cookies, new cookie = " + newCookie);
return newCookie;
}
}
@Nullable
private CaptchaInfo getCaptchaInfo() throws IOException {
if (BackgroundUtils.isMainThread()) {
throw new RuntimeException("Must not be executed on the main thread");
}
if (googleCookie.isEmpty()) {
throw new IllegalStateException("Google cookies are not supposed to be null here");
}
String recaptchaUrl = recaptchaUrlBase + siteKey;
Request request = new Request.Builder()
.url(recaptchaUrl)
.header("Referer", baseUrl)
.header("User-Agent", userAgentHeader)
.header("Accept", acceptHeader)
.header("Accept-Encoding", acceptEncodingHeader)
.header("Accept-Language", acceptLanguageHeader)
.header("Cookie", googleCookie)
.build();
try (Response response = okHttpClient.newCall(request).execute()) {
return handleGetRecaptchaResponse(response);
}
}
private RequestBody createResponseBody(
CaptchaInfo prevCaptchaInfo,
List<Integer> selectedIds
@ -254,6 +360,33 @@ public class CaptchaNoJsPresenterV2 {
resultBody);
}
@NonNull
private String handleGetGoogleCookiesResponse(Response response) {
if (response.code() != SUCCESS_STATUS_CODE) {
Logger.w(TAG, "Get google cookies request returned bad status code = " + response.code());
return defaultGoogleCookies;
}
Headers headers = response.headers();
for (String headerName : headers.names()) {
if (headerName.equalsIgnoreCase(setCookieHeaderName)) {
String setCookieHeader = headers.get(headerName);
if (setCookieHeader != null) {
String[] split = setCookieHeader.split(";");
for (String splitPart : split) {
if (splitPart.startsWith("NID")) {
return splitPart;
}
}
}
}
}
Logger.d(TAG, "Could not find the NID cookie in the headers");
return defaultGoogleCookies;
}
@Nullable
private CaptchaInfo handleGetRecaptchaResponse(Response response) {
try {
@ -284,6 +417,7 @@ public class CaptchaNoJsPresenterV2 {
if (bodyString.contains(verificationTokenString)) {
// got the token
String verificationToken = parser.parseVerificationToken(bodyString);
Logger.d(TAG, "Got the verification token");
if (callbacks != null) {
callbacks.onVerificationDone(verificationToken);
@ -293,6 +427,7 @@ public class CaptchaNoJsPresenterV2 {
} else {
// got the challenge
CaptchaInfo captchaInfo = parser.parseHtml(bodyString, siteKey);
Logger.d(TAG, "Got new challenge");
if (callbacks != null) {
callbacks.onCaptchaInfoParsed(captchaInfo);
@ -327,16 +462,22 @@ public class CaptchaNoJsPresenterV2 {
public enum VerifyError {
Ok,
NoImagesSelected,
AlreadyInProgress
AlreadyInProgress,
AlreadyShutdown
}
public enum RequestCaptchaInfoError {
Ok,
AlreadyInProgress,
HoldYourHorses
HoldYourHorses,
AlreadyShutdown
}
public interface AuthenticationCallbacks {
void onGetGoogleCookieError(boolean shouldFallback, Throwable error);
void onGoogleCookiesRefreshed();
void onCaptchaInfoParsed(CaptchaInfo captchaInfo);
void onCaptchaInfoParseError(Throwable error);

@ -64,6 +64,9 @@ public class BehaviourSettingsController extends SettingsController {
// when user disables the new captcha window also disable the usage of the google cookies
if (!ChanSettings.useNewCaptchaWindow.get()) {
ChanSettings.useRealGoogleCookies.set(false);
// reset the old google cookie as well
ChanSettings.googleCookie.set("");
}
rebuildPreferences();

@ -47,26 +47,38 @@
<android.support.v7.widget.AppCompatButton
android:id="@+id/captcha_layout_v2_use_old_captcha_button"
android:layout_width="128dp"
android:layout_width="112dp"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:paddingStart="16dp"
android:paddingLeft="16dp"
android:paddingEnd="16dp"
android:paddingRight="16dp"
android:text="@string/captcha_layout_v2_use_old_captcha"
android:textColor="#ffffff"
app:layout_constraintBottom_toBottomOf="@+id/captcha_layout_v2_reload_button"
app:layout_constraintEnd_toStartOf="@+id/captcha_layout_v2_reload_button"
app:layout_constraintBottom_toBottomOf="@+id/captcha_layout_v2_verify_button"
app:layout_constraintEnd_toStartOf="@+id/captcha_layout_v2_refresh_cookies"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/captcha_layout_v2_reload_button" />
app:layout_constraintTop_toTopOf="@+id/captcha_layout_v2_verify_button" />
<android.support.v7.widget.AppCompatButton
android:id="@+id/captcha_layout_v2_refresh_cookies"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:text="@string/captcha_layout_v2_refresh_cookies"
android:textColor="#ffffff"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/captcha_layout_v2_verify_button"
app:layout_constraintEnd_toStartOf="@+id/captcha_layout_v2_reload_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/captcha_layout_v2_use_old_captcha_button"
app:layout_constraintTop_toTopOf="@+id/captcha_layout_v2_verify_button" />
<android.support.v7.widget.AppCompatButton
android:id="@+id/captcha_layout_v2_reload_button"
android:layout_width="96dp"
android:layout_width="64dp"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
@ -76,12 +88,12 @@
app:layout_constraintBottom_toBottomOf="@+id/captcha_layout_v2_verify_button"
app:layout_constraintEnd_toStartOf="@+id/captcha_layout_v2_verify_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/captcha_layout_v2_use_old_captcha_button"
app:layout_constraintStart_toEndOf="@+id/captcha_layout_v2_refresh_cookies"
app:layout_constraintTop_toTopOf="@+id/captcha_layout_v2_verify_button" />
<android.support.v7.widget.AppCompatButton
android:id="@+id/captcha_layout_v2_verify_button"
android:layout_width="96dp"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
@ -90,6 +102,7 @@
android:textColor="#ffffff"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/captcha_layout_v2_reload_button"
app:layout_constraintTop_toTopOf="parent" />

@ -582,7 +582,7 @@ Don't have a 4chan Pass?<br>
<string name="settings_captcha_group" tools:ignore="MissingTranslation">Captcha</string>
<string name="settings_use_new_captcha_window" tools:ignore="MissingTranslation">Use new captcha window for no-js captcha</string>
<string name="settings_use_real_google_cookies" tools:ignore="MissingTranslation">Use real google cookies instead of hardcoded ones</string>
<string name="settings_use_real_google_cookies_description" tools:ignore="MissingTranslation">By using the real google cookies a GET request to the google.com will be executed to get the google cookies (SID, HSID, SSID, NID) which will be stored on the device and used for captcha authentication. Why would you need them? Because the hardcoded ones sometimes will make you re-enter the captcha dozens of times. Those cookies will be updated automatically (once in a month). But it will also be possible to update them manually</string>
<string name="settings_use_real_google_cookies_description" tools:ignore="MissingTranslation">When the real google cookies a GET request to the google.com will be executed to get the google cookies (NID) which will be used for captcha authentication. Why would you need them? Because the hardcoded ones sometimes will make you re-enter the captcha dozens of times. Those cookies will be updated automatically (when posting the first time after the app start). But it will also be possible to update them manually if necessary (for example, when the old ones give too many challenges in a row).</string>
<string name="captcha_layout_v2_verify_button_text" tools:ignore="MissingTranslation">Verify</string>
<string name="captcha_layout_v2_reload" tools:ignore="MissingTranslation">Reload</string>
@ -591,4 +591,5 @@ Don't have a 4chan Pass?<br>
<string name="captcha_layout_v2_verification_already_in_progress" tools:ignore="MissingTranslation">Verification is already in progress</string>
<string name="captcha_layout_v2_captcha_request_is_already_in_progress" tools:ignore="MissingTranslation">Captcha request is already in progress</string>
<string name="captcha_layout_v2_you_are_requesting_captcha_too_fast" tools:ignore="MissingTranslation">You are requesting captcha too fast</string>
<string name="captcha_layout_v2_refresh_cookies" tools:ignore="MissingTranslation">Refresh cookies</string>
</resources>

Loading…
Cancel
Save