Skip to content

Commit 0a10f72

Browse files
authored
Retrofit (#16)
- Adding Retrofit API - Add URL Shortening
1 parent 2446290 commit 0a10f72

File tree

10 files changed

+491
-291
lines changed

10 files changed

+491
-291
lines changed

TODO.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# TODO
2+
3+
- Implement Retrofit into Current Settings
4+
- ❓ Implement Room into Retrofit (or keep separate)
5+
- ❓ Replace SettingsActivity with a SettingsFragment
6+
- ❓ Update Navigation with addToBackStack (like Zipline)
7+
8+
## Layouts
9+
10+
- Server Setup (Login)
11+
- File(s) Preview
12+
- Text Preview
13+
- URL Preview
14+
- Add Server
15+
- Edit Server
16+
17+
# Retrofit
18+
19+
- Server and Version Check
20+
- Authentication
21+
- ❓ Server Settings

app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ dependencies {
4747
implementation(libs.androidx.activity)
4848
implementation(libs.androidx.preference.ktx)
4949
implementation(libs.okhttp)
50+
implementation(libs.retrofit)
51+
implementation(libs.retrofit.gson)
5052
implementation(libs.room.ktx)
5153
implementation(libs.room.runtime)
5254
ksp(libs.room.compiler)

app/proguard-rules.pro

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,60 @@
2828
public static int w(...);
2929
public static int e(...);
3030
}
31+
32+
# Retrofit
33+
-keep class com.djangofiles.djangofiles.api.ServerApi$* { *; }
34+
35+
-keepclassmembers class * {
36+
@com.google.gson.annotations.SerializedName <fields>;
37+
}
38+
39+
# https://github.com/square/retrofit/blob/trunk/retrofit/src/main/resources/META-INF/proguard/retrofit2.pro
40+
# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
41+
# EnclosingMethod is required to use InnerClasses.
42+
-keepattributes Signature, InnerClasses, EnclosingMethod
43+
44+
# Retrofit does reflection on method and parameter annotations.
45+
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
46+
47+
# Keep annotation default values (e.g., retrofit2.http.Field.encoded).
48+
-keepattributes AnnotationDefault
49+
50+
# Retain service method parameters when optimizing.
51+
-keepclassmembers,allowshrinking,allowobfuscation interface * {
52+
@retrofit2.http.* <methods>;
53+
}
54+
55+
# Ignore annotation used for build tooling.
56+
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
57+
58+
# Ignore JSR 305 annotations for embedding nullability information.
59+
-dontwarn javax.annotation.**
60+
61+
# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath.
62+
-dontwarn kotlin.Unit
63+
64+
# Top-level functions that can only be used by Kotlin.
65+
-dontwarn retrofit2.KotlinExtensions
66+
-dontwarn retrofit2.KotlinExtensions$*
67+
68+
# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
69+
# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
70+
-if interface * { @retrofit2.http.* <methods>; }
71+
-keep,allowobfuscation interface <1>
72+
73+
# Keep inherited services.
74+
-if interface * { @retrofit2.http.* <methods>; }
75+
-keep,allowobfuscation interface * extends <1>
76+
77+
# With R8 full mode generic signatures are stripped for classes that are not
78+
# kept. Suspend functions are wrapped in continuations where the type argument
79+
# is used.
80+
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
81+
82+
# R8 full mode strips generic signatures from return types if not kept.
83+
-if interface * { @retrofit2.http.* public *** *(...); }
84+
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
85+
86+
# With R8 full mode generic signatures are stripped for classes that are not kept.
87+
-keep,allowobfuscation,allowshrinking class retrofit2.Response

app/src/main/java/com/djangofiles/djangofiles/MainActivity.kt

Lines changed: 194 additions & 226 deletions
Large diffs are not rendered by default.
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package com.djangofiles.djangofiles.api
2+
3+
import android.content.Context
4+
import android.content.Context.MODE_PRIVATE
5+
import android.content.SharedPreferences
6+
import android.util.Log
7+
import com.google.gson.annotations.SerializedName
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.withContext
10+
import okhttp3.Cookie
11+
import okhttp3.CookieJar
12+
import okhttp3.HttpUrl
13+
import okhttp3.MediaType.Companion.toMediaTypeOrNull
14+
import okhttp3.MultipartBody
15+
import okhttp3.OkHttpClient
16+
import okhttp3.RequestBody.Companion.toRequestBody
17+
import okhttp3.ResponseBody
18+
import retrofit2.Response
19+
import retrofit2.Retrofit
20+
import retrofit2.converter.gson.GsonConverterFactory
21+
import retrofit2.http.Body
22+
import retrofit2.http.Header
23+
import retrofit2.http.Multipart
24+
import retrofit2.http.POST
25+
import retrofit2.http.Part
26+
import java.io.InputStream
27+
import java.net.URLConnection
28+
29+
30+
// TODO: Pass preferences instead of context since context is not used
31+
class ServerApi(context: Context, host: String) {
32+
val api: ApiService
33+
val authToken: String
34+
val preferences: SharedPreferences =
35+
context.getSharedPreferences("AppPreferences", MODE_PRIVATE)
36+
37+
private lateinit var cookieJar: SimpleCookieJar
38+
private lateinit var client: OkHttpClient
39+
40+
init {
41+
api = createRetrofit(host).create(ApiService::class.java)
42+
authToken = preferences.getString("auth_token", null) ?: ""
43+
}
44+
45+
suspend fun upload(fileName: String, inputStream: InputStream): Response<FileResponse> {
46+
Log.d("upload", "fileName: $fileName")
47+
val multiPart: MultipartBody.Part = inputStreamToMultipart(inputStream, fileName)
48+
return api.postUpload(authToken, multiPart)
49+
}
50+
51+
suspend fun shorten(url: String): Response<ShortResponse> {
52+
Log.d("shorten", "url: $url")
53+
return api.postShort(authToken, url)
54+
}
55+
56+
// TODO: Use VersionResponse
57+
suspend fun version(version: String): Response<ResponseBody> {
58+
Log.d("version", "version: $version")
59+
return api.postVersion(authToken, VersionRequest(version))
60+
}
61+
62+
interface ApiService {
63+
@Multipart
64+
@POST("upload")
65+
suspend fun postUpload(
66+
@Header("Authorization") token: String,
67+
@Part file: MultipartBody.Part,
68+
@Header("Format") format: String? = null,
69+
@Header("Expires-At") expiresAt: String? = null,
70+
@Header("Strip-GPS") stripGps: String? = null,
71+
@Header("Strip-EXIF") stripExif: String? = null,
72+
@Header("Private") private: String? = null,
73+
@Header("Password") password: String? = null,
74+
): Response<FileResponse>
75+
76+
@POST("shorten")
77+
suspend fun postShort(
78+
@Header("Authorization") token: String,
79+
@Header("URL") url: String? = null,
80+
@Header("Vanity") vanity: String? = null,
81+
@Header("Max-Views") maxViews: Number? = null,
82+
): Response<ShortResponse>
83+
84+
// TODO: Use VersionResponse
85+
@POST("version")
86+
suspend fun postVersion(
87+
@Header("Authorization") token: String,
88+
@Body version: VersionRequest
89+
): Response<ResponseBody>
90+
}
91+
92+
data class FileResponse(
93+
val url: String,
94+
val raw: String,
95+
val name: String,
96+
val size: Long
97+
)
98+
99+
data class ShortResponse(
100+
val url: String,
101+
val vanity: String,
102+
@SerializedName("max-views") val maxViews: Number,
103+
)
104+
105+
data class VersionRequest(val version: String)
106+
data class VersionResponse(
107+
val version: String,
108+
val valid: Boolean,
109+
)
110+
111+
private suspend fun inputStreamToMultipart(
112+
file: InputStream,
113+
fileName: String
114+
): MultipartBody.Part {
115+
val contentType =
116+
URLConnection.guessContentTypeFromName(fileName) ?: "application/octet-stream"
117+
Log.d("inputStreamToMultipart", "contentType: $contentType")
118+
val bytes = withContext(Dispatchers.IO) { file.readBytes() }
119+
val requestBody = bytes.toRequestBody(contentType.toMediaTypeOrNull())
120+
return MultipartBody.Part.createFormData("file", fileName, requestBody)
121+
}
122+
123+
private fun createRetrofit(host: String): Retrofit {
124+
val baseUrl = "${host}/api/"
125+
Log.d("createRetrofit", "baseUrl: $baseUrl")
126+
cookieJar = SimpleCookieJar()
127+
client = OkHttpClient.Builder()
128+
.cookieJar(cookieJar)
129+
.build()
130+
return Retrofit.Builder()
131+
.baseUrl(baseUrl)
132+
.addConverterFactory(GsonConverterFactory.create())
133+
.client(client)
134+
.build()
135+
}
136+
137+
inner class SimpleCookieJar : CookieJar {
138+
private val cookieStore = mutableMapOf<String, List<Cookie>>()
139+
140+
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
141+
cookieStore[url.host] = cookies
142+
}
143+
144+
override fun loadForRequest(url: HttpUrl): List<Cookie> {
145+
return cookieStore[url.host] ?: emptyList()
146+
}
147+
148+
//fun setCookie(url: HttpUrl, rawCookie: String) {
149+
// val cookies = Cookie.parseAll(url, Headers.headersOf("Set-Cookie", rawCookie))
150+
// cookieStore[url.host] = cookies
151+
//}
152+
}
153+
}

app/src/main/java/com/djangofiles/djangofiles/settings/SettingsFragment.kt

Lines changed: 38 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ import androidx.room.Query
2020
import androidx.room.Room
2121
import androidx.room.RoomDatabase
2222
import com.djangofiles.djangofiles.R
23+
import com.djangofiles.djangofiles.api.ServerApi
24+
import com.djangofiles.djangofiles.cleanUrl
2325
import kotlinx.coroutines.CoroutineScope
2426
import kotlinx.coroutines.Dispatchers
2527
import kotlinx.coroutines.launch
2628
import kotlinx.coroutines.withContext
27-
import okhttp3.OkHttpClient
28-
import okhttp3.Request
2929

3030
//import org.json.JSONArray
3131
//import android.util.Patterns
@@ -34,25 +34,32 @@ import okhttp3.Request
3434

3535
class SettingsFragment : PreferenceFragmentCompat() {
3636
private lateinit var dao: ServerDao
37-
private val client = OkHttpClient()
37+
private lateinit var versionName: String
3838

3939
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
4040
preferenceManager.sharedPreferencesName = "AppPreferences"
4141
setPreferencesFromResource(R.xml.pref_root, rootKey)
4242

43+
versionName = requireContext()
44+
.packageManager
45+
.getPackageInfo(requireContext().packageName, 0)
46+
.versionName ?: "Invalid Version"
47+
4348
dao = ServerDatabase.getInstance(requireContext()).serverDao()
4449

4550
buildServerList()
4651
setupAddServer()
4752

48-
CoroutineScope(Dispatchers.IO).launch {
49-
val serverList = dao.getAll() // Fetch data in background
50-
Log.d("onCreatePreferences", "serverList: $serverList")
51-
withContext(Dispatchers.Main) {
52-
// Update the UI on the main thread
53-
Log.d("onCreatePreferences", "IM ON THE UI BABY")
54-
}
55-
}
53+
// lifecycleScope.launch {
54+
// val serverList = withContext(Dispatchers.IO) {
55+
// dao.getAll()
56+
// }
57+
// Log.d("onCreatePreferences", "serverList: $serverList")
58+
// withContext(Dispatchers.Main) {
59+
// // Update the UI on the main thread
60+
// Log.d("onCreatePreferences", "IM ON THE UI BABY")
61+
// }
62+
// }
5663
}
5764

5865
//override fun onPause() {
@@ -81,75 +88,46 @@ class SettingsFragment : PreferenceFragmentCompat() {
8188
.setPositiveButton("Add", null)
8289
.show().apply {
8390
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
91+
// TODO: DUPLICATION: MainActivity
8492
var url = editText.text.toString().trim()
85-
Log.d("showSettingsDialog", "setPositiveButton URL: $url")
86-
87-
// TODO: Duplicate - MainActivity - make this a function
93+
Log.d("AddServer", "setPositiveButton URL: $url")
94+
url = cleanUrl(url)
95+
Log.d("AddServer", "cleanUrl: $url")
8896
if (url.isEmpty()) {
89-
Log.d("showSettingsDialog", "URL is Empty")
97+
Log.i("AddServer", "URL is Empty")
9098
editText.error = "This field is required."
9199
} else {
92-
if (!url.startsWith("http://") && !url.startsWith("https://")) {
93-
url = "https://$url"
94-
}
95-
if (url.endsWith("/")) {
96-
url = url.substring(0, url.length - 1)
97-
}
98-
100+
Log.d("AddServer", "Processing URL: $url")
99101
//val servers = loadServers().toMutableList()
100-
//Log.d("showSettingsDialog", "servers: $servers")
101-
102-
CoroutineScope(Dispatchers.IO).launch {
103-
val response = checkUrl(url)
104-
Log.d("showSettingsDialog", "response: $response")
102+
//Log.d("AddServer", "servers: $servers")
103+
val api = ServerApi(requireContext(), url)
104+
lifecycleScope.launch {
105+
val response = api.version(versionName)
106+
Log.d("AddServer", "response: $response")
105107
withContext(Dispatchers.Main) {
106-
if (response) {
107-
Log.d("showSettingsDialog", "SUCCESS")
108+
if (response.isSuccessful) {
109+
Log.d("AddServer", "SUCCESS")
110+
val dao: ServerDao =
111+
ServerDatabase.getInstance(requireContext()).serverDao()
112+
Log.d("showSettingsDialog", "dao.add Server url = $url")
113+
withContext(Dispatchers.IO) {
114+
dao.add(Server(url = url))
115+
}
108116
buildServerList()
109117
cancel()
110118
} else {
111-
Log.d("showSettingsDialog", "FAILURE")
119+
Log.d("AddServer", "FAILURE")
112120
editText.error = "Invalid URL"
113121
}
114122
}
115123
}
116124
}
117125
}
118126
}
119-
120127
true
121128
}
122129
}
123130

124-
private fun checkUrl(url: String): Boolean {
125-
Log.d("checkUrl", "checkUrl URL: $url")
126-
val existingServer = dao.getByUrl(url)
127-
Log.d("checkUrl", "existingServer: $existingServer")
128-
if (existingServer != null) {
129-
Log.d("checkUrl", "Error: Server Exists!")
130-
return false
131-
}
132-
133-
val authUrl = "${url}/api/auth/methods/"
134-
Log.d("showSettingsDialog", "Auth URL: $authUrl")
135-
136-
// TODO: Change this to HEAD or use response data...
137-
val request = Request.Builder().header("User-Agent", "DF").url(authUrl).get().build()
138-
return try {
139-
val response = client.newCall(request).execute()
140-
if (response.isSuccessful) {
141-
Log.d("checkUrl", "Success: Remote OK.")
142-
dao.add(Server(url = url))
143-
} else {
144-
Log.d("checkUrl", "Error: Remote code: ${response.code}")
145-
}
146-
response.isSuccessful
147-
} catch (e: Exception) {
148-
Log.d("checkUrl", "Exception: $e")
149-
false
150-
}
151-
}
152-
153131
private fun buildServerList() {
154132
lifecycleScope.launch {
155133
val servers = withContext(Dispatchers.IO) {

0 commit comments

Comments
 (0)