Add login flow
parent
56308aa8aa
commit
6e10692962
|
@ -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>
|
|
@ -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>
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue