Skip to content

Commit f57d0c5

Browse files
authored
Native Login (#28)
1 parent bbbd613 commit f57d0c5

25 files changed

+837
-501
lines changed

README.md

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -84,34 +84,35 @@ To use, share or open any file and choose the Django Files app.
8484
The app will then upload the file to your Django Files server.
8585
Additionally, the URL is copied to the clipboard and the preview show in the app.
8686

87-
> [!IMPORTANT]
88-
> If you use 2Factor, Local or GitHub OAuth is recommended.
89-
9087
## Features
9188

92-
- Share or Open any file (or multiple) and automatically copy the URL to the clipboard.
89+
- Share or Open any file and automatically copy the URL to the clipboard.
9390
- Ability to add multiple servers and switch on the fly from the Server List menu.
94-
- Supports Local Login, GitHub OAuth, Google OAuth, Discord OAuth (w/o passkeys).
91+
- Supports Native Local Login, GitHub OAuth, Google OAuth, and Discord OAuth.
9592
- Native Upload feature from Navigation Drawer and Upload Shortcut on Icon long press.
96-
- Basic Native File List with Share/Open links, Infinite Scroll, Custom Per Page option.
93+
- Basic Native File List with Links, Preview, Infinite Scroll, Custom Per Page option.
9794

9895
### Planned
9996

100-
- Ability to Shorten URL's.
101-
- Widget with Stats and quick options.
102-
- Option to Disable Preview for files and links.
103-
- Option to Preview file(s) before uploading w/ server selector and upload options.
104-
- Native App Login to support Discord passkeys and Google App Authentication.
105-
- Ability to Authenticate if your session (cookie) is expired or invalidated.
97+
- Add Default Upload Options.
98+
- Add Custom Options to Preview page.
99+
- Option to Disable Preview Page on Open, Share, and for Links.
100+
- Ability to Authenticate if/when your session (cookie) is expired.
101+
- Files List
102+
- File Options on Preview
103+
- Context Menu Button w/ Options
104+
- Multi-Select w/ Options
105+
- Response Caching for Infinite Scroll
106106

107107
### Known Issues
108108

109-
- Login with Discord OAuth passkeys does not work.
110-
- Login with Google OAuth gives an error; however, may succeed if you wait ~30 seconds.
111109
- The app gets logged out if the session expires; however, sharing continues to work.
112-
- Upon logging back in, you may need to leave the app open (in the background) for ~30 seconds to allow the cookie to be saved.
113-
- Deleting the active server does not deactivate it, and it remains active until selecting another server.
114-
- Uploading files from the website works; however, taking picture and recording video and audio does not.
110+
- Uploading files from the website works; however, taking picture and recording video/audio does not.
111+
- Logging out and deleting servers may have some unexpected results but should work.
112+
113+
Note: If the app gets in a bad state, clear the app data or reinstall the application.
114+
115+
For more planned features you can check out the internal [TODO.md](TODO.md).
115116

116117
# Development
117118

TODO.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
# TODO
22

3-
- Implement Retrofit into Current Settings
4-
- ❓ Implement Room into Retrofit (or keep separate)
5-
- ❓ Replace SettingsActivity with a SettingsFragment
3+
## Upload
64

7-
## Layouts
5+
- Add Upload Options to Previews
6+
- Add Support for Multiple Files
87

9-
- Server Setup (Login)
10-
- Text Preview
11-
- Add Server
12-
- Edit Server
8+
## File List
139

14-
# Retrofit
10+
- Add Context Menu
11+
- Add Multi-Select
12+
- Add Options to Preview
13+
- Add Swiper to Preview
1514

16-
- Server and Version Check
17-
- Authentication
18-
- ❓ Server Settings
15+
## Settings
16+
17+
- Overhaul Server Selector
18+
- Add Default Upload Options
19+
20+
## Retrofit
21+
22+
- Implement Version Check

app/build.gradle.kts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ plugins {
22
alias(libs.plugins.android.application)
33
alias(libs.plugins.kotlin.android)
44
alias(libs.plugins.ksp)
5-
alias(libs.plugins.kotlin.parcelize).apply {
6-
javaClass.getDeclaredField("version").apply { isAccessible = true }.set(this, null)
7-
}
5+
//alias(libs.plugins.kotlin.kapt)
6+
alias(libs.plugins.kotlin.parcelize)
87
//alias(libs.plugins.androidx.navigation.safeargs.kotlin-gradle-plugin)
98
//id("androidx.navigation.safeargs.kotlin") version "2.8.9" // Use the correct version
109
}
@@ -48,6 +47,7 @@ android {
4847
}
4948
buildFeatures {
5049
viewBinding = true
50+
//dataBinding = true
5151
}
5252
}
5353

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

Lines changed: 146 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,28 @@ import android.net.Uri
1111
import android.os.Build
1212
import android.os.Bundle
1313
import android.util.Log
14+
import android.webkit.CookieManager
1415
import android.widget.TextView
1516
import android.widget.Toast
1617
import androidx.activity.enableEdgeToEdge
1718
import androidx.activity.result.ActivityResultLauncher
1819
import androidx.activity.result.contract.ActivityResultContracts
1920
import androidx.activity.viewModels
2021
import androidx.appcompat.app.AppCompatActivity
22+
import androidx.core.content.edit
23+
import androidx.core.net.toUri
2124
import androidx.core.view.GravityCompat
2225
import androidx.drawerlayout.widget.DrawerLayout
26+
import androidx.lifecycle.lifecycleScope
2327
import androidx.navigation.NavController
2428
import androidx.navigation.NavOptions
2529
import androidx.navigation.fragment.NavHostFragment
2630
import androidx.navigation.ui.NavigationUI
2731
import com.djangofiles.djangofiles.databinding.ActivityMainBinding
2832
import com.djangofiles.djangofiles.ui.home.HomeViewModel
33+
import kotlinx.coroutines.Dispatchers
34+
import kotlinx.coroutines.launch
35+
import kotlinx.coroutines.withContext
2936
import java.net.URL
3037

3138
class MainActivity : AppCompatActivity() {
@@ -149,29 +156,35 @@ class MainActivity : AppCompatActivity() {
149156

150157
private fun handleIntent(intent: Intent, savedInstanceState: Bundle?) {
151158
Log.d("handleIntent", "intent: $intent")
152-
Log.d("handleIntent", "intent.data: ${intent.data}")
153-
Log.d("handleIntent", "intent.type: ${intent.type}")
154-
Log.d("handleIntent", "intent.action: ${intent.action}")
159+
val data = intent.data
160+
val type = intent.type
161+
val action = intent.action
162+
Log.d("handleIntent", "data: $data")
163+
Log.d("handleIntent", "type: $type")
164+
Log.d("handleIntent", "action: $action")
155165

156166
val extraText = intent.getStringExtra(Intent.EXTRA_TEXT)
157167
Log.d("handleIntent", "extraText: $extraText")
158168

159169
val sharedPreferences = getSharedPreferences("AppPreferences", MODE_PRIVATE)
160170
val savedUrl = sharedPreferences.getString("saved_url", null)
161171
Log.d("handleIntent", "savedUrl: $savedUrl")
172+
Log.d("handleIntent", "data?.host: ${data?.host}")
162173
//val authToken = sharedPreferences.getString("auth_token", null)
163174
//Log.d("handleIntent", "authToken: $authToken")
164175

165-
if (savedUrl.isNullOrEmpty()) {
166-
Log.i("handleIntent", "Missing Saved URL or Token...")
176+
Log.d("handleIntent", "data?.host: ${data?.host}")
177+
if (data?.host != "oauth" && savedUrl.isNullOrEmpty()) {
178+
Log.i("handleIntent", "Missing Saved URL or Token! Showing Login...")
167179

180+
setDrawerLockMode(false)
168181
navController.navigate(
169-
R.id.nav_item_setup, null, NavOptions.Builder()
182+
R.id.nav_item_login, null, NavOptions.Builder()
170183
.setPopUpTo(R.id.nav_item_home, true)
171184
.build()
172185
)
173186

174-
} else if (Intent.ACTION_MAIN == intent.action) {
187+
} else if (Intent.ACTION_MAIN == action) {
175188
Log.d("handleIntent", "ACTION_MAIN: ${savedInstanceState?.size()}")
176189

177190
binding.drawerLayout.closeDrawers()
@@ -207,7 +220,7 @@ class MainActivity : AppCompatActivity() {
207220
filePickerLauncher.launch(arrayOf("*/*"))
208221
}
209222

210-
} else if (Intent.ACTION_SEND == intent.action) {
223+
} else if (Intent.ACTION_SEND == action) {
211224
Log.d("handleIntent", "ACTION_SEND")
212225

213226
val fileUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -243,10 +256,10 @@ class MainActivity : AppCompatActivity() {
243256
Log.w("handleIntent", "NOT IMPLEMENTED")
244257
}
245258
} else {
246-
showPreview(fileUri, intent.type)
259+
showPreview(fileUri, type)
247260
}
248261

249-
} else if (Intent.ACTION_SEND_MULTIPLE == intent.action) {
262+
} else if (Intent.ACTION_SEND_MULTIPLE == action) {
250263
Log.d("handleIntent", "ACTION_SEND_MULTIPLE")
251264

252265
val fileUris = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -267,27 +280,35 @@ class MainActivity : AppCompatActivity() {
267280
Toast.makeText(this, "Not Yet Implemented!", Toast.LENGTH_LONG).show()
268281
Log.w("handleIntent", "NOT IMPLEMENTED")
269282

270-
} else if (Intent.ACTION_VIEW == intent.action) {
283+
} else if (Intent.ACTION_VIEW == action) {
271284
Log.d("handleIntent", "ACTION_VIEW")
272285

273-
if ("djangofiles" == intent.data?.scheme) {
274-
Log.d("handleIntent", "scheme: ${intent.data?.scheme}")
275-
val host = intent.data?.host
276-
Log.d("handleIntent", "host: $host")
277-
if ("serverlist" == host) {
286+
if (data == null) {
287+
Toast.makeText(this, "That's a Bug!", Toast.LENGTH_LONG).show()
288+
Log.e("handleIntent", "BUG: UNKNOWN action: $action")
289+
return
290+
}
291+
if ("djangofiles" == data.scheme) {
292+
Log.d("handleIntent", "scheme: ${data.scheme}")
293+
Log.d("handleIntent", "host: ${data.host}")
294+
if ("serverlist" == data.host) {
278295
Log.d("handleIntent", "djangofiles://serverlist")
279296
navController.navigate(R.id.nav_item_settings)
297+
} else if ("logout" == data.host) {
298+
processLogout()
299+
} else if ("oauth" == data.host) {
300+
processOauth(data)
280301
} else {
281302
Toast.makeText(this, "Unknown DeepLink!", Toast.LENGTH_LONG).show()
282303
Log.w("handleIntent", "Unknown DeepLink!")
283304
}
284305
} else {
285-
Log.d("handleIntent", "File URI: ${intent.data}")
286-
showPreview(intent.data, intent.type)
306+
Log.d("handleIntent", "File URI: $data")
307+
showPreview(data, type)
287308
}
288309
} else {
289310
Toast.makeText(this, "That's a Bug!", Toast.LENGTH_LONG).show()
290-
Log.e("handleIntent", "BUG: UNKNOWN intent.action: ${intent.action}")
311+
Log.e("handleIntent", "BUG: UNKNOWN action: $action")
291312
}
292313
}
293314

@@ -315,6 +336,112 @@ class MainActivity : AppCompatActivity() {
315336
// .build()
316337
//navController.navigate(R.id.nav_item_preview, bundle, navOptions)
317338
}
339+
340+
private fun processOauth(data: Uri) {
341+
// TODO: Can do this in a fragment to show loading screen/errors eventually...
342+
Log.d("handleIntent", "processOauth: data: $data")
343+
val token = data.getQueryParameter("token")
344+
val sessionKey = data.getQueryParameter("session_key")
345+
val error = data.getQueryParameter("error")
346+
347+
// TODO: Handle null data and errors
348+
Log.d("handleIntent", "token: $token")
349+
Log.d("handleIntent", "session_key: $sessionKey")
350+
Log.d("handleIntent", "error: $error")
351+
352+
// TODO: Determine how to better get oauthUrl
353+
val sharedPreferences = this.getSharedPreferences("AppPreferences", MODE_PRIVATE)
354+
val oauthUrl = sharedPreferences.getString("oauth_host", null)
355+
Log.d("handleIntent", "oauthUrl: $oauthUrl")
356+
if (oauthUrl == null) {
357+
// TODO: Handle this error...
358+
Log.e("handleIntent", "oauthUrl is null")
359+
return
360+
}
361+
362+
sharedPreferences.edit {
363+
putString("saved_url", oauthUrl)
364+
putString("auth_token", token)
365+
}
366+
lifecycleScope.launch {
367+
val dao: ServerDao =
368+
ServerDatabase.getInstance(this@MainActivity).serverDao()
369+
withContext(Dispatchers.IO) {
370+
dao.add(Server(url = oauthUrl, token = token!!, active = true))
371+
}
372+
}
373+
374+
val cookieManager = CookieManager.getInstance()
375+
//cookieManager.setAcceptThirdPartyCookies(webView, true)
376+
val cookie = "sessionid=$sessionKey; Path=/; HttpOnly; Secure"
377+
Log.d("handleIntent", "cookie: $cookie")
378+
379+
val uri = oauthUrl.toUri()
380+
val origin = "${uri.scheme}://${uri.authority}"
381+
Log.d("handleIntent", "origin: $origin")
382+
cookieManager.setCookie(origin, cookie) { cookieManager.flush() }
383+
384+
Log.d("handleIntent", "navigate: nav_item_home - setPopUpTo: nav_item_login")
385+
setDrawerLockMode(true)
386+
navController.navigate(
387+
R.id.nav_item_home, null, NavOptions.Builder()
388+
.setPopUpTo(R.id.nav_item_login, true)
389+
.build()
390+
)
391+
}
392+
393+
private fun processLogout() {
394+
val sharedPreferences = this.getSharedPreferences("AppPreferences", MODE_PRIVATE)
395+
val savedUrl = sharedPreferences.getString("saved_url", null)
396+
Log.d("processLogout", "savedUrl: $savedUrl")
397+
sharedPreferences.edit {
398+
remove("saved_url")
399+
remove("auth_token")
400+
}
401+
val dao: ServerDao = ServerDatabase.getInstance(this).serverDao()
402+
lifecycleScope.launch {
403+
if (savedUrl != null) {
404+
Log.d("processLogout", "dao.delete: $savedUrl")
405+
val server = Server(url = savedUrl)
406+
Log.d("processLogout", "server: $server")
407+
withContext(Dispatchers.IO) { dao.delete(server) }
408+
}
409+
val servers = withContext(Dispatchers.IO) { dao.getAll() }
410+
Log.d("processLogout", "servers: $servers")
411+
if (servers.isEmpty()) {
412+
Log.d("processLogout", "NO MORE SERVERS - LOCK TO LOGIN")
413+
//(requireActivity() as MainActivity).setDrawerLockMode(false)
414+
setDrawerLockMode(false)
415+
// TODO: Confirm this removes history and locks user to login
416+
navController.navigate(
417+
R.id.nav_item_login, null, NavOptions.Builder()
418+
.setPopUpTo(R.id.nav_item_home, true)
419+
.build()
420+
)
421+
} else {
422+
Log.d("processLogout", "MORE SERVERS - ACTIVATE ONE")
423+
//servers.firstOrNull()?.let { dao.activate(it.url) }
424+
val server = servers.first()
425+
Log.d("processLogout", "server: $server")
426+
withContext(Dispatchers.IO) { dao.activate(server.url) }
427+
428+
sharedPreferences.edit().apply {
429+
putString("saved_url", server.url)
430+
putString("auth_token", server.token)
431+
apply()
432+
}
433+
434+
Log.d("processLogout", "navigate: nav_item_login")
435+
// TODO: Determine proper navigate call here...
436+
navController.navigate(R.id.nav_item_settings)
437+
//findNavController().navigate(
438+
// R.id.nav_item_settings, null, NavOptions.Builder()
439+
// .setPopUpTo(R.id.nav_item_home, true)
440+
// .build()
441+
//)
442+
}
443+
}
444+
}
318445
}
319446

320447
fun copyToClipboard(context: Context, url: String) {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ interface ServerDao {
2929
@Insert
3030
fun add(server: Server)
3131

32+
@Query("UPDATE server SET active = 1 WHERE url = :url")
33+
fun activate(url: String)
34+
3235
@Delete
3336
fun delete(server: Server)
3437
}

0 commit comments

Comments
 (0)