diff --git a/app/build.gradle b/app/build.gradle index ebe5ff2..63d2b94 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "eu.rickvanschijndel.solargraph" minSdkVersion 16 targetSdkVersion 27 - versionCode 7 - versionName "1.6" + versionCode 8 + versionName "1.7" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -31,8 +31,6 @@ dependencies { implementation 'com.github.PhilJay:MPAndroidChart:v3.0.3' implementation 'com.squareup.okhttp3:okhttp:3.10.0' implementation 'com.squareup.okhttp3:okhttp-urlconnection:3.10.0' - implementation 'com.github.franmontiel:PersistentCookieJar:v1.0.1' - implementation 'org.jsoup:jsoup:1.11.3' implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' implementation 'io.reactivex.rxjava2:rxjava:2.1.15' diff --git a/app/src/main/java/eu/rickvanschijndel/solargraph/BasicAuthInterceptor.kt b/app/src/main/java/eu/rickvanschijndel/solargraph/BasicAuthInterceptor.kt new file mode 100644 index 0000000..886a288 --- /dev/null +++ b/app/src/main/java/eu/rickvanschijndel/solargraph/BasicAuthInterceptor.kt @@ -0,0 +1,20 @@ +package eu.rickvanschijndel.solargraph + +/** + * taken from: Alphaaa, https://stackoverflow.com/a/36056355 + */ + +import okhttp3.Credentials +import okhttp3.Interceptor +import okhttp3.Response + +class BasicAuthInterceptor(user: String, password: String): Interceptor { + private val credentials = Credentials.basic(user, password) + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val authenticatedRequest = request.newBuilder() + .header("Authorization", credentials).build() + return chain.proceed(authenticatedRequest) + } +} diff --git a/app/src/main/java/eu/rickvanschijndel/solargraph/Login.kt b/app/src/main/java/eu/rickvanschijndel/solargraph/Login.kt deleted file mode 100644 index d41d79c..0000000 --- a/app/src/main/java/eu/rickvanschijndel/solargraph/Login.kt +++ /dev/null @@ -1,154 +0,0 @@ -package eu.rickvanschijndel.solargraph - -import android.content.Context -import android.util.Log -import com.franmontiel.persistentcookiejar.PersistentCookieJar -import okhttp3.* -import org.jsoup.Jsoup -import java.io.IOException -import java.lang.ref.WeakReference - -class Login(context: Context, client: OkHttpClient) { - private val mContext: WeakReference = WeakReference(context) - private val mHttpClient: WeakReference = WeakReference(client) - private var mUsername: String? = null - private var mPassword: String? = null - - private companion object { - const val TAG = "Login" - const val BASE_URL = "https://my.autarco.com" - const val LOGIN_ROUTE = "/auth/login" - const val SESSION_COOKIE_NAME = "autarco_session" - - const val USERNAME_FIELD = "username" - const val PASSWORD_FIELD = "password" - const val TOKEN_FIELD = "_token" - } - - private fun statusChanged(updateMessage: String?) { - update(LoginCallback.LoginEvent.STATUS_CHANGED, updateMessage) - } - - private fun loginSuccess() { - update(LoginCallback.LoginEvent.LOGGED_IN, "Logged in") - } - - private fun update(event: LoginCallback.LoginEvent, updateMessage: String?) { - val context = mContext.get() - if (context is LoginCallback) { - context.onUpdate(event, updateMessage) - } - } - - private fun isAlreadyLoggedIn(): Boolean { - val url = HttpUrl.parse(BASE_URL) ?: throw IllegalArgumentException("BASE_URL") - val cookies = mHttpClient.get()?.cookieJar()?.loadForRequest(url) - if (cookies?.find { it.name() == SESSION_COOKIE_NAME } != null) { - return true - } - return false - } - - private fun runLoginSequence() { - if (isAlreadyLoggedIn()) { - loginSuccess() - return - } - statusChanged(mContext.get()?.getString(R.string.requesting_login_token)) - - val request = Request.Builder() - .url(BASE_URL + LOGIN_ROUTE) - .build() - mHttpClient.get()?.newCall(request)?.enqueue(object : Callback { - override fun onFailure(call: Call?, e: IOException?) { - e?.printStackTrace() - } - - override fun onResponse(call: Call?, response: Response?) { - if (response == null || !response.isSuccessful) { - if (response != null) { - Log.w(TAG, response.message()) - statusChanged(response.message()) - } else { - statusChanged("Could not get a response") - } - Log.w(TAG, "Unsuccessful request") - return - } - Log.d(TAG, "Got a response") - val loginPage = response.body()?.string() - val document = Jsoup.parse(loginPage) - val inputElements = document.select("input") - for (inputElement in inputElements) { - if (inputElement.attr("name") == TOKEN_FIELD) { - val token = inputElement.attr("value") - if (token.isNullOrBlank()) { - statusChanged("Couldn't get a login token") - return - } - doLogin(token) - return - } - } - } - }) - } - - private fun doLogin(token: String) { - if (mUsername.isNullOrBlank() || mPassword.isNullOrBlank()) { - statusChanged("No username or password set") - return - } - - statusChanged("Logging in") - val loginRequestBody = MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart(USERNAME_FIELD, mUsername!!) - .addFormDataPart(PASSWORD_FIELD, mPassword!!) - .addFormDataPart(TOKEN_FIELD, token) - .build() - - val request = Request.Builder() - .url(BASE_URL + LOGIN_ROUTE) - .post(loginRequestBody) - .build() - mHttpClient.get()?.newCall(request)?.enqueue(object : Callback { - override fun onFailure(call: Call?, e: IOException?) { - e?.printStackTrace() - } - - override fun onResponse(call: Call?, response: Response?) { - if (response == null || !response.isSuccessful) { - Log.d(TAG, "Invalid response") - val cookieJar = mHttpClient.get()?.cookieJar() as PersistentCookieJar - cookieJar.clear() - update(LoginCallback.LoginEvent.LOGIN_FAILURE, response?.message()) - return - } - loginSuccess() - } - }) - } - - fun setUsername(username: String) { - if (username.isBlank()) return - mUsername = username - } - - fun setPassword(password: String) { - if (password.isBlank()) return - mPassword = password - } - - fun login() { - if (mUsername.isNullOrBlank()) { - update(LoginCallback.LoginEvent.NO_CREDENTIALS, "username") - return - } - if (mPassword.isNullOrBlank()) { - update(LoginCallback.LoginEvent.NO_CREDENTIALS, "password") - return - } - runLoginSequence() - } -} diff --git a/app/src/main/java/eu/rickvanschijndel/solargraph/LoginCallback.kt b/app/src/main/java/eu/rickvanschijndel/solargraph/LoginCallback.kt deleted file mode 100644 index 2a146ab..0000000 --- a/app/src/main/java/eu/rickvanschijndel/solargraph/LoginCallback.kt +++ /dev/null @@ -1,12 +0,0 @@ -package eu.rickvanschijndel.solargraph - -interface LoginCallback { - fun onUpdate(event: LoginEvent, updateMessage: String?) - - enum class LoginEvent { - STATUS_CHANGED, - LOGGED_IN, - LOGIN_FAILURE, - NO_CREDENTIALS, - } -} diff --git a/app/src/main/java/eu/rickvanschijndel/solargraph/activities/GraphActivity.kt b/app/src/main/java/eu/rickvanschijndel/solargraph/activities/GraphActivity.kt index b0e0394..864fcd4 100644 --- a/app/src/main/java/eu/rickvanschijndel/solargraph/activities/GraphActivity.kt +++ b/app/src/main/java/eu/rickvanschijndel/solargraph/activities/GraphActivity.kt @@ -1,6 +1,7 @@ package eu.rickvanschijndel.solargraph.activities import android.content.Context +import android.content.Intent import android.graphics.Color import android.net.ConnectivityManager import android.net.NetworkInfo @@ -9,44 +10,36 @@ import android.os.Bundle import android.preference.PreferenceManager import android.support.design.widget.Snackbar import android.util.Log -import com.franmontiel.persistentcookiejar.PersistentCookieJar -import com.franmontiel.persistentcookiejar.cache.SetCookieCache -import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor import com.github.mikephil.charting.data.Entry import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineDataSet import com.github.mikephil.charting.highlight.Highlight import com.github.mikephil.charting.listener.OnChartValueSelectedListener -import eu.rickvanschijndel.solargraph.Login -import eu.rickvanschijndel.solargraph.LoginCallback import eu.rickvanschijndel.solargraph.R import eu.rickvanschijndel.solargraph.models.ProductionResponse -import eu.rickvanschijndel.solargraph.rest.ApiClient -import eu.rickvanschijndel.solargraph.rest.ApiInterface +import eu.rickvanschijndel.solargraph.rest.ApiImpl import io.reactivex.Single import io.reactivex.SingleObserver import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers -import kotlinx.android.synthetic.main.activity_graph.* -import okhttp3.OkHttpClient import retrofit2.Response import java.text.SimpleDateFormat import java.util.Locale import java.util.Date -import java.util.concurrent.TimeUnit -class GraphActivity : AppCompatActivity(), LoginCallback { - private lateinit var client: OkHttpClient - private lateinit var apiClient: ApiInterface - private lateinit var cookieJar: PersistentCookieJar - private lateinit var login: Login - private var retries = 0 +import kotlinx.android.synthetic.main.activity_graph.network_info +import kotlinx.android.synthetic.main.activity_graph.today_power +import kotlinx.android.synthetic.main.activity_graph.monthly_power +import kotlinx.android.synthetic.main.activity_graph.total_power +import kotlinx.android.synthetic.main.activity_graph.graph +import kotlinx.android.synthetic.main.activity_graph.graph_snack_layout + +class GraphActivity : AppCompatActivity() { + private var apiImpl: ApiImpl? = null companion object { private const val TAG = "GraphActivity" - private const val MAX_RETRIES = 3 - private const val MAX_TIMEOUT_SECONDS = 20L private const val RESPONSE_OK = 200 private const val RESPONSE_UNAUTHORIZED = 401 } @@ -57,18 +50,10 @@ class GraphActivity : AppCompatActivity(), LoginCallback { setupGraph() - cookieJar = PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(this)) - client = OkHttpClient.Builder() - .readTimeout(MAX_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .cookieJar(cookieJar) - .build() - login = Login(this, client) - apiClient = ApiClient(client).client.create(ApiInterface::class.java) - val networkInfo = getActiveNetworkInfo() if (networkInfo?.isConnected == true) { loadUsernameAndPassword() - login.login() + retrieveData() } else { network_info.setText(R.string.no_connection) @@ -76,49 +61,30 @@ class GraphActivity : AppCompatActivity(), LoginCallback { } private fun loadUsernameAndPassword() { - val username = PreferenceManager.getDefaultSharedPreferences(this).getString(LoginActivity.USERNAME_PREFERENCE_NAME, "") - val password = PreferenceManager.getDefaultSharedPreferences(this).getString(LoginActivity.PASSWORD_PREFERENCE_NAME, "") - login.setUsername(username) - login.setPassword(password) + val username = PreferenceManager.getDefaultSharedPreferences(applicationContext).getString(LoginActivity.USERNAME_PREFERENCE_NAME, "") + val password = PreferenceManager.getDefaultSharedPreferences(applicationContext).getString(LoginActivity.PASSWORD_PREFERENCE_NAME, "") + apiImpl = ApiImpl(username, password) } - override fun onUpdate(event: LoginCallback.LoginEvent, updateMessage: String?) { - when(event) { - LoginCallback.LoginEvent.STATUS_CHANGED -> { - runOnUiThread { - if (updateMessage != null) { - network_info.text = updateMessage - } - } - } - LoginCallback.LoginEvent.LOGGED_IN -> { - retrieveData() - retries = 0 - } - LoginCallback.LoginEvent.NO_CREDENTIALS -> { - runOnUiThread { - network_info.setText(R.string.no_credentials) - } - } - LoginCallback.LoginEvent.LOGIN_FAILURE -> { - runOnUiThread { - network_info.setText(R.string.retry_logging_in) - retries++ - if (retries <= MAX_RETRIES) { - login.login() - return@runOnUiThread - } - else { - network_info.setText(R.string.max_retries_exceeded) - } - } - } - } + private fun removeUsernameAndPassword() { + val editor = PreferenceManager.getDefaultSharedPreferences(applicationContext).edit() + editor.remove(LoginActivity.USERNAME_PREFERENCE_NAME) + editor.remove(LoginActivity.PASSWORD_PREFERENCE_NAME) + editor.apply() + } + + private fun restartApplication() { + val startActivity = Intent(applicationContext, RedirectActivity::class.java) + startActivity(startActivity) + finish() } - private fun getProductionData(): Single> { - return apiClient.getAvailableSites().flatMap({ - return@flatMap apiClient.getProductionData(it.body()!!.toTypedArray()[0].publicKey!!) + private fun getProductionData(): Single>? { + if (apiImpl == null || apiImpl?.client == null) { + return null + } + return apiImpl?.client?.getAvailableSites()?.flatMap({ + return@flatMap apiImpl?.client?.getProductionData(it.body()!!.toTypedArray()[0].publicKey!!) }) } @@ -127,9 +93,9 @@ class GraphActivity : AppCompatActivity(), LoginCallback { network_info.setText(R.string.retrieving_data) } getProductionData() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object: SingleObserver> { + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object: SingleObserver> { override fun onSuccess(response: Response) { when(response.code()) { RESPONSE_OK -> { @@ -137,8 +103,8 @@ class GraphActivity : AppCompatActivity(), LoginCallback { onDataRetrieved(body) } RESPONSE_UNAUTHORIZED -> { - cookieJar.clear() - login.login() + removeUsernameAndPassword() + restartApplication() } else -> { network_info.text = response.message() @@ -166,7 +132,6 @@ class GraphActivity : AppCompatActivity(), LoginCallback { monthly_power.text = getString(R.string.month_power, outputMonth) total_power.text = getString(R.string.total_power, outputTotal) - val realTimePowerMap = responseData.stats?.graphs?.realTimePower val dateFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) var dataPoints = arrayOf() @@ -202,7 +167,6 @@ class GraphActivity : AppCompatActivity(), LoginCallback { // redraw graph.invalidate() - } private fun getActiveNetworkInfo(): NetworkInfo? { @@ -238,7 +202,6 @@ class GraphActivity : AppCompatActivity(), LoginCallback { val roundedDate = Date(value.toLong()) roundedDate.minutes = (roundedDate.minutes + 8) / 15 * 15 roundedDate.seconds = 0 - Log.d(TAG, roundedDate.minutes.toString() + " " + roundedDate) timeFormatter.format(value) } } diff --git a/app/src/main/java/eu/rickvanschijndel/solargraph/activities/LoginActivity.kt b/app/src/main/java/eu/rickvanschijndel/solargraph/activities/LoginActivity.kt index 972fac2..72ad22a 100644 --- a/app/src/main/java/eu/rickvanschijndel/solargraph/activities/LoginActivity.kt +++ b/app/src/main/java/eu/rickvanschijndel/solargraph/activities/LoginActivity.kt @@ -1,29 +1,37 @@ package eu.rickvanschijndel.solargraph.activities +import android.content.Context import android.content.Intent +import android.net.ConnectivityManager +import android.net.NetworkInfo import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.preference.PreferenceManager import android.support.design.widget.Snackbar -import com.franmontiel.persistentcookiejar.PersistentCookieJar -import com.franmontiel.persistentcookiejar.cache.SetCookieCache -import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor -import eu.rickvanschijndel.solargraph.Login -import eu.rickvanschijndel.solargraph.LoginCallback import eu.rickvanschijndel.solargraph.R -import kotlinx.android.synthetic.main.activity_login.* -import okhttp3.OkHttpClient - -class LoginActivity : AppCompatActivity(), LoginCallback { - private lateinit var cookieJar: PersistentCookieJar - private lateinit var client: OkHttpClient - private lateinit var login: Login +import eu.rickvanschijndel.solargraph.models.SiteResponse +import eu.rickvanschijndel.solargraph.rest.ApiImpl +import io.reactivex.SingleObserver +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import retrofit2.Response +import kotlinx.android.synthetic.main.activity_login.email_address +import kotlinx.android.synthetic.main.activity_login.password +import kotlinx.android.synthetic.main.activity_login.snack_layout +import kotlinx.android.synthetic.main.activity_login.login_button +class LoginActivity : AppCompatActivity() { companion object { const val USERNAME_PREFERENCE_NAME = "username" const val PASSWORD_PREFERENCE_NAME = "password" } + private fun getActiveNetworkInfo(): NetworkInfo? { + val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + return connectivityManager.activeNetworkInfo + } + private fun saveUsernameAndPassword() { val editor = PreferenceManager.getDefaultSharedPreferences(this).edit() editor.putString(USERNAME_PREFERENCE_NAME, email_address.editText?.text.toString()) @@ -31,45 +39,67 @@ class LoginActivity : AppCompatActivity(), LoginCallback { editor.apply() } - override fun onUpdate(event: LoginCallback.LoginEvent, updateMessage: String?) { - when(event) { - LoginCallback.LoginEvent.STATUS_CHANGED -> { - - } - LoginCallback.LoginEvent.LOGGED_IN -> { - val graphActivity = Intent(this, GraphActivity::class.java) - startActivity(graphActivity) - finish() - } - LoginCallback.LoginEvent.LOGIN_FAILURE -> { - runOnUiThread { - Snackbar.make(snack_layout, R.string.login_failure, Snackbar.LENGTH_LONG).show() - } - } - LoginCallback.LoginEvent.NO_CREDENTIALS -> { - - } + private fun notifyLoginFailure(message: String) { + runOnUiThread { + Snackbar.make(snack_layout, getString(R.string.login_failure, message), Snackbar.LENGTH_LONG).show() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) - cookieJar = PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(this)) - client = OkHttpClient.Builder() - .cookieJar(cookieJar) - .build() - login = Login(this, client) login_button.setOnClickListener { _ -> - if (email_address.editText?.text.toString().isBlank() || password.editText?.text.toString().isBlank()) { + val networkInfo = getActiveNetworkInfo() + if (networkInfo == null || !networkInfo.isConnected) { + Snackbar.make(snack_layout, R.string.no_connection, Snackbar.LENGTH_INDEFINITE).show() + return@setOnClickListener + } + + val email = email_address.editText?.text.toString() + val password = password.editText?.text.toString() + if (email.isBlank() || password.isBlank()) { Snackbar.make(snack_layout, R.string.incomplete_credentials, Snackbar.LENGTH_LONG).show() return@setOnClickListener } - login.setUsername(email_address.editText?.text.toString()) - login.setPassword(password.editText?.text.toString()) - saveUsernameAndPassword() - login.login() + val apiImpl = ApiImpl(email, password) + apiImpl.client.getAvailableSites() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object: SingleObserver>> { + override fun onSuccess(response: Response>) { + when(response.code()) { + 200 -> { + saveUsernameAndPassword() + val graphActivity = Intent(applicationContext, GraphActivity::class.java) + startActivity(graphActivity) + finish() + } + + 401 -> { + notifyLoginFailure(response.message()) + return + } + + else -> { + notifyLoginFailure(response.message()) + return + } + } + } + + override fun onSubscribe(d: Disposable) { + // + } + + override fun onError(e: Throwable) { + e.printStackTrace() + val message = e.message + if (message != null) { + notifyLoginFailure(message) + } + } + }) } } } diff --git a/app/src/main/java/eu/rickvanschijndel/solargraph/activities/RedirectActivity.kt b/app/src/main/java/eu/rickvanschijndel/solargraph/activities/RedirectActivity.kt index a0df238..f699c2d 100644 --- a/app/src/main/java/eu/rickvanschijndel/solargraph/activities/RedirectActivity.kt +++ b/app/src/main/java/eu/rickvanschijndel/solargraph/activities/RedirectActivity.kt @@ -15,11 +15,11 @@ class RedirectActivity : Activity() { val username = PreferenceManager.getDefaultSharedPreferences(applicationContext).getString("username", "") val password = PreferenceManager.getDefaultSharedPreferences(applicationContext).getString("password", "") if (username.isEmpty() || password.isEmpty()) { - val loginActivity = Intent(this, LoginActivity::class.java) + val loginActivity = Intent(applicationContext, LoginActivity::class.java) startActivity(loginActivity) } else { - val graphActivity = Intent(this, GraphActivity::class.java) + val graphActivity = Intent(applicationContext, GraphActivity::class.java) startActivity(graphActivity) } finish() diff --git a/app/src/main/java/eu/rickvanschijndel/solargraph/rest/ApiImpl.kt b/app/src/main/java/eu/rickvanschijndel/solargraph/rest/ApiImpl.kt new file mode 100644 index 0000000..b318ef8 --- /dev/null +++ b/app/src/main/java/eu/rickvanschijndel/solargraph/rest/ApiImpl.kt @@ -0,0 +1,20 @@ +package eu.rickvanschijndel.solargraph.rest + +import eu.rickvanschijndel.solargraph.BasicAuthInterceptor +import okhttp3.OkHttpClient +import java.util.concurrent.TimeUnit + +class ApiImpl(username: String, password: String) { + val client: ApiInterface + companion object { + private const val MAX_TIMEOUT_SECONDS = 20L + } + + init { + val httpClient = OkHttpClient.Builder() + .readTimeout(MAX_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .addInterceptor(BasicAuthInterceptor(username, password)) + .build() + client = ApiClient(httpClient).client.create(ApiInterface::class.java) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63b0bf4..5d2e332 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,18 +1,6 @@ SolarGraph - - Email - Password (optional) - Sign in or register - Sign in - This email address is invalid - This password is too short - This password is incorrect - This field is required - "Contacts permissions are needed for providing email - completions." - Requesting login token… Logging in… Retrieving data… @@ -32,5 +20,5 @@ Power today MyAutarco Fill in a username and password - Could not log in + Could not log in: %1$s