[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 //Google Maps SDK
implementation 'com.google.android.gms:play-services-maps:19.1.0' implementation 'com.google.android.gms:play-services-maps:19.1.0'
implementation 'com.google.android.gms:play-services-location:21.3.0'
// Glide // Glide
implementation 'com.github.bumptech.glide:glide:4.16.0' // Check for the latest version 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.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <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_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_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
@@ -39,6 +40,9 @@
android:name=".workers.SleepTimerHelper" android:name=".workers.SleepTimerHelper"
android:foregroundServiceType="specialUse" /> android:foregroundServiceType="specialUse" />
<service android:name=".workers.ShareLocationBackgroundWorker"
android:foregroundServiceType="location" />
<receiver <receiver
android:name="com.aldo.apps.familyhelpers.DoerflingerHelpersDeviceAdminReceiver" android:name="com.aldo.apps.familyhelpers.DoerflingerHelpersDeviceAdminReceiver"
android:exported="true" android:exported="true"
@@ -51,14 +55,6 @@
<action android:name="android.app.action.DEVICE_ADMIN_DISABLED" /> <action android:name="android.app.action.DEVICE_ADMIN_DISABLED" />
</intent-filter> </intent-filter>
</receiver> </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> </application>
</manifest> </manifest>

View File

@@ -8,7 +8,7 @@ import androidx.annotation.NonNull;
public class DoerflingerHelpersDeviceAdminReceiver extends DeviceAdminReceiver { public class DoerflingerHelpersDeviceAdminReceiver extends DeviceAdminReceiver {
@Override @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. // Called when the app is enabled as a device administrator.
} }

View File

@@ -1,7 +1,6 @@
package com.aldo.apps.familyhelpers; package com.aldo.apps.familyhelpers;
import static android.Manifest.permission.POST_NOTIFICATIONS; import static android.Manifest.permission.POST_NOTIFICATIONS;
import static com.aldo.apps.familyhelpers.utils.GlobalConstants.SIGN_IN_PROVIDERS; import static com.aldo.apps.familyhelpers.utils.GlobalConstants.SIGN_IN_PROVIDERS;
import android.content.Intent; 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.HelperGroupTile;
import com.aldo.apps.familyhelpers.ui.SleepTimerPopup; import com.aldo.apps.familyhelpers.ui.SleepTimerPopup;
import com.aldo.apps.familyhelpers.utils.DevicePolicyManagerHelper; 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.DatabaseHelper;
import com.aldo.apps.familyhelpers.workers.LocationHelper;
import com.firebase.ui.auth.AuthUI; import com.firebase.ui.auth.AuthUI;
import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract; import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract;
import com.firebase.ui.auth.IdpResponse; import com.firebase.ui.auth.IdpResponse;
@@ -67,11 +66,10 @@ public class HelperGridActivity extends AppCompatActivity {
private FirebaseUser mCurrentUser = FirebaseAuth.getInstance().getCurrentUser(); 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. * The {@link ActivityResultLauncher} to ask for the NotificationPermission.
*/ */
private final ActivityResultLauncher<String> mRequestPermissionLauncher = 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 @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -108,7 +100,6 @@ public class HelperGridActivity extends AppCompatActivity {
} else { } else {
mWelcomeMessageView.setText(String.format(getString(R.string.welcome_message_placeholder), mWelcomeMessageView.setText(String.format(getString(R.string.welcome_message_placeholder),
mCurrentUser.getDisplayName())); mCurrentUser.getDisplayName()));
mDbHelper = new DatabaseHelper();
mLocationHelper = LocationHelper.getInstance(this); mLocationHelper = LocationHelper.getInstance(this);
} }
} }
@@ -159,7 +150,13 @@ public class HelperGridActivity extends AppCompatActivity {
@Override @Override
public void launchHelper() { public void launchHelper() {
Log.d(TAG, "launchHelper: Clicked ShareLocation"); 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"); Log.d(TAG, "launchHelper: Permission already granted");
final Intent intent = new Intent(HelperGridActivity.this, ShareLocationActivity.class); final Intent intent = new Intent(HelperGridActivity.this, ShareLocationActivity.class);
startActivity(intent); startActivity(intent);
@@ -196,14 +193,13 @@ public class HelperGridActivity extends AppCompatActivity {
* *
* @param result The result code, used to determine whether is succeeded or not. * @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(); final IdpResponse idpResponse = result.getIdpResponse();
if (result.getResultCode() == RESULT_OK) { if (result.getResultCode() == RESULT_OK) {
mCurrentUser = FirebaseAuth.getInstance().getCurrentUser(); mCurrentUser = FirebaseAuth.getInstance().getCurrentUser();
Log.d(TAG, "onSignInResult: Successfully logged in [" + mCurrentUser.getDisplayName() + "]"); Log.d(TAG, "onSignInResult: Successfully logged in [" + mCurrentUser.getDisplayName() + "]");
mWelcomeMessageView.setText(String.format(getString(R.string.welcome_message_placeholder), mWelcomeMessageView.setText(String.format(getString(R.string.welcome_message_placeholder),
mCurrentUser.getDisplayName())); mCurrentUser.getDisplayName()));
mDbHelper = new DatabaseHelper();
mLocationHelper = LocationHelper.getInstance(this); mLocationHelper = LocationHelper.getInstance(this);
} else { } else {
Log.w(TAG, "onSignInResult: Sign-In failed"); Log.w(TAG, "onSignInResult: Sign-In failed");
@@ -213,7 +209,7 @@ public class HelperGridActivity extends AppCompatActivity {
Log.w(TAG, "onSignInResult: User canceled, cannot continue"); Log.w(TAG, "onSignInResult: User canceled, cannot continue");
} else { } else {
Log.e(TAG, "onSignInResult: Login failed with errorCode [" 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 String[] permissions,
@NonNull final int[] grantResults) { @NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, 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); final Intent intent = new Intent(HelperGridActivity.this, ShareLocationActivity.class);
startActivity(intent); 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_LATITUDE;
import static com.aldo.apps.familyhelpers.utils.GlobalConstants.DEFAULT_HOME_LONGITUDE; 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 static com.aldo.apps.familyhelpers.utils.GlobalConstants.METER_PER_SECOND_TO_KMH_CONVERTER;
import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.CompoundButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; 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.ui.ActiveShareAdapter;
import com.aldo.apps.familyhelpers.workers.DatabaseHelper; import com.aldo.apps.familyhelpers.workers.DatabaseHelper;
import com.aldo.apps.familyhelpers.workers.LocationHelper; import com.aldo.apps.familyhelpers.workers.LocationHelper;
import com.aldo.apps.familyhelpers.workers.ShareLocationBackgroundWorker;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap; 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.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser; import com.google.firebase.auth.FirebaseUser;
import org.checkerframework.checker.units.qual.A;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
@@ -61,95 +57,77 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea
* Tag for debugging purpose. * Tag for debugging purpose.
*/ */
private static final String TAG = "ShareLocationActivity"; 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. * The {@link GoogleMap} view.
*/ */
private GoogleMap mGmap; private GoogleMap mGmap;
/** /**
* The {@link DatabaseHelper} to read and load data from. * The {@link DatabaseHelper} to read and load data from.
*/ */
private DatabaseHelper mDbHelper; private DatabaseHelper mDbHelper;
/** /**
* The currently logged in {@link FirebaseUser}. * The currently logged in {@link FirebaseUser}.
*/ */
private FirebaseUser mCurrentUser; private FirebaseUser mCurrentUser;
/** /**
* The ID of the location updates to listen to. * The ID of the location updates to listen to.
*/ */
private String mShareId; private String mShareId;
/** /**
* The InfoBox {@link ImageView} showing the user icon. * The InfoBox {@link ImageView} showing the user icon.
*/ */
private ImageView mInfoBoxIcon; private ImageView mInfoBoxIcon;
/** /**
* The InfoBox {@link TextView} showing the title. * The InfoBox {@link TextView} showing the title.
*/ */
private TextView mInfoBoxTitle; private TextView mInfoBoxTitle;
/** /**
* The InfoBox {@link TextView} showing the Location information. * The InfoBox {@link TextView} showing the Location information.
*/ */
private TextView mInfoBoxLocation; private TextView mInfoBoxLocation;
/** /**
* The InfoBox {@link TextView} showing the altitude.. * The InfoBox {@link TextView} showing the altitude..
*/ */
private TextView mInfoBoxAltitude; private TextView mInfoBoxAltitude;
/** /**
* The InfoBox {@link TextView} showing the last received speed. * The InfoBox {@link TextView} showing the last received speed.
*/ */
private TextView mInfoBoxSpeed; private TextView mInfoBoxSpeed;
/** /**
* The InfoBox {@link TextView} showing the timestamp of the last update. * The InfoBox {@link TextView} showing the timestamp of the last update.
*/ */
private TextView mInfoBoxTimeStamp; private TextView mInfoBoxTimeStamp;
/** /**
* The {@link Disposable} holding the subscription to the {@link LocationObject} * The {@link Disposable} holding the subscription to the {@link LocationObject}
* {@link io.reactivex.rxjava3.subjects.BehaviorSubject} in order to have it cancellable. * {@link io.reactivex.rxjava3.subjects.BehaviorSubject} in order to have it cancellable.
*/ */
private Disposable mLocationUpdateSubscription; private Disposable mLocationUpdateSubscription;
/** /**
* The {@link Disposable} holding the subscription to the {@link String} * The {@link Disposable} holding the subscription to the {@link String}
* {@link io.reactivex.rxjava3.subjects.BehaviorSubject} in order to know whom to follow. * {@link io.reactivex.rxjava3.subjects.BehaviorSubject} in order to know whom to follow.
*/ */
private Disposable mCurrentlyFollowingSubscription; private Disposable mCurrentlyFollowingSubscription;
/** /**
* The {@link Disposable} holding the subscription to {@link User} * The {@link Disposable} holding the subscription to {@link User}
* {@link io.reactivex.rxjava3.subjects.BehaviorSubject} in order to have it cancellable. * {@link io.reactivex.rxjava3.subjects.BehaviorSubject} in order to have it cancellable.
*/ */
private Disposable mUserSubscription; private Disposable mUserSubscription;
/** /**
* Boolean flag indicating whether the system is currently in night mode or not. * Boolean flag indicating whether the system is currently in night mode or not.
*/ */
private boolean mIsNightMode; private boolean mIsNightMode;
/** /**
* Reference to the Google Maps Marker, to update it's position rather than adding a new one. * Reference to the Google Maps Marker, to update it's position rather than adding a new one.
*/ */
private Marker mGmapsMarker; 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. * 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); mInfoBoxTimeStamp = findViewById(R.id.share_location_info_timestamp);
mNoActiveShares = findViewById(R.id.tv_no_active_shares); mNoActiveShares = findViewById(R.id.tv_no_active_shares);
mActiveShares = findViewById(R.id.active_share_layout); 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)); mActiveShares.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
mActiveShareAdapter = new ActiveShareAdapter(this); mActiveShareAdapter = new ActiveShareAdapter(this);
@@ -225,6 +187,8 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea
if (mLocationUpdateSubscription != null) { if (mLocationUpdateSubscription != null) {
mLocationUpdateSubscription.dispose(); mLocationUpdateSubscription.dispose();
} }
initAndSetupShareToggle();
mUserSubscription = mDbHelper.getAllUsers() mUserSubscription = mDbHelper.getAllUsers()
.subscribe(this::handleAllUsers, this::handleUserSubscriptionFailed); .subscribe(this::handleAllUsers, this::handleUserSubscriptionFailed);
mLocationUpdateSubscription = mDbHelper.subscribeToAllLocationUpdates() mLocationUpdateSubscription = mDbHelper.subscribeToAllLocationUpdates()
@@ -233,6 +197,28 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea
.subscribe(this::handleCurrentlyFollowingChanged, this::handleSubscriptionError); .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 @Override
protected void onPause() { protected void onPause() {
super.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. * Removed all markers that became invalid because the sharing was stopped.
* *
* @param locationObjectMap The {@link Map} matching shareIds to {@link LocationObject}s. * @param locationObjectMap The {@link Map} matching shareIds to {@link LocationObject}s.
*/ */
private void removeInvalidExistingMarkers(final Map<String, LocationObject> locationObjectMap) { private void removeInvalidExistingMarkers(final Map<String, LocationObject> locationObjectMap) {
final List<String> toBeRemovedKeys = new ArrayList<>(); final List<String> toBeRemovedKeys = new ArrayList<>();
for (Map.Entry<String, Marker> markerEntry : mMarkerMap.entrySet()) { for (Map.Entry<String, Marker> markerEntry : mMarkerMap.entrySet()) {

View File

@@ -68,6 +68,19 @@ public class LocationObject {
this.timestamp = timestamp; 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. * Returns the unique identifier for the shared location.
* *
@@ -122,20 +135,6 @@ public class LocationObject {
return timestamp; 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 @NonNull
@Override @Override
public String toString() { public String toString() {

View File

@@ -55,6 +55,30 @@ public class User {
this.creationDate = creationDate; 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. * Returns the unique Identifies of the user.
* *
@@ -91,31 +115,6 @@ public class User {
return creationDate; 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 @Override
public String toString() { public String toString() {
return "User{" + return "User{" +

View File

@@ -1,7 +1,6 @@
package com.aldo.apps.familyhelpers.ui; package com.aldo.apps.familyhelpers.ui;
import android.content.Context; import android.content.Context;
import android.graphics.PorterDuff;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -10,7 +9,6 @@ import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.aldo.apps.familyhelpers.R; import com.aldo.apps.familyhelpers.R;
@@ -20,20 +18,46 @@ import com.bumptech.glide.Glide;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import de.hdodenhof.circleimageview.CircleImageView; import de.hdodenhof.circleimageview.CircleImageView;
import io.reactivex.rxjava3.subjects.BehaviorSubject; 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> { public class ActiveShareAdapter extends RecyclerView.Adapter<ActiveShareAdapter.ActiveShareViewHolder> {
/**
* Tag for debugging purposes.
*/
private static final String TAG = "ActiveShareAdapter"; private static final String TAG = "ActiveShareAdapter";
/**
* {@link BehaviorSubject} holding the ID of the currently selected user.
*/
private final BehaviorSubject<String> mCurrentlySelectedSubject = BehaviorSubject.createDefault(""); private final BehaviorSubject<String> mCurrentlySelectedSubject = BehaviorSubject.createDefault("");
/**
* {@link List} of all currently sharing {@link User}s.
*/
private final List<User> mSharingUserList = new ArrayList<>(); private final List<User> mSharingUserList = new ArrayList<>();
/**
* A {@link WeakReference} to the calling {@link Context}.
*/
private final WeakReference<Context> mContextRef; 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) { public ActiveShareAdapter(final Context context) {
mContextRef = new WeakReference<>(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) { public ActiveShareViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final View view = LayoutInflater.from(parent.getContext()) final View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.active_share_view_holder, parent, false); .inflate(R.layout.active_share_view_holder, parent, false);
mAllItems.add(view);
return new ActiveShareViewHolder(view); return new ActiveShareViewHolder(view);
} }
@@ -58,24 +83,60 @@ public class ActiveShareAdapter extends RecyclerView.Adapter<ActiveShareAdapter.
return mSharingUserList.size(); 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) { public void applyNewData(final List<User> newUsers) {
mSharingUserList.clear(); mSharingUserList.clear();
mSharingUserList.addAll(newUsers); mSharingUserList.addAll(newUsers);
mAllItems.clear();
notifyDataSetChanged(); 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() { public BehaviorSubject<String> getCurrentlySelectedSubject() {
return mCurrentlySelectedSubject; 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 { 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) { public ActiveShareViewHolder(@NonNull final View itemView) {
super(itemView); super(itemView);
mProfilePicture = itemView.findViewById(R.id.active_share_profile_picture); mProfilePicture = itemView.findViewById(R.id.active_share_profile_picture);
@@ -83,6 +144,11 @@ public class ActiveShareAdapter extends RecyclerView.Adapter<ActiveShareAdapter.
mItemView = itemView; mItemView = itemView;
} }
/**
* Helper method to apply userData to a View.
*
* @param user The {@link User} to be attached.
*/
public void setUserData(final User user) { public void setUserData(final User user) {
if (user == null) { if (user == null) {
Log.w(TAG, "setUserData: Skip update, no valid update yet"); Log.w(TAG, "setUserData: Skip update, no valid update yet");
@@ -90,7 +156,15 @@ public class ActiveShareAdapter extends RecyclerView.Adapter<ActiveShareAdapter.
} }
mItemView.setOnClickListener(v -> { mItemView.setOnClickListener(v -> {
Log.d(TAG, "setUserData: Clicked on [" + user + "]"); 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()); mName.setText(user.getDisplayName());
final Context context = mContextRef.get(); 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}. * Returns the singleton instance of the {@link DevicePolicyManagerHelper}.
* *
* @param context The {@link Context} from where this was called. * @param context The {@link Context} from where this was called.
*
* @return the singleton instance of the {@link DevicePolicyManagerHelper}. * @return the singleton instance of the {@link DevicePolicyManagerHelper}.
*/ */
public static DevicePolicyManagerHelper getInstance(final Context context) { 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 com.firebase.ui.auth.AuthUI;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
@@ -10,88 +11,75 @@ import java.util.List;
*/ */
public final class GlobalConstants { public final class GlobalConstants {
/**
* Private C'tor to prevent instantiation.
*/
private GlobalConstants() {}
/** /**
* ID of the NotificationChannel for the SleepTimer Notification. * ID of the NotificationChannel for the SleepTimer Notification.
*/ */
public static final String SLEEP_TIMER_CHANNEL_ID = "CountdownChannel"; public static final String SLEEP_TIMER_CHANNEL_ID = "CountdownChannel";
/** /**
* ID of the NotificationChannel for the ShareLocation Notification. * ID of the NotificationChannel for the ShareLocation Notification.
*/ */
public static final String SHARE_LOCATION_CHANNEL_ID = "LocationShareChannel"; public static final String SHARE_LOCATION_CHANNEL_ID = "LocationShareChannel";
/** /**
* Factor to calculate seconds from milliseconds and vice versa. * Factor to calculate seconds from milliseconds and vice versa.
*/ */
public static final int ONE_SECOND_IN_MILLIS = 1000; public static final int ONE_SECOND_IN_MILLIS = 1000;
/** /**
* Factor to calculate hours from minutes and vice versa. * Factor to calculate hours from minutes and vice versa.
*/ */
public static final int ONE_HOUR_IN_MINUTES = 60; public static final int ONE_HOUR_IN_MINUTES = 60;
/** /**
* The NotificationID of the SleepTimer notification. * The NotificationID of the SleepTimer notification.
*/ */
public static final int SLEEP_TIMER_NOTIFICATION_ID = 1; public static final int SLEEP_TIMER_NOTIFICATION_ID = 1;
/** /**
* The NotificationID of the ShareLocation notification. * The NotificationID of the ShareLocation notification.
*/ */
public static final int SHARE_LOCATION_NOTIFICATION_ID = 2; public static final int SHARE_LOCATION_NOTIFICATION_ID = 2;
/** /**
* The action to be invoked when the sharing of location should be cancelled. * 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"; 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, * The key of the extra to be applied to the starting intent of the sleepTimer service,
* holding the initial duration in millis. * holding the initial duration in millis.
*/ */
public static final String SLEEP_TIMER_DURATION_MILLIS_EXTRA = "sleep_timer_duration"; public static final String SLEEP_TIMER_DURATION_MILLIS_EXTRA = "sleep_timer_duration";
/** /**
* Action to cancel an ongoing sleep timer. * Action to cancel an ongoing sleep timer.
*/ */
public static final String SLEEP_TIMER_CANCEL_ACTION = "SLEEP_TIMER_CANCEL"; public static final String SLEEP_TIMER_CANCEL_ACTION = "SLEEP_TIMER_CANCEL";
/** /**
* Default minimum time interval between two location updates. * Default minimum time interval between two location updates.
* Currently set to 5 Seconds. * Currently set to 5 Seconds.
*/ */
public static final int DEFAULT_MINIMUM_LOCATION_INTERVAL_MILLIS = 5000; public static final int DEFAULT_MINIMUM_LOCATION_INTERVAL_MILLIS = 5000;
/** /**
* Default minimum distance interval between two location updates. * Default minimum distance interval between two location updates.
* Currently set to 5 Meters. * Currently set to 5 Meters.
*/ */
public static final int DEFAULT_MINIMUM_LOCATION_INTERVAL_METERS = 5; public static final int DEFAULT_MINIMUM_LOCATION_INTERVAL_METERS = 5;
/** /**
* Conversion constants to convert m/s into km/h * Conversion constants to convert m/s into km/h
*/ */
public static final double METER_PER_SECOND_TO_KMH_CONVERTER = 3.6; public static final double METER_PER_SECOND_TO_KMH_CONVERTER = 3.6;
/** /**
* Latitude of the default home (== Blaustein) * Latitude of the default home (== Blaustein)
*/ */
public static final double DEFAULT_HOME_LATITUDE = 48.41965746149261; public static final double DEFAULT_HOME_LATITUDE = 48.41965746149261;
/** /**
* Longitude of the default home (== Blaustein) * Longitude of the default home (== Blaustein)
*/ */
public static final double DEFAULT_HOME_LONGITUDE = 9.909289365473684; public static final double DEFAULT_HOME_LONGITUDE = 9.909289365473684;
/** /**
* List of available Firebase signIn/Login providers. * 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() 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_LOCATION;
import static com.aldo.apps.familyhelpers.utils.DatabaseConstants.DB_COLL_USERS; 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 android.util.Log;
import androidx.annotation.Nullable;
import com.aldo.apps.familyhelpers.model.LocationObject; import com.aldo.apps.familyhelpers.model.LocationObject;
import com.aldo.apps.familyhelpers.model.User; import com.aldo.apps.familyhelpers.model.User;
import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser; import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.EventListener;
import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.FirebaseFirestoreException;
import com.google.firebase.firestore.ListenerRegistration; import com.google.firebase.firestore.ListenerRegistration;
import com.google.firebase.firestore.QuerySnapshot;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@@ -64,7 +58,7 @@ public class DatabaseHelper {
/** /**
* {@link Map} containing all users. * {@link Map} containing all users.
*/ */
private Map<String, User> mAllUserMap = new HashMap<>(); private final Map<String, User> mAllUserMap = new HashMap<>();
/** /**
* C'tor. * C'tor.
@@ -119,6 +113,13 @@ public class DatabaseHelper {
return mAllUsers; 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) { public User getUserForId(final String userId) {
Log.d(TAG, "getUserForId() called with: userId = [" + userId + "]"); Log.d(TAG, "getUserForId() called with: userId = [" + userId + "]");
if (mAllUserMap.containsKey(userId)) { if (mAllUserMap.containsKey(userId)) {
@@ -162,25 +163,25 @@ public class DatabaseHelper {
mLocationUpdateListener = mDatabase.collection(DB_COLL_LOCATION) mLocationUpdateListener = mDatabase.collection(DB_COLL_LOCATION)
.document(shareId) .document(shareId)
.addSnapshotListener((value, error) -> { .addSnapshotListener((value, error) -> {
if (value == null) { if (value == null) {
Log.d(TAG, "onEvent: Location was deleted"); Log.d(TAG, "onEvent: Location was deleted");
mObservedLocation.onComplete(); mObservedLocation.onComplete();
return; return;
} }
final LocationObject updatedLocation = value.toObject(LocationObject.class); final LocationObject updatedLocation = value.toObject(LocationObject.class);
if (updatedLocation == null) { if (updatedLocation == null) {
Log.w(TAG, "onEvent: Error while parsing, ignore"); Log.w(TAG, "onEvent: Error while parsing, ignore");
return; return;
} }
mObservedLocation.onNext(updatedLocation); mObservedLocation.onNext(updatedLocation);
}); });
} }
/** /**
* Helper method to start listening for all available location sharings in progress. * 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 * @return The {@link BehaviorSubject} containing a {@link Map} of all {@link LocationObject} matched
* to the sharing ID. * to the sharing ID.
*/ */
public BehaviorSubject<Map<String, LocationObject>> subscribeToAllLocationUpdates() { public BehaviorSubject<Map<String, LocationObject>> subscribeToAllLocationUpdates() {
final Map<String, LocationObject> locationMap = new HashMap<>(); final Map<String, LocationObject> locationMap = new HashMap<>();
@@ -191,18 +192,18 @@ public class DatabaseHelper {
} }
mLocationUpdateListener = mDatabase.collection(DB_COLL_LOCATION) mLocationUpdateListener = mDatabase.collection(DB_COLL_LOCATION)
.addSnapshotListener((value, error) -> { .addSnapshotListener((value, error) -> {
if (value == null || value.isEmpty()) { if (value == null || value.isEmpty()) {
Log.d(TAG, "onEvent: No ongoing location shares, return empty map."); Log.d(TAG, "onEvent: No ongoing location shares, return empty map.");
behaviorSubject.onNext(new HashMap<>()); behaviorSubject.onNext(new HashMap<>());
return; return;
} }
final List<DocumentSnapshot> allDocs = value.getDocuments(); final List<DocumentSnapshot> allDocs = value.getDocuments();
locationMap.clear(); locationMap.clear();
for (final DocumentSnapshot documentSnapshot : allDocs) { for (final DocumentSnapshot documentSnapshot : allDocs) {
locationMap.put(documentSnapshot.getId(), documentSnapshot.toObject(LocationObject.class)); locationMap.put(documentSnapshot.getId(), documentSnapshot.toObject(LocationObject.class));
} }
behaviorSubject.onNext(locationMap); behaviorSubject.onNext(locationMap);
}); });
return behaviorSubject; return behaviorSubject;
} }

View File

@@ -1,8 +1,7 @@
package com.aldo.apps.familyhelpers.workers; package com.aldo.apps.familyhelpers.workers;
import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_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_CANCEL_ACTION;
import static com.aldo.apps.familyhelpers.utils.GlobalConstants.SHARE_LOCATION_CHANNEL_ID; import static com.aldo.apps.familyhelpers.utils.GlobalConstants.SHARE_LOCATION_CHANNEL_ID;
import static com.aldo.apps.familyhelpers.utils.GlobalConstants.SHARE_LOCATION_NOTIFICATION_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.location.LocationManager;
import android.os.Looper; import android.os.Looper;
import android.util.Log; import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
@@ -25,9 +25,9 @@ import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import com.aldo.apps.familyhelpers.R; import com.aldo.apps.familyhelpers.R;
import com.aldo.apps.familyhelpers.ShareLocationActivity;
import com.aldo.apps.familyhelpers.model.LocationObject; import com.aldo.apps.familyhelpers.model.LocationObject;
import com.aldo.apps.familyhelpers.ui.NotificationHelper; import com.aldo.apps.familyhelpers.ui.NotificationHelper;
import com.aldo.apps.familyhelpers.utils.CancelLocationSharingReceiver;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.List; 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. * 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 { 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. * 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}. * The singleton instance of this {@link LocationHelper}.
*/ */
private static LocationHelper sInstance; private static LocationHelper sInstance;
private final WeakReference<Context> mContextRef;
/** /**
* The {@link LocationManager} object to listen to location updates. * The {@link LocationManager} object to listen to location updates.
*/ */
private final LocationManager mLocationManager; 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. * The {@link BehaviorSubject} for the {@link LocationObject} for clients to subscribe to.
* TODO: Check if needed. * TODO: Check if needed.
*/ */
private final BehaviorSubject<LocationObject> mLocationSubject = BehaviorSubject.create(); private final BehaviorSubject<LocationObject> mLocationSubject = BehaviorSubject.create();
/** /**
* The {@link DatabaseHelper} for db access. * The {@link DatabaseHelper} for db access.
*/ */
private final DatabaseHelper mDbHelper; private final DatabaseHelper mDbHelper;
/** /**
* The {@link NotificationHelper} to show and update {@link Notification}s. * The {@link NotificationHelper} to show and update {@link Notification}s.
*/ */
private final NotificationHelper mNotificationHelper; private final NotificationHelper mNotificationHelper;
/** /**
* The {@link BehaviorSubject} holding the state of current subscription. * The {@link BehaviorSubject} holding the state of current subscription.
*/ */
private final BehaviorSubject<Boolean> mSharingStateSubject = BehaviorSubject.createDefault(false); 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. * Private C'Tor for singleton instance.
@@ -91,7 +92,6 @@ public final class LocationHelper implements LocationListener {
*/ */
private LocationHelper(final Context context) { private LocationHelper(final Context context) {
mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
mContextRef = new WeakReference<>(context);
mNotificationHelper = new NotificationHelper(context); mNotificationHelper = new NotificationHelper(context);
mDbHelper = new DatabaseHelper(); mDbHelper = new DatabaseHelper();
mNotificationHelper.createAndRegisterNotificationChannel( mNotificationHelper.createAndRegisterNotificationChannel(
@@ -135,16 +135,41 @@ public final class LocationHelper implements LocationListener {
return true; 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. * 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 requestCode The code of the permission request.
* @param grantResults The result codes. * @param grantResults The result codes.
* @return true if permission was granted, false otherwise. * @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) { 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 grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
} }
return false; return false;
@@ -153,17 +178,17 @@ public final class LocationHelper implements LocationListener {
/** /**
* Helper method to start listening for updates. * 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 minIntervalMillis The minimum time interval in millisecond
* @param minDistance The minimum distance for an update to be received. * @param minDistance The minimum distance for an update to be received.
* @return true if subscription started, false otherwise. * @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) { if (mIsSubscribed) {
Log.d(TAG, "startLocationUpdates: Already subscribed, no need to update"); Log.d(TAG, "startLocationUpdates: Already subscribed, no need to update");
return true; return true;
} }
if (ContextCompat.checkSelfPermission(activity, ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(context, ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
mLocationManager.requestLocationUpdates( mLocationManager.requestLocationUpdates(
LocationManager.FUSED_PROVIDER, LocationManager.FUSED_PROVIDER,
minIntervalMillis, minIntervalMillis,
@@ -200,6 +225,7 @@ public final class LocationHelper implements LocationListener {
* @return true if location sharing is active, false otherwise. * @return true if location sharing is active, false otherwise.
*/ */
public boolean isCurrentlySharing() { public boolean isCurrentlySharing() {
Log.d(TAG, "isCurrentlySharing: Called with currently sharing = [" + mIsSubscribed + "]");
return mIsSubscribed; return mIsSubscribed;
} }
@@ -218,8 +244,12 @@ public final class LocationHelper implements LocationListener {
* @param context The {@link Context} from where this is called. * @param context The {@link Context} from where this is called.
* @return The {@link Notification} to be shown. * @return The {@link Notification} to be shown.
*/ */
private Notification buildNotification(final Context context) { public Notification buildNotification(final Context context) {
final Intent deleteIntent = new Intent(context, CancelLocationSharingReceiver.class); 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); deleteIntent.setAction(SHARE_LOCATION_CANCEL_ACTION);
final PendingIntent pendingDeleteIntent = PendingIntent.getBroadcast(context, 0, final PendingIntent pendingDeleteIntent = PendingIntent.getBroadcast(context, 0,
deleteIntent, PendingIntent.FLAG_IMMUTABLE); deleteIntent, PendingIntent.FLAG_IMMUTABLE);
@@ -227,6 +257,7 @@ public final class LocationHelper implements LocationListener {
return new NotificationCompat.Builder(context, SHARE_LOCATION_CHANNEL_ID) return new NotificationCompat.Builder(context, SHARE_LOCATION_CHANNEL_ID)
.setContentTitle(context.getString(R.string.title_share_location)) .setContentTitle(context.getString(R.string.title_share_location))
.setContentText(context.getString(R.string.share_location_notification_content)) .setContentText(context.getString(R.string.share_location_notification_content))
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_location_helper) .setSmallIcon(R.drawable.ic_location_helper)
.setDeleteIntent(pendingDeleteIntent) .setDeleteIntent(pendingDeleteIntent)
.addAction(android.R.drawable.ic_menu_close_clear_cancel, .addAction(android.R.drawable.ic_menu_close_clear_cancel,
@@ -238,14 +269,9 @@ public final class LocationHelper implements LocationListener {
@Override @Override
public void onLocationChanged(@NonNull final Location location) { public void onLocationChanged(@NonNull final Location location) {
final LocationObject locationObject = LocationObject.fromLocation(location); final LocationObject locationObject = LocationObject.fromLocation(location);
Log.d(TAG, "onLocationChanged: Received update with " + locationObject);
mLocationSubject.onNext(locationObject); mLocationSubject.onNext(locationObject);
mDbHelper.insertOrUpdateLocation(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 @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. * Tag for debugging purpose.
*/ */
private static final String TAG = "SleepTimerHelper"; private static final String TAG = "SleepTimerHelper";
/** /**
* {@link Handler} on the MainThread in order to perform the actual locking. * {@link Handler} on the MainThread in order to perform the actual locking.
*/ */
@@ -87,7 +88,7 @@ public class SleepTimerHelper extends Service {
@Nullable @Nullable
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(final Intent intent) {
return null; return null;
} }
@@ -144,7 +145,6 @@ public class SleepTimerHelper extends Service {
* Formats the provided time in milliseconds in a human readable format. * Formats the provided time in milliseconds in a human readable format.
* *
* @param millis The remaining time in milliseconds. * @param millis The remaining time in milliseconds.
*
* @return The String representation of the milliseconds. * @return The String representation of the milliseconds.
*/ */
private String formatTime(long millis) { 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" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="75dp" android:layout_width="75dp"
android:layout_height="75dp" android:layout_height="75dp"
android:background="@drawable/currently_followed_user_selector"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> 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_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_info_timestamp">This update was received at: %s</string>
<string name="share_location_toggle_button">Share your location</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> </resources>