[ShareLocation] Added background service functionality

Now the sharing of the location should also be working from
the background.
This commit is contained in:
Alexander Dörflinger
2025-03-21 13:37:32 +01:00
parent ae50dd89df
commit eab7153ea1
18 changed files with 379 additions and 248 deletions

View File

@@ -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

View File

@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
@@ -39,6 +40,9 @@
android:name=".workers.SleepTimerHelper"
android:foregroundServiceType="specialUse" />
<service android:name=".workers.ShareLocationBackgroundWorker"
android:foregroundServiceType="location" />
<receiver
android:name="com.aldo.apps.familyhelpers.DoerflingerHelpersDeviceAdminReceiver"
android:exported="true"
@@ -51,14 +55,6 @@
<action android:name="android.app.action.DEVICE_ADMIN_DISABLED" />
</intent-filter>
</receiver>
<receiver
android:name=".utils.CancelLocationSharingReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.aldo.apps.familyhelpers.CANCEL_LOCATION_SHARING" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -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<Intent> mSignInLauncher =
registerForActivityResult(new FirebaseAuthUIActivityResultContract(), this::onSignInResult); /**
* The {@link ActivityResultLauncher} to ask for the NotificationPermission.
*/
private final ActivityResultLauncher<String> mRequestPermissionLauncher =
@@ -87,12 +85,6 @@ public class HelperGridActivity extends AppCompatActivity {
}
});
/**
* The {@link ActivityResultLauncher} for the sign in of a firebase user.
*/
private final ActivityResultLauncher<Intent> 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);
@@ -203,7 +200,6 @@ public class HelperGridActivity extends AppCompatActivity {
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");
@@ -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);
}
}
}

View File

@@ -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<String, Marker> mMarkerMap = new HashMap<>();
/**
* {@link List} of all {@link User}s.
*/
private final List<User> 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<String, Marker> mMarkerMap = new HashMap<>();
/**
* {@link List} of all {@link User}s.
*/
private final List<User> 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();

View File

@@ -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() {

View File

@@ -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{" +

View File

@@ -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<ActiveShareAdapter.ActiveShareViewHolder> {
/**
* Tag for debugging purposes.
*/
private static final String TAG = "ActiveShareAdapter";
/**
* {@link BehaviorSubject} holding the ID of the currently selected user.
*/
private final BehaviorSubject<String> mCurrentlySelectedSubject = BehaviorSubject.createDefault("");
/**
* {@link List} of all currently sharing {@link User}s.
*/
private final List<User> mSharingUserList = new ArrayList<>();
/**
* A {@link WeakReference} to the calling {@link Context}.
*/
private final WeakReference<Context> mContextRef;
/**
* List of all {@link View}s so the focus can be cleared again if another view is touched.
*/
private final List<View> 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<ActiveShareAdapter.
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);
mAllItems.add(view);
return new ActiveShareViewHolder(view);
}
@@ -58,24 +83,60 @@ public class ActiveShareAdapter extends RecyclerView.Adapter<ActiveShareAdapter.
return mSharingUserList.size();
}
/**
* Helper method to apply a new List of sharing {@link User}s.
* TODO: Make it smarter.
*/
public void applyNewData(final List<User> 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<String> 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<ActiveShareAdapter.
mItemView = itemView;
}
/**
* Helper method to apply userData to a View.
*
* @param user The {@link User} to be attached.
*/
public void setUserData(final User user) {
if (user == null) {
Log.w(TAG, "setUserData: Skip update, no valid update yet");
@@ -90,7 +156,15 @@ public class ActiveShareAdapter extends RecyclerView.Adapter<ActiveShareAdapter.
}
mItemView.setOnClickListener(v -> {
Log.d(TAG, "setUserData: Clicked on [" + user + "]");
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();

View File

@@ -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();
}
}
}

View File

@@ -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) {

View File

@@ -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<AuthUI.IdpConfig> SIGN_IN_PROVIDERS = Arrays.asList(
public static final List<AuthUI.IdpConfig> SIGN_IN_PROVIDERS = Collections.singletonList(
new AuthUI.IdpConfig.GoogleBuilder().build()
);
/**
* Private C'tor to prevent instantiation.
*/
private GlobalConstants() {
}
}

View File

@@ -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<String, User> mAllUserMap = new HashMap<>();
private final Map<String, User> 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)) {

View File

@@ -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<Context> 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<LocationObject> 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<Boolean> 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

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:layout_height="match_parent">
<solid android:color="@color/md_theme_secondaryContainer_mediumContrast" />
<corners
android:bottomLeftRadius="4dp"
android:bottomRightRadius="4dp"
android:topLeftRadius="4dp"
android:topRightRadius="4dp" />
</shape>
</item>
<item android:state_selected="false">
<shape>
<solid android:color="@color/md_theme_background" />
<corners
android:bottomLeftRadius="4dp"
android:bottomRightRadius="4dp"
android:topLeftRadius="4dp"
android:topRightRadius="4dp" />
</shape>
</item>
</selector>

View File

@@ -2,6 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="75dp"
android:layout_height="75dp"
android:background="@drawable/currently_followed_user_selector"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

View File

@@ -31,5 +31,6 @@
<string name="share_location_info_speed">The last received velocity was %.2f m/s (%.1f km/h)</string>
<string name="share_location_info_timestamp">This update was received at: %s</string>
<string name="share_location_toggle_button">Share your location</string>
<string name="share_location_background_permission_rationale">In order to share your position also while in the background, please grant the always permission</string>
</resources>