Skip to content

Commit

Permalink
Merge pull request #48 from CMPUT301W23T09/isun
Browse files Browse the repository at this point in the history
Added display of qrcode info for profile, some initial tests, and Andy's percentile rank toast implementation
  • Loading branch information
jmmabanta authored Mar 13, 2023
2 parents 0f61768 + f5c58cf commit c1da94d
Show file tree
Hide file tree
Showing 10 changed files with 445 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.cmput301w23t09.qrhunter.profile;

import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;

import android.Manifest;
import android.content.Intent;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule;
import com.cmput301w23t09.qrhunter.GameActivity;
import com.cmput301w23t09.qrhunter.GameController;
import com.cmput301w23t09.qrhunter.R;
import com.cmput301w23t09.qrhunter.database.DatabaseConsumer;
import com.cmput301w23t09.qrhunter.database.DatabaseQueryResults;
import com.cmput301w23t09.qrhunter.player.Player;
import com.cmput301w23t09.qrhunter.player.PlayerDatabase;
import com.cmput301w23t09.qrhunter.qrcode.QRCode;
import com.robotium.solo.Solo;
import java.util.ArrayList;
import java.util.UUID;
import junit.framework.TestCase;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

/** Test classes for profile activity */
public class TestProfileFragment {
private Solo solo;
private String mockPlayerID;
private UUID mockUUID;
private Player mockPlayer;
private ArrayList<QRCode> mockQRCodes;

@Rule
public GrantPermissionRule permissionRule = GrantPermissionRule.grant(Manifest.permission.CAMERA);

@Rule
public ActivityScenarioRule<GameActivity> activityScenarioRule =
new ActivityScenarioRule<>(GameActivity.class);

/**
* Runs before all tests and creates solo instance
*
* @throws Exception
*/
@Before
public void setUp() throws Exception {
// create a mock player
mockPlayerID = "001";
mockUUID = UUID.randomUUID();
mockPlayer =
new Player(mockPlayerID, mockUUID, "Irene", "5873571506", "isun@ualberta.ca", null);
// create mock qr codes
mockQRCodes = new ArrayList<>();

// Mock PlayerDatabase
PlayerDatabase mockPlayerDatabase = mock(PlayerDatabase.class);
doAnswer(
invocation -> {
DatabaseConsumer<Player> callback = invocation.getArgument(1);
callback.accept(new DatabaseQueryResults<>(mockPlayer));
return null;
})
.when(mockPlayerDatabase)
.getPlayerByDeviceId(any(UUID.class), any(DatabaseConsumer.class));
PlayerDatabase.mockInstance(mockPlayerDatabase);

// get solo
activityScenarioRule
.getScenario()
.onActivity(
activity -> {
activity.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
solo = new Solo(InstrumentationRegistry.getInstrumentation(), activity);
});

// navigate to profile fragment
solo.clickOnView(solo.getView(R.id.navigation_my_profile));
await()
.until(
() ->
((GameActivity) solo.getCurrentActivity()).getController().getBody()
instanceof ProfileFragment);
}

/** Checks if the current fragment is correct */
@Test
public void testStartProfile() {
// get current fragment and check if it is a profile fragment
GameController gc = ((GameActivity) solo.getCurrentActivity()).getController();
assertTrue(gc.getBody() instanceof ProfileFragment);
}

/** Checks if the spinners are displayed correctly */
@Test
public void testSpinnerView() {
/*// click on spinner
solo.clickOnView(solo.getView(R.id.order_spinner));
// check if both Ascending and Descending options appear on screen
TestCase.assertTrue(solo.waitForText("Ascending", 1, 2000));
TestCase.assertTrue(solo.waitForText("Descending", 1, 2000));
// click on "ascending" option(since default is "descending")
TestCase.assertFalse(solo.searchText("Descending"));
TestCase.assertTrue(solo.searchText("Ascending"));
*/
}

/** Checks if the username is correctly displayed */
@Test
public void testUsernameView() {
// check if mockPlayer's username is displayed
TestCase.assertTrue(solo.waitForText(mockPlayer.getUsername(), 1, 2000));
}

/** Checks if the fragment is properly changed when the settings button is clicked */
@Test
public void testSettingsButton() {
/*
// click the settings button
solo.clickOnView(solo.getView(R.id.contact_info_button));
// check the current fragment
GameController gc = ((GameActivity) solo.getCurrentActivity()).getController();
assertTrue(gc.getBody() instanceof ProfileSettingsFragment);
*/
}

/** Checks if the player info is properly displayed in the settings */
@Test
public void testSettingsView() {
// click the settings button
solo.clickOnView(solo.getView(R.id.contact_info_button));
// search for the player's phone and email info
assertTrue(solo.waitForText(mockPlayer.getPhoneNo(), 1, 2000));
assertTrue(solo.waitForText(mockPlayer.getEmail(), 1, 2000));
}

/** Checks if the save changes button in the settings works */
@Test
public void testSaveChangesBtn() {
/*// click the settings button
solo.clickOnView(solo.getView(R.id.contact_info_button));
// try changing the phone number
solo.enterText((EditText) solo.getView(R.id.settings_screen_phoneTextField), "1");
// press save changes button
solo.clickOnView(solo.getView(R.id.settings_save_button));
// go back to profile
solo.clickOnView(solo.getView(R.id.settings_back_button));
// go to settings again
solo.clickOnView(solo.getView(R.id.contact_info_button));
// check phone number
assertTrue(solo.searchText(mockPlayer.getPhoneNo() + "1"));*/
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -278,15 +278,6 @@ private Map<String, Object> playerToDBValues(Player player) {
return values;
}

/**
* Manually set PlayerDatabase to some mocked instance for testing
*
* @param mockInstance The mocked PlayerDatabase
*/
public static void mockInstance(PlayerDatabase mockInstance) {
INSTANCE = mockInstance;
}

/**
* Retrieves the PlayerDatabase
*
Expand All @@ -299,4 +290,13 @@ public static PlayerDatabase getInstance() {

return INSTANCE;
}

/**
* Sets instance to a mocked PlayerDatabase for testing purposes.
*
* @param mockPlayerDB The mocked PlayerDatabase to use.
*/
public static void mockInstance(PlayerDatabase mockPlayerDB) {
INSTANCE = mockPlayerDB;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.cmput301w23t09.qrhunter.profile;

import android.content.Context;
import android.view.View;
import android.widget.AdapterView;
import android.widget.GridView;
Expand All @@ -13,12 +14,15 @@
import com.cmput301w23t09.qrhunter.player.PlayerDatabase;
import com.cmput301w23t09.qrhunter.qrcode.QRCode;
import com.cmput301w23t09.qrhunter.qrcode.QRCodeAdapter;
import com.cmput301w23t09.qrhunter.qrcode.QRCodeFragment;
import com.cmput301w23t09.qrhunter.qrcode.ScoreComparator;
import com.cmput301w23t09.qrhunter.util.DeviceUtils;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.firebase.firestore.CollectionReference;
import com.google.firebase.firestore.EventListener;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.FirebaseFirestoreException;
import com.google.firebase.firestore.Query;
import com.google.firebase.firestore.QueryDocumentSnapshot;
import com.google.firebase.firestore.QuerySnapshot;
import java.util.ArrayList;
Expand Down Expand Up @@ -128,20 +132,25 @@ public void onEvent(
// get list of players that have scanned the qr code
ArrayList<String> players = (ArrayList<String>) doc.get("players");
// add the qr code if the current player has scanned it
assert players != null;
if (players.contains(playerID)) {
if (players != null && players.contains(playerID)) {
String hash = doc.getId();
Integer score = (int) (long) doc.get("score");
qrCodes.add(new QRCode(hash, null, null, score, null, null, null, null));
}
}
// update qr code statistics
totalPoints.setText(
fragment.getString(R.string.total_points_txt, getTotalScore()));
gameController
.getActivity()
.getString(R.string.total_points_txt, getTotalScore()));
totalCodes.setText(
fragment.getString(R.string.total_codes_txt, qrCodes.size()));
gameController
.getActivity()
.getString(R.string.total_codes_txt, qrCodes.size()));
topCodeScore.setText(
fragment.getString(R.string.top_code_txt, getTopScore()));
gameController
.getActivity()
.getString(R.string.top_code_txt, getTopScore()));
// sort codes and update qr code list view
updateQRListSort(orderSpinner);
}
Expand Down Expand Up @@ -217,6 +226,22 @@ private void updateQRListSort(Spinner orderSpinner) {
qrCodeAdapter.notifyDataSetChanged();
}

/**
* This is the onclicklistener for qr codes displayed in the profile
*
* @return Return the onclicklistener
*/
public AdapterView.OnItemClickListener handleQRSelect() {
return new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
QRCode qrCode = qrCodes.get(position);
QRCodeFragment.newInstance(qrCode, gameController.getActivePlayer())
.show(fragment.getParentFragmentManager(), "");
}
};
}

/**
* This computes the sum of code scores
*
Expand Down Expand Up @@ -251,6 +276,75 @@ public long getTopScore() {
* @param msg The message to display
*/
private void showMsg(String msg) {
Toast.makeText(fragment.getActivity(), msg, Toast.LENGTH_SHORT).show();
Toast.makeText(gameController.getActivity(), msg, Toast.LENGTH_SHORT).show();
}

/**
* Finds the position of the user's top QR code relative to all QR codes
*
* @param queryDocumentSnapshots Documents for all QR codes
* @param topQR The user's highest scoring QR code
* @return -1 if the user's top QR code was not found in the collection
* @return The user's top QR position relative to all the other QR code positions
*/
private int getTopQRPosition(QuerySnapshot queryDocumentSnapshots, QRCode topQR) {
int position = 1;

for (QueryDocumentSnapshot documentSnapshot : queryDocumentSnapshots) {
String qrHash = documentSnapshot.getString("hash");

if (topQR != null && qrHash.equals(topQR.getHash())) {
return position;
}

position++;
}

return -1;
}

/** Calculates the percentile rank of the user's top QR code by score relative to all QR codes */
public void calculateRankOfHighestQRScore() {
if (qrCodes.size() <= 0) {
return;
}

qrCodes.sort(new ScoreComparator().reversed());
QRCode topQR = qrCodes.get(0);
Query query = qrcodeCollection.orderBy("score", Query.Direction.ASCENDING);
query
.get()
.addOnSuccessListener(
new OnSuccessListener<QuerySnapshot>() {
@Override
public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
int topQRPosition = getTopQRPosition(queryDocumentSnapshots, topQR);
int totalNumQRCodes = queryDocumentSnapshots.size();

if (topQRPosition == -1) {
return;
}

float percentileRank = ((topQRPosition - 1) / (float) totalNumQRCodes) * 100;
displayHighestQRScoreToast(percentileRank);
}
});
}

/**
* Displays the percentile rank of the user's top QR code by score relative to all QR codes
*
* @param percentile Percentile value for the user's top QR code
*/
private void displayHighestQRScoreToast(float percentile) {
int duration = Toast.LENGTH_SHORT;
Context context = gameController.getActivity();
String formattedPercentile = String.format("%.2f", 100.0 - percentile);
String message =
String.format(
"Your highest scoring unique QR code is in the top %s%% in terms of points.",
formattedPercentile);
Toast toast = Toast.makeText(context, message, duration);
toast.show();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public View onCreateView(

controller = new ProfileController(this, getGameController(), deviceUUID);
createProfile(view);
handleProfileHeaderEstimates(view);
return view;
}

Expand All @@ -83,6 +84,7 @@ private void createProfile(View view) {
// setup profile elements
controller.setUpUsername(username);
controller.setUpQRList(qrCodeList, totalPoints, totalCodes, topCodeScore, sortOrderSpinner);
qrCodeList.setOnItemClickListener(controller.handleQRSelect());
}

/** Sets the profile elements to a blank/default state */
Expand Down Expand Up @@ -146,4 +148,24 @@ private void createSpinner(Spinner spinner, @ArrayRes int spinnerOptionsResource
// add listeners for item selection
spinner.setOnItemSelectedListener(controller.handleSpinnerSelect(sortOrderSpinner));
}

/**
* Creates click listener for highest QR code score estimates
*
* @param view The view of the fragment's layout
*/
private void handleProfileHeaderEstimates(View view) {
topCodeScore.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
controller.calculateRankOfHighestQRScore();
}
});
}

/** Gets the controller of the profile fragment, for UI testing only */
public ProfileController getController() {
return controller;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ private void setupViews(View view) throws ExecutionException, InterruptedExcepti
* @see LocationPhotoController
*/
public void updateLocationPhoto() {
if (qrCode.getPhotos().size() > 0) {
if (qrCode.getPhotos() != null && qrCode.getPhotos().size() > 0) {
takeLocationPhotoBtn.setText(R.string.remove_location_photo);
locationPhoto.setImageBitmap(qrCode.getPhotos().get(0).getPhoto());
} else {
Expand Down
1 change: 0 additions & 1 deletion app/src/main/res/layout/fragment_qrcode.xml
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,4 @@
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@android:drawable/ic_delete" />


</androidx.constraintlayout.widget.ConstraintLayout>
Loading

0 comments on commit c1da94d

Please sign in to comment.