diff --git a/CHANGELOG b/CHANGELOG index 75834c8..1b23628 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +V 1.2.0: + - No longer requires users to log in each time + - Uses new internal model for API calls V 1.1.0: - Now uses V2 of the API V 1.0.5: diff --git a/CHANGELOG-de-DE b/CHANGELOG-de-DE index 0e1fa8f..f7e4289 100644 --- a/CHANGELOG-de-DE +++ b/CHANGELOG-de-DE @@ -1,3 +1,6 @@ +V 1.2.0: + - Nutzer müssen nicht länger jedes Mal sich neu einloggen + - Verwendet neues internes Model zur API Nutzung V 1.1.0: - Verwendet nun V2 der API V 1.0.5: diff --git a/app/build.gradle b/app/build.gradle index ac68f54..776a0b3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,6 +40,9 @@ android { packagingOptions { exclude 'META-INF/*' } + testOptions { + unitTests.returnDefaultValues = true + } } dependencies { @@ -47,9 +50,9 @@ dependencies { api "com.android.support:support-v4:27.1.1" api "com.android.support:cardview-v7:27.1.1" api "org.jetbrains.anko:anko-commons:0.10.5" - api 'com.google.code.gson:gson:2.8.5' + api "com.google.code.gson:gson:2.8.5" api "com.squareup.okhttp3:okhttp:3.10.0" - testImplementation 'org.json:json:20180130' + testImplementation "org.json:json:20180130" // api project(':lib') } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5df4d4a..c88100b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,7 +28,7 @@ along with bundesliga-tippspiel-android. If not, see @@ -36,9 +36,9 @@ along with bundesliga-tippspiel-android. If not, see - - - + + + diff --git a/app/src/main/kotlin/net/namibsun/hktipp/LoginActivity.kt b/app/src/main/kotlin/net/namibsun/hktipp/LoginActivity.kt deleted file mode 100644 index c7598ed..0000000 --- a/app/src/main/kotlin/net/namibsun/hktipp/LoginActivity.kt +++ /dev/null @@ -1,172 +0,0 @@ -/* -Copyright 2017 Hermann Krumrey - -This file is part of bundesliga-tippspiel-android. - -bundesliga-tippspiel-android is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -bundesliga-tippspiel-android is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with bundesliga-tippspiel-android. If not, see . -*/ - -package net.namibsun.hktipp -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.support.v7.app.AppCompatActivity -import android.util.Log -import android.view.View -import android.view.animation.AnimationUtils -import android.widget.CheckBox -import android.widget.EditText -import net.namibsun.hktipp.helper.showErrorDialog -import net.namibsun.hktipp.helper.getApiKeyFromSharedPreferences -import net.namibsun.hktipp.helper.getUsernameFromPreferences -import net.namibsun.hktipp.helper.storeApiKeyInSharedPreferences -import net.namibsun.hktipp.helper.storeUsernameInSharedPreferences -import net.namibsun.hktipp.helper.request -import net.namibsun.hktipp.helper.HTTPMETHOD -import org.jetbrains.anko.doAsync -import org.json.JSONObject - -/** - * The Login Screen that enables the user to log in to the bundesliga-tippspiel - * Service using an API key, which will be generated during the login process. - * Credentials can be stored locally on the device, though the API key is stored - * instead of a password - */ -class LoginActivity : AppCompatActivity() { - - /** - * Initializes the Login Activity. Sets the OnClickListener of the - * login button and sets the input fields with stored data if available - * @param savedInstanceState: The Instance Information of the app. - */ - override fun onCreate(savedInstanceState: Bundle?) { - - super.onCreate(savedInstanceState) - this.setContentView(R.layout.login) - - this.findViewById(R.id.login_screen_button).setOnClickListener { this.login() } - this.findViewById(R.id.login_screen_logo).setOnClickListener { this.login() } - - // Set input elements with stored data - if (getApiKeyFromSharedPreferences(this) != null) { - this.findViewById(R.id.login_screen_password).setText("******") - // We set the password to "******" if the stored api key should be used - // So basically, this WILL be problematic if a user has the password "******" - // TODO Find another way of doing this without discriminating against "******"-passwords - } - val username = getUsernameFromPreferences(this) - if (username != null) { - this.findViewById(R.id.login_screen_username).setText(username) - } - - this.findViewById(R.id.login_screen_register_button).setOnClickListener { - val uri = Uri.parse("https://hk-tippspiel.com/register") - val intent = Intent(Intent.ACTION_VIEW, uri) - this.startActivity(intent) - } - } - - /** - * Attempts to log in the user. Will fetch the username, password/api key from either the - * EditTexts or from the shared preferences, then asynchronously attempt to either log in - * using a password or authorize and existing API Key - */ - private fun login() { - - val username = this.findViewById(R.id.login_screen_username).text.toString() - val password = this.findViewById(R.id.login_screen_password).text.toString() - val apiKey = getApiKeyFromSharedPreferences(this@LoginActivity) - Log.i("LoginActivity", "$username trying to log in.") - - this.setUiElementEnabledState(false) - val animation = AnimationUtils.loadAnimation(this, R.anim.rotate) - this.findViewById(R.id.login_screen_logo).startAnimation(animation) - - this@LoginActivity.doAsync { - - // Log in or authorize the existing API Key - val response = if (apiKey != "" && password == "******") { - Log.i("LoginActivity", "Authorizing existing API key") - request("authorize", HTTPMETHOD.GET, mutableMapOf(), apiKey) - } else { - Log.i("LoginActivity", "Attempting to log in using password") - val json = mutableMapOf("username" to username, "password" to password) - request("api_key", HTTPMETHOD.POST, json) - } - - this@LoginActivity.runOnUiThread { - this@LoginActivity.setUiElementEnabledState(true) - this@LoginActivity.findViewById(R.id.login_screen_logo).clearAnimation() - } - this@LoginActivity.handleLoginResponse(response, username, apiKey) - } - } - - /** - * Handles the login response generated in the [login] method. - * On success, this method stores the username and API key in the shared preferences, then - * starts the Bet Activity - * If the attempt was not successful, an error dialog will be shown - * @param response: The JSON response of the login/authorization attempt - * @param username: The username of the user tying to log in - * @param apiKey: The stored API key, which can be null if no API key was stored - */ - private fun handleLoginResponse(response: JSONObject, username: String, apiKey: String?) { - - if (response.getString("status") == "ok") { // Login successful - - Log.i("LoginActivity", "Login Successful") - - // Check for valid API key - val responseData = response.getJSONObject("data") - val validApiKey = if (responseData.has("api_key")) { - responseData.getString("api_key") // Login API Action - } else { - apiKey!! // Authorize API Action - } - - // Store the username and API key if remember box is checked - val check = this@LoginActivity.findViewById(R.id.login_screen_remember) - if (check.isChecked) { - Log.i("LoginActivity", "Storing credentials in shared preferences") - storeUsernameInSharedPreferences(this@LoginActivity, username) - storeApiKeyInSharedPreferences(this@LoginActivity, validApiKey) - } - this@LoginActivity.runOnUiThread { - net.namibsun.hktipp.helper.switchActivity( - this@LoginActivity, BetActivity::class.java, username, validApiKey) - } - } else { // Login failed - - Log.i("LoginActivity", "Login Failed") - - this@LoginActivity.runOnUiThread { - showErrorDialog(this@LoginActivity, - R.string.login_error_title, R.string.login_error_body) - } - } - } - - /** - * Enables or disables all user-editable UI elements - * @param state: Sets the enabled state of the elements - */ - private fun setUiElementEnabledState(state: Boolean) { - this.findViewById(R.id.login_screen_logo).isEnabled = state - this.findViewById(R.id.login_screen_button).isEnabled = state - this.findViewById(R.id.login_screen_username).isEnabled = state - this.findViewById(R.id.login_screen_password).isEnabled = state - this.findViewById(R.id.login_screen_remember).isEnabled = state - } -} diff --git a/app/src/main/kotlin/net/namibsun/hktipp/SingleMatchActivity.kt b/app/src/main/kotlin/net/namibsun/hktipp/SingleMatchActivity.kt deleted file mode 100644 index 4b61d26..0000000 --- a/app/src/main/kotlin/net/namibsun/hktipp/SingleMatchActivity.kt +++ /dev/null @@ -1,189 +0,0 @@ -/* -Copyright 2017 Hermann Krumrey - -This file is part of bundesliga-tippspiel-android. - -bundesliga-tippspiel-android is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -bundesliga-tippspiel-android is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with bundesliga-tippspiel-android. If not, see . -*/ - -package net.namibsun.hktipp - -import android.os.Bundle -import android.support.v7.app.AppCompatActivity -import android.view.View -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView -import net.namibsun.hktipp.data.MatchData -import net.namibsun.hktipp.helper.getBetsForMatch -import net.namibsun.hktipp.helper.getGoalsForMatch -import net.namibsun.hktipp.singletons.Logos -import net.namibsun.hktipp.views.SingleMatchBetView -import net.namibsun.hktipp.views.SingleMatchGoalView -import org.jetbrains.anko.doAsync - -/** - * Activity that display information about a single match - */ -class SingleMatchActivity : AppCompatActivity() { - - /** - * The match data to display - */ - private var matchData: MatchData? = null - - /** - * Variable that indicates if the activity is currently active or not - */ - private var active = false - - /** - * The username to use with authentication actions - */ - private var username: String? = null - - /** - * The API key to use with authentication actions - */ - private var apiKey: String? = null - - /** - * Displays the match data - * @param savedInstanceState: The saved instance state - */ - override fun onCreate(savedInstanceState: Bundle?) { - - this.setContentView(R.layout.single_match) - super.onCreate(savedInstanceState) - - this.username = this.intent.extras.getString("username") - this.apiKey = this.intent.extras.getString("api_key") - this.matchData = this.intent.extras.get("match_data") as MatchData - - this.update() - - this.findViewById(R.id.single_match_update_button).setOnClickListener { - this@SingleMatchActivity.update(true) - } - - // this.startUpdateChecker() - } - - /** - * Updates all of the UI elements - * @param scoreUpdate: Forces an update of the score widgets with new data if set to true - */ - private fun update(scoreUpdate: Boolean = false) { - this@SingleMatchActivity.loadLogos() - this@SingleMatchActivity.loadGoals() - this@SingleMatchActivity.loadBets() - this@SingleMatchActivity.displayCurrentScores(scoreUpdate) - } - - /** - * Displays the current match score - * @param reload: Reloads new data from the API instead of using stored values - */ - private fun displayCurrentScores(reload: Boolean = false) { - - if (reload) { - // TODO fetch stuff - } - - if (this.matchData!!.started) { - val homeScore = "${this.matchData!!.homeFtScore}" - val awayScore = "${this.matchData!!.awayFtScore}" - this.findViewById(R.id.home_team_score).text = homeScore - this.findViewById(R.id.away_team_score).text = awayScore - } else { - this.findViewById(R.id.home_team_score).text = "-" - this.findViewById(R.id.away_team_score).text = "-" - } - } - - /** - * Loads the logo bitmaps into the image views reserved for them - */ - private fun loadLogos() { - this.doAsync { - val homeLogo = this@SingleMatchActivity.findViewById(R.id.home_team_logo) - val awayLogo = this@SingleMatchActivity.findViewById(R.id.away_team_logo) - val homeBitmap = Logos.getLogo(this@SingleMatchActivity.matchData!!.homeTeam) - val awayBitmap = Logos.getLogo(this@SingleMatchActivity.matchData!!.awayTeam) - - this@SingleMatchActivity.runOnUiThread { - homeLogo.setImageBitmap(homeBitmap) - awayLogo.setImageBitmap(awayBitmap) - } - } - } - - /** - * Retrieves goals for the game using the API - */ - private fun loadGoals() { - val goalList = this.findViewById(R.id.match_goals_list) - goalList.removeAllViews() - this.findViewById(R.id.single_match_goals_progress).visibility = View.VISIBLE - this.doAsync { - val goals = getGoalsForMatch( - this@SingleMatchActivity.apiKey!!, - this@SingleMatchActivity.matchData!!.id - ) - this@SingleMatchActivity.runOnUiThread { - this@SingleMatchActivity.findViewById(R.id.single_match_goals_progress) - .visibility = View.INVISIBLE - goals.map { - SingleMatchGoalView(this@SingleMatchActivity, it) - }.forEach { goalList.addView(it) } - } - } - } - - /** - * Retrieves the bets for the game by other users using the API - */ - private fun loadBets() { - val betList = this.findViewById(R.id.match_bets_list) - betList.removeAllViews() - this.findViewById(R.id.single_match_bets_progress).visibility = View.VISIBLE - this.doAsync { - val bets = getBetsForMatch( - this@SingleMatchActivity.apiKey!!, - this@SingleMatchActivity.matchData!!.id - ) - this@SingleMatchActivity.runOnUiThread { - this@SingleMatchActivity.findViewById(R.id.single_match_bets_progress) - .visibility = View.INVISIBLE - bets.keys.map { - SingleMatchBetView(this@SingleMatchActivity, it, bets[it]) - }.forEach { betList.addView(it) } - } - } - } - - /** - * Starts an Async Task that periodically checks the current score while this - * activity is active - */ - private fun startUpdateChecker() { - this.active = true - this.doAsync { - while (this@SingleMatchActivity.active) { - Thread.sleep(60000) - this@SingleMatchActivity.update(true) - } - } - } -} diff --git a/app/src/main/kotlin/net/namibsun/hktipp/activities/AbstractActivities.kt b/app/src/main/kotlin/net/namibsun/hktipp/activities/AbstractActivities.kt new file mode 100644 index 0000000..870f7d9 --- /dev/null +++ b/app/src/main/kotlin/net/namibsun/hktipp/activities/AbstractActivities.kt @@ -0,0 +1,148 @@ +/* +Copyright 2017 Hermann Krumrey + +This file is part of bundesliga-tippspiel-android. + +bundesliga-tippspiel-android is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +bundesliga-tippspiel-android is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with bundesliga-tippspiel-android. If not, see . +*/ + +package net.namibsun.hktipp.activities + +import android.app.AlertDialog +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.util.Log +import net.namibsun.hktipp.api.ApiConnection +import org.jetbrains.anko.doAsync + +/** + * Base class that implements some common methods + */ +abstract class BaseActivity : AppCompatActivity() { + + /** + * The shared preferences used for storing stuff like API keys + */ + lateinit var sharedPreferences: SharedPreferences + + /** + * Initializes the Shared preferences + * @param savedInstanceState: The bundle provided by a previous activity + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + this.sharedPreferences = this.getSharedPreferences( + "SHARED_PREFS", Context.MODE_PRIVATE + ) + } + + /** + * Shows an error dialog. + * @param titleResource: The resource of the error message title + * @param bodyResource: The resource of the error message body + */ + protected fun showErrorDialog(titleResource: Int, bodyResource: Int) { + + val errorTitle = this.getString(titleResource) + val errorBody = this.getString(bodyResource) + val errorDialogBuilder = AlertDialog.Builder(this) + + errorDialogBuilder.setTitle(errorTitle) + errorDialogBuilder.setMessage(errorBody) + errorDialogBuilder.setCancelable(true) + errorDialogBuilder.setPositiveButton("Ok") { + dialog, _ -> dialog!!.dismiss() + } + errorDialogBuilder.create() + errorDialogBuilder.show() + } + + /** + * Starts a new activity + * @param target: The activity to start + * @param finish: Specifies whether or not the activity will be finished + * @param bundle: Optional bundle object to pass on to the new activity + */ + fun startActivity(target: Class<*>, finish: Boolean = true, bundle: Bundle? = null) { + val intent = Intent(this, target) + if (bundle != null) { + intent.putExtras(bundle) + } + this.startActivity(intent) + if (finish) { + this.finish() + } + } + + /** + * Starts the loading animation + */ + protected abstract fun startLoadingAnimation() + + /** + * Stops the loading animation + */ + protected abstract fun stopLoadingAnimation() +} + +/** + * Activity class that should be used by activities that require an authorized API connection + */ +abstract class AuthorizedActivity : BaseActivity() { + + /** + * The API connection to use for API calls + */ + protected lateinit var apiConnection: ApiConnection + + /** + * Initializes the Activity's apiConnection + * @param savedInstanceState: The bundle provided by a previous activity + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val apiConnection = ApiConnection.loadStored(this) + + if (apiConnection == null) { + this.logout() + } else { + this.apiConnection = apiConnection + } + } + + /** + * Logs out and returns to the Login Activity + */ + protected fun logout() { + + Log.i("Activity", "Logging out") + + val apiConnection = ApiConnection.loadStored(this) + + val editor = this.sharedPreferences.edit() + editor.remove("api_key") + editor.apply() + + if (apiConnection != null) { + this.doAsync { + apiConnection.logout(this@AuthorizedActivity) + } + } + + this.startActivity(LoginActivity::class.java, true) + } +} diff --git a/app/src/main/kotlin/net/namibsun/hktipp/BetActivity.kt b/app/src/main/kotlin/net/namibsun/hktipp/activities/BetActivity.kt similarity index 58% rename from app/src/main/kotlin/net/namibsun/hktipp/BetActivity.kt rename to app/src/main/kotlin/net/namibsun/hktipp/activities/BetActivity.kt index 65b8ee0..1d1b0bc 100644 --- a/app/src/main/kotlin/net/namibsun/hktipp/BetActivity.kt +++ b/app/src/main/kotlin/net/namibsun/hktipp/activities/BetActivity.kt @@ -17,49 +17,33 @@ You should have received a copy of the GNU General Public License along with bundesliga-tippspiel-android. If not, see . */ -package net.namibsun.hktipp +package net.namibsun.hktipp.activities import android.os.Bundle -import android.support.v7.app.AppCompatActivity import android.util.Log import android.view.View import android.widget.Button import android.widget.LinearLayout +import android.widget.PopupMenu import android.widget.TextView -import net.namibsun.hktipp.data.BetData -import net.namibsun.hktipp.data.MatchData -import net.namibsun.hktipp.helper.getMatches -import net.namibsun.hktipp.helper.switchActivity -import net.namibsun.hktipp.helper.getBets -import net.namibsun.hktipp.helper.showErrorDialog -import net.namibsun.hktipp.helper.logout -import net.namibsun.hktipp.helper.placeBets +import net.namibsun.hktipp.R +import net.namibsun.hktipp.models.Bet +import net.namibsun.hktipp.models.Match +import net.namibsun.hktipp.models.MinimalBet import net.namibsun.hktipp.views.BetView import org.jetbrains.anko.doAsync -import org.json.JSONArray import java.io.IOException -import java.io.Serializable /** * This activity allows a user to place bets, as well as view already placed bets */ -class BetActivity : AppCompatActivity() { +class BetActivity : AuthorizedActivity() { /** * The Bet Views for the currently selected match day */ private val betViews = mutableMapOf>() - /** - * The username of the logged in user - */ - private var username: String? = null - - /** - * The API Key of the logged in user - */ - private var apiKey: String? = null - /** * The Match Day to be displayed. -1 indicates that the current match day should be used */ @@ -73,15 +57,10 @@ class BetActivity : AppCompatActivity() { */ override fun onCreate(savedInstanceState: Bundle?) { - this.setContentView(R.layout.bets) super.onCreate(savedInstanceState) - - this.username = this.intent.extras.getString("username") - this.apiKey = this.intent.extras.getString("api_key") + this.setContentView(R.layout.bets) this.findViewById(R.id.bets_submit_button).setOnClickListener { this.placeBets() } - - // Set button actions to go to next or previous matchday this.findViewById(R.id.bets_prev_button).setOnClickListener { this.adjustMatchday(false) } @@ -89,25 +68,73 @@ class BetActivity : AppCompatActivity() { this.adjustMatchday(true) } - // Set listener for Leaderboard Activity button - this.findViewById(R.id.leaderboard_button).setOnClickListener { - net.namibsun.hktipp.helper.switchActivity( - this, LeaderboardActivity::class.java, this.username, this.apiKey) + val menuButton = this.findViewById(R.id.menu_button) + menuButton.setOnClickListener { + val popup = PopupMenu(this@BetActivity, menuButton) + popup.menuInflater.inflate(R.menu.main_menu, popup.menu) + popup.setOnMenuItemClickListener { + if (it.itemId == R.id.leaderboard_menu_option) { + this@BetActivity.startActivity(LeaderboardActivity::class.java, false) + } else if (it.itemId == R.id.logout_menu_option) { + Log.i("BetActivity", "Logging out.") + this.logout() + } + true + } + popup.show() } + // onResume will be called, so we don't need to call updateData here + } - // Get Data for current matchday + /** + * Updates the data when the activity restarts + */ + override fun onResume() { + super.onResume() this.updateData() } + /** + * Starts the loading animation + */ + override fun startLoadingAnimation() { + + this.findViewById