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"?>
<project version="4">
<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>
<Target>
<type value="QUICK_BOOT_TARGET" />
@ -12,6 +23,6 @@
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-05-03T06:48:46.574240300Z" />
<timeTargetWasSelectedWithDropDown value="2023-05-16T08:14:59.213052500Z" />
</component>
</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)
}
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 var player: ExoPlayer? = null
@ -135,12 +140,23 @@ import gallery.memories.databinding.ActivityMainBinding
webSettings.allowContentAccess = true
webSettings.domStorageEnabled = true
webSettings.databaseEnabled = true
webSettings.userAgentString = "memories-native-android/0.0"
webSettings.userAgentString = USER_AGENT
binding.webview.clearCache(true)
binding.webview.addJavascriptInterface(mNativeX, "nativex")
binding.webview.loadUrl("file:///android_asset/welcome.html");
binding.webview.setBackgroundColor(Color.TRANSPARENT)
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) {

View File

@ -1,6 +1,5 @@
package gallery.memories
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.view.SoundEffectConstants
@ -10,23 +9,20 @@ import android.webkit.WebResourceResponse
import android.widget.Toast
import androidx.media3.common.util.UnstableApi
import gallery.memories.mapper.SystemImage
import gallery.memories.service.AccountService
import gallery.memories.service.DownloadService
import gallery.memories.service.ImageService
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.net.URLDecoder
@UnstableApi class NativeX(private val mActivity: MainActivity) {
val TAG = "NativeX"
private val mImageService: ImageService = ImageService(mActivity)
private val mQuery: TimelineQuery = TimelineQuery(mActivity)
private var themeStored = false
private val mImageService = ImageService(mActivity)
private val mQuery = TimelineQuery(mActivity)
val mAccountService = AccountService(mActivity)
object API {
val DAYS = Regex("^/api/days$")
@ -105,34 +101,7 @@ import java.net.URLDecoder
@JavascriptInterface
fun login(baseUrl: String?, loginFlowUrl: String?) {
if (baseUrl == null || loginFlowUrl == null) return;
// 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")
}
mAccountService.login(baseUrl, loginFlowUrl)
}
@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
}
}