Add login flow

pull/653/merge
Varun Patil 2023-05-16 03:17:45 -07:00
parent 56308aa8aa
commit 6e10692962
5 changed files with 205 additions and 39 deletions

View File

@ -1,6 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="deploymentTargetDropDown"> <component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\varun\.android\avd\Pixel_6_API_33_2.avd" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<targetSelectedWithDropDown> <targetSelectedWithDropDown>
<Target> <Target>
<type value="QUICK_BOOT_TARGET" /> <type value="QUICK_BOOT_TARGET" />
@ -12,6 +23,6 @@
</deviceKey> </deviceKey>
</Target> </Target>
</targetSelectedWithDropDown> </targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-05-03T06:48:46.574240300Z" /> <timeTargetWasSelectedWithDropDown value="2023-05-16T08:14:59.213052500Z" />
</component> </component>
</project> </project>

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Memories</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<img src="memories.svg" alt="Memories Logo" class="logo">
<p>
Waiting for login to complete
</p>
</body>
</html>

View File

@ -25,6 +25,11 @@ import gallery.memories.databinding.ActivityMainBinding
ActivityMainBinding.inflate(layoutInflater) ActivityMainBinding.inflate(layoutInflater)
} }
companion object {
// replicate chrome: https://www.whatismybrowser.com/guides/the-latest-user-agent/chrome
val USER_AGENT = "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.76 Mobile Safari/537.36 Memories/0.0"
}
private lateinit var mNativeX: NativeX private lateinit var mNativeX: NativeX
private var player: ExoPlayer? = null private var player: ExoPlayer? = null
@ -135,12 +140,23 @@ import gallery.memories.databinding.ActivityMainBinding
webSettings.allowContentAccess = true webSettings.allowContentAccess = true
webSettings.domStorageEnabled = true webSettings.domStorageEnabled = true
webSettings.databaseEnabled = true webSettings.databaseEnabled = true
webSettings.userAgentString = "memories-native-android/0.0" webSettings.userAgentString = USER_AGENT
binding.webview.clearCache(true) binding.webview.clearCache(true)
binding.webview.addJavascriptInterface(mNativeX, "nativex") binding.webview.addJavascriptInterface(mNativeX, "nativex")
binding.webview.loadUrl("file:///android_asset/welcome.html");
binding.webview.setBackgroundColor(Color.TRANSPARENT) binding.webview.setBackgroundColor(Color.TRANSPARENT)
WebView.setWebContentsDebuggingEnabled(true); WebView.setWebContentsDebuggingEnabled(true);
// Load accounts
mNativeX.mAccountService.refreshAuthHeader()
val authHeader = mNativeX.mAccountService.authHeader
val memoriesUrl = mNativeX.mAccountService.memoriesUrl
if (authHeader != null && memoriesUrl != null) {
binding.webview.loadUrl(memoriesUrl, mapOf(
"Authorization" to authHeader
))
} else {
binding.webview.loadUrl("file:///android_asset/welcome.html");
}
} }
fun initializePlayer(uri: Uri, uid: String) { fun initializePlayer(uri: Uri, uid: String) {

View File

@ -1,6 +1,5 @@
package gallery.memories package gallery.memories
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import android.view.SoundEffectConstants import android.view.SoundEffectConstants
@ -10,23 +9,20 @@ import android.webkit.WebResourceResponse
import android.widget.Toast import android.widget.Toast
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import gallery.memories.mapper.SystemImage import gallery.memories.mapper.SystemImage
import gallery.memories.service.AccountService
import gallery.memories.service.DownloadService import gallery.memories.service.DownloadService
import gallery.memories.service.ImageService import gallery.memories.service.ImageService
import gallery.memories.service.TimelineQuery import gallery.memories.service.TimelineQuery
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.net.URLDecoder import java.net.URLDecoder
@UnstableApi class NativeX(private val mActivity: MainActivity) { @UnstableApi class NativeX(private val mActivity: MainActivity) {
val TAG = "NativeX" val TAG = "NativeX"
private val mImageService: ImageService = ImageService(mActivity)
private val mQuery: TimelineQuery = TimelineQuery(mActivity)
private var themeStored = false private var themeStored = false
private val mImageService = ImageService(mActivity)
private val mQuery = TimelineQuery(mActivity)
val mAccountService = AccountService(mActivity)
object API { object API {
val DAYS = Regex("^/api/days$") val DAYS = Regex("^/api/days$")
@ -105,34 +101,7 @@ import java.net.URLDecoder
@JavascriptInterface @JavascriptInterface
fun login(baseUrl: String?, loginFlowUrl: String?) { fun login(baseUrl: String?, loginFlowUrl: String?) {
if (baseUrl == null || loginFlowUrl == null) return; if (baseUrl == null || loginFlowUrl == null) return;
mAccountService.login(baseUrl, loginFlowUrl)
// Make POST request to login flow URL
val client = OkHttpClient()
val request = Request.Builder()
.url(loginFlowUrl)
.post("".toRequestBody("application/json".toMediaTypeOrNull()))
.build()
val response = client.newCall(request).execute()
// Read response body
val body = response.body?.string()
if (body == null) {
toast("Failed to get login flow response")
return
}
// Parse response body as JSON
val json = JSONObject(body)
try {
val loginUrl = json.getString("login")
toast("Opening login page...")
// Open login page in browser
mActivity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(loginUrl)))
} catch (e: Exception) {
Log.e(TAG, "login: ", e)
toast("Failed to parse login flow response")
}
} }
@JavascriptInterface @JavascriptInterface

View File

@ -0,0 +1,155 @@
package gallery.memories.service
import android.content.Intent
import android.net.Uri
import android.util.Base64
import android.util.Log
import android.widget.Toast
import gallery.memories.MainActivity
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
class AccountService(private val mActivity: MainActivity) {
companion object {
val TAG = "AccountService"
}
var authHeader: String? = null
var memoriesUrl: String? = null
private fun toast(message: String) {
mActivity.runOnUiThread {
Toast.makeText(mActivity, message, Toast.LENGTH_LONG).show()
}
}
fun login(baseUrl: String, loginFlowUrl: String) {
// Make POST request to login flow URL
val client = OkHttpClient()
val request = Request.Builder()
.url(loginFlowUrl)
.header("User-Agent", "Memories")
.post("".toRequestBody("application/json".toMediaTypeOrNull()))
.build()
val response = client.newCall(request).execute()
// Read response body
val body = response.body?.string()
response.body?.close()
if (body == null) {
toast("Failed to get login flow response")
return
}
// Parse response body as JSON
val json = JSONObject(body)
val pollToken: String
val pollUrl: String
val loginUrl: String
try {
val pollObj = json.getJSONObject("poll")
pollToken = pollObj.getString("token")
pollUrl = pollObj.getString("endpoint")
loginUrl = json.getString("login")
toast("Opening login page...")
// Open login page in browser
mActivity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(loginUrl)))
} catch (e: Exception) {
Log.e(TAG, "login: ", e)
toast("Failed to parse login flow response")
return
}
// Start polling in background
Thread {
pollLogin(pollUrl, pollToken, baseUrl)
}.start()
}
private fun pollLogin(pollUrl: String, pollToken: String, baseUrl: String) {
mActivity.binding.webview.post {
mActivity.binding.webview.loadUrl("file:///android_asset/waiting.html")
}
val client = OkHttpClient()
val rbody = "token=$pollToken".toRequestBody("application/x-www-form-urlencoded".toMediaTypeOrNull())
var pollCount = 0
while (true) {
pollCount += 3
if (pollCount >= 10 * 60) return
// Sleep for 3s
Thread.sleep(3000)
// Poll login flow URL
val request = Request.Builder()
.url(pollUrl)
.post(rbody)
.build()
val response = client.newCall(request).execute()
Log.v(TAG, "pollLogin: Got status code ${response.code}")
// Check status code
if (response.code != 200) {
response.body?.close()
continue
}
// Read response body
val body = response.body!!.string()
response.body?.close()
val json = JSONObject(body)
val loginName = json.getString("loginName")
val appPassword = json.getString("appPassword")
mActivity.runOnUiThread {
// Save login info (also updates header)
storeCredentials(baseUrl, loginName, appPassword)
// Load main view
mActivity.binding.webview.loadUrl(baseUrl, mapOf(
"Authorization" to authHeader
))
}
return;
}
}
fun storeCredentials(url: String, user: String, password: String) {
mActivity.getSharedPreferences("credentials", 0).edit()
.putString("memoriesUrl", url)
.putString("user", user)
.putString("password", password)
.apply()
setAuthHeader(Pair(user, password))
}
fun getCredentials(): Pair<String, String>? {
val prefs = mActivity.getSharedPreferences("credentials", 0)
memoriesUrl = prefs.getString("memoriesUrl", null)
val user = prefs.getString("user", null)
val password = prefs.getString("password", null)
if (user == null || password == null) return null
return Pair(user, password)
}
fun refreshAuthHeader() {
setAuthHeader(getCredentials())
}
private fun setAuthHeader(credentials: Pair<String, String>?) {
if (credentials != null) {
val auth = "${credentials.first}:${credentials.second}"
authHeader = "Basic ${Base64.encodeToString(auth.toByteArray(), Base64.NO_WRAP)}"
return
}
authHeader = null
}
}