diff --git a/app/build.gradle b/app/build.gradle index 30eb02e..0cead88 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -46,6 +46,10 @@ if (project.hasProperty('sign')) { android.buildTypes.release.signingConfig android.signingConfigs.release } +ext { + retrofitVersion = "2.9.0" +} + dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version") implementation("androidx.appcompat:appcompat:1.6.0") @@ -54,7 +58,10 @@ dependencies { implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0") implementation("com.squareup.okhttp3:okhttp-sse:4.10.0") implementation("com.github.nextcloud:Android-SingleSignOn:0.6.1") - implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:retrofit:$retrofitVersion") + implementation("com.squareup.retrofit2:converter-gson:$retrofitVersion") + implementation("com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion") implementation("io.reactivex.rxjava2:rxjava:2.2.21") implementation("androidx.work:work-runtime-ktx:2.7.1") + implementation("com.google.android.material:material:1.8.0") } diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/account/Account.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/account/Account.kt index 6ab4e4e..26c4a49 100644 --- a/app/src/main/java/org/unifiedpush/distributor/nextpush/account/Account.kt +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/account/Account.kt @@ -6,11 +6,32 @@ import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundExce import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException import org.unifiedpush.distributor.nextpush.utils.TAG -private const val PREF_NAME = "NextPush" +internal const val PREF_NAME = "NextPush" private const val PREF_DEVICE_ID = "deviceId" +private const val PREF_ACCOUNT_TYPE = "account::type" + +enum class AccountType { + SSO, + Direct; + fun toInt(): Int { + return this.ordinal + } +} + +private fun Int.toAccountType(): AccountType { + return AccountType.values().getOrNull(this) ?: AccountType.SSO +} + object Account { private var account: AccountFactory? = null + var Context.accountType: AccountType + get() = this.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .getInt(PREF_ACCOUNT_TYPE, 0).toAccountType() + private set(value) = this.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit().putInt(PREF_ACCOUNT_TYPE, value.toInt()) + .apply() + var Context.deviceId: String? get() = this.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) .getString(PREF_DEVICE_ID, null) @@ -29,25 +50,48 @@ object Account { fun getAccount(context: Context, uninitialized: Boolean = false): AccountFactory? { return account ?: run { - try { - SSOAccountFactory().apply { - initAccount(context) - account = this + Log.d(TAG, "New account, type=${context.accountType}") + when (context.accountType) { + AccountType.SSO -> { + try { + SSOAccountFactory().apply { + initAccount(context) + account = this + } + } catch (e: NextcloudFilesAppAccountNotFoundException) { + Log.w(TAG, "Nextcloud application is not found") + null + } catch (e: NoCurrentAccountSelectedException) { + if (uninitialized) { + SSOAccountFactory() + } else { + null + } + } } - } catch (e: NextcloudFilesAppAccountNotFoundException) { - Log.w(TAG, "Nextcloud application is not found") - null - } catch (e: NoCurrentAccountSelectedException) { - if (uninitialized) { - SSOAccountFactory() - } else { - null + AccountType.Direct -> { + DirectAccountFactory().apply { + initAccount(context) + account = this + } } } } } + fun Context.setTypeSSO() { + account = null + accountType = AccountType.SSO + DirectAccountFactory.setCredentials(this, null, null, null) + } + + fun Context.setTypeDirect(url: String, username: String, password: String) { + account = null + accountType = AccountType.Direct + DirectAccountFactory.setCredentials(this, url, username, password) + } + fun isConnected(context: Context): Boolean { - return getAccount(context)?.isConnected(context) == true + return getAccount(context)?.initAccount(context) == true } } diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/account/AccountFactory.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/account/AccountFactory.kt index daa38ac..e0f24f5 100644 --- a/app/src/main/java/org/unifiedpush/distributor/nextpush/account/AccountFactory.kt +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/account/AccountFactory.kt @@ -5,12 +5,11 @@ import android.content.Context import android.content.Intent interface AccountFactory { - val apiFactory: Class<*> - val name: String? - val url: String? - fun initAccount(context: Context) - fun isConnected(context: Context): Boolean + var name: String? + var url: String? + fun initAccount(context: Context): Boolean fun connect(activity: Activity) fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?, block: (success: Boolean) -> Unit) fun getAccount(context: Context): Any? + fun logout(context: Context) } diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/account/DirectAccountFactory.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/account/DirectAccountFactory.kt new file mode 100644 index 0000000..43e9c02 --- /dev/null +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/account/DirectAccountFactory.kt @@ -0,0 +1,159 @@ +package org.unifiedpush.distributor.nextpush.account + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.util.Log +import okhttp3.* // ktlint-disable no-wildcard-imports +import org.unifiedpush.distributor.nextpush.activities.StartActivity +import org.unifiedpush.distributor.nextpush.api.provider.ApiProvider.Companion.mApiEndpoint +import java.io.IOException +import java.util.concurrent.TimeUnit + +private const val PREF_CONNECTED = "direct_account::connected" +private const val PREF_URL = "direct_account::url" +private const val PREF_USERNAME = "direct_account::username" +private const val PREF_PASSWORD = "direct_account::password" + +class DirectAccountFactory : AccountFactory { + override var name: String? = null + override var url: String? = null + + private var Context.connected: Boolean + get() = this.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .getBoolean(PREF_CONNECTED, false) + set(value) = this.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit().putBoolean(PREF_CONNECTED, value) + .apply() + + override fun initAccount(context: Context): Boolean { + url = context.url + name = context.username + + return context.connected + } + + override fun connect(activity: Activity) { + activity.connected = false + val client = getAccount(activity) as OkHttpClient? ?: return retActivity(activity) + val url = activity.url ?: return retActivity(activity) + + val request = Request.Builder() + .url("$url/$mApiEndpoint/") + .build() + + val call = client.newCall(request) + call.enqueue(object : Callback { + private val TAG = "DirectAccountCallback" + override fun onFailure(call: Call, e: IOException) { + Log.e(TAG, "Could not connect", e) + retActivity(activity) + } + + override fun onResponse(call: Call, response: Response) { + Log.e(TAG, "Status: ${response.code}") + activity.connected = response.code == 200 + response.close() + retActivity(activity) + } + }) + } + + override fun onActivityResult( + activity: Activity, + requestCode: Int, + resultCode: Int, + data: Intent?, + block: (success: Boolean) -> Unit + ) { + block(activity.connected) + } + + override fun getAccount(context: Context): Any? { + val username = context.username ?: return null + val password = context.password ?: return null + return OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .authenticator(DirectAuth(username, password)) + .followRedirects(false) + .build() + } + + override fun logout(context: Context) { + context.connected = false + setCredentials(context, null, null, null) + } + + private fun retActivity(activity: Activity) { + (activity as StartActivity).onActivityResult(0, 0, null) + } + + inner class DirectAuth(private val username: String, private val password: String) : Authenticator { + override fun authenticate(route: Route?, response: Response): Request? { + if (responseCount(response) >= 3) { + return null + } + val credential = Credentials.basic(username, password) + return response.request.newBuilder().header("Authorization", credential).build() + } + + private fun responseCount(_response: Response): Int { + var response = _response + var result = 1 + while (response.priorResponse?.also { + response = it + } != null + ) { + result++ + } + return result + } + } + + companion object { + private var Context.url: String? + get() = this.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .getString(PREF_URL, null) + set(value) = value?.let { + this.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit().putString(PREF_URL, it) + .apply() + } ?: run { + this.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit().remove(PREF_URL) + .apply() + } + + private var Context.username: String? + get() = this.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .getString(PREF_USERNAME, null) + set(value) = value?.let { + this.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit().putString(PREF_USERNAME, it) + .apply() + } ?: run { + this.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit().remove(PREF_USERNAME) + .apply() + } + + private var Context.password: String? + get() = this.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .getString(PREF_PASSWORD, null) + set(value) = value?.let { + this.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit().putString(PREF_PASSWORD, it) + .apply() + } ?: run { + this.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit().remove(PREF_PASSWORD) + .apply() + } + + fun setCredentials(context: Context, url: String?, username: String?, password: String?) { + context.url = url + context.username = username + context.password = password + } + } +} diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/account/SSOAccountFactory.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/account/SSOAccountFactory.kt index 27e2938..bd834fa 100644 --- a/app/src/main/java/org/unifiedpush/distributor/nextpush/account/SSOAccountFactory.kt +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/account/SSOAccountFactory.kt @@ -17,23 +17,17 @@ import com.nextcloud.android.sso.helper.SingleAccountHelper import com.nextcloud.android.sso.model.SingleSignOnAccount import com.nextcloud.android.sso.ui.UiExceptionManager import org.unifiedpush.distributor.nextpush.R -import org.unifiedpush.distributor.nextpush.api.provider.ApiSSOFactory import org.unifiedpush.distributor.nextpush.utils.TAG class SSOAccountFactory : AccountFactory { - override val apiFactory: Class<*> = ApiSSOFactory::class.java - override val name: String? + override var name: String? = null get() = ssoAccount?.name - override val url: String? + override var url: String? = null get() = ssoAccount?.url private var ssoAccount: SingleSignOnAccount? = null - override fun initAccount(context: Context) { - ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context) - } - - override fun isConnected(context: Context): Boolean { + override fun initAccount(context: Context): Boolean { try { ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context) } catch (e: NextcloudFilesAppAccountNotFoundException) { @@ -93,6 +87,14 @@ class SSOAccountFactory : AccountFactory { return ssoAccount } + override fun logout(context: Context) { + AccountImporter.clearAllAuthTokens(context) + AccountImporter.getSharedPreferences(context) + .edit() + .remove("PREF_CURRENT_ACCOUNT_STRING") + .apply() + } + private fun nextcloudAppNotInstalledDialog(context: Context) { val message = TextView(context) val builder = AlertDialog.Builder(context) diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/MainActivity.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/MainActivity.kt index d1992d7..63d7ba0 100644 --- a/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/MainActivity.kt +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/MainActivity.kt @@ -9,8 +9,6 @@ import android.view.MenuItem import android.view.View import android.widget.* // ktlint-disable no-wildcard-imports import androidx.appcompat.app.AppCompatActivity -import com.nextcloud.android.sso.AccountImporter -import com.nextcloud.android.sso.AccountImporter.clearAllAuthTokens import org.unifiedpush.distributor.nextpush.R import org.unifiedpush.distributor.nextpush.account.Account.getAccount import org.unifiedpush.distributor.nextpush.account.Account.isConnected @@ -90,11 +88,7 @@ class MainActivity : AppCompatActivity() { alert.setMessage(R.string.logout_alert_content) alert.setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() - clearAllAuthTokens(this) - AccountImporter.getSharedPreferences(this) - .edit() - .remove("PREF_CURRENT_ACCOUNT_STRING") - .apply() + getAccount(this)?.logout(this) deleteDevice(this) { StartService.stopService() FailureHandler.clearFails() diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/StartActivity.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/StartActivity.kt index ba1f89b..7faff5b 100644 --- a/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/StartActivity.kt +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/StartActivity.kt @@ -4,15 +4,27 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle +import android.text.InputType +import android.util.Log import android.widget.Button +import android.widget.EditText +import android.widget.ImageView +import android.widget.RelativeLayout +import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isGone +import com.google.android.material.textfield.TextInputEditText import org.unifiedpush.distributor.nextpush.R import org.unifiedpush.distributor.nextpush.account.Account +import org.unifiedpush.distributor.nextpush.account.Account.setTypeDirect +import org.unifiedpush.distributor.nextpush.account.Account.setTypeSSO import org.unifiedpush.distributor.nextpush.activities.MainActivity.Companion.goToMainActivity import org.unifiedpush.distributor.nextpush.activities.PermissionsRequest.requestAppPermissions +import org.unifiedpush.distributor.nextpush.utils.TAG class StartActivity : AppCompatActivity() { private var onResult: ((activity: Activity, requestCode: Int, resultCode: Int, data: Intent?, block: (success: Boolean) -> Unit) -> Unit)? = null + private var passwordIsVisible = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -22,20 +34,43 @@ class StartActivity : AppCompatActivity() { goToMainActivity(this) finish() } - findViewById