diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index c9bb603..0c0c338 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -3,20 +3,7 @@ - - - - - - - - - - - - - - + diff --git a/app/build.gradle b/app/build.gradle index fa33149..5e578bc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -44,6 +44,8 @@ dependencies { implementation 'com.github.bumptech.glide:glide:4.16.0' // Check for the latest version annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0' + //CircleImageView + implementation 'de.hdodenhof:circleimageview:3.1.0' //Firebase Dependencies implementation platform('com.google.firebase:firebase-bom:33.10.0') diff --git a/app/src/main/java/com/aldo/apps/familyhelpers/HelperGridActivity.java b/app/src/main/java/com/aldo/apps/familyhelpers/HelperGridActivity.java index 26780f2..66f126e 100644 --- a/app/src/main/java/com/aldo/apps/familyhelpers/HelperGridActivity.java +++ b/app/src/main/java/com/aldo/apps/familyhelpers/HelperGridActivity.java @@ -97,9 +97,8 @@ public class HelperGridActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - mLocationHelper = LocationHelper.getInstance(this); mWelcomeMessageView = findViewById(R.id.tv_welcome_message); - if (mCurrentUser == null) { + if (mCurrentUser == null || mCurrentUser.isAnonymous() || mCurrentUser.getUid() == null) { final Intent signInIntent = AuthUI.getInstance() .createSignInIntentBuilder() .setAvailableProviders(SIGN_IN_PROVIDERS) @@ -110,6 +109,7 @@ public class HelperGridActivity extends AppCompatActivity { mWelcomeMessageView.setText(String.format(getString(R.string.welcome_message_placeholder), mCurrentUser.getDisplayName())); mDbHelper = new DatabaseHelper(); + mLocationHelper = LocationHelper.getInstance(this); } } @@ -161,7 +161,6 @@ public class HelperGridActivity extends AppCompatActivity { Log.d(TAG, "launchHelper: Clicked ShareLocation"); if (mLocationHelper.requestLocationPermissions(HelperGridActivity.this)) { Log.d(TAG, "launchHelper: Permission already granted"); - mLocationHelper.toggleUpdate(HelperGridActivity.this); final Intent intent = new Intent(HelperGridActivity.this, ShareLocationActivity.class); startActivity(intent); } @@ -205,6 +204,7 @@ public class HelperGridActivity extends AppCompatActivity { mWelcomeMessageView.setText(String.format(getString(R.string.welcome_message_placeholder), mCurrentUser.getDisplayName())); mDbHelper = new DatabaseHelper(); + mLocationHelper = LocationHelper.getInstance(this); } else { Log.w(TAG, "onSignInResult: Sign-In failed"); mWelcomeMessageView.setText(String.format(getString(R.string.welcome_message_placeholder), @@ -224,7 +224,8 @@ public class HelperGridActivity extends AppCompatActivity { @NonNull final int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (mLocationHelper.handlePermissionResult(requestCode, grantResults)) { - mLocationHelper.toggleUpdate(HelperGridActivity.this); + final Intent intent = new Intent(HelperGridActivity.this, ShareLocationActivity.class); + startActivity(intent); } } diff --git a/app/src/main/java/com/aldo/apps/familyhelpers/ShareLocationActivity.java b/app/src/main/java/com/aldo/apps/familyhelpers/ShareLocationActivity.java index 63c8948..4f62661 100644 --- a/app/src/main/java/com/aldo/apps/familyhelpers/ShareLocationActivity.java +++ b/app/src/main/java/com/aldo/apps/familyhelpers/ShareLocationActivity.java @@ -1,18 +1,32 @@ package com.aldo.apps.familyhelpers; +import static com.aldo.apps.familyhelpers.utils.GlobalConstants.DEFAULT_HOME_LATITUDE; +import static com.aldo.apps.familyhelpers.utils.GlobalConstants.DEFAULT_HOME_LONGITUDE; +import static com.aldo.apps.familyhelpers.utils.GlobalConstants.DEFAULT_MINIMUM_LOCATION_INTERVAL_METERS; +import static com.aldo.apps.familyhelpers.utils.GlobalConstants.DEFAULT_MINIMUM_LOCATION_INTERVAL_MILLIS; import static com.aldo.apps.familyhelpers.utils.GlobalConstants.METER_PER_SECOND_TO_KMH_CONVERTER; import android.content.res.Configuration; +import android.net.Uri; import android.os.Bundle; +import android.text.TextUtils; import android.util.Log; +import android.view.View; +import android.widget.CompoundButton; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.aldo.apps.familyhelpers.model.LocationObject; +import com.aldo.apps.familyhelpers.model.User; +import com.aldo.apps.familyhelpers.ui.ActiveShareAdapter; import com.aldo.apps.familyhelpers.workers.DatabaseHelper; +import com.aldo.apps.familyhelpers.workers.LocationHelper; import com.bumptech.glide.Glide; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; @@ -22,12 +36,19 @@ import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.MapStyleOptions; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; +import com.google.android.material.materialswitch.MaterialSwitch; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseUser; +import org.checkerframework.checker.units.qual.A; + 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 io.reactivex.rxjava3.disposables.Disposable; @@ -97,6 +118,18 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea */ private Disposable mLocationUpdateSubscription; + /** + * The {@link Disposable} holding the subscription to the {@link String} + * {@link io.reactivex.rxjava3.subjects.BehaviorSubject} in order to know whom to follow. + */ + private Disposable mCurrentlyFollowingSubscription; + + /** + * The {@link Disposable} holding the subscription to {@link User} + * {@link io.reactivex.rxjava3.subjects.BehaviorSubject} in order to have it cancellable. + */ + private Disposable mUserSubscription; + /** * Boolean flag indicating whether the system is currently in night mode or not. */ @@ -107,6 +140,36 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea */ private Marker mGmapsMarker; + /** + * Map containing all markers and the corresponding ID. + */ + private final Map mMarkerMap = new HashMap<>(); + + /** + * {@link List} of all {@link User}s. + */ + private final List mAllUsers = new ArrayList<>(); + + /** + * The ID of who the user is currently following. + */ + private String mCurrentlyFollowing; + + /** + * Displays an error if no active shares. + */ + private TextView mNoActiveShares; + + /** + * Displays icons of the users of active shares. + */ + private RecyclerView mActiveShares; + + /** + * The {@link ActiveShareAdapter} to populate the activeShare view. + */ + private ActiveShareAdapter mActiveShareAdapter; + @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -116,14 +179,34 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea .findFragmentById(R.id.map); mCurrentUser = FirebaseAuth.getInstance().getCurrentUser(); mInfoBoxIcon = findViewById(R.id.share_location_info_user_icon); - Glide.with(this) - .load(mCurrentUser.getPhotoUrl()) - .into(mInfoBoxIcon); mInfoBoxTitle = findViewById(R.id.share_location_info_title); mInfoBoxLocation = findViewById(R.id.share_location_info_location); mInfoBoxAltitude = findViewById(R.id.share_location_info_altitude); mInfoBoxSpeed = findViewById(R.id.share_location_info_speed); mInfoBoxTimeStamp = findViewById(R.id.share_location_info_timestamp); + mNoActiveShares = findViewById(R.id.tv_no_active_shares); + mActiveShares = findViewById(R.id.active_share_layout); + final MaterialSwitch shareLocationToggle = findViewById(R.id.switch_start_stop_sharing_location); + final LocationHelper locationHelper = LocationHelper.getInstance(this); + shareLocationToggle.setChecked(locationHelper.isCurrentlySharing()); + shareLocationToggle.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) { + locationHelper.startLocationUpdates(ShareLocationActivity.this, + DEFAULT_MINIMUM_LOCATION_INTERVAL_MILLIS, + DEFAULT_MINIMUM_LOCATION_INTERVAL_METERS) + ; + } else { + locationHelper.stopLocationUpdates(); + } + }); + + locationHelper.getSharingStateSubject().subscribe(isSharing -> shareLocationToggle.setChecked(isSharing), + this::handleSubscriptionError); + + mActiveShares.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); + mActiveShareAdapter = new ActiveShareAdapter(this); + + mActiveShares.setAdapter(mActiveShareAdapter); mapFragment.getMapAsync(this); mDbHelper = new DatabaseHelper(); } @@ -139,30 +222,167 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea if (mShareId == null) { mShareId = mCurrentUser.getUid(); } - mDbHelper.startListeningForLocationUpdates(mShareId); if (mLocationUpdateSubscription != null) { mLocationUpdateSubscription.dispose(); } - mLocationUpdateSubscription = mDbHelper.getLocationSubject() - .subscribe(this::handleReceivedLocation, this::handleSubscriptionError); + mUserSubscription = mDbHelper.getAllUsers() + .subscribe(this::handleAllUsers, this::handleUserSubscriptionFailed); + mLocationUpdateSubscription = mDbHelper.subscribeToAllLocationUpdates() + .subscribe(this::handleReceivedLocations, this::handleSubscriptionError); + mCurrentlyFollowingSubscription = mActiveShareAdapter.getCurrentlySelectedSubject() + .subscribe(this::handleCurrentlyFollowingChanged, this::handleSubscriptionError); } @Override protected void onPause() { super.onPause(); - if (mLocationUpdateSubscription != null) { + if (mUserSubscription != null && !mUserSubscription.isDisposed()) { + mUserSubscription.dispose(); + } + if (mCurrentlyFollowingSubscription != null && !mCurrentlyFollowingSubscription.isDisposed()) { + mCurrentlyFollowingSubscription.dispose(); + } + if (mLocationUpdateSubscription != null && !mLocationUpdateSubscription.isDisposed()) { mLocationUpdateSubscription.dispose(); } } /** - * Helper method to handle the received location by updating both the Map and the Info box. + * Helper method to handle all received locations. Will remove existing invalid, update existing + * valid and add newly added Markers to the map. * - * @param locationObject The received {@link LocationObject} holding the new information. + * @param locationObjectMap The {@link Map} matching shareIds to {@link LocationObject}s. */ - private void handleReceivedLocation(final LocationObject locationObject) { - Log.d(TAG, "handleReceivedLocation() called with: locationObject = [" + locationObject + "]"); - mInfoBoxTitle.setText(String.format(getString(R.string.share_location_info_title), mCurrentUser.getDisplayName())); + private void handleReceivedLocations(final Map locationObjectMap) { + if (TextUtils.isEmpty(mCurrentlyFollowing)) { + clearInfoBox(); + } + // If empty received, remove all markers. + if (locationObjectMap.isEmpty()) { + for (final Marker marker : mMarkerMap.values()) { + marker.remove(); + } + mMarkerMap.clear(); + clearInfoBox(); + mNoActiveShares.setVisibility(View.VISIBLE); + mActiveShares.setVisibility(View.GONE); + } else { + mNoActiveShares.setVisibility(View.GONE); + mActiveShares.setVisibility(View.VISIBLE); + } + + if (mGmapsMarker != null) { + mGmapsMarker.remove(); + } + //Update the available active shares. + updateActiveShares(locationObjectMap); + // If non-empty received, remove all invalid existing markers + removeInvalidExistingMarkers(locationObjectMap); + // Update/Insert all existing/new markers + updateExistingOrCreateValidMarkers(locationObjectMap); + } + + private void updateActiveShares(final Map locationObjectMap) { + final List activeShares = new ArrayList<>(); + for (final String shareId : locationObjectMap.keySet()) { + activeShares.add(mDbHelper.getUserForId(shareId)); + } + mActiveShareAdapter.applyNewData(activeShares); + } + + + /** + * Removed all markers that became invalid because the sharing was stopped. + * + * @param locationObjectMap The {@link Map} matching shareIds to {@link LocationObject}s. + */ + private void removeInvalidExistingMarkers(final Map locationObjectMap) { + final List toBeRemovedKeys = new ArrayList<>(); + for (Map.Entry markerEntry : mMarkerMap.entrySet()) { + if (!locationObjectMap.containsKey(markerEntry.getKey())) { + Log.d(TAG, "handleReceivedLocations: Marker not valid anymore, remove"); + markerEntry.getValue().remove(); + toBeRemovedKeys.add(markerEntry.getKey()); + } + } + // Remove from the map as well + for (final String removeKey : toBeRemovedKeys) { + if (TextUtils.equals(removeKey, mCurrentlyFollowing)) { + clearInfoBox(); + mCurrentlyFollowing = null; + } + mMarkerMap.remove(removeKey); + } + } + + /** + * Helper method to update or create the valid markers on the map. Updates the position of a + * marker if it is existing before, adds a new marker if nor yet existing. + * + * @param locationObjectMap The {@link Map} matching shareIds to {@link LocationObject}s. + */ + private void updateExistingOrCreateValidMarkers(final Map locationObjectMap) { + for (final Map.Entry locationSet : locationObjectMap.entrySet()) { + final LocationObject newReceived = locationSet.getValue(); + final LatLng receivedLocation = new LatLng(newReceived.getLatitude(), newReceived.getLongitude()); + final String sharingId = locationSet.getKey(); + final User locationUser = mDbHelper.getUserForId(sharingId); + + if (TextUtils.equals(mCurrentlyFollowing, sharingId)) { + final LocationObject locationObject = locationSet.getValue(); + updateInfoBox(locationObject); + mGmap.animateCamera(CameraUpdateFactory.newLatLngZoom( + new LatLng(locationObject.getLatitude(), locationObject.getLongitude()), + 15.0f)); + } + if (mMarkerMap.containsKey(sharingId)) { + final Marker existing = mMarkerMap.get(sharingId); + if (existing == null) { + Log.w(TAG, "updateExistingOrCreateValidMarkers: Marker somehow not existing, skip"); + continue; + } + Log.d(TAG, "handleReceivedLocations: Marker still available, update position"); + existing.setPosition(receivedLocation); + + } else { + Log.d(TAG, "handleReceivedLocations: New Marker to be created."); + final Marker marker = mGmap.addMarker(new MarkerOptions() + .position(receivedLocation) + .title(locationUser == null ? "" : locationUser.getDisplayName())); + mMarkerMap.put(locationSet.getKey(), marker); + } + } + } + + /** + * Helper method to clear the info box. + */ + private void clearInfoBox() { + mInfoBoxTitle.setText(R.string.share_location_info_title_empty); + mInfoBoxLocation.setText(""); + mInfoBoxAltitude.setText(""); + mInfoBoxSpeed.setText(""); + mInfoBoxTimeStamp.setText(""); + mInfoBoxIcon.setImageResource(R.drawable.ic_unknown_user); + } + + /** + * Update the info box with information of a LocationShare. + * + * @param locationObject The {@link LocationObject} holding the information. + */ + private void updateInfoBox(final LocationObject locationObject) { + if (mCurrentlyFollowing != null) { + final User followed = mDbHelper.getUserForId(mCurrentlyFollowing); + if (followed != null) { + mInfoBoxTitle.setText(String.format(getString(R.string.share_location_info_title), + followed.getDisplayName())); + Glide.with(this) + .load(Uri.parse(followed.getPhotoUrl())) + .centerCrop() + .into(mInfoBoxIcon); + } + } mInfoBoxLocation.setText(String.format(getString(R.string.share_location_info_location), locationObject.getLatitude(), locationObject.getLongitude())); mInfoBoxAltitude.setText(String.format(getString(R.string.share_location_info_altitude), @@ -170,19 +390,8 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea mInfoBoxSpeed.setText(String.format(getString(R.string.share_location_info_speed), locationObject.getSpeed(), locationObject.getSpeed() * METER_PER_SECOND_TO_KMH_CONVERTER)); - mInfoBoxTimeStamp.setText(String.format(getString(R.string.share_location_info_timestamp), formatTime(locationObject.getTimestamp()))); - - final LatLng received = new LatLng(locationObject.getLatitude(), locationObject.getLongitude()); - - if (mGmapsMarker == null) { - mGmap.addMarker(new MarkerOptions() - .position(received) - .title(mCurrentUser.getDisplayName())); - } else { - mGmapsMarker.setPosition(received); - } - mGmap.moveCamera(CameraUpdateFactory.newLatLngZoom(received, 15.0f)); - + mInfoBoxTimeStamp.setText(String.format(getString(R.string.share_location_info_timestamp), + formatTime(locationObject.getTimestamp()))); } /** @@ -198,7 +407,6 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea * Formats the provided time in milliseconds in a human readable format. * * @param millis The remaining time in milliseconds. - * * @return The String representation of the milliseconds. */ private String formatTime(final long millis) { @@ -207,6 +415,42 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea return simpleDateFormat.format(date); } + /** + * Handle all available users. + * + * @param allUsers The {@link List} of available users. + */ + private void handleAllUsers(final List allUsers) { + mAllUsers.clear(); + mAllUsers.addAll(allUsers); + } + + /** + * Handlles the callback when the selection of currently sharedd Id changes. Re-Established the + * subscription to all locations and updates the info box. + * + * @param shareId The ID of the user to be followed. + */ + private void handleCurrentlyFollowingChanged(final String shareId) { + Log.d(TAG, "onCreate: Currently selected = [" + shareId + "]"); + mCurrentlyFollowing = shareId; + // Re-setup the subscription. + if (mLocationUpdateSubscription != null) { + mLocationUpdateSubscription.dispose(); + } + mLocationUpdateSubscription = mDbHelper.subscribeToAllLocationUpdates() + .subscribe(this::handleReceivedLocations, this::handleSubscriptionError); + } + + /** + * Handle subscription to users failing. + * + * @param throwable The {@link Throwable} associated. + */ + private void handleUserSubscriptionFailed(final Throwable throwable) { + Log.e(TAG, "handleUserSubscriptionFailed: Subscription to all users failed, keep cached state"); + } + @Override public void onMapReady(@NonNull final GoogleMap googleMap) { mGmap = googleMap; @@ -215,17 +459,15 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea } else { mGmap.setMapStyle(MapStyleOptions.loadRawResourceStyle(this, R.raw.map_style_day)); } - final LatLng defaultHome = new LatLng(48.41965746149261, 9.909289365473684); + final LatLng defaultHome = new LatLng(DEFAULT_HOME_LATITUDE, DEFAULT_HOME_LONGITUDE); if (mGmapsMarker == null) { - mGmap.addMarker(new MarkerOptions() + mGmapsMarker = mGmap.addMarker(new MarkerOptions() .position(defaultHome) .title("Default - Home")); } else { mGmapsMarker.setPosition(defaultHome); } - mGmap.moveCamera(CameraUpdateFactory.newLatLngZoom(defaultHome, 15.0f)); + mGmap.moveCamera(CameraUpdateFactory.newLatLngZoom(defaultHome, 8.0f)); } - - } \ No newline at end of file diff --git a/app/src/main/java/com/aldo/apps/familyhelpers/model/User.java b/app/src/main/java/com/aldo/apps/familyhelpers/model/User.java index 3ab4090..0eb7103 100644 --- a/app/src/main/java/com/aldo/apps/familyhelpers/model/User.java +++ b/app/src/main/java/com/aldo/apps/familyhelpers/model/User.java @@ -115,4 +115,14 @@ public class User { } return new User(uid,displayName, photoUrl == null ? null : photoUrl.toString(), creationDate); } + + @Override + public String toString() { + return "User{" + + "uId='" + uId + '\'' + + ", displayName='" + displayName + '\'' + + ", photoUrl='" + photoUrl + '\'' + + ", creationDate=" + creationDate + + '}'; + } } diff --git a/app/src/main/java/com/aldo/apps/familyhelpers/ui/ActiveShareAdapter.java b/app/src/main/java/com/aldo/apps/familyhelpers/ui/ActiveShareAdapter.java new file mode 100644 index 0000000..f4b659e --- /dev/null +++ b/app/src/main/java/com/aldo/apps/familyhelpers/ui/ActiveShareAdapter.java @@ -0,0 +1,109 @@ +package com.aldo.apps.familyhelpers.ui; + +import android.content.Context; +import android.graphics.PorterDuff; +import android.net.Uri; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.aldo.apps.familyhelpers.R; +import com.aldo.apps.familyhelpers.model.User; +import com.bumptech.glide.Glide; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import de.hdodenhof.circleimageview.CircleImageView; +import io.reactivex.rxjava3.subjects.BehaviorSubject; + +public class ActiveShareAdapter extends RecyclerView.Adapter { + private static final String TAG = "ActiveShareAdapter"; + + private final BehaviorSubject mCurrentlySelectedSubject = BehaviorSubject.createDefault(""); + + private final List mSharingUserList = new ArrayList<>(); + + private final WeakReference mContextRef; + + public ActiveShareAdapter(final Context context) { + mContextRef = new WeakReference<>(context); + } + + @NonNull + @Override + public ActiveShareViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { + final View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.active_share_view_holder, parent, false); + return new ActiveShareViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull final ActiveShareViewHolder holder, final int position) { + final User user = mSharingUserList.get(position); + Log.d(TAG, "onBindViewHolder: called with user [" + user + "]"); + holder.setUserData(user); + } + + @Override + public int getItemCount() { + return mSharingUserList.size(); + } + + public void applyNewData(final List newUsers) { + mSharingUserList.clear(); + mSharingUserList.addAll(newUsers); + notifyDataSetChanged(); + } + + public BehaviorSubject getCurrentlySelectedSubject() { + return mCurrentlySelectedSubject; + } + + public class ActiveShareViewHolder extends RecyclerView.ViewHolder { + + private CircleImageView mProfilePicture; + + private TextView mName; + + private View mItemView; + + public ActiveShareViewHolder(@NonNull final View itemView) { + super(itemView); + mProfilePicture = itemView.findViewById(R.id.active_share_profile_picture); + mName = itemView.findViewById(R.id.active_share_name); + mItemView = itemView; + } + + public void setUserData(final User user) { + if (user == null) { + Log.w(TAG, "setUserData: Skip update, no valid update yet"); + return; + } + mItemView.setOnClickListener(v -> { + Log.d(TAG, "setUserData: Clicked on [" + user + "]"); + mCurrentlySelectedSubject.onNext(user.getuId()); + }); + mName.setText(user.getDisplayName()); + final Context context = mContextRef.get(); + if (context == null) { + mProfilePicture.setImageResource(R.drawable.ic_unknown_user); + return; + } + Glide.with(context) + .load(Uri.parse(user.getPhotoUrl())) + .centerCrop() + .dontAnimate() + .placeholder(R.drawable.ic_unknown_user) + .into(mProfilePicture); + } + } +} diff --git a/app/src/main/java/com/aldo/apps/familyhelpers/utils/GlobalConstants.java b/app/src/main/java/com/aldo/apps/familyhelpers/utils/GlobalConstants.java index 28bb884..5e6027a 100644 --- a/app/src/main/java/com/aldo/apps/familyhelpers/utils/GlobalConstants.java +++ b/app/src/main/java/com/aldo/apps/familyhelpers/utils/GlobalConstants.java @@ -73,8 +73,21 @@ public final class GlobalConstants { */ public static final int DEFAULT_MINIMUM_LOCATION_INTERVAL_METERS = 5; + /** + * Conversion constants to convert m/s into km/h + */ public static final double METER_PER_SECOND_TO_KMH_CONVERTER = 3.6; + /** + * Latitude of the default home (== Blaustein) + */ + public static final double DEFAULT_HOME_LATITUDE = 48.41965746149261; + + /** + * Longitude of the default home (== Blaustein) + */ + public static final double DEFAULT_HOME_LONGITUDE = 9.909289365473684; + /** * List of available Firebase signIn/Login providers. */ diff --git a/app/src/main/java/com/aldo/apps/familyhelpers/workers/DatabaseHelper.java b/app/src/main/java/com/aldo/apps/familyhelpers/workers/DatabaseHelper.java index 040d92e..6c52c5f 100644 --- a/app/src/main/java/com/aldo/apps/familyhelpers/workers/DatabaseHelper.java +++ b/app/src/main/java/com/aldo/apps/familyhelpers/workers/DatabaseHelper.java @@ -6,15 +6,23 @@ import static com.aldo.apps.familyhelpers.utils.DatabaseConstants.DB_DOC_USER_ID import android.util.Log; +import androidx.annotation.Nullable; + import com.aldo.apps.familyhelpers.model.LocationObject; import com.aldo.apps.familyhelpers.model.User; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.firestore.DocumentSnapshot; +import com.google.firebase.firestore.EventListener; import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.ListenerRegistration; +import com.google.firebase.firestore.QuerySnapshot; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import io.reactivex.rxjava3.subjects.BehaviorSubject; @@ -48,8 +56,16 @@ public class DatabaseHelper { */ private BehaviorSubject mObservedLocation = BehaviorSubject.create(); + /** + * The {@link ListenerRegistration} to listen for location updates. + */ private ListenerRegistration mLocationUpdateListener; + /** + * {@link Map} containing all users. + */ + private Map mAllUserMap = new HashMap<>(); + /** * C'tor. */ @@ -73,7 +89,6 @@ public class DatabaseHelper { */ private void subscribeToAllUsers() { mDatabase.collection(DB_COLL_USERS) - .whereNotEqualTo(DB_DOC_USER_ID, mCurrentUser.getUid()) .addSnapshotListener((value, error) -> { if (error != null) { Log.e(TAG, "onEvent: ", error); @@ -82,8 +97,12 @@ public class DatabaseHelper { if (value != null && !value.isEmpty()) { final List allUsers = new ArrayList<>(); value.getDocuments() - .forEach(documentSnapshot - -> allUsers.add(documentSnapshot.toObject(User.class))); + .forEach(documentSnapshot -> { + final User user = documentSnapshot.toObject(User.class); + Log.d(TAG, "subscribeToAllUsers: Got user [" + user + "]"); + allUsers.add(user); + mAllUserMap.put(user.getuId(), user); + }); mAllUsers.onNext(allUsers); } else { Log.w(TAG, "onEvent: Read failed, do not update local values."); @@ -100,6 +119,16 @@ public class DatabaseHelper { return mAllUsers; } + public User getUserForId(final String userId) { + Log.d(TAG, "getUserForId() called with: userId = [" + userId + "]"); + if (mAllUserMap.containsKey(userId)) { + Log.d(TAG, "getUserForId: Returning [" + mAllUserMap.get(userId) + "]"); + return mAllUserMap.get(userId); + } + Log.d(TAG, "getUserForId: returning null"); + return null; + } + /** * Returns the {@link BehaviorSubject} containing the {@link LocationObject} to be observed. * @@ -127,6 +156,9 @@ public class DatabaseHelper { */ public void startListeningForLocationUpdates(final String shareId) { mObservedLocation = BehaviorSubject.create(); + if (mLocationUpdateListener != null) { + mLocationUpdateListener.remove(); + } mLocationUpdateListener = mDatabase.collection(DB_COLL_LOCATION) .document(shareId) .addSnapshotListener((value, error) -> { @@ -144,6 +176,37 @@ public class DatabaseHelper { }); } + /** + * Helper method to start listening for all available location sharings in progress. + * + * @return The {@link BehaviorSubject} containing a {@link Map} of all {@link LocationObject} matched + * to the sharing ID. + */ + public BehaviorSubject> subscribeToAllLocationUpdates() { + final Map locationMap = new HashMap<>(); + final BehaviorSubject> behaviorSubject + = BehaviorSubject.createDefault(locationMap); + if (mLocationUpdateListener != null) { + mLocationUpdateListener.remove(); + } + mLocationUpdateListener = mDatabase.collection(DB_COLL_LOCATION) + .addSnapshotListener((value, error) -> { + if (value == null || value.isEmpty()) { + Log.d(TAG, "onEvent: No ongoing location shares, return empty map."); + behaviorSubject.onNext(new HashMap<>()); + return; + } + final List allDocs = value.getDocuments(); + locationMap.clear(); + for (final DocumentSnapshot documentSnapshot : allDocs) { + locationMap.put(documentSnapshot.getId(), documentSnapshot.toObject(LocationObject.class)); + } + behaviorSubject.onNext(locationMap); + }); + + return behaviorSubject; + } + /** * Cancels (by deleting) the ongoing {@link LocationObject} sharing in the database. */ diff --git a/app/src/main/java/com/aldo/apps/familyhelpers/workers/LocationHelper.java b/app/src/main/java/com/aldo/apps/familyhelpers/workers/LocationHelper.java index 6551e4a..90acb51 100644 --- a/app/src/main/java/com/aldo/apps/familyhelpers/workers/LocationHelper.java +++ b/app/src/main/java/com/aldo/apps/familyhelpers/workers/LocationHelper.java @@ -72,13 +72,18 @@ public final class LocationHelper implements LocationListener { /** * The {@link DatabaseHelper} for db access. */ - private final DatabaseHelper mDbHelper = new DatabaseHelper(); + private final DatabaseHelper mDbHelper; /** * The {@link NotificationHelper} to show and update {@link Notification}s. */ private final NotificationHelper mNotificationHelper; + /** + * The {@link BehaviorSubject} holding the state of current subscription. + */ + private final BehaviorSubject mSharingStateSubject = BehaviorSubject.createDefault(false); + /** * Private C'Tor for singleton instance. * @@ -88,6 +93,7 @@ public final class LocationHelper implements LocationListener { mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); mContextRef = new WeakReference<>(context); mNotificationHelper = new NotificationHelper(context); + mDbHelper = new DatabaseHelper(); mNotificationHelper.createAndRegisterNotificationChannel( SHARE_LOCATION_CHANNEL_ID, R.string.share_location_notification_channel); } @@ -153,6 +159,10 @@ public final class LocationHelper implements LocationListener { * @return true if subscription started, false otherwise. */ public boolean startLocationUpdates(final Activity activity, final int minIntervalMillis, final int minDistance) { + if (mIsSubscribed) { + Log.d(TAG, "startLocationUpdates: Already subscribed, no need to update"); + return true; + } if (ContextCompat.checkSelfPermission(activity, ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { mLocationManager.requestLocationUpdates( LocationManager.FUSED_PROVIDER, @@ -162,37 +172,44 @@ public final class LocationHelper implements LocationListener { Looper.getMainLooper() ); mIsSubscribed = true; - return true; + } else { + mIsSubscribed = false; } - mIsSubscribed = false; - return false; + mSharingStateSubject.onNext(mIsSubscribed); + return mIsSubscribed; } /** * Stops listening for updates. */ public void stopLocationUpdates() { + if (!mIsSubscribed) { + Log.d(TAG, "stopLocationUpdates: Subscription already ended, no need to end again"); + return; + } mLocationManager.removeUpdates(this); mIsSubscribed = false; + mSharingStateSubject.onNext(false); mDbHelper.cancelOngoingLocationSharing(); mNotificationHelper.cancelNotification(SHARE_LOCATION_NOTIFICATION_ID); } /** - * Helper method to toggle the subscription state, if already running, cancel it, if not yet, - * start it. + * Returns the current state of subscription. * - * @param activity The {@link Activity} from where this was called. + * @return true if location sharing is active, false otherwise. */ - public void toggleUpdate(final Activity activity) { - if (mIsSubscribed) { - Log.d(TAG, "toggleUpdate: Stop subscription"); - stopLocationUpdates(); - } else { - Log.d(TAG, "toggleUpdate: Start subscription"); - startLocationUpdates(activity, DEFAULT_MINIMUM_LOCATION_INTERVAL_MILLIS, - DEFAULT_MINIMUM_LOCATION_INTERVAL_METERS); - } + public boolean isCurrentlySharing() { + return mIsSubscribed; + } + + /** + * Returns the {@link BehaviorSubject} holding the state of current subscription. + * + * @return The {@link BehaviorSubject} holding the state of current subscription. + */ + public BehaviorSubject getSharingStateSubject() { + return mSharingStateSubject; } /** diff --git a/app/src/main/res/drawable/ic_unknown_user.xml b/app/src/main/res/drawable/ic_unknown_user.xml new file mode 100644 index 0000000..22e9712 --- /dev/null +++ b/app/src/main/res/drawable/ic_unknown_user.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/active_share_view_holder.xml b/app/src/main/res/layout/active_share_view_holder.xml new file mode 100644 index 0000000..d776506 --- /dev/null +++ b/app/src/main/res/layout/active_share_view_holder.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_share_location.xml b/app/src/main/res/layout/activity_share_location.xml index ec59725..ec779cb 100644 --- a/app/src/main/res/layout/activity_share_location.xml +++ b/app/src/main/res/layout/activity_share_location.xml @@ -6,14 +6,48 @@ android:layout_height="match_parent" tools:context=".ShareLocationActivity"> + + + + + + + + + + + Share Location ShareLocation You are sharing your current location within the family + Currently nobody is sharing their location + You are currently not following anyone You are following %s Last Received Location = %f;%f Last received latitude = %.2f Meters The last received velocity was %.2f m/s (%.1f km/h) This update was received at: %s + Share your location \ No newline at end of file