diff --git a/app/src/main/java/com/kitty/geotracker/MapsActivity.java b/app/src/main/java/com/kitty/geotracker/MapsActivity.java index d058b1c..cf658ab 100644 --- a/app/src/main/java/com/kitty/geotracker/MapsActivity.java +++ b/app/src/main/java/com/kitty/geotracker/MapsActivity.java @@ -30,6 +30,7 @@ import com.google.maps.android.heatmaps.HeatmapTileProvider; import com.kitty.geotracker.dialogs.JoinSession; import com.kitty.geotracker.dialogs.StartSession; +import com.kitty.geotracker.dialogs.ViewSession; import java.util.ArrayList; import java.util.HashMap; @@ -44,11 +45,12 @@ public class MapsActivity extends FragmentActivity implements View.OnClickListener, StartSession.StartSessionListener, JoinSession.JoinSessionListener, + ViewSession.ViewSessionListener, MeteorController.MeteorControllerListener { private GoogleMap mMap; private FloatingActionsMenu floatingMenu; - private FloatingActionButton btnJoinSession, btnStartSession, btnLeaveSession, btnEndSession; + private FloatingActionButton btnJoinSession, btnStartSession, btnLeaveSession, btnEndSession, btnViewSession; private MeteorController meteorController; private HashMap mapMarkers = new HashMap<>(); private Intent serviceIntent; @@ -72,6 +74,7 @@ protected void onCreate(Bundle savedInstanceState) { FloatingActionButton btnSettings = (FloatingActionButton) findViewById(R.id.btn_settings); btnJoinSession = (FloatingActionButton) findViewById(R.id.btn_join_session); btnStartSession = (FloatingActionButton) findViewById(R.id.btn_start_session); + btnViewSession = (FloatingActionButton) findViewById(R.id.btn_view_session); btnLeaveSession = (FloatingActionButton) findViewById(R.id.btn_leave_session); btnEndSession = (FloatingActionButton) findViewById(R.id.btn_end_session); btnSettings.setOnClickListener(this); @@ -79,6 +82,7 @@ protected void onCreate(Bundle savedInstanceState) { btnStartSession.setOnClickListener(this); btnLeaveSession.setOnClickListener(this); btnEndSession.setOnClickListener(this); + btnViewSession.setOnClickListener(this); // Get map fragment and register callback SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager() @@ -124,6 +128,12 @@ public void onClick(View v) { openJoinSessionDialog(); break; + case R.id.btn_view_session: + // Create the start session dialog + ViewSession viewSession = new ViewSession(); + viewSession.show(getSupportFragmentManager(), viewSession.getClass().getSimpleName()); + break; + case R.id.btn_leave_session: leaveSession(); break; @@ -284,6 +294,7 @@ public void onSessionManage(String sessionName) { btnJoinSession.setVisibility(View.GONE); btnLeaveSession.setVisibility(View.GONE); btnEndSession.setVisibility(View.VISIBLE); + btnViewSession.setVisibility(View.GONE); // Keep the screen on getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); @@ -307,6 +318,20 @@ public void onSessionJoined(final String sessionName) { btnJoinSession.setVisibility(View.GONE); btnLeaveSession.setVisibility(View.VISIBLE); btnEndSession.setVisibility(View.GONE); + btnViewSession.setVisibility(View.GONE); + } + + @Override + public void onSessionViewed(String sessionName) { + // View the session + meteorController.viewSession(sessionName); + + // Hide the start and join session buttons, show the leave session button. + btnStartSession.setVisibility(View.GONE); + btnJoinSession.setVisibility(View.GONE); + btnViewSession.setVisibility(View.GONE); + btnLeaveSession.setVisibility(View.VISIBLE); + btnEndSession.setVisibility(View.GONE); } /** @@ -359,6 +384,7 @@ public void leaveSession() { // Hide the leave session button, show the start and join session buttons btnStartSession.setVisibility(View.VISIBLE); btnJoinSession.setVisibility(View.VISIBLE); + btnViewSession.setVisibility(View.VISIBLE); btnLeaveSession.setVisibility(View.GONE); // Stop location updates @@ -413,6 +439,7 @@ public void onSuccess(String result) { // Hide/show buttons btnStartSession.setVisibility(View.VISIBLE); btnJoinSession.setVisibility(View.VISIBLE); + btnViewSession.setVisibility(View.VISIBLE); btnEndSession.setVisibility(View.GONE); // Show confirmation diff --git a/app/src/main/java/com/kitty/geotracker/MeteorController.java b/app/src/main/java/com/kitty/geotracker/MeteorController.java index 424fd40..ca9a5da 100644 --- a/app/src/main/java/com/kitty/geotracker/MeteorController.java +++ b/app/src/main/java/com/kitty/geotracker/MeteorController.java @@ -42,6 +42,7 @@ public class MeteorController implements MeteorCallback, SharedPreferences.OnSha public static final int STATE_NO_SESSION = 0; public static final int STATE_CREATED_SESSION = 1; public static final int STATE_JOINED_SESSION = 2; + public static final int STATE_VIEWING_SESSION = 3; // Sessions public static final String COLLECTION_SESSIONS = "Sessions"; @@ -459,6 +460,46 @@ public void leaveSession() { clearSession(); } + /** + * View a finished session + * + * @param sessionName Session to view + */ + public void viewSession(final String sessionName) { + Log.d(TAG, "[View Session] Viewing session \"" + sessionName + "\""); + setState(STATE_VIEWING_SESSION); + session = sessionName; + Document document = meteor + .getDatabase() + .getCollection(COLLECTION_SESSIONS) + .whereEqual(COLLECTION_SESSIONS_COLUMN_TITLE, sessionName) + .whereEqual(COLLECTION_SESSIONS_COLUMN_ACTIVE, false) + .findOne(); + + if (document == null) { + mListener.onSessionMessage("Session cannot be viewed", false); + return; + } + + // Subscribe to the session + meteor.subscribe(sessionName, null, new SubscribeListener() { + @Override + public void onSuccess() { + Log.d(TAG, "[View Session] Session viewing successfully."); + } + + @Override + public void onError(String error, String reason, String details) { + Log.e(TAG, "[View Session] Failed to view session \"" + sessionName + "\""); + Log.e(TAG, "[View Session] Error: " + error); + Log.e(TAG, "[View Session] Reason: " + reason); + Log.e(TAG, "[View Session] Details: " + details); + clearSession(); + mListener.onSessionMessage("Failed to view session", false); + } + }); + } + /** * Clear the current session */ @@ -474,7 +515,17 @@ public void clearSession() { * @return List of sessions */ public Document[] getSessions() { - return database.getCollection(COLLECTION_SESSIONS).whereEqual(COLLECTION_SESSIONS_COLUMN_ACTIVE, true).find(); + return getSessions(true); + } + + /** + * Get all sessions + * + * @param active Whether the session is active + * @return Session list + */ + public Document[] getSessions(boolean active) { + return database.getCollection(COLLECTION_SESSIONS).whereEqual(COLLECTION_SESSIONS_COLUMN_ACTIVE, active).find(); } /** @@ -565,7 +616,7 @@ public void onDataAdded(String collectionName, String documentID, String newValu // Only trigger GPS data listener if the user created the session if (collectionName.equals(COLLECTION_GPS_DATA)) { - if (getState() == STATE_CREATED_SESSION && getSession() != null) { + if ((getState() == STATE_CREATED_SESSION || getState() == STATE_VIEWING_SESSION) && getSession() != null) { mListener.onReceivedGPSData(documentID); } } diff --git a/app/src/main/java/com/kitty/geotracker/dialogs/ViewSession.java b/app/src/main/java/com/kitty/geotracker/dialogs/ViewSession.java new file mode 100644 index 0000000..b959bf2 --- /dev/null +++ b/app/src/main/java/com/kitty/geotracker/dialogs/ViewSession.java @@ -0,0 +1,170 @@ +package com.kitty.geotracker.dialogs; + +import android.app.Activity; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.widget.ArrayAdapter; + +import com.kitty.geotracker.MeteorController; +import com.kitty.geotracker.R; + +import java.util.ArrayList; +import java.util.HashMap; + +import im.delight.android.ddp.Meteor; +import im.delight.android.ddp.MeteorCallback; +import im.delight.android.ddp.db.Collection; +import im.delight.android.ddp.db.Database; +import im.delight.android.ddp.db.Document; + + +public class ViewSession extends DialogFragment implements MeteorCallback, DialogInterface.OnClickListener { + + private ViewSessionListener mListener; + private Meteor mMeteor; + private MeteorController meteorController; + private Database database; + private ArrayList items = new ArrayList<>(); + private HashMap documentMap = new HashMap<>(); + private ArrayAdapter adapter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + adapter = new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, items); + meteorController = MeteorController.getInstance(); + mMeteor = meteorController.getMeteor(); + mMeteor.addCallback(this); + database = mMeteor.getDatabase(); + refreshData(); + } + + @Override + public void onDestroy() { + mMeteor.removeCallback(this); + super.onDestroy(); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(R.string.view_session) + .setAdapter(adapter, this); + return builder.create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + Log.d(getClass().getSimpleName(), "Selected item " + String.valueOf(which)); + mListener.onSessionViewed(items.get(which)); + } + + // Override the Fragment.onAttach() method to instantiate the ViewSessionListener + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + // Verify that the host activity implements the callback interface + try { + // Instantiate the ViewSessionListener so we can send events to the host + mListener = (ViewSessionListener) activity; + } catch (ClassCastException e) { + // The activity doesn't implement the interface, throw exception + throw new ClassCastException(activity.toString() + " must implement ViewSessionListener"); + } + } + + /* The activity that creates an instance of this dialog fragment must + * implement this interface in order to receive event callbacks. + * Each method passes the DialogFragment in case the host needs to query it. */ + public interface ViewSessionListener { + public void onSessionViewed(final String sessionName); + } + + private void refreshData() { + items.clear(); + documentMap.clear(); + for (Document document : meteorController.getSessions(false)) { + String title = document.getField(MeteorController.COLLECTION_SESSIONS_COLUMN_TITLE).toString(); + items.add(title); + documentMap.put(document.getId(), title); + } + } + + @Override + public void onConnect(boolean signedInAutomatically) {} + + @Override + public void onDisconnect() {} + + @Override + public void onException(Exception e) {} + + @Override + public void onDataAdded(String collectionName, String documentID, String newValuesJson) { + Log.d(getClass().getSimpleName(), "New data added to " + collectionName + ": " + documentID); + if (collectionName.equals(MeteorController.COLLECTION_SESSIONS)) { + Collection collection = database.getCollection(collectionName); + if (!documentMap.containsKey(documentID)) { + Document document = collection.getDocument(documentID); + boolean active = (boolean) document.getField(MeteorController.COLLECTION_SESSIONS_COLUMN_ACTIVE); + + // If this session is active, don't add it to the list + if (active) { + return; + } + + String title = document.getField(MeteorController.COLLECTION_SESSIONS_COLUMN_TITLE).toString(); + items.add(title); + documentMap.put(documentID, title); + adapter.notifyDataSetChanged(); + } + } + } + + @Override + public void onDataRemoved(String collectionName, String documentID) { + Log.d(getClass().getSimpleName(), "Data removed from " + collectionName + ": " + documentID); + if (collectionName.equals(MeteorController.COLLECTION_SESSIONS)) { + String title = documentMap.get(documentID); + if (title != null) { + items.remove(title); + documentMap.remove(documentID); + adapter.notifyDataSetChanged(); + } + } + } + + @Override + public void onDataChanged(String collectionName, String documentID, String updatedValuesJson, + String removedValuesJson) { + Log.d(getClass().getSimpleName(), "Data changed in " + collectionName + ": " + documentID); + if (collectionName.equals(MeteorController.COLLECTION_SESSIONS)) { + + Collection collection = database.getCollection(collectionName); + Document document = collection.getDocument(documentID); + + boolean active = (boolean) document.getField(MeteorController.COLLECTION_SESSIONS_COLUMN_ACTIVE); + String title = (String) document.getField(MeteorController.COLLECTION_SESSIONS_COLUMN_TITLE); + + if (active && !items.contains(title)) { + // Session becomes active + items.remove(title); + documentMap.remove(documentID); + adapter.notifyDataSetChanged(); + } else if (!active && items.contains(title)) { + // Session becomes inactive + items.add(title); + documentMap.put(documentID, title); + adapter.notifyDataSetChanged(); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_maps.xml b/app/src/main/res/layout/activity_maps.xml index 98af5b4..c9a00a5 100644 --- a/app/src/main/res/layout/activity_maps.xml +++ b/app/src/main/res/layout/activity_maps.xml @@ -32,7 +32,7 @@ android:layout_height="wrap_content" app:fab_colorNormal="@color/white" app:fab_size="mini" - app:fab_title="Settings" + app:fab_title="@string/title_activity_settings" app:fab_icon="@drawable/ic_build_black_24dp" app:fab_colorPressed="@color/white_pressed"/> @@ -42,7 +42,7 @@ android:layout_height="wrap_content" app:fab_colorNormal="@color/white" app:fab_size="mini" - app:fab_title="Start Session" + app:fab_title="@string/start_session" app:fab_icon="@drawable/ic_add_black_24dp" app:fab_colorPressed="@color/white_pressed"/> @@ -52,7 +52,17 @@ android:layout_height="wrap_content" app:fab_colorNormal="@color/white" app:fab_size="mini" - app:fab_title="Join Session" + app:fab_title="@string/join_session" + app:fab_icon="@drawable/ic_add_black_24dp" + app:fab_colorPressed="@color/white_pressed"/> + + @@ -63,7 +73,7 @@ android:visibility="gone" app:fab_colorNormal="@color/white" app:fab_size="mini" - app:fab_title="End Session" + app:fab_title="@string/end_session" app:fab_icon="@drawable/ic_cancel_black_24dp" app:fab_colorPressed="@color/white_pressed"/> @@ -74,7 +84,7 @@ android:visibility="gone" app:fab_colorNormal="@color/white" app:fab_size="mini" - app:fab_title="Leave Session" + app:fab_title="@string/leave_session" app:fab_icon="@drawable/ic_cancel_black_24dp" app:fab_colorPressed="@color/white_pressed"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 280b8a5..a9c6581 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,6 +6,7 @@ Start Session Join Session + View Session Leave Session Session Name @@ -15,5 +16,6 @@ Meteor IP Location permission is required + End Session