commit 57db9db189a2c32a7f5302e19f228632daa5fc89 Author: Rakantor Date: Sun Jan 24 19:52:01 2021 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..ebdd23d --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100755 index 0000000..51fde29 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +IUBH Gamer App \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100755 index 0000000..3279b6b --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..61a9130 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100755 index 0000000..279b808 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..a5f05cd --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100755 index 0000000..2925c96 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100755 index 0000000..9b770a6 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100755 index 0000000..3543521 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100755 index 0000000..17ae0cd --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,45 @@ +apply plugin: 'com.android.application' +apply plugin: 'com.google.gms.google-services' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.2" + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } + defaultConfig { + applicationId "com.example.iubhgamerapp" + minSdkVersion 16 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + multiDexEnabled true + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.vectordrawable:vectordrawable:1.0.1' + implementation 'androidx.navigation:navigation-fragment:2.0.0' + implementation 'androidx.navigation:navigation-ui:2.0.0' + implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' + implementation 'androidx.annotation:annotation:1.0.2' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + // Firebase + implementation 'com.google.firebase:firebase-auth:19.2.0' + implementation 'com.firebaseui:firebase-ui-auth:4.3.2' + implementation 'com.google.firebase:firebase-database:19.2.0' +} diff --git a/app/google-services.json b/app/google-services.json new file mode 100755 index 0000000..950e2b7 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,48 @@ +{ + "project_info": { + "project_number": "188834974456", + "firebase_url": "https://iubh-gamer-app.firebaseio.com", + "project_id": "iubh-gamer-app", + "storage_bucket": "iubh-gamer-app.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:188834974456:android:51737864d594977bd7490d", + "android_client_info": { + "package_name": "com.example.iubhgamerapp" + } + }, + "oauth_client": [ + { + "client_id": "188834974456-mtfb78k43snrjdf49283bmi8cg9ba9ji.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.example.iubhgamerapp", + "certificate_hash": "b90c0d7aaba6aae69e76f1b2c3a28a6fdaea5311" + } + }, + { + "client_id": "188834974456-u95ei6cncprj4i98rrol8nbth6ugqa0f.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDjJjmSoKzTT-k1mJLjJQk1A0drFKNAIIo" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "188834974456-u95ei6cncprj4i98rrol8nbth6ugqa0f.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100755 index 0000000..6e7ffa9 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/com/example/iubhgamerapp/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/iubhgamerapp/ExampleInstrumentedTest.java new file mode 100755 index 0000000..61f0e7e --- /dev/null +++ b/app/src/androidTest/java/com/example/iubhgamerapp/ExampleInstrumentedTest.java @@ -0,0 +1,27 @@ +package com.example.iubhgamerapp; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.example.iubhgamerapp", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100755 index 0000000..8a98a90 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/iubhgamerapp/LoginActivity.java b/app/src/main/java/com/example/iubhgamerapp/LoginActivity.java new file mode 100755 index 0000000..0dff2c3 --- /dev/null +++ b/app/src/main/java/com/example/iubhgamerapp/LoginActivity.java @@ -0,0 +1,72 @@ +package com.example.iubhgamerapp; + +import android.content.Intent; +import android.os.Bundle; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; + +public class LoginActivity extends AppCompatActivity { + private FirebaseAuth mAuth; + private EditText user, pw; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + + user = findViewById(R.id.username); + pw = findViewById(R.id.password); + Button btnSignIn = findViewById(R.id.login); + btnSignIn.setOnClickListener(v -> signInUser()); + + // Initialize Firebase Auth + mAuth = FirebaseAuth.getInstance(); + + // Check if user is signed in (non-null) and update UI accordingly + FirebaseUser currentUser = mAuth.getCurrentUser(); + if(currentUser != null) startMainActivity(); + } + + /** + * Starts the main activity and closes the login activity. + */ + private void startMainActivity() { + startActivity(new Intent(LoginActivity.this, MainActivity.class)); + finish(); + } + + /** + * Authenticates the user with Firebase using email and password. + */ + private void signInUser() { + String sUser = user.getText().toString().trim(); + String sPassword = pw.getText().toString().trim(); + + // Display an error message if the user didn't fill in their email or password + if(sUser.equals("") || sPassword.equals("")) { + Toast.makeText(LoginActivity.this, R.string.firebase_credentials_missing, Toast.LENGTH_SHORT).show(); + } + // Otherwise, attempt authentication with Firebase. + // Start main activity on success or display error message + else { + Toast.makeText(getApplicationContext(), R.string.firebase_signin_progress, Toast.LENGTH_SHORT).show(); + mAuth.signInWithEmailAndPassword(sUser, sPassword) + .addOnCompleteListener(this, task -> { + if(task.isSuccessful()) { + // Sign in success, update UI with the signed-in user's information + startMainActivity(); + } else { + // If sign in fails, display an error message to the user + Toast.makeText(LoginActivity.this, R.string.firebase_auth_failed, + Toast.LENGTH_LONG).show(); + } + }); + } + } +} diff --git a/app/src/main/java/com/example/iubhgamerapp/MainActivity.java b/app/src/main/java/com/example/iubhgamerapp/MainActivity.java new file mode 100755 index 0000000..5ca2122 --- /dev/null +++ b/app/src/main/java/com/example/iubhgamerapp/MainActivity.java @@ -0,0 +1,29 @@ +package com.example.iubhgamerapp; + +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; +import androidx.navigation.ui.AppBarConfiguration; +import androidx.navigation.ui.NavigationUI; + +import com.google.android.material.bottomnavigation.BottomNavigationView; + +public class MainActivity extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + BottomNavigationView navView = findViewById(R.id.nav_view); + // Passing each menu ID as a set of Ids because each + // menu should be considered as top level destinations. + AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder( + R.id.navigation_home, R.id.navigation_rate, R.id.navigation_chat) + .build(); + NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment); + NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration); + NavigationUI.setupWithNavController(navView, navController); + } +} diff --git a/app/src/main/java/com/example/iubhgamerapp/RVAdapter.java b/app/src/main/java/com/example/iubhgamerapp/RVAdapter.java new file mode 100755 index 0000000..a122b5b --- /dev/null +++ b/app/src/main/java/com/example/iubhgamerapp/RVAdapter.java @@ -0,0 +1,57 @@ +package com.example.iubhgamerapp; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.cardview.widget.CardView; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.iubhgamerapp.ui.ChatFragment; + +import java.util.List; + +public class RVAdapter extends RecyclerView.Adapter { + private List chatMessages; + + public RVAdapter(List chatMessages){ + this.chatMessages = chatMessages; + } + + static class MessageViewHolder extends RecyclerView.ViewHolder { + private CardView cv; + private TextView sender, time, text; + + MessageViewHolder(View itemView) { + super(itemView); + cv = itemView.findViewById(R.id.cv); + sender = itemView.findViewById(R.id.chat_sender); + time = itemView.findViewById(R.id.chat_time); + text = itemView.findViewById(R.id.chat_text); + } + } + + @Override + public MessageViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { + View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.cardview_item, viewGroup, false); + return new MessageViewHolder(v); + } + + @Override + public void onBindViewHolder(MessageViewHolder messageViewHolder, int i) { + messageViewHolder.sender.setText(chatMessages.get(i).senderName); + messageViewHolder.time.setText(chatMessages.get(i).date); + messageViewHolder.text.setText(chatMessages.get(i).text); + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + } + + @Override + public int getItemCount() { + return chatMessages.size(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/iubhgamerapp/ui/ChatFragment.java b/app/src/main/java/com/example/iubhgamerapp/ui/ChatFragment.java new file mode 100755 index 0000000..8bd4480 --- /dev/null +++ b/app/src/main/java/com/example/iubhgamerapp/ui/ChatFragment.java @@ -0,0 +1,154 @@ +package com.example.iubhgamerapp.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.iubhgamerapp.R; +import com.example.iubhgamerapp.RVAdapter; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.ValueEventListener; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +public class ChatFragment extends Fragment { + private FirebaseUser mUser; + private DatabaseReference refChatMessages, refUsers; + private Map users; + private List chatMessages; + private RecyclerView rv; + private EditText input; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.fragment_chat, container, false); + rv = root.findViewById(R.id.rv); + input = root.findViewById(R.id.chat_input); + Button btnSend = root.findViewById(R.id.chat_send); + btnSend.setOnClickListener(v -> sendTextMessage()); + + LinearLayoutManager llm = new LinearLayoutManager(getContext()); + rv.setLayoutManager(llm); + + // Connect to Firebase realtime database + mUser = FirebaseAuth.getInstance().getCurrentUser(); + refChatMessages = FirebaseDatabase.getInstance().getReference().child("nachrichten"); + refUsers = FirebaseDatabase.getInstance().getReference().child("spieler"); + + // Get list of registered users from Firebase database + refUsers.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(@NonNull DataSnapshot dataSnapshot) { + // Save users in HashMap object (key = UID; value = nickname) + users = new HashMap<>(); + for(DataSnapshot ds : dataSnapshot.getChildren()) { + users.put(ds.getKey(), (String)ds.child("nickname").getValue()); + } + + getMessagesFromDatabase(); + } + + @Override + public void onCancelled(@NonNull DatabaseError databaseError) { + Toast.makeText(getContext(), R.string.db_comm_err, Toast.LENGTH_SHORT).show(); + } + }); + + return root; + } + + /** + * Reads the most recent chat messages from the Firebase realtime database. + * Limited to the 100 most recent messages. + */ + private void getMessagesFromDatabase() { + refChatMessages.orderByKey().limitToLast(100).addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(@NonNull DataSnapshot dataSnapshot) { + chatMessages = new ArrayList<>(); + + for(DataSnapshot ds : dataSnapshot.getChildren()) { + long timestamp = Long.parseLong(Objects.requireNonNull(ds.getKey())); + String user = (String)ds.child("absender").getValue(); + String text = (String)ds.child("text").getValue(); + + chatMessages.add(new ChatMessage(timestamp, user, text)); + + // Initialize custom RecyclerView.Adapter and scroll to the bottom + RVAdapter adapter = new RVAdapter(chatMessages); + rv.setAdapter(adapter); + rv.scrollToPosition(adapter.getItemCount() - 1); + } + } + + @Override + public void onCancelled(@NonNull DatabaseError databaseError) { + Toast.makeText(getContext(), R.string.db_comm_err, Toast.LENGTH_SHORT).show(); + } + }); + } + + /** + * Writes a new text message to database. Metadata includes the current timestamp + * (Unix epoch time), the sender's user id and the actual message. + */ + private void sendTextMessage() { + long currentEpochTimestamp = System.currentTimeMillis() / 1000L; + String senderUid = mUser.getUid(); + String textMessage = input.getText().toString().trim(); + + // If the input TextView isn't empty, write to database + if(!textMessage.isEmpty()) { + input.getText().clear(); + refChatMessages.child(String.valueOf(currentEpochTimestamp)).child("absender").setValue(senderUid); + refChatMessages.child(String.valueOf(currentEpochTimestamp)).child("text").setValue(textMessage); + } + } + + /** + * This class represents a chat message object. + * It contains the text message and metadata such as the sender's name and user id as well as + * the exact time the message was sent (Unix epoch timestamp). + */ + public class ChatMessage { + final public String senderUID, senderName, text, date; + final long timestamp; + + ChatMessage(long timestamp, String senderUID, String text) { + this.timestamp = timestamp; + this.senderUID = senderUID; + this.text = text; + + // Convert epoch timestamp to formatted date string + Date date = new Date(timestamp * 1000L); + this.date = new SimpleDateFormat("dd.MM.YYYY HH:mm", Locale.getDefault()).format(date); + + // Check if the sender's user id still exist in the database. + // If so, extract their nickname + if(users.containsKey(senderUID)) this.senderName = users.get(senderUID); + // If the user was deleted at some point, use generic term instead + else this.senderName = getString(R.string.chat_invalid_user); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/iubhgamerapp/ui/HomeFragment.java b/app/src/main/java/com/example/iubhgamerapp/ui/HomeFragment.java new file mode 100755 index 0000000..ab75518 --- /dev/null +++ b/app/src/main/java/com/example/iubhgamerapp/ui/HomeFragment.java @@ -0,0 +1,256 @@ +package com.example.iubhgamerapp.ui; + +import android.app.AlertDialog; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import com.example.iubhgamerapp.LoginActivity; +import com.example.iubhgamerapp.R; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.ValueEventListener; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +public class HomeFragment extends Fragment { + private View root; + private FirebaseUser mUser; + private DatabaseReference refUsers, refGames, refEventDates; + private DataSnapshot dsUsers, dsGames; + private ProgressBar progressBar; + private TextView welcome, nextDate, nextHost; + private Button btnSignOut, btnVote, btnSuggest; + private Spinner spinnerGames; + private Map games = new HashMap<>(); + private String userNickname, nextDateID; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + root = inflater.inflate(R.layout.fragment_home, container, false); + welcome = root.findViewById(R.id.text_home); + nextDate = root.findViewById(R.id.text_home2); + nextHost = root.findViewById(R.id.text_home5); + btnSignOut = root.findViewById(R.id.logout); + btnVote = root.findViewById(R.id.vote); + btnSuggest = root.findViewById(R.id.suggest); + spinnerGames = root.findViewById(R.id.spinner_games); + progressBar = root.findViewById(R.id.progressBar); + + // Set button listeners + btnSignOut.setOnClickListener(v -> signOutUser()); + btnVote.setOnClickListener(v -> + refEventDates.child(nextDateID).child("abstimmung_spiele").child(mUser.getUid()).setValue(spinnerGames.getSelectedItemId())); + btnSuggest.setOnClickListener(v -> showInputDialog()); + + // Display loading bar + setProgressBar(true); + + // Connect to Firebase realtime database + mUser = FirebaseAuth.getInstance().getCurrentUser(); + refEventDates = FirebaseDatabase.getInstance().getReference("termine"); + refGames = FirebaseDatabase.getInstance().getReference("spiele"); + refUsers = FirebaseDatabase.getInstance().getReference("spieler"); + + // Get list of registered users from database + refUsers.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(@NonNull DataSnapshot dataSnapshot) { + dsUsers = dataSnapshot; + setValues(); + } + + @Override + public void onCancelled(@NonNull DatabaseError databaseError) { + Toast.makeText(getContext(), R.string.db_comm_err, Toast.LENGTH_SHORT).show(); + } + }); + + // Get list of available games from database + refGames.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(@NonNull DataSnapshot dataSnapshot) { + dsGames = dataSnapshot; + updateGamesList(); + } + + @Override + public void onCancelled(@NonNull DatabaseError databaseError) { + Toast.makeText(getContext(), R.string.db_comm_err, Toast.LENGTH_SHORT).show(); + } + }); + + return root; + } + + /** + * Displays a circular loading bar. + * While active, user interaction is disabled. + * @param isActive is the progress bar visible (true) or not (false) + */ + private void setProgressBar(boolean isActive) { + if(isActive) { + progressBar.setVisibility(View.VISIBLE); + // Disable user interaction while progress bar is visible + Objects.requireNonNull(getActivity()).getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); + } else if(progressBar.getVisibility() == View.VISIBLE) { + progressBar.setVisibility(View.GONE); + // Re-enable user interaction when progress bar is gone + Objects.requireNonNull(getActivity()).getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); + } + } + + /** + * Displays a popup dialog with an text input field. The user is supposed to type in the title + * of a game that he would like to add to the database. + */ + private void showInputDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + builder.setTitle(R.string.input_dialog_title); + + // Set up the input + final EditText input = new EditText(getContext()); + builder.setView(input); + + // Set up the buttons + builder.setPositiveButton("OK", (dialog, which) -> { + String newGameTitle = input.getText().toString().trim(); + + // On button press, write to database if input field isn't empty + if(!newGameTitle.isEmpty()) { + refGames.child(String.valueOf(dsGames.getChildrenCount())).setValue(newGameTitle); + } + }); + builder.setNegativeButton(R.string.btn_cancel, (dialog, which) -> dialog.cancel()); + + // Show dialog on screen + builder.show(); + } + + private void updateGamesList() { + List list = new ArrayList<>(); + for(long i = 0; i < dsGames.getChildrenCount(); i++) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append((String)dsGames.child(String.valueOf(i)).getValue()); + if(games.containsKey(i)) stringBuilder.append(" (").append(games.get(i)).append(" votes)"); + + list.add(stringBuilder.toString()); + } + ArrayAdapter adapter = new ArrayAdapter<>(root.getContext(), + android.R.layout.simple_spinner_item, list); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinnerGames.setAdapter(adapter); + } + + private void setValues() { + // Get details of upcoming event from database + refEventDates.orderByKey().limitToLast(1).addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(@NonNull DataSnapshot dataSnapshot) { + for(DataSnapshot ds : dataSnapshot.getChildren()) { + nextDateID = ds.getKey(); + long epoch = Long.parseLong(nextDateID); + + if(epoch < (System.currentTimeMillis() / 1000L)) { + addUpcomingEvent(epoch); + return; + } + + Date date = new Date(epoch * 1000L); + String s = new SimpleDateFormat("dd. MMMM YYYY", Locale.getDefault()).format(date); + nextDate.setText(s); + + String sNextHostUID = (String) ds.child("gastgeber").getValue(); + String sNextHost = (String)dsUsers.child(sNextHostUID).child("nickname").getValue(); + nextHost.setText(sNextHost + "'s place"); + + if(ds.hasChild("abstimmung_spiele")) { + games = new HashMap<>(); + for(DataSnapshot dataSnapshot1 : ds.child("abstimmung_spiele").getChildren()) { + long curInt = (long)dataSnapshot1.getValue(); + + if(games.containsKey(curInt)) games.put(curInt, games.get(curInt)+1); + else games.put(curInt, 1); + } + } + } + updateGamesList(); + setProgressBar(false); + } + + @Override + public void onCancelled(@NonNull DatabaseError databaseError) { + Toast.makeText(getContext(), R.string.db_comm_err, Toast.LENGTH_SHORT).show(); + } + }); + + userNickname = (String)dsUsers.child(mUser.getUid()).child("nickname").getValue(); + + String sWelcome = userNickname + getString(R.string.welcome_back); + welcome.setText(sWelcome); + } + + /** + * Determines the date and host of the next event and writes the details to the database. + * If this was a real life app and not just an example project, it would be advisable to handle + * event setup automatically with scheduled functions running on the server (a possible + * solution could be Google Cloud Functions for Firebase). + * @param prevEventTimestamp Unix epoch time of the previous event + */ + private void addUpcomingEvent(long prevEventTimestamp) { + // Programmatically determine the host of the upcoming event by iterating through the list + // of registered users and comparing the epoch timestamps of their most recently hosted event. + // The lowest timestamp will determine the new host. + String nextHostUID = null; + long ll = 0; + for(DataSnapshot dataSnapshot : dsUsers.getChildren()) { + long ts = (long)dataSnapshot.child("zuletzt_gehostet").getValue(); + if(ll == 0 || ts < ll) { + ll = ts; + nextHostUID = dataSnapshot.getKey(); + } + } + + // Calculate epoch timestamp of upcoming event + // It will take place exactly 1 week after the last event. + // 7 days * 24 hours * 60 minutes * 60 seconds = 604800 + long nextEventTimestamp = prevEventTimestamp + 604800; + + // Write to database + refEventDates.child(String.valueOf(nextEventTimestamp)).child("gastgeber").setValue(nextHostUID); + } + + /** + * Signs out the current Firebase user and switches to the login interface. + */ + private void signOutUser() { + FirebaseAuth.getInstance().signOut(); + startActivity(new Intent(getActivity(), LoginActivity.class)); + Objects.requireNonNull(getActivity()).finish(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/iubhgamerapp/ui/RateFragment.java b/app/src/main/java/com/example/iubhgamerapp/ui/RateFragment.java new file mode 100755 index 0000000..5e36ab6 --- /dev/null +++ b/app/src/main/java/com/example/iubhgamerapp/ui/RateFragment.java @@ -0,0 +1,240 @@ +package com.example.iubhgamerapp.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.RatingBar; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import com.example.iubhgamerapp.R; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.ValueEventListener; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class RateFragment extends Fragment { + private EventDate selectedEvent; + private FirebaseUser mUser; + private DatabaseReference refUsers, refEvents; + private View root; + private List eventDates; + private Map users; + private Spinner spinnerPastEvents; + private RatingBar rbOverall, rbFood, rbHost; + private TextView tvHost; + private TextView tvOverallRatings, tvFoodRatings, tvHostRatings; + private Button btnRate; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + root = inflater.inflate(R.layout.fragment_rate, container, false); + spinnerPastEvents = root.findViewById(R.id.spinner_rating); + rbOverall = root.findViewById(R.id.ratingBar_overall); + rbFood = root.findViewById(R.id.ratingBar_food); + rbHost = root.findViewById(R.id.ratingBar_host); + tvHost = root.findViewById(R.id.textView_rateHost); + tvOverallRatings = root.findViewById(R.id.textView_rate1); + tvFoodRatings = root.findViewById(R.id.textView_rate2); + tvHostRatings = root.findViewById(R.id.textView_rate3); + btnRate = root.findViewById(R.id.rate); + + // Set rate button listener + btnRate.setOnClickListener(v -> ratePastEvent()); + + // Set spinner listener + spinnerPastEvents.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + selectedEvent = eventDates.get(position); + + rbOverall.setRating(selectedEvent.ownOverallRating); + rbFood.setRating(selectedEvent.ownFoodRating); + rbHost.setRating(selectedEvent.ownHostRating); + + tvHost.setText("Host (" + selectedEvent.hostName + ")"); + + String sNoRatings = getString(R.string.zero_ratings); + + String str1 = getString(R.string.other_ratings, selectedEvent.avgOverallRating, selectedEvent.overallRatingsCount); + tvOverallRatings.setText(selectedEvent.overallRatingsCount > 0 ? str1 : sNoRatings); + + String str2 = getString(R.string.other_ratings, selectedEvent.avgFoodRating, selectedEvent.foodRatingsCount); + tvFoodRatings.setText(selectedEvent.foodRatingsCount > 0 ? str2 : sNoRatings); + + String str3 = getString(R.string.other_ratings, selectedEvent.avgHostRating, selectedEvent.hostRatingsCount); + tvHostRatings.setText(selectedEvent.hostRatingsCount > 0 ? str3 : sNoRatings); + } + + @Override + public void onNothingSelected(AdapterView parent) { + // Nothing to do here + } + }); + + // Connect to Firebase realtime database + mUser = FirebaseAuth.getInstance().getCurrentUser(); + refUsers = FirebaseDatabase.getInstance().getReference().child("spieler"); + refEvents = FirebaseDatabase.getInstance().getReference().child("termine"); + + // Get list of registered users from Firebase database + refUsers.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(@NonNull DataSnapshot dataSnapshot) { + // Save users in HashMap object (key = UID; value = nickname) + users = new HashMap<>(); + for(DataSnapshot ds : dataSnapshot.getChildren()) { + users.put(ds.getKey(), (String)ds.child("nickname").getValue()); + } + + getPastEvents(); + } + + @Override + public void onCancelled(@NonNull DatabaseError databaseError) { + Toast.makeText(getContext(), R.string.db_comm_err, Toast.LENGTH_SHORT).show(); + } + }); + + return root; + } + + /** + * Writes the user's rating of the selected event to database. + */ + private void ratePastEvent() { + long ownOverallRating = (long)rbOverall.getRating(); + long ownFoodRating = (long)rbFood.getRating(); + long ownHostRating = (long)rbHost.getRating(); + + // Write ratings to database IF the user rated every required item + if(ownOverallRating > 0 && ownFoodRating > 0 && ownHostRating > 0) { + DatabaseReference refRatings = refEvents.child(String.valueOf(selectedEvent.epochTimestamp)).child("bewertungen"); + refRatings.child("allgemein").child(mUser.getUid()).setValue(ownOverallRating); + refRatings.child("essen").child(mUser.getUid()).setValue(ownFoodRating); + refRatings.child("gastgeber").child(mUser.getUid()).setValue(ownHostRating); + } + // Otherwise, display error message + else { + Toast.makeText(getContext(), R.string.rate_error, Toast.LENGTH_SHORT).show(); + } + } + + /** + * Reads the most recent events from the database. The user can rate the overall experience, + * food & drinks as well as the event host. + * Only the last 3 events can be rated. + */ + private void getPastEvents() { + refEvents.orderByKey().limitToLast(4).addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(@NonNull DataSnapshot dataSnapshot) { + // Iterate through events to extract metadata + eventDates = new ArrayList<>(); + for(DataSnapshot ds : dataSnapshot.getChildren()) { + // Check if the event date is actually in the future. + // Skip it, as only past events are unlocked for rating. + long epochTimestamp = Long.parseLong(ds.getKey()); + if(epochTimestamp > (System.currentTimeMillis() / 1000L)) break; + + // Extract event host user id + String hostUID = (String)ds.child("gastgeber").getValue(); + + // Extract individual rating data + Map overallRatings = new HashMap<>(); + Map foodRatings = new HashMap<>(); + Map hostRatings = new HashMap<>(); + for(DataSnapshot dsOverallRatings : ds.child("bewertungen").child("allgemein").getChildren()) { + overallRatings.put(dsOverallRatings.getKey(), (long)dsOverallRatings.getValue()); + } + for(DataSnapshot dsFoodRatings : ds.child("bewertungen").child("essen").getChildren()) { + foodRatings.put(dsFoodRatings.getKey(), (long)dsFoodRatings.getValue()); + } + for(DataSnapshot dsHostRatings : ds.child("bewertungen").child("gastgeber").getChildren()) { + hostRatings.put(dsHostRatings.getKey(), (long)dsHostRatings.getValue()); + } + + // Add event to ArrayList to make data available to other methods + eventDates.add(new EventDate(epochTimestamp, hostUID, overallRatings, foodRatings, hostRatings)); + } + + // Update spinner; set selection to most recent event + List spinnerEntries = new ArrayList<>(); + for(int i = 0; i < eventDates.size(); i++) spinnerEntries.add(eventDates.get(i).getFormattedDate()); + ArrayAdapter adapter = new ArrayAdapter<>(root.getContext(), + android.R.layout.simple_spinner_item, spinnerEntries); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinnerPastEvents.setAdapter(adapter); + spinnerPastEvents.setSelection(adapter.getCount() - 1); + } + + @Override + public void onCancelled(@NonNull DatabaseError databaseError) { + Toast.makeText(getContext(), R.string.db_comm_err, Toast.LENGTH_SHORT).show(); + } + }); + } + + class EventDate { + final long epochTimestamp; + final long ownOverallRating, ownFoodRating, ownHostRating; + final int overallRatingsCount, foodRatingsCount, hostRatingsCount; + final long avgOverallRating, avgFoodRating, avgHostRating; + final String hostName; + + EventDate(long epochTimestamp, String hostUID, Map overallRatings, Map foodRatings, Map hostRatings) { + final String ownUID = mUser.getUid(); + this.epochTimestamp = epochTimestamp; + this.hostName = users.get(hostUID); + + this.overallRatingsCount = overallRatings.size(); + this.foodRatingsCount = foodRatings.size(); + this.hostRatingsCount = hostRatings.size(); + + if(overallRatings.containsKey(ownUID)) ownOverallRating = overallRatings.get(ownUID); + else ownOverallRating = 0; + + if(foodRatings.containsKey(ownUID)) ownFoodRating = foodRatings.get(ownUID); + else ownFoodRating = 0; + + if(hostRatings.containsKey(ownUID)) ownHostRating = hostRatings.get(ownUID); + else ownHostRating = 0; + + long temp = 0; + for(Map.Entry entry : overallRatings.entrySet()) temp += entry.getValue(); + avgOverallRating = overallRatingsCount > 0 ? temp / overallRatingsCount : 0; + + temp = 0; + for(Map.Entry entry : foodRatings.entrySet()) temp += entry.getValue(); + avgFoodRating = foodRatingsCount > 0 ? temp / foodRatingsCount : 0; + + temp = 0; + for(Map.Entry entry : hostRatings.entrySet()) temp += entry.getValue(); + avgHostRating = hostRatingsCount > 0 ? temp / hostRatingsCount : 0; + } + + String getFormattedDate() { + Date date = new Date(this.epochTimestamp * 1000L); + return new SimpleDateFormat("EEEE, dd. MMMM YYYY", Locale.getDefault()).format(date); + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100755 index 0000000..971add5 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_dashboard_black_24dp.xml b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml new file mode 100755 index 0000000..46fc8de --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_black_24dp.xml b/app/src/main/res/drawable/ic_home_black_24dp.xml new file mode 100755 index 0000000..f8bb0b5 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100755 index 0000000..eed7a42 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_black_24dp.xml new file mode 100755 index 0000000..78b75c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100755 index 0000000..3d637ef --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,78 @@ + + + + + + + +