diff --git a/app/build.gradle b/app/build.gradle index 5e578bc..7822a96 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,6 +39,7 @@ dependencies { //Google Maps SDK implementation 'com.google.android.gms:play-services-maps:19.1.0' + implementation 'com.google.android.gms:play-services-location:21.3.0' // Glide implementation 'com.github.bumptech.glide:glide:4.16.0' // Check for the latest version diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 533cec3..9591bd0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + @@ -39,6 +40,9 @@ android:name=".workers.SleepTimerHelper" android:foregroundServiceType="specialUse" /> + + - - - - - - \ No newline at end of file diff --git a/app/src/main/java/com/aldo/apps/familyhelpers/DoerflingerHelpersDeviceAdminReceiver.java b/app/src/main/java/com/aldo/apps/familyhelpers/DoerflingerHelpersDeviceAdminReceiver.java index 114981b..983e8eb 100644 --- a/app/src/main/java/com/aldo/apps/familyhelpers/DoerflingerHelpersDeviceAdminReceiver.java +++ b/app/src/main/java/com/aldo/apps/familyhelpers/DoerflingerHelpersDeviceAdminReceiver.java @@ -8,7 +8,7 @@ import androidx.annotation.NonNull; public class DoerflingerHelpersDeviceAdminReceiver extends DeviceAdminReceiver { @Override - public void onEnabled(@NonNull final Context context, @NonNull final Intent intent) { + public void onEnabled(@NonNull final Context context, @NonNull final Intent intent) { // Called when the app is enabled as a device administrator. } 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 66f126e..9c276bd 100644 --- a/app/src/main/java/com/aldo/apps/familyhelpers/HelperGridActivity.java +++ b/app/src/main/java/com/aldo/apps/familyhelpers/HelperGridActivity.java @@ -1,7 +1,6 @@ package com.aldo.apps.familyhelpers; import static android.Manifest.permission.POST_NOTIFICATIONS; - import static com.aldo.apps.familyhelpers.utils.GlobalConstants.SIGN_IN_PROVIDERS; import android.content.Intent; @@ -20,8 +19,8 @@ import androidx.core.content.ContextCompat; import com.aldo.apps.familyhelpers.ui.HelperGroupTile; import com.aldo.apps.familyhelpers.ui.SleepTimerPopup; import com.aldo.apps.familyhelpers.utils.DevicePolicyManagerHelper; -import com.aldo.apps.familyhelpers.workers.LocationHelper; import com.aldo.apps.familyhelpers.workers.DatabaseHelper; +import com.aldo.apps.familyhelpers.workers.LocationHelper; import com.firebase.ui.auth.AuthUI; import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract; import com.firebase.ui.auth.IdpResponse; @@ -67,11 +66,10 @@ public class HelperGridActivity extends AppCompatActivity { private FirebaseUser mCurrentUser = FirebaseAuth.getInstance().getCurrentUser(); /** - * The instance of the {@link DatabaseHelper}. + * The {@link ActivityResultLauncher} for the sign in of a firebase user. */ - private DatabaseHelper mDbHelper; - - /** + private final ActivityResultLauncher mSignInLauncher = + registerForActivityResult(new FirebaseAuthUIActivityResultContract(), this::onSignInResult); /** * The {@link ActivityResultLauncher} to ask for the NotificationPermission. */ private final ActivityResultLauncher mRequestPermissionLauncher = @@ -87,12 +85,6 @@ public class HelperGridActivity extends AppCompatActivity { } }); - /** - * The {@link ActivityResultLauncher} for the sign in of a firebase user. - */ - private final ActivityResultLauncher mSignInLauncher = - registerForActivityResult(new FirebaseAuthUIActivityResultContract(), this::onSignInResult); - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -108,7 +100,6 @@ public class HelperGridActivity extends AppCompatActivity { } else { mWelcomeMessageView.setText(String.format(getString(R.string.welcome_message_placeholder), mCurrentUser.getDisplayName())); - mDbHelper = new DatabaseHelper(); mLocationHelper = LocationHelper.getInstance(this); } } @@ -159,7 +150,13 @@ public class HelperGridActivity extends AppCompatActivity { @Override public void launchHelper() { Log.d(TAG, "launchHelper: Clicked ShareLocation"); - if (mLocationHelper.requestLocationPermissions(HelperGridActivity.this)) { + // First check if the notification permission is granted + if (!requestNotificationPermission()) { + Log.d(TAG, "launchHelper: Notifications not allowed, return..."); + return; + } + if (mLocationHelper.requestLocationPermissions(HelperGridActivity.this) + && mLocationHelper.requestBackgroundLocationPermission(HelperGridActivity.this)) { Log.d(TAG, "launchHelper: Permission already granted"); final Intent intent = new Intent(HelperGridActivity.this, ShareLocationActivity.class); startActivity(intent); @@ -196,14 +193,13 @@ public class HelperGridActivity extends AppCompatActivity { * * @param result The result code, used to determine whether is succeeded or not. */ - private void onSignInResult(final FirebaseAuthUIAuthenticationResult result) { + private void onSignInResult(final FirebaseAuthUIAuthenticationResult result) { final IdpResponse idpResponse = result.getIdpResponse(); if (result.getResultCode() == RESULT_OK) { mCurrentUser = FirebaseAuth.getInstance().getCurrentUser(); Log.d(TAG, "onSignInResult: Successfully logged in [" + mCurrentUser.getDisplayName() + "]"); 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"); @@ -213,7 +209,7 @@ public class HelperGridActivity extends AppCompatActivity { Log.w(TAG, "onSignInResult: User canceled, cannot continue"); } else { Log.e(TAG, "onSignInResult: Login failed with errorCode [" - + idpResponse.getError().getErrorCode() + "]"); + + idpResponse.getError().getErrorCode() + "]"); } } } @@ -223,10 +219,12 @@ public class HelperGridActivity extends AppCompatActivity { @NonNull final String[] permissions, @NonNull final int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (mLocationHelper.handlePermissionResult(requestCode, grantResults)) { + if (mLocationHelper.handlePermissionResult(HelperGridActivity.this, requestCode, grantResults)) { final Intent intent = new Intent(HelperGridActivity.this, ShareLocationActivity.class); startActivity(intent); } } + + } \ No newline at end of file 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 4f62661..3949052 100644 --- a/app/src/main/java/com/aldo/apps/familyhelpers/ShareLocationActivity.java +++ b/app/src/main/java/com/aldo/apps/familyhelpers/ShareLocationActivity.java @@ -2,19 +2,16 @@ 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.Intent; 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; @@ -27,6 +24,7 @@ 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.aldo.apps.familyhelpers.workers.ShareLocationBackgroundWorker; import com.bumptech.glide.Glide; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; @@ -40,8 +38,6 @@ 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; @@ -61,95 +57,77 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea * Tag for debugging purpose. */ private static final String TAG = "ShareLocationActivity"; - + /** + * 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 {@link GoogleMap} view. */ private GoogleMap mGmap; - /** * The {@link DatabaseHelper} to read and load data from. */ private DatabaseHelper mDbHelper; - /** * The currently logged in {@link FirebaseUser}. */ private FirebaseUser mCurrentUser; - /** * The ID of the location updates to listen to. */ private String mShareId; - /** * The InfoBox {@link ImageView} showing the user icon. */ private ImageView mInfoBoxIcon; - /** * The InfoBox {@link TextView} showing the title. */ private TextView mInfoBoxTitle; - /** * The InfoBox {@link TextView} showing the Location information. */ private TextView mInfoBoxLocation; - /** * The InfoBox {@link TextView} showing the altitude.. */ private TextView mInfoBoxAltitude; - /** * The InfoBox {@link TextView} showing the last received speed. */ private TextView mInfoBoxSpeed; - /** * The InfoBox {@link TextView} showing the timestamp of the last update. */ private TextView mInfoBoxTimeStamp; - /** * The {@link Disposable} holding the subscription to the {@link LocationObject} * {@link io.reactivex.rxjava3.subjects.BehaviorSubject} in order to have it cancellable. */ 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. */ private boolean mIsNightMode; - /** * Reference to the Google Maps Marker, to update it's position rather than adding a new one. */ 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. */ @@ -186,22 +164,6 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea 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); @@ -225,6 +187,8 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea if (mLocationUpdateSubscription != null) { mLocationUpdateSubscription.dispose(); } + + initAndSetupShareToggle(); mUserSubscription = mDbHelper.getAllUsers() .subscribe(this::handleAllUsers, this::handleUserSubscriptionFailed); mLocationUpdateSubscription = mDbHelper.subscribeToAllLocationUpdates() @@ -233,6 +197,28 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea .subscribe(this::handleCurrentlyFollowingChanged, this::handleSubscriptionError); } + /** + * Initializes the share switch button. + */ + private void initAndSetupShareToggle() { + 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) { + final Intent shareLocationService = new Intent(this, ShareLocationBackgroundWorker.class); + startForegroundService(shareLocationService); + } else { + final Intent stopLocationService = new Intent(this, ShareLocationBackgroundWorker.class); + stopService(stopLocationService); + } + }); + + locationHelper.getSharingStateSubject().subscribe(isSharing -> shareLocationToggle.setChecked(isSharing), + this::handleSubscriptionError); + } + @Override protected void onPause() { super.onPause(); @@ -291,11 +277,11 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea } - /** - * Removed all markers that became invalid because the sharing was stopped. - * - * @param locationObjectMap The {@link Map} matching shareIds to {@link LocationObject}s. - */ + /** + * 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()) { diff --git a/app/src/main/java/com/aldo/apps/familyhelpers/model/LocationObject.java b/app/src/main/java/com/aldo/apps/familyhelpers/model/LocationObject.java index 7fc5597..ff70331 100644 --- a/app/src/main/java/com/aldo/apps/familyhelpers/model/LocationObject.java +++ b/app/src/main/java/com/aldo/apps/familyhelpers/model/LocationObject.java @@ -68,6 +68,19 @@ public class LocationObject { this.timestamp = timestamp; } + /** + * Helper method to create a {@link LocationObject} from a handed in {@link Location}. + * + * @param location The received {@link Location} from the locationManager. + * @return The created {@link LocationObject}. + */ + public static LocationObject fromLocation(final Location location) { + final FirebaseUser firebaseUser = FirebaseAuth.getInstance().getCurrentUser(); + return new LocationObject(firebaseUser.getUid(), location.getLatitude(), + location.getLongitude(), location.getAltitude(), location.getSpeed(), + location.getTime()); + } + /** * Returns the unique identifier for the shared location. * @@ -122,20 +135,6 @@ public class LocationObject { return timestamp; } - /** - * Helper method to create a {@link LocationObject} from a handed in {@link Location}. - * - * @param location The received {@link Location} from the locationManager. - * - * @return The created {@link LocationObject}. - */ - public static LocationObject fromLocation(final Location location) { - final FirebaseUser firebaseUser = FirebaseAuth.getInstance().getCurrentUser(); - return new LocationObject(firebaseUser.getUid(), location.getLatitude(), - location.getLongitude(), location.getAltitude(), location.getSpeed(), - location.getTime()); - } - @NonNull @Override public String toString() { 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 0eb7103..676addf 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 @@ -55,6 +55,30 @@ public class User { this.creationDate = creationDate; } + /** + * Creates a {@link User} from a handed in {@link FirebaseUser}. + * + * @param firebaseUser The {@link FirebaseUser} to read the fields from. + * @return The {@link User} representation of the {@link FirebaseUser}, or null if no + * {@link FirebaseUser} is available. + */ + public static User fromFirebaseUser(final FirebaseUser firebaseUser) { + if (firebaseUser == null) { + return null; + } + final String uid = firebaseUser.getUid(); + final String displayName = firebaseUser.getDisplayName(); + final Uri photoUrl = firebaseUser.getPhotoUrl(); + final FirebaseUserMetadata metaData = firebaseUser.getMetadata(); + final long creationDate; + if (metaData == null) { + creationDate = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC); + } else { + creationDate = metaData.getCreationTimestamp(); + } + return new User(uid, displayName, photoUrl == null ? null : photoUrl.toString(), creationDate); + } + /** * Returns the unique Identifies of the user. * @@ -91,31 +115,6 @@ public class User { return creationDate; } - /** - * Creates a {@link User} from a handed in {@link FirebaseUser}. - * - * @param firebaseUser The {@link FirebaseUser} to read the fields from. - * - * @return The {@link User} representation of the {@link FirebaseUser}, or null if no - * {@link FirebaseUser} is available. - */ - public static User fromFirebaseUser(final FirebaseUser firebaseUser) { - if (firebaseUser == null) { - return null; - } - final String uid = firebaseUser.getUid(); - final String displayName = firebaseUser.getDisplayName(); - final Uri photoUrl = firebaseUser.getPhotoUrl(); - final FirebaseUserMetadata metaData = firebaseUser.getMetadata(); - final long creationDate; - if (metaData == null) { - creationDate = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC); - } else { - creationDate = metaData.getCreationTimestamp(); - } - return new User(uid,displayName, photoUrl == null ? null : photoUrl.toString(), creationDate); - } - @Override public String toString() { return "User{" + 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 index f4b659e..414d2c1 100644 --- a/app/src/main/java/com/aldo/apps/familyhelpers/ui/ActiveShareAdapter.java +++ b/app/src/main/java/com/aldo/apps/familyhelpers/ui/ActiveShareAdapter.java @@ -1,7 +1,6 @@ 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; @@ -10,7 +9,6 @@ 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; @@ -20,20 +18,46 @@ 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; +/** + * The {@link RecyclerView.Adapter} for the {@link ActiveShareAdapter.ActiveShareViewHolder} to display + * which user is currently sharing their location and to offer a selection on whom to follow. + */ public class ActiveShareAdapter extends RecyclerView.Adapter { + + /** + * Tag for debugging purposes. + */ private static final String TAG = "ActiveShareAdapter"; + /** + * {@link BehaviorSubject} holding the ID of the currently selected user. + */ private final BehaviorSubject mCurrentlySelectedSubject = BehaviorSubject.createDefault(""); + /** + * {@link List} of all currently sharing {@link User}s. + */ private final List mSharingUserList = new ArrayList<>(); + /** + * A {@link WeakReference} to the calling {@link Context}. + */ private final WeakReference mContextRef; + /** + * List of all {@link View}s so the focus can be cleared again if another view is touched. + */ + private final List mAllItems = new ArrayList<>(); + + /** + * C'Tor. + * + * @param context Calling context. + */ public ActiveShareAdapter(final Context context) { mContextRef = new WeakReference<>(context); } @@ -43,6 +67,7 @@ public class ActiveShareAdapter extends RecyclerView.Adapter newUsers) { mSharingUserList.clear(); mSharingUserList.addAll(newUsers); + mAllItems.clear(); notifyDataSetChanged(); } + /** + * Returns the {@link BehaviorSubject} holding the ID of the currently selected user. + * + * @return The {@link BehaviorSubject} holding the ID of the currently selected user. + */ public BehaviorSubject getCurrentlySelectedSubject() { return mCurrentlySelectedSubject; } + /** + * Helper method to clear the selected state of all views, when a new one is tapped. + */ + private void clearFocusedView() { + for (final View view : mAllItems) { + view.setSelected(false); + } + } + + /** + * {@link RecyclerView.ViewHolder} implementation for the {@link ActiveShareViewHolder}. + */ public class ActiveShareViewHolder extends RecyclerView.ViewHolder { - private CircleImageView mProfilePicture; + /** + * The {@link CircleImageView} holding the profile picture of the user. + */ + private final CircleImageView mProfilePicture; - private TextView mName; + /** + * The {@link TextView} showing the name of the user. + */ + private final TextView mName; - private View mItemView; + /** + * The overall {@link View}. + */ + private final View mItemView; + /** + * C'Tor. + * + * @param itemView The already inflated itemView. + */ public ActiveShareViewHolder(@NonNull final View itemView) { super(itemView); mProfilePicture = itemView.findViewById(R.id.active_share_profile_picture); @@ -83,6 +144,11 @@ public class ActiveShareAdapter extends RecyclerView.Adapter { Log.d(TAG, "setUserData: Clicked on [" + user + "]"); - mCurrentlySelectedSubject.onNext(user.getuId()); + if (mItemView.isSelected()) { + mCurrentlySelectedSubject.onNext(""); + clearFocusedView(); + mItemView.setSelected(false); + } else { + mCurrentlySelectedSubject.onNext(user.getuId()); + clearFocusedView(); + mItemView.setSelected(true); + } }); mName.setText(user.getDisplayName()); final Context context = mContextRef.get(); diff --git a/app/src/main/java/com/aldo/apps/familyhelpers/utils/CancelLocationSharingReceiver.java b/app/src/main/java/com/aldo/apps/familyhelpers/utils/CancelLocationSharingReceiver.java deleted file mode 100644 index 671f2df..0000000 --- a/app/src/main/java/com/aldo/apps/familyhelpers/utils/CancelLocationSharingReceiver.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.aldo.apps.familyhelpers.utils; - -import static com.aldo.apps.familyhelpers.utils.GlobalConstants.SHARE_LOCATION_CANCEL_ACTION; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import com.aldo.apps.familyhelpers.workers.LocationHelper; - -/** - * Broadcast received to cancel an ongoing sharing of location. - */ -public class CancelLocationSharingReceiver extends BroadcastReceiver { - - /** - * Tag for debugging purpose. - */ - private static final String TAG = "CancelLocationSharingRe"; - - /** - * Empty C'tor. - */ - public CancelLocationSharingReceiver() { - // Empty C'tor. - } - - - @Override - public void onReceive(final Context context, final Intent intent) { - if (intent != null && SHARE_LOCATION_CANCEL_ACTION.equalsIgnoreCase(intent.getAction())) { - Log.d(TAG, "onReceive: Notification cancelled, cancel location sharing"); - final LocationHelper locationHelper = LocationHelper.getInstance(context); - locationHelper.stopLocationUpdates(); - } - } -} diff --git a/app/src/main/java/com/aldo/apps/familyhelpers/utils/DevicePolicyManagerHelper.java b/app/src/main/java/com/aldo/apps/familyhelpers/utils/DevicePolicyManagerHelper.java index 344d020..892dfe1 100644 --- a/app/src/main/java/com/aldo/apps/familyhelpers/utils/DevicePolicyManagerHelper.java +++ b/app/src/main/java/com/aldo/apps/familyhelpers/utils/DevicePolicyManagerHelper.java @@ -54,7 +54,6 @@ public final class DevicePolicyManagerHelper { * Returns the singleton instance of the {@link DevicePolicyManagerHelper}. * * @param context The {@link Context} from where this was called. - * * @return the singleton instance of the {@link DevicePolicyManagerHelper}. */ public static DevicePolicyManagerHelper getInstance(final Context context) { 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 5e6027a..a1ce84c 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 @@ -3,6 +3,7 @@ package com.aldo.apps.familyhelpers.utils; import com.firebase.ui.auth.AuthUI; import java.util.Arrays; +import java.util.Collections; import java.util.List; /** @@ -10,88 +11,75 @@ import java.util.List; */ public final class GlobalConstants { - /** - * Private C'tor to prevent instantiation. - */ - private GlobalConstants() {} - /** * ID of the NotificationChannel for the SleepTimer Notification. */ public static final String SLEEP_TIMER_CHANNEL_ID = "CountdownChannel"; - /** * ID of the NotificationChannel for the ShareLocation Notification. */ public static final String SHARE_LOCATION_CHANNEL_ID = "LocationShareChannel"; - /** * Factor to calculate seconds from milliseconds and vice versa. */ public static final int ONE_SECOND_IN_MILLIS = 1000; - /** * Factor to calculate hours from minutes and vice versa. */ public static final int ONE_HOUR_IN_MINUTES = 60; - /** * The NotificationID of the SleepTimer notification. */ public static final int SLEEP_TIMER_NOTIFICATION_ID = 1; - /** * The NotificationID of the ShareLocation notification. */ public static final int SHARE_LOCATION_NOTIFICATION_ID = 2; - /** * The action to be invoked when the sharing of location should be cancelled. */ public static final String SHARE_LOCATION_CANCEL_ACTION = "com.aldo.apps.familyhelpers.CANCEL_LOCATION_SHARING"; - /** * The key of the extra to be applied to the starting intent of the sleepTimer service, * holding the initial duration in millis. */ public static final String SLEEP_TIMER_DURATION_MILLIS_EXTRA = "sleep_timer_duration"; - /** * Action to cancel an ongoing sleep timer. */ public static final String SLEEP_TIMER_CANCEL_ACTION = "SLEEP_TIMER_CANCEL"; - /** * Default minimum time interval between two location updates. * Currently set to 5 Seconds. */ public static final int DEFAULT_MINIMUM_LOCATION_INTERVAL_MILLIS = 5000; - /** * Default minimum distance interval between two location updates. * Currently set to 5 Meters. */ 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. */ - public static final List SIGN_IN_PROVIDERS = Arrays.asList( + public static final List SIGN_IN_PROVIDERS = Collections.singletonList( new AuthUI.IdpConfig.GoogleBuilder().build() ); + + /** + * Private C'tor to prevent instantiation. + */ + private GlobalConstants() { + } } 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 6c52c5f..aba3a90 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 @@ -2,22 +2,16 @@ package com.aldo.apps.familyhelpers.workers; import static com.aldo.apps.familyhelpers.utils.DatabaseConstants.DB_COLL_LOCATION; import static com.aldo.apps.familyhelpers.utils.DatabaseConstants.DB_COLL_USERS; -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; @@ -64,7 +58,7 @@ public class DatabaseHelper { /** * {@link Map} containing all users. */ - private Map mAllUserMap = new HashMap<>(); + private final Map mAllUserMap = new HashMap<>(); /** * C'tor. @@ -119,6 +113,13 @@ public class DatabaseHelper { return mAllUsers; } + /** + * Returns a {@link User} object for the given ID. + * + * @param userId The identifier of the wanted user. + * + * @return The identified user or null of not existing. + */ public User getUserForId(final String userId) { Log.d(TAG, "getUserForId() called with: userId = [" + userId + "]"); if (mAllUserMap.containsKey(userId)) { @@ -162,25 +163,25 @@ public class DatabaseHelper { mLocationUpdateListener = mDatabase.collection(DB_COLL_LOCATION) .document(shareId) .addSnapshotListener((value, error) -> { - if (value == null) { - Log.d(TAG, "onEvent: Location was deleted"); - mObservedLocation.onComplete(); - return; - } - final LocationObject updatedLocation = value.toObject(LocationObject.class); - if (updatedLocation == null) { - Log.w(TAG, "onEvent: Error while parsing, ignore"); - return; - } - mObservedLocation.onNext(updatedLocation); - }); + if (value == null) { + Log.d(TAG, "onEvent: Location was deleted"); + mObservedLocation.onComplete(); + return; + } + final LocationObject updatedLocation = value.toObject(LocationObject.class); + if (updatedLocation == null) { + Log.w(TAG, "onEvent: Error while parsing, ignore"); + return; + } + mObservedLocation.onNext(updatedLocation); + }); } /** * 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. + * to the sharing ID. */ public BehaviorSubject> subscribeToAllLocationUpdates() { final Map locationMap = new HashMap<>(); @@ -191,18 +192,18 @@ public class DatabaseHelper { } 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); - }); + 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; } 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 90acb51..d2a0f61 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 @@ -1,8 +1,7 @@ package com.aldo.apps.familyhelpers.workers; +import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION; import static android.Manifest.permission.ACCESS_FINE_LOCATION; -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.SHARE_LOCATION_CANCEL_ACTION; import static com.aldo.apps.familyhelpers.utils.GlobalConstants.SHARE_LOCATION_CHANNEL_ID; import static com.aldo.apps.familyhelpers.utils.GlobalConstants.SHARE_LOCATION_NOTIFICATION_ID; @@ -18,6 +17,7 @@ import android.location.LocationListener; import android.location.LocationManager; import android.os.Looper; import android.util.Log; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; @@ -25,9 +25,9 @@ import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; import com.aldo.apps.familyhelpers.R; +import com.aldo.apps.familyhelpers.ShareLocationActivity; import com.aldo.apps.familyhelpers.model.LocationObject; import com.aldo.apps.familyhelpers.ui.NotificationHelper; -import com.aldo.apps.familyhelpers.utils.CancelLocationSharingReceiver; import java.lang.ref.WeakReference; import java.util.List; @@ -36,6 +36,7 @@ import io.reactivex.rxjava3.subjects.BehaviorSubject; /** * Helper class to encapsulate all Location specific calls into one utility class. + * TODO: Change logic to make use of the FusedLocationProvider. */ public final class LocationHelper implements LocationListener { @@ -47,42 +48,42 @@ public final class LocationHelper implements LocationListener { /** * The request code of the for the location permission request. */ - private static final int LOCATION_PERMISSION_REQUEST_CODE = 1; + private static final int LOCATION_PERMISSION_REQUEST_CODE = 100; + + /** + * The request code of the for the background location permission request. + */ + private static final int BACKGROUND_LOCATION_REQUEST_CODE = 101; /** * The singleton instance of this {@link LocationHelper}. */ private static LocationHelper sInstance; - private final WeakReference mContextRef; + /** * The {@link LocationManager} object to listen to location updates. */ private final LocationManager mLocationManager; - - /** - * Boolean flag to check whether a subscription for location updates is already running or not. - */ - private boolean mIsSubscribed; - /** * The {@link BehaviorSubject} for the {@link LocationObject} for clients to subscribe to. * TODO: Check if needed. */ - private final BehaviorSubject mLocationSubject = BehaviorSubject.create(); - + private final BehaviorSubject mLocationSubject = BehaviorSubject.create(); /** * The {@link DatabaseHelper} for db access. */ 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); + /** + * Boolean flag to check whether a subscription for location updates is already running or not. + */ + private boolean mIsSubscribed; /** * Private C'Tor for singleton instance. @@ -91,7 +92,6 @@ public final class LocationHelper implements LocationListener { */ private LocationHelper(final Context context) { mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - mContextRef = new WeakReference<>(context); mNotificationHelper = new NotificationHelper(context); mDbHelper = new DatabaseHelper(); mNotificationHelper.createAndRegisterNotificationChannel( @@ -135,16 +135,41 @@ public final class LocationHelper implements LocationListener { return true; } + /** + * Request the permission for {@link android.Manifest.permission#ACCESS_BACKGROUND_LOCATION} if not + * yet granted before. + * + * @param activity The {@link Activity} from where this was called. + * @return true if permission was already granted before, false otherwise. + */ + public boolean requestBackgroundLocationPermission(final Activity activity) { + if (ContextCompat.checkSelfPermission(activity, ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED) { + // Provide a justification to the user. + Toast.makeText(activity, R.string.share_location_background_permission_rationale, + Toast.LENGTH_LONG).show(); + ActivityCompat.requestPermissions(activity, new String[]{ACCESS_BACKGROUND_LOCATION}, + BACKGROUND_LOCATION_REQUEST_CODE); + return false; + } + return true; + } + /** * Helper method to handle the result of the permission request dialog. * + * @param activity The calling {@link Activity}. * @param requestCode The code of the permission request. * @param grantResults The result codes. * @return true if permission was granted, false otherwise. */ - public boolean handlePermissionResult(final int requestCode, final int[] grantResults) { + public boolean handlePermissionResult(final Activity activity, final int requestCode, final int[] grantResults) { if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) { - // Permission granted, proceed with location updates + // Permission granted, ask for background permission. + requestBackgroundLocationPermission(activity); + return false; + } + if (requestCode == BACKGROUND_LOCATION_REQUEST_CODE) { + // Permission granted, continue with updates. return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; } return false; @@ -153,17 +178,17 @@ public final class LocationHelper implements LocationListener { /** * Helper method to start listening for updates. * - * @param activity The {@link Activity} from where this was called. + * @param context The {@link Context} from where this was called. * @param minIntervalMillis The minimum time interval in millisecond * @param minDistance The minimum distance for an update to be received. * @return true if subscription started, false otherwise. */ - public boolean startLocationUpdates(final Activity activity, final int minIntervalMillis, final int minDistance) { + public boolean startLocationUpdates(final Context context, 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) { + if (ContextCompat.checkSelfPermission(context, ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { mLocationManager.requestLocationUpdates( LocationManager.FUSED_PROVIDER, minIntervalMillis, @@ -200,6 +225,7 @@ public final class LocationHelper implements LocationListener { * @return true if location sharing is active, false otherwise. */ public boolean isCurrentlySharing() { + Log.d(TAG, "isCurrentlySharing: Called with currently sharing = [" + mIsSubscribed + "]"); return mIsSubscribed; } @@ -218,8 +244,12 @@ public final class LocationHelper implements LocationListener { * @param context The {@link Context} from where this is called. * @return The {@link Notification} to be shown. */ - private Notification buildNotification(final Context context) { - final Intent deleteIntent = new Intent(context, CancelLocationSharingReceiver.class); + public Notification buildNotification(final Context context) { + final Intent notificationIntent = new Intent(context, ShareLocationActivity.class); + final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, + notificationIntent, PendingIntent.FLAG_IMMUTABLE); + + final Intent deleteIntent = new Intent(context, ShareLocationBackgroundWorker.class); deleteIntent.setAction(SHARE_LOCATION_CANCEL_ACTION); final PendingIntent pendingDeleteIntent = PendingIntent.getBroadcast(context, 0, deleteIntent, PendingIntent.FLAG_IMMUTABLE); @@ -227,6 +257,7 @@ public final class LocationHelper implements LocationListener { return new NotificationCompat.Builder(context, SHARE_LOCATION_CHANNEL_ID) .setContentTitle(context.getString(R.string.title_share_location)) .setContentText(context.getString(R.string.share_location_notification_content)) + .setContentIntent(pendingIntent) .setSmallIcon(R.drawable.ic_location_helper) .setDeleteIntent(pendingDeleteIntent) .addAction(android.R.drawable.ic_menu_close_clear_cancel, @@ -238,14 +269,9 @@ public final class LocationHelper implements LocationListener { @Override public void onLocationChanged(@NonNull final Location location) { final LocationObject locationObject = LocationObject.fromLocation(location); + Log.d(TAG, "onLocationChanged: Received update with " + locationObject); mLocationSubject.onNext(locationObject); mDbHelper.insertOrUpdateLocation(locationObject); - final Context context = mContextRef.get(); - if (context != null) { - mNotificationHelper.updateNotification(SHARE_LOCATION_NOTIFICATION_ID, buildNotification(context)); - } - - Log.d(TAG, "onLocationChanged: Received update with " + locationObject); } @Override diff --git a/app/src/main/java/com/aldo/apps/familyhelpers/workers/ShareLocationBackgroundWorker.java b/app/src/main/java/com/aldo/apps/familyhelpers/workers/ShareLocationBackgroundWorker.java new file mode 100644 index 0000000..7cc695c --- /dev/null +++ b/app/src/main/java/com/aldo/apps/familyhelpers/workers/ShareLocationBackgroundWorker.java @@ -0,0 +1,76 @@ +package com.aldo.apps.familyhelpers.workers; + +import static com.aldo.apps.familyhelpers.utils.GlobalConstants.SHARE_LOCATION_CANCEL_ACTION; +import static com.aldo.apps.familyhelpers.utils.GlobalConstants.SLEEP_TIMER_NOTIFICATION_ID; + +import android.app.Service; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.Nullable; + +/** + * Service implementation to keep location updates running also in the background. + */ +public class ShareLocationBackgroundWorker extends Service { + + /** + * Tag for debugging purpose. + */ + private static final String TAG = "ShareLocationBackground"; + + /** + * {@link Handler} on the MainThread in order to perform the location update. + */ + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + /** + * The {@link LocationHelper} to update the location in the database. + */ + private LocationHelper mLocationHelper; + + /** + * The {@link Runnable} to execute the task in. + */ + private Runnable mShareLocationRunnable; + + @Override + public void onCreate() { + super.onCreate(); + mLocationHelper = LocationHelper.getInstance(this); + } + + @Override + public int onStartCommand(final Intent intent, final int flags, int startId) { + if (intent == null) { + Log.e(TAG, "onStartCommand: Invalid intent intent received, do nothing"); + return START_REDELIVER_INTENT; + } + if (intent.getAction() != null && intent.getAction().equals(SHARE_LOCATION_CANCEL_ACTION)) { + stopSelf(); + mLocationHelper.stopLocationUpdates(); + return START_NOT_STICKY; + } + mShareLocationRunnable = () -> + mLocationHelper.startLocationUpdates(ShareLocationBackgroundWorker.this, + 1000, 0); + mHandler.post(mShareLocationRunnable); + startForeground(SLEEP_TIMER_NOTIFICATION_ID, mLocationHelper.buildNotification(this)); + return super.onStartCommand(intent, flags, startId); + } + + @Nullable + @Override + public IBinder onBind(final Intent intent) { + return null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + mHandler.removeCallbacks(mShareLocationRunnable); + } +} diff --git a/app/src/main/java/com/aldo/apps/familyhelpers/workers/SleepTimerHelper.java b/app/src/main/java/com/aldo/apps/familyhelpers/workers/SleepTimerHelper.java index aea0cc5..30f1167 100644 --- a/app/src/main/java/com/aldo/apps/familyhelpers/workers/SleepTimerHelper.java +++ b/app/src/main/java/com/aldo/apps/familyhelpers/workers/SleepTimerHelper.java @@ -36,6 +36,7 @@ public class SleepTimerHelper extends Service { * Tag for debugging purpose. */ private static final String TAG = "SleepTimerHelper"; + /** * {@link Handler} on the MainThread in order to perform the actual locking. */ @@ -87,7 +88,7 @@ public class SleepTimerHelper extends Service { @Nullable @Override - public IBinder onBind(Intent intent) { + public IBinder onBind(final Intent intent) { return null; } @@ -144,7 +145,6 @@ public class SleepTimerHelper extends Service { * 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(long millis) { diff --git a/app/src/main/res/drawable/currently_followed_user_selector.xml b/app/src/main/res/drawable/currently_followed_user_selector.xml new file mode 100644 index 0000000..702a8fa --- /dev/null +++ b/app/src/main/res/drawable/currently_followed_user_selector.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/active_share_view_holder.xml b/app/src/main/res/layout/active_share_view_holder.xml index d776506..0558b1c 100644 --- a/app/src/main/res/layout/active_share_view_holder.xml +++ b/app/src/main/res/layout/active_share_view_holder.xml @@ -2,6 +2,7 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 26336ef..e009b0b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,5 +31,6 @@ The last received velocity was %.2f m/s (%.1f km/h) This update was received at: %s Share your location + In order to share your position also while in the background, please grant the always permission \ No newline at end of file