Skip to content

Commit d1b035d

Browse files
committed
added support for new server
1 parent 96dfbad commit d1b035d

File tree

5 files changed

+209
-28
lines changed

5 files changed

+209
-28
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
1313

1414
<application
15+
android:name=".PrivacyFirstApp"
1516
android:allowBackup="true"
1617
android:icon="@mipmap/ic_launcher"
1718
android:label="@string/app_name"

app/src/main/java/com/secure/privacyfirst/network/ApiModels.kt

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,49 @@ data class DeleteUrlRequest(
2727
val url: String
2828
)
2929

30-
// Response models
30+
// Response models with proper validation and defaults
3131
data class LoginResponse(
3232
@SerializedName("token")
3333
val token: String,
3434
@SerializedName("expiresIn")
35-
val expiresIn: String
36-
)
35+
val expiresIn: String = "1h"
36+
) {
37+
init {
38+
require(token.isNotBlank()) { "Token cannot be empty" }
39+
}
40+
}
3741

3842
data class WhitelistResponse(
3943
@SerializedName("urls")
40-
val urls: List<String>,
44+
val urls: List<String> = emptyList(),
4145
@SerializedName("count")
42-
val count: Int
43-
)
46+
val count: Int = 0
47+
) {
48+
init {
49+
require(count >= 0) { "Count cannot be negative" }
50+
require(urls.size == count) { "URLs list size must match count" }
51+
}
52+
}
4453

4554
data class ApiErrorResponse(
4655
@SerializedName("message")
47-
val message: String
56+
val message: String = "Unknown error"
4857
)
4958

5059
data class SuccessResponse(
5160
@SerializedName("message")
52-
val message: String
61+
val message: String = "Success",
62+
@SerializedName("doc")
63+
val doc: WhitelistDoc? = null
64+
)
65+
66+
data class WhitelistDoc(
67+
@SerializedName("url")
68+
val url: String,
69+
@SerializedName("addedBy")
70+
val addedBy: String,
71+
@SerializedName("createdAt")
72+
val createdAt: String,
73+
@SerializedName("_id")
74+
val id: String
5375
)

app/src/main/java/com/secure/privacyfirst/network/ApiService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface ApiService {
88
@POST("api/login")
99
suspend fun login(@Body request: LoginRequest): Response<LoginResponse>
1010

11-
@GET("api/whitelist/")
11+
@GET("api/whitelist")
1212
suspend fun getWhitelist(@Header("Authorization") token: String): Response<WhitelistResponse>
1313

1414
@POST("api/whitelist/add")

app/src/main/java/com/secure/privacyfirst/network/RetrofitClient.kt

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,34 @@ package com.secure.privacyfirst.network
22

33
import com.google.gson.Gson
44
import com.google.gson.GsonBuilder
5+
import okhttp3.Cache
6+
import okhttp3.CacheControl
7+
import okhttp3.Interceptor
58
import okhttp3.OkHttpClient
69
import okhttp3.logging.HttpLoggingInterceptor
710
import retrofit2.Retrofit
811
import retrofit2.converter.gson.GsonConverterFactory
12+
import java.io.File
913
import java.util.concurrent.TimeUnit
14+
import android.content.Context
1015

1116
object RetrofitClient {
1217

13-
// Server IP - update this with your server IP
18+
// Server IP - updated to match provided documentation
1419
private const val BASE_URL = "http://192.168.2.244:5001/"
1520

21+
// Cache size: 10 MB
22+
private const val CACHE_SIZE = 10 * 1024 * 1024L
23+
24+
private var cacheDir: File? = null
25+
26+
/**
27+
* Initialize the cache directory. Call this from Application context.
28+
*/
29+
fun init(context: Context) {
30+
cacheDir = File(context.cacheDir, "http_cache")
31+
}
32+
1633
private val gson: Gson = GsonBuilder()
1734
.setLenient()
1835
.create()
@@ -21,18 +38,61 @@ object RetrofitClient {
2138
level = HttpLoggingInterceptor.Level.BODY
2239
}
2340

24-
private val okHttpClient = OkHttpClient.Builder()
25-
.addInterceptor(loggingInterceptor)
26-
.connectTimeout(30, TimeUnit.SECONDS)
27-
.readTimeout(30, TimeUnit.SECONDS)
28-
.writeTimeout(30, TimeUnit.SECONDS)
29-
.build()
41+
/**
42+
* Cache interceptor for GET requests
43+
*/
44+
private val cacheInterceptor = Interceptor { chain ->
45+
val response = chain.proceed(chain.request())
46+
val cacheControl = CacheControl.Builder()
47+
.maxAge(5, TimeUnit.MINUTES) // Cache whitelist for 5 minutes
48+
.build()
49+
response.newBuilder()
50+
.header("Cache-Control", cacheControl.toString())
51+
.build()
52+
}
3053

31-
private val retrofit: Retrofit = Retrofit.Builder()
32-
.baseUrl(BASE_URL)
33-
.client(okHttpClient)
34-
.addConverterFactory(GsonConverterFactory.create(gson))
35-
.build()
54+
/**
55+
* Offline cache interceptor
56+
*/
57+
private val offlineCacheInterceptor = Interceptor { chain ->
58+
var request = chain.request()
59+
if (request.method == "GET") {
60+
val cacheControl = CacheControl.Builder()
61+
.maxStale(7, TimeUnit.DAYS) // Serve stale cache for up to 7 days if offline
62+
.build()
63+
request = request.newBuilder()
64+
.cacheControl(cacheControl)
65+
.build()
66+
}
67+
chain.proceed(request)
68+
}
3669

37-
val apiService: ApiService = retrofit.create(ApiService::class.java)
70+
private val okHttpClient by lazy {
71+
OkHttpClient.Builder()
72+
.addInterceptor(loggingInterceptor)
73+
.addNetworkInterceptor(cacheInterceptor)
74+
.addInterceptor(offlineCacheInterceptor)
75+
.apply {
76+
cacheDir?.let {
77+
cache(Cache(it, CACHE_SIZE))
78+
}
79+
}
80+
.connectTimeout(15, TimeUnit.SECONDS) // Reduced from 30s for faster failure detection
81+
.readTimeout(20, TimeUnit.SECONDS)
82+
.writeTimeout(20, TimeUnit.SECONDS)
83+
.retryOnConnectionFailure(true) // Auto retry on connection failure
84+
.build()
85+
}
86+
87+
private val retrofit: Retrofit by lazy {
88+
Retrofit.Builder()
89+
.baseUrl(BASE_URL)
90+
.client(okHttpClient)
91+
.addConverterFactory(GsonConverterFactory.create(gson))
92+
.build()
93+
}
94+
95+
val apiService: ApiService by lazy {
96+
retrofit.create(ApiService::class.java)
97+
}
3898
}

app/src/main/java/com/secure/privacyfirst/network/WhitelistRepository.kt

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,18 @@ class WhitelistRepository(private val context: Context) {
1010
private val apiService = RetrofitClient.apiService
1111
private val tokenManager = TokenManager(context)
1212

13+
// In-memory cache for whitelist
14+
private var cachedWhitelist: List<String>? = null
15+
private var cacheTimestamp: Long = 0
16+
private val cacheDuration = 5 * 60 * 1000L // 5 minutes
17+
1318
companion object {
1419
private const val TAG = "WhitelistRepository"
1520
private const val ADMIN_USERNAME = "admin"
16-
private const val ADMIN_PASSWORD = "pass123"
21+
// Updated password to match API documentation
22+
private const val ADMIN_PASSWORD = "Byf8\$G&F*G8vGEfuhfuhfEHU!89f2qfiHT88%ffyutf7^s"
23+
private const val MAX_RETRY_ATTEMPTS = 3
24+
private const val INITIAL_RETRY_DELAY = 1000L // 1 second
1725
}
1826

1927
/**
@@ -41,15 +49,22 @@ class WhitelistRepository(private val context: Context) {
4149
}
4250

4351
/**
44-
* Get whitelist URLs from server
52+
* Get whitelist URLs from server with caching and retry logic
4553
*/
46-
suspend fun getWhitelist(): Result<List<String>> {
54+
suspend fun getWhitelist(forceRefresh: Boolean = false): Result<List<String>> {
4755
return withContext(Dispatchers.IO) {
4856
try {
57+
// Check in-memory cache first
58+
if (!forceRefresh && cachedWhitelist != null &&
59+
System.currentTimeMillis() - cacheTimestamp < cacheDuration) {
60+
Log.d(TAG, "Returning cached whitelist: ${cachedWhitelist!!.size} URLs")
61+
return@withContext Result.success(cachedWhitelist!!)
62+
}
63+
4964
// Check if token is valid, if not login first
5065
if (!tokenManager.isTokenValid()) {
5166
Log.d(TAG, "Token expired or missing, logging in...")
52-
val loginResult = login()
67+
val loginResult = loginWithRetry()
5368
if (loginResult.isFailure) {
5469
return@withContext Result.failure(loginResult.exceptionOrNull() ?: Exception("Login failed"))
5570
}
@@ -61,23 +76,106 @@ class WhitelistRepository(private val context: Context) {
6176
return@withContext Result.failure(Exception("No authentication token"))
6277
}
6378

64-
val response = apiService.getWhitelist(authHeader)
79+
// Fetch with retry logic
80+
val response = retryApiCall { apiService.getWhitelist(authHeader) }
81+
6582
if (response.isSuccessful && response.body() != null) {
6683
val urls = response.body()!!.urls
84+
// Update cache
85+
cachedWhitelist = urls
86+
cacheTimestamp = System.currentTimeMillis()
6787
Log.d(TAG, "Whitelist fetched successfully: ${urls.size} URLs")
6888
Result.success(urls)
89+
} else if (response.code() == 401) {
90+
// Token might be invalid, try re-login and retry
91+
Log.d(TAG, "Received 401, attempting re-login...")
92+
val loginResult = loginWithRetry()
93+
if (loginResult.isSuccess) {
94+
val newAuthHeader = tokenManager.getAuthHeader()!!
95+
val retryResponse = apiService.getWhitelist(newAuthHeader)
96+
if (retryResponse.isSuccessful && retryResponse.body() != null) {
97+
val urls = retryResponse.body()!!.urls
98+
cachedWhitelist = urls
99+
cacheTimestamp = System.currentTimeMillis()
100+
Log.d(TAG, "Whitelist fetched after re-login: ${urls.size} URLs")
101+
return@withContext Result.success(urls)
102+
}
103+
}
104+
Result.failure(Exception("Authentication failed"))
69105
} else {
70106
val error = "Failed to fetch whitelist: ${response.code()}"
71107
Log.e(TAG, error)
72108
Result.failure(Exception(error))
73109
}
74110
} catch (e: Exception) {
75111
Log.e(TAG, "Error fetching whitelist: ${e.message}", e)
76-
Result.failure(e)
112+
// Return cached data if available even on error
113+
if (cachedWhitelist != null) {
114+
Log.d(TAG, "Returning stale cache due to error")
115+
Result.success(cachedWhitelist!!)
116+
} else {
117+
Result.failure(e)
118+
}
77119
}
78120
}
79121
}
80122

123+
/**
124+
* Login with exponential backoff retry
125+
*/
126+
private suspend fun loginWithRetry(): Result<String> {
127+
var lastException: Exception? = null
128+
repeat(MAX_RETRY_ATTEMPTS) { attempt ->
129+
try {
130+
val result = login()
131+
if (result.isSuccess) {
132+
return result
133+
}
134+
lastException = result.exceptionOrNull() as? Exception
135+
} catch (e: Exception) {
136+
lastException = e
137+
}
138+
139+
if (attempt < MAX_RETRY_ATTEMPTS - 1) {
140+
val delay = INITIAL_RETRY_DELAY * (1 shl attempt) // Exponential backoff
141+
Log.d(TAG, "Login attempt ${attempt + 1} failed, retrying in ${delay}ms...")
142+
kotlinx.coroutines.delay(delay)
143+
}
144+
}
145+
return Result.failure(lastException ?: Exception("Login failed after $MAX_RETRY_ATTEMPTS attempts"))
146+
}
147+
148+
/**
149+
* Generic retry logic for API calls
150+
*/
151+
private suspend fun <T> retryApiCall(
152+
apiCall: suspend () -> retrofit2.Response<T>
153+
): retrofit2.Response<T> {
154+
var lastException: Exception? = null
155+
repeat(MAX_RETRY_ATTEMPTS) { attempt ->
156+
try {
157+
return apiCall()
158+
} catch (e: Exception) {
159+
lastException = e
160+
if (attempt < MAX_RETRY_ATTEMPTS - 1) {
161+
val delay = INITIAL_RETRY_DELAY * (1 shl attempt)
162+
Log.d(TAG, "API call attempt ${attempt + 1} failed, retrying in ${delay}ms...")
163+
kotlinx.coroutines.delay(delay)
164+
}
165+
}
166+
}
167+
throw lastException ?: Exception("API call failed after $MAX_RETRY_ATTEMPTS attempts")
168+
}
169+
170+
/**
171+
* Clear the in-memory cache
172+
*/
173+
fun clearCache() {
174+
cachedWhitelist = null
175+
cacheTimestamp = 0
176+
Log.d(TAG, "Cache cleared")
177+
}
178+
81179
/**
82180
* Add URL to whitelist
83181
*/

0 commit comments

Comments
 (0)