Skip to content

Commit ef08599

Browse files
committed
Merge branch 'feature/tv-ascii-captcha' into 'develop'
feat: Add ASCII captcha support for TV login and signup flows See merge request ws/client/androidapp!329
2 parents 698630f + 986ce6f commit ef08599

File tree

9 files changed

+212
-17
lines changed

9 files changed

+212
-17
lines changed

CLAUDE.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Build Commands
6+
7+
### Building the App
8+
```bash
9+
./gradlew assembleDebug # Build debug APK
10+
./gradlew assembleRelease # Build release APK
11+
./gradlew bundleGoogleRelease # Build Google Play AAB
12+
./gradlew bundleFdroidRelease # Build F-Droid AAB
13+
```
14+
15+
### Testing
16+
```bash
17+
./gradlew test # Run unit tests
18+
./gradlew connectedAndroidTest # Run instrumented tests
19+
./gradlew testDebug # Run debug unit tests
20+
```
21+
22+
### Code Quality
23+
```bash
24+
./gradlew ktlintCheck # Check Kotlin code style
25+
./gradlew ktlintFormat # Format Kotlin code
26+
./gradlew dependencyCheckAnalyze # Security dependency analysis
27+
```
28+
29+
### Cleaning
30+
```bash
31+
./gradlew clean # Clean build artifacts
32+
```
33+
34+
## Project Architecture
35+
36+
### Module Structure
37+
- **base/**: Core VPN logic, database, networking, and shared components
38+
- **mobile/**: Android phone/tablet UI implementation
39+
- **tv/**: Android TV interface implementation
40+
- **openvpn/**: OpenVPN protocol implementation
41+
- **strongswan/**: IKEv2/IPSec protocol implementation
42+
- **wgtunnel/**: WireGuard tunnel implementation
43+
- **common/**: Shared utilities and common code
44+
- **test/**: Shared test utilities and mocks
45+
46+
### Application Classes
47+
- **Windscribe** (base): Main application class with dependency injection
48+
- **PhoneApplication** (mobile): Mobile-specific implementation
49+
- **TVApplication** (tv): TV-specific implementation
50+
51+
### Key Technologies
52+
- **Dependency Injection**: Dagger 2 for component management
53+
- **Database**: Room for local data persistence
54+
- **Networking**: App → ApiCallManager → wsnet library → API endpoints
55+
- **VPN Protocols**: OpenVPN, WireGuard, IKEv2/IPSec
56+
- **UI**: Mix of traditional Android Views and Jetpack Compose
57+
- **Background Processing**: WorkManager for scheduled tasks
58+
- **Languages**: Kotlin (preferred) and Java (legacy)
59+
60+
### Build Variants
61+
- **google**: Google Play Store version with Firebase and billing
62+
- **fdroid**: F-Droid version without proprietary dependencies
63+
64+
## Development Workflow
65+
66+
### Code Style
67+
- Use ktlint with default rules for Kotlin code
68+
- Follow grandcentrix-AndroidCodeStyle for Java code
69+
- Prefer Kotlin over Java for new code
70+
- Use coroutines and Kotlin flows for async operations
71+
72+
### Testing Strategy
73+
- Unit tests for business logic
74+
- Instrumented tests for Android components
75+
- Mock test data available in `test/` module
76+
- Test runner: `com.windscribe.vpn.CustomRunner`
77+
78+
### Key Components to Understand
79+
1. **VPN Management**: Multi-protocol VPN controller in base module
80+
2. **Connection State**: Centralized connection state management
81+
3. **Server Selection**: Location and server selection logic
82+
4. **User Authentication**: Account management and authentication
83+
5. **Settings**: App preferences and configuration
84+
6. **Auto-Connection**: Background connection management
85+
86+
## Common Development Tasks
87+
88+
### Adding New VPN Features
89+
1. Modify base module for core functionality
90+
2. Update mobile/TV UIs as needed
91+
3. Add appropriate tests
92+
4. Update database schema if needed
93+
94+
### UI Changes
95+
- Mobile: Update fragments and activities in mobile module
96+
- TV: Update leanback components in tv module
97+
- Use existing design patterns and components
98+
99+
### Network/API Changes
100+
- Update Retrofit interfaces in base module
101+
- Add corresponding data models
102+
- Update database entities if needed
103+
104+
### Adding Dependencies
105+
- Add to base module build.gradle for shared dependencies
106+
- Use appropriate product flavors for platform-specific dependencies
107+
- Update dependency check configuration if needed
108+
109+
## Security Considerations
110+
- VPN credentials and certificates are stored securely
111+
- Network security config varies by build type
112+
- SSL pinning implemented for API calls
113+
- Proguard/R8 obfuscation enabled for release builds

tv/src/main/java/com/windscribe/tv/welcome/WelcomeActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,8 @@ class WelcomeActivity :
339339
replaceFragment(fragment, true)
340340
}
341341

342-
override fun onAuthSignUpClick() {
343-
342+
override fun onAuthSignUpClick(username: String, password: String, email: String?) {
343+
presenter.onAuthSignUpClick(username, password, email)
344344
}
345345

346346
private fun permissionGranted(): Boolean {

tv/src/main/java/com/windscribe/tv/welcome/WelcomePresenter.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ interface WelcomePresenter {
2727
)
2828

2929
fun onAuthLoginClick(username: String, password: String)
30+
fun onAuthSignUpClick(username: String, password: String, email: String?)
3031
}

tv/src/main/java/com/windscribe/tv/welcome/WelcomePresenterImpl.kt

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,54 @@ class WelcomePresenterImpl @Inject constructor(
234234
}
235235
}
236236

237+
override fun onAuthSignUpClick(username: String, password: String, email: String?) {
238+
isRegistration = true
239+
welcomeView.hideSoftKeyboard()
240+
if (validateLoginInputs(username, password, email ?: "", false)) {
241+
logger.info("Requesting auth signup token.")
242+
welcomeView.prepareUiForApiCallStart()
243+
interactor.getCompositeDisposable().add(
244+
interactor.getApiCallManager()
245+
.authTokenSignup(true)
246+
.doOnSubscribe { welcomeView.updateCurrentProcess("Preparing signup...") }
247+
.subscribeOn(Schedulers.io())
248+
.observeOn(AndroidSchedulers.mainThread())
249+
.subscribeWith(
250+
object :
251+
DisposableSingleObserver<GenericResponseClass<AuthToken?, ApiErrorResponse?>?>() {
252+
override fun onError(e: Throwable) {
253+
logger.error("Auth signup token: {}", e.message)
254+
onLoginFailed()
255+
}
256+
257+
override fun onSuccess(
258+
response: GenericResponseClass<AuthToken?, ApiErrorResponse?>
259+
) {
260+
response.dataClass?.let {
261+
if (it.captcha != null) {
262+
val captchaArt = it.captcha!!.asciiArt!!
263+
logger.info("Signup captcha received: $captchaArt")
264+
welcomeView.prepareUiForApiCallFinished()
265+
welcomeView.captchaReceived(
266+
username,
267+
password,
268+
it.token,
269+
captchaArt
270+
)
271+
} else {
272+
logger.info("Starting signup without captcha")
273+
startSignUpProcess(username, password, email, true, it.token, null)
274+
}
275+
} ?: response.errorClass?.let {
276+
logger.error("Signup: {}", it)
277+
onLoginResponseError(it, username, password)
278+
}
279+
}
280+
})
281+
)
282+
}
283+
}
284+
237285
override fun startLoginProcess(
238286
username: String,
239287
password: String,
@@ -312,8 +360,8 @@ class WelcomePresenterImpl @Inject constructor(
312360
null,
313361
email,
314362
"",
315-
null,
316-
null,
363+
secureToken,
364+
captcha,
317365
floatArrayOf(),
318366
floatArrayOf()
319367
)

tv/src/main/java/com/windscribe/tv/welcome/fragment/CaptchaFragment.kt

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
package com.windscribe.tv.welcome.fragment
55

66
import android.content.Context
7+
import android.graphics.Bitmap
8+
import android.graphics.Canvas
9+
import android.graphics.Color
10+
import android.graphics.Paint
711
import android.graphics.Typeface
812
import android.os.Bundle
913
import android.view.LayoutInflater
@@ -47,15 +51,48 @@ class CaptchaFragment : Fragment(), WelcomeActivityCallback {
4751
return null
4852
}
4953

54+
private fun createAsciiArtBitmap(text: String): Bitmap {
55+
val paint = Paint().apply {
56+
typeface = Typeface.MONOSPACE
57+
textSize = 24f
58+
color = Color.GREEN
59+
isAntiAlias = false
60+
}
61+
62+
val lines = text.split("\n")
63+
val maxWidth = lines.maxOfOrNull { paint.measureText(it) }?.toInt() ?: 0
64+
val lineHeight = paint.fontMetrics.let { it.bottom - it.top }
65+
val totalHeight = (lineHeight * lines.size).toInt()
66+
67+
val bitmap = Bitmap.createBitmap(
68+
maxWidth + 20,
69+
totalHeight + 20,
70+
Bitmap.Config.ARGB_8888
71+
)
72+
73+
val canvas = Canvas(bitmap)
74+
canvas.drawColor(Color.BLACK)
75+
76+
var y = -paint.fontMetrics.top + 10
77+
for (line in lines) {
78+
canvas.drawText(line, 10f, y, paint)
79+
y += lineHeight
80+
}
81+
82+
return bitmap
83+
}
84+
5085
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
5186
super.onViewCreated(view, savedInstanceState)
5287
username = arguments?.getString("username")
5388
password = arguments?.getString("password")
5489
val captchaArt = arguments?.getString("captchaArt")
5590
val captchaText = decodeBase64ToArt(captchaArt!!)
5691
val token = arguments?.getString("secureToken")
57-
binding.asciiView.text = captchaText
58-
binding.asciiView.typeface = Typeface.MONOSPACE
92+
captchaText?.let { text ->
93+
val bitmap = createAsciiArtBitmap(text)
94+
binding.asciiView.setImageBitmap(bitmap)
95+
}
5996
binding.back.setOnClickListener {
6097
fragmentCallBack?.onBackButtonPressed()
6198
}

tv/src/main/java/com/windscribe/tv/welcome/fragment/FragmentCallback.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ interface FragmentCallback {
1717
fun onGenerateCodeClick()
1818
fun onLoginButtonClick(username: String, password: String, twoFa: String?, secureToken: String?, captcha: String?)
1919
fun onAuthLoginClick(username: String, password: String)
20-
fun onAuthSignUpClick()
20+
fun onAuthSignUpClick(username: String, password: String, email: String?)
2121
fun onLoginClick()
2222
fun onSignUpButtonClick(
2323
username: String,

tv/src/main/java/com/windscribe/tv/welcome/fragment/LoginFragment.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class LoginFragment : Fragment(), WelcomeActivityCallback {
8080
callBack?.onGenerateCodeClick()
8181
}
8282
binding.loginSignUp.setOnClickListener {
83-
callBack?.onLoginButtonClick(binding.usernameEdit.text.toString(), binding.passwordEdit.text.toString(), null, null, null)
83+
callBack?.onAuthLoginClick(binding.usernameEdit.text.toString(), binding.passwordEdit.text.toString())
8484
}
8585
binding.passwordContainer.setOnClickListener {
8686
binding.showPassword.visibility = View.VISIBLE

tv/src/main/java/com/windscribe/tv/welcome/fragment/SignUpFragment.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ class SignUpFragment : Fragment(), WelcomeActivityCallback {
8989
binding.passwordEdit.text.toString(), "", true
9090
)
9191
} else {
92-
fragmentCallBack?.onSignUpButtonClick(
92+
fragmentCallBack?.onAuthSignUpClick(
9393
binding.usernameEdit.text.toString(),
94-
binding.passwordEdit.text.toString(), "", true, null, null
94+
binding.passwordEdit.text.toString(), ""
9595
)
9696
}
9797
}

tv/src/main/res/layout/fragment_captcha.xml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,12 @@
3838
app:layout_constraintTop_toBottomOf="@id/error"
3939
android:scrollbars="horizontal">
4040

41-
<TextView
41+
<ImageView
4242
android:id="@+id/asciiView"
4343
android:layout_width="wrap_content"
4444
android:layout_height="wrap_content"
45-
android:textSize="24sp"
46-
android:typeface="monospace"
47-
android:lineSpacingExtra="8dp"
48-
android:textColor="#00FF00"
49-
android:textAlignment="viewStart"
50-
android:textIsSelectable="true" />
45+
android:scaleType="matrix"
46+
android:contentDescription="ASCII Captcha" />
5147
</HorizontalScrollView>
5248

5349
<androidx.appcompat.widget.AppCompatEditText

0 commit comments

Comments
 (0)