Reorganize and group modules

This commit is contained in:
MM20
2022-12-13 17:37:26 +01:00
parent bac24baad2
commit 3f8880a90a
995 changed files with 501 additions and 298 deletions
@@ -0,0 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="de.mm20.launcher2.nextcloud.LoginActivity"
android:label="@string/preference_category_services_nextcloud"
android:taskAffinity="de.mm20.launcher2.nextcloud"
android:theme="@style/NextcloudLoginTheme" />
</application>
</manifest>
@@ -0,0 +1,85 @@
package de.mm20.launcher2.nextcloud
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import de.mm20.launcher2.nextcloud.databinding.ActivityNextcloudLoginBinding
import kotlinx.coroutines.*
class LoginActivity : AppCompatActivity() {
private val nextcloudClient = NextcloudApiHelper(this)
private lateinit var binding: ActivityNextcloudLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityNextcloudLoginBinding.inflate(LayoutInflater.from(this))
setContentView(binding.root)
binding.nextButton.setOnClickListener {
binding.serverUrlInputLayout.error = null
lifecycleScope.launch {
var url = binding.serverUrlInput.text.toString()
if (!(url.startsWith("http://") || url.startsWith("https://"))) {
url = "https://$url"
}
if (url.isBlank()) {
binding.serverUrlInputLayout.error = getString(R.string.nextcloud_server_url_empty)
return@launch
}
if (nextcloudClient.checkNextcloudInstallation(url)) {
openLoginPage(url)
} else {
binding.serverUrlInputLayout.error = getString(R.string.nextcloud_server_invalid_url)
}
}
}
}
private fun openLoginPage(url: String) {
val webView = WebView(this)
webView.settings.userAgentString = getString(R.string.app_name)
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
if (request?.url?.scheme == "nc") {
val path = request.url?.path?.trim('/') ?: run {
setResult(0)
finish()
return false
}
val segments = path.split('&')
var username: String? = null
var token: String? = null
var server: String? = null
for (segment in segments) {
when {
segment.startsWith("server") -> server = segment.substringAfter(":")
segment.startsWith("user") -> username = segment.substringAfter(":")
segment.startsWith("password") -> token = segment.substringAfter(":")
}
}
if (username != null && server != null && token != null) {
nextcloudClient.setServer(server, username, token)
}
setResult(Activity.RESULT_OK)
finish()
return true
}
webView.loadUrl(request?.url?.toString() ?: "")
return false
}
}
webView.settings.javaScriptEnabled = true
setContentView(webView)
val headers = mapOf(
"OCS-APIREQUEST" to "true"
)
webView.loadUrl("$url/index.php/login/flow", headers)
}
}
@@ -0,0 +1,6 @@
package de.mm20.launcher2.nextcloud
data class NcUser(
val displayName: String,
val username: String
)
@@ -0,0 +1,204 @@
package de.mm20.launcher2.nextcloud
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import de.mm20.launcher2.webdav.WebDavApi
import de.mm20.launcher2.webdav.WebDavFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.*
import org.json.JSONObject
import java.io.File
import java.io.IOException
class NextcloudApiHelper(val context: Context) {
private val httpClient by lazy {
OkHttpClient.Builder()
.authenticator(object : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
if (response.priorResponse?.priorResponse != null) return null
return response.request
.newBuilder()
.addHeader("Authorization", getAuthorization() ?: return null)
.build()
}
})
.build()
}
private val preferences by lazy {
createPreferences()
}
private fun createPreferences(catchErrors: Boolean = true): SharedPreferences {
try {
val masterKey =
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
return EncryptedSharedPreferences.create(
context,
"nextcloud",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
} catch (e: IOException) {
if (!catchErrors) throw e
File(context.filesDir, "../shared_prefs/nextcloud.xml").delete()
return createPreferences(false)
}
}
fun getLoginIntent(): Intent {
return Intent(context, LoginActivity::class.java)
}
fun login(activity: Activity) {
activity.startActivity(getLoginIntent())
}
suspend fun checkNextcloudInstallation(url: String): Boolean {
var url = url
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "https://$url"
}
val request = Request.Builder()
.url("$url/remote.php/dav")
.build()
val response = runCatching {
withContext(Dispatchers.IO) {
httpClient.newCall(request).execute()
}
}.getOrNull() ?: return false
return response.code == 200 || response.code == 401
}
suspend fun getLoggedInUser(): NcUser? {
val server = getServer()
val username = getUserName()
val token = getToken()
if (server == null || username == null || token == null) {
return null
}
val displayName = getDisplayName() ?: return null
return NcUser(
displayName,
username
)
}
/**
* Returns the user's display name or user name if the user is logged in and their token has
* not been revoked,
* returns null if they are not logged in.
*/
private suspend fun getDisplayName(): String? {
val displayname = preferences.getString("displayname", null)
if (displayname != null) {
return displayname
}
val server = getServer() ?: return null
val request = Request.Builder()
.addHeader("OCS-APIRequest", "true")
.url("$server/ocs/v1.php/cloud/user?format=json")
.build()
val response = runCatching {
withContext(Dispatchers.IO) {
httpClient.newCall(request).execute()
}
}.getOrNull() ?: return getUserName()
if (response.code != 200) {
logout()
return null
}
val body = response.body ?: return getUserName()
return withContext(Dispatchers.IO) {
val json = JSONObject(body.string())
val name = json.optJSONObject("ocs")
?.optJSONObject("data")
?.optString("display-name")
preferences.edit {
putString("displayname", name)
}
return@withContext name
?: getUserName()
}
}
private fun getAuthorization(): String? {
return Credentials.basic(getUserName() ?: return null, getToken() ?: return null)
}
fun getServer(): String? {
return preferences.getString("server", null)
}
fun getUserName(): String? {
return preferences.getString("username", null)
}
private fun getToken(): String? {
return preferences.getString("token", null)
}
internal fun setServer(server: String, username: String, token: String) {
preferences.edit {
putString("server", server)
putString("username", username)
putString("token", token)
}
}
suspend fun logout() {
val server = getServer()
val username = getUserName()
val token = getToken()
if (server == null || username == null || token == null) return
val request = Request.Builder()
.addHeader("OCS-APIREQUEST", "true")
.delete()
.url("$server/ocs/v2.php/core/apppassword")
.build()
withContext(Dispatchers.IO) {
val response = httpClient.newCall(request).execute()
response
}
preferences.edit {
putString("server", null)
putString("username", null)
putString("token", null)
putString("displayname", null)
}
}
val files by lazy {
FilesApi()
}
inner class FilesApi internal constructor() {
suspend fun search(query: String): List<WebDavFile> {
val server = getServer() ?: return emptyList()
val username = getUserName() ?: return emptyList()
return WebDavApi.search("$server/remote.php/dav/", username, query, httpClient)
}
}
}
@@ -0,0 +1,13 @@
<vector android:height="106.01157dp" android:viewportHeight="94.62735"
android:viewportWidth="133.89203" android:width="150dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M67.033,10C55.228,10 45.222,18.003 42.12,28.847 39.425,23.095 33.585,19.066 26.857,19.066 17.605,19.066 10,26.671 10,35.923 10,45.175 17.605,52.783 26.857,52.783 33.585,52.783 39.425,48.751 42.12,42.999 45.222,53.843 55.228,61.849 67.033,61.849 78.751,61.849 88.706,53.964 91.886,43.242 94.631,48.864 100.4,52.783 107.031,52.783 116.283,52.783 123.892,45.175 123.892,35.923 123.892,26.671 116.283,19.066 107.031,19.066 100.4,19.066 94.631,22.983 91.886,28.604 88.706,17.882 78.751,10 67.033,10ZM67.033,19.895C75.944,19.895 83.064,27.011 83.064,35.923 83.064,44.834 75.944,51.953 67.033,51.953 58.121,51.953 51.006,44.834 51.006,35.923 51.006,27.011 58.121,19.895 67.033,19.895ZM26.857,28.961C30.761,28.961 33.822,32.018 33.822,35.923 33.822,39.827 30.761,42.888 26.857,42.888 22.953,42.888 19.896,39.827 19.896,35.923 19.896,32.018 22.953,28.961 26.857,28.961ZM107.031,28.961C110.936,28.961 113.997,32.018 113.997,35.923 113.997,39.827 110.936,42.888 107.031,42.888 103.127,42.888 100.07,39.827 100.07,35.923 100.07,32.018 103.127,28.961 107.031,28.961Z"/>
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M39.108,73.761C41.884,73.761 43.436,75.737 43.436,78.7 43.436,78.983 43.201,79.218 42.919,79.218H35.439C35.486,81.852 37.321,83.357 39.438,83.357 40.755,83.357 41.696,82.793 42.166,82.417 42.448,82.228 42.683,82.275 42.824,82.558L42.966,82.793C43.107,83.028 43.06,83.263 42.824,83.451 42.26,83.875 41.037,84.58 39.391,84.58 36.333,84.58 33.981,82.369 33.981,79.171 34.028,75.784 36.286,73.761 39.108,73.761ZM41.978,78.183C41.884,76.019 40.567,74.937 39.061,74.937 37.321,74.937 35.816,76.066 35.486,78.183Z"/>
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M57.562,75.267V74.091,71.645C57.562,71.315 57.75,71.127 58.079,71.127H58.456C58.785,71.127 58.926,71.315 58.926,71.645V74.091H61.043C61.372,74.091 61.56,74.279 61.56,74.608V74.749C61.56,75.079 61.372,75.22 61.043,75.22H58.926V80.394C58.926,82.793 60.384,83.075 61.184,83.122 61.607,83.169 61.748,83.263 61.748,83.64V83.922C61.748,84.251 61.607,84.392 61.184,84.392 58.926,84.392 57.562,83.028 57.562,80.582Z"/>
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M68.334,73.761C70.121,73.761 71.25,74.514 71.767,74.937 72.003,75.126 72.05,75.361 71.814,75.643L71.673,75.878C71.485,76.16 71.25,76.16 70.968,75.972 70.497,75.643 69.604,75.031 68.381,75.031 66.123,75.031 64.335,76.725 64.335,79.218 64.335,81.664 66.123,83.357 68.381,83.357 69.839,83.357 70.827,82.699 71.297,82.275 71.579,82.087 71.767,82.134 71.956,82.417L72.097,82.605C72.238,82.887 72.191,83.075 71.956,83.31 71.438,83.734 70.168,84.627 68.287,84.627 65.229,84.627 62.877,82.417 62.877,79.218 62.924,76.019 65.276,73.761 68.334,73.761Z"/>
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M74.59,70.422C74.59,70.092 74.402,69.904 74.731,69.904H75.107C75.436,69.904 75.954,70.092 75.954,70.422V81.664C75.954,82.981 76.565,83.122 77.036,83.169 77.271,83.169 77.459,83.31 77.459,83.64V83.969C77.459,84.298 77.318,84.486 76.942,84.486 76.095,84.486 74.59,84.204 74.59,81.946Z"/>
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M84.233,73.761C87.243,73.761 89.689,76.066 89.689,79.124 89.689,82.228 87.243,84.58 84.233,84.58 81.222,84.58 78.776,82.228 78.776,79.124 78.776,76.066 81.222,73.761 84.233,73.761ZM84.233,83.357C86.443,83.357 88.231,81.57 88.231,79.124 88.231,76.772 86.443,75.031 84.233,75.031 82.022,75.031 80.187,76.819 80.187,79.124 80.234,81.523 82.022,83.357 84.233,83.357Z"/>
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M107.705,73.761C110.198,73.761 111.092,75.831 111.092,75.831H111.139C111.139,75.831 111.092,75.502 111.092,75.031V70.375C111.092,70.045 110.951,69.857 111.28,69.857H111.656C111.985,69.857 112.503,70.045 112.503,70.375V83.781C112.503,84.11 112.362,84.298 112.032,84.298H111.703C111.374,84.298 111.186,84.157 111.186,83.828V83.028C111.186,82.652 111.28,82.369 111.28,82.369H111.233C111.233,82.369 110.339,84.533 107.658,84.533 104.883,84.533 103.142,82.322 103.142,79.124 103.048,75.925 104.977,73.761 107.705,73.761ZM107.752,83.357C109.492,83.357 111.092,82.134 111.092,79.171 111.092,77.054 110.01,75.031 107.799,75.031 105.964,75.031 104.459,76.537 104.459,79.171 104.506,81.711 105.823,83.357 107.752,83.357Z"/>
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M21.86,84.345H22.236C22.565,84.345 22.753,84.157 22.753,83.828V73.727C22.753,72.128 24.494,70.986 26.469,70.986 28.445,70.986 30.185,72.128 30.185,73.727V83.828C30.185,84.157 30.374,84.345 30.703,84.345H31.079C31.408,84.345 31.55,84.157 31.55,83.828V73.667C31.55,70.986 28.868,69.669 26.422,69.669V69.669,69.669 69.669,69.669C24.07,69.669 21.389,70.986 21.389,73.667V83.828C21.389,84.157 21.53,84.345 21.86,84.345Z"/>
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M100.367,73.997H99.991C99.661,73.997 99.473,74.185 99.473,74.514V80.206C99.473,81.805 98.438,83.263 96.416,83.263 94.44,83.263 93.358,81.805 93.358,80.206V74.514C93.358,74.185 93.17,73.997 92.841,73.997H92.464C92.135,73.997 91.994,74.185 91.994,74.514V80.582C91.994,83.263 93.97,84.58 96.416,84.58V84.58C96.416,84.58 96.416,84.58 96.416,84.58 96.416,84.58 96.416,84.58 96.416,84.58V84.58C98.862,84.58 100.837,83.263 100.837,80.582V74.514C100.884,74.185 100.696,73.997 100.367,73.997Z"/>
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M53.803,73.919C53.687,73.938 53.577,74.015 53.471,74.141L51.567,76.41 50.142,78.108 47.985,75.536 46.815,74.141C46.709,74.015 46.589,73.946 46.465,73.935 46.34,73.924 46.211,73.972 46.085,74.077L45.797,74.319C45.545,74.531 45.558,74.765 45.769,75.017L47.674,77.287 49.253,79.168 46.941,81.923C46.939,81.925 46.938,81.928 46.936,81.93L45.769,83.32C45.558,83.572 45.581,83.838 45.834,84.049L46.122,84.29C46.374,84.502 46.603,84.448 46.815,84.196L48.718,81.927 50.143,80.229 52.3,82.801C52.302,82.802 52.304,82.804 52.305,82.805L53.472,84.196C53.684,84.449 53.948,84.471 54.2,84.26L54.489,84.018C54.741,83.806 54.728,83.572 54.516,83.32L52.612,81.051 51.033,79.169 53.345,76.414C53.347,76.412 53.348,76.409 53.35,76.407L54.516,75.017C54.728,74.765 54.704,74.5 54.452,74.288L54.164,74.046C54.038,73.94 53.918,73.901 53.803,73.919Z"/>
</vector>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/serverUrlView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="32dp"
android:orientation="vertical">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_nextcloud_logo" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/serverUrlInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:hint="@string/nextcloud_server_url"
app:helperTextEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/serverUrlInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/nextButton"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_flow_continue" />
</LinearLayout>
</FrameLayout>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NextcloudLoginTheme" parent="Theme.MaterialComponents.NoActionBar">
<item name="colorAccent">#0082c9</item>
<item name="colorPrimary">#0082c9</item>
<item name="colorPrimaryDark">@color/settings_color_primary_dark</item>
</style>
</resources>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NextcloudLoginTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar">true</item>
<item name="colorAccent">#0082c9</item>
<item name="colorPrimary">#0082c9</item>
<item name="colorPrimaryDark">@color/settings_color_primary_dark</item>
<item name="android:navigationBarColor">@color/settings_color_primary_dark</item>
</style>
</resources>