[ShareLocation] Initial "read data from db" logic

Added some base GoogleMap widget logic and a way to listen to ones own
location updates. In future this needs to be extended with a way to
listen to the updates of other users.
This commit is contained in:
Alexander Dörflinger
2025-03-19 16:05:13 +01:00
parent 89804eb842
commit f6abb8fac0
15 changed files with 648 additions and 23 deletions

View File

@@ -37,6 +37,14 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'io.reactivex.rxjava3:rxjava:3.1.5'
//Google Maps SDK
implementation 'com.google.android.gms:play-services-maps:19.1.0'
// Glide
implementation 'com.github.bumptech.glide:glide:4.16.0' // Check for the latest version
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
//Firebase Dependencies
implementation platform('com.google.firebase:firebase-bom:33.10.0')
implementation 'com.google.firebase:firebase-analytics'

View File

@@ -19,6 +19,10 @@
android:supportsRtl="true"
android:theme="@style/Theme.MyApplication"
tools:targetApi="31">
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyBrwogmBLNjbhngmON_RWhKrV1woF6ypR4" />
<activity
android:name=".HelperGridActivity"
android:exported="true">
@@ -29,11 +33,16 @@
</intent-filter>
</activity>
<service android:name=".workers.SleepTimerHelper" android:foregroundServiceType="specialUse"/>
<activity android:name=".ShareLocationActivity"/>
<receiver android:name="com.aldo.apps.familyhelpers.DoerflingerHelpersDeviceAdminReceiver"
android:permission="android.permission.BIND_DEVICE_ADMIN"
android:exported="true">
<service
android:name=".workers.SleepTimerHelper"
android:foregroundServiceType="specialUse" />
<receiver
android:name="com.aldo.apps.familyhelpers.DoerflingerHelpersDeviceAdminReceiver"
android:exported="true"
android:permission="android.permission.BIND_DEVICE_ADMIN">
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin" />
@@ -43,7 +52,8 @@
</intent-filter>
</receiver>
<receiver android:name=".utils.CancelLocationSharingReceiver"
<receiver
android:name=".utils.CancelLocationSharingReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.aldo.apps.familyhelpers.CANCEL_LOCATION_SHARING" />

View File

@@ -4,14 +4,16 @@ import android.app.admin.DeviceAdminReceiver;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
public class DoerflingerHelpersDeviceAdminReceiver extends DeviceAdminReceiver {
@Override
public void onEnabled(final Context context,final Intent intent) {
public void onEnabled(@NonNull final Context context, @NonNull final Intent intent) {
// Called when the app is enabled as a device administrator.
}
@Override
public void onDisabled(final Context context, final Intent intent) {
public void onDisabled(@NonNull final Context context, @NonNull final Intent intent) {
// Called when the app is disabled as a device administrator.
}
}

View File

@@ -162,15 +162,16 @@ public class HelperGridActivity extends AppCompatActivity {
if (mLocationHelper.requestLocationPermissions(HelperGridActivity.this)) {
Log.d(TAG, "launchHelper: Permission already granted");
mLocationHelper.toggleUpdate(HelperGridActivity.this);
final Intent intent = new Intent(HelperGridActivity.this, ShareLocationActivity.class);
startActivity(intent);
}
}
};
mShareLocationTile.setLogo(R.drawable.ic_location_helper);
mShareLocationTile.setTitle(R.string.title_share_location);
}
/**
* Helper method to request the NotificationPermission as it is required for the service to run.
*

View File

@@ -0,0 +1,215 @@
package com.aldo.apps.familyhelpers;
import static com.aldo.apps.familyhelpers.utils.GlobalConstants.METER_PER_SECOND_TO_KMH_CONVERTER;
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.aldo.apps.familyhelpers.model.LocationObject;
import com.aldo.apps.familyhelpers.workers.DatabaseHelper;
import com.bumptech.glide.Glide;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MapStyleOptions;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import io.reactivex.rxjava3.disposables.Disposable;
/**
* Activity showing a Map to display the shared location plus additional information.
*/
public class ShareLocationActivity extends AppCompatActivity implements OnMapReadyCallback {
/**
* Tag for debugging purpose.
*/
private static final String TAG = "ShareLocationActivity";
/**
* 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;
/**
* Boolean flag indicating whether the system is currently in night mode or not.
*/
private boolean mIsNightMode;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_share_location);
final SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
.findFragmentById(R.id.map);
mCurrentUser = FirebaseAuth.getInstance().getCurrentUser();
mInfoBoxIcon = findViewById(R.id.share_location_info_user_icon);
Glide.with(this)
.load(mCurrentUser.getPhotoUrl())
.into(mInfoBoxIcon);
mInfoBoxTitle = findViewById(R.id.share_location_info_title);
mInfoBoxLocation = findViewById(R.id.share_location_info_location);
mInfoBoxAltitude = findViewById(R.id.share_location_info_altitude);
mInfoBoxSpeed = findViewById(R.id.share_location_info_speed);
mInfoBoxTimeStamp = findViewById(R.id.share_location_info_timestamp);
mapFragment.getMapAsync(this);
mDbHelper = new DatabaseHelper();
}
@Override
protected void onResume() {
super.onResume();
final Configuration configuration = getResources().getConfiguration();
mIsNightMode = (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK)
== Configuration.UI_MODE_NIGHT_YES;
if (mShareId == null) {
mShareId = mCurrentUser.getUid();
}
mDbHelper.startListeningForLocationUpdates(mShareId);
if (mLocationUpdateSubscription != null) {
mLocationUpdateSubscription.dispose();
}
mLocationUpdateSubscription = mDbHelper.getLocationSubject()
.subscribe(this::handleReceivedLocation, this::handleSubscriptionError);
}
@Override
protected void onPause() {
super.onPause();
if (mLocationUpdateSubscription != null) {
mLocationUpdateSubscription.dispose();
}
}
/**
* Helper method to handle the received location by updating both the Map and the Info box.
*
* @param locationObject The received {@link LocationObject} holding the new information.
*/
private void handleReceivedLocation(final LocationObject locationObject) {
Log.d(TAG, "handleReceivedLocation() called with: locationObject = [" + locationObject + "]");
mInfoBoxTitle.setText(String.format(getString(R.string.share_location_info_title), mCurrentUser.getDisplayName()));
mInfoBoxLocation.setText(String.format(getString(R.string.share_location_info_location),
locationObject.getLatitude(), locationObject.getLongitude()));
mInfoBoxAltitude.setText(String.format(getString(R.string.share_location_info_altitude),
locationObject.getAltitude()));
mInfoBoxSpeed.setText(String.format(getString(R.string.share_location_info_speed),
locationObject.getSpeed(),
locationObject.getSpeed() * METER_PER_SECOND_TO_KMH_CONVERTER));
mInfoBoxTimeStamp.setText(String.format(getString(R.string.share_location_info_timestamp), formatTime(locationObject.getTimestamp())));
final LatLng received = new LatLng(locationObject.getLatitude(), locationObject.getLongitude());
mGmap.addMarker(new MarkerOptions()
.position(received)
.title(mCurrentUser.getDisplayName()));
mGmap.moveCamera(CameraUpdateFactory.newLatLngZoom(received, 15.0f));
}
/**
* Handle potential errors on the subscription to the Location.
*
* @param error The error that occurred.
*/
private void handleSubscriptionError(final Throwable error) {
Log.e(TAG, "handleSubscriptionError: ", error);
}
/**
* Formats the provided time in milliseconds in a human readable format.
*
* @param millis The remaining time in milliseconds.
*
* @return The String representation of the milliseconds.
*/
private String formatTime(final long millis) {
final Date date = new Date(millis);
final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd.MM.yyyy - HH:mm:ss", Locale.getDefault());
return simpleDateFormat.format(date);
}
@Override
public void onMapReady(@NonNull final GoogleMap googleMap) {
mGmap = googleMap;
if (mIsNightMode) {
mGmap.setMapStyle(MapStyleOptions.loadRawResourceStyle(this, R.raw.map_style_night));
} else {
mGmap.setMapStyle(MapStyleOptions.loadRawResourceStyle(this, R.raw.map_style_day));
}
final LatLng defaultHome = new LatLng(48.41965746149261, 9.909289365473684);
mGmap.addMarker(new MarkerOptions().position(defaultHome).title("Default - Home"));
mGmap.moveCamera(CameraUpdateFactory.newLatLngZoom(defaultHome, 15.0f));
}
}

View File

@@ -38,7 +38,7 @@ public abstract class HelperGroupTile implements View.OnClickListener {
/**
* The {@link WeakReference} of the {@link Context} from where this was instantiated.
*/
private WeakReference<Context> mContextRef;
private final WeakReference<Context> mContextRef;
/**
* The name of the helper, mainly used for logging purpose.
@@ -50,7 +50,7 @@ public abstract class HelperGroupTile implements View.OnClickListener {
*
* @param rootLayout The previously inflated view of the root layout.
*/
public HelperGroupTile(final View rootLayout) {
protected HelperGroupTile(final View rootLayout) {
mContextRef = new WeakReference<>(rootLayout.getContext());
mHelperLogo = rootLayout.findViewById(R.id.iv_helper_group_icon);
mHelperTitle = rootLayout.findViewById(R.id.tv_helper_group_title);
@@ -77,7 +77,7 @@ public abstract class HelperGroupTile implements View.OnClickListener {
if (mHelperTitle != null) {
mHelperTitle.setText(titleId);
} else {
Log.d(TAG, "setLogo: Cannot set Logo for [" + mHelperName + "]");
Log.d(TAG, "setTitle: Cannot set Title for [" + mHelperName + "]");
}
}
@@ -91,7 +91,7 @@ public abstract class HelperGroupTile implements View.OnClickListener {
if (mHelperTitle != null) {
mHelperTitle.setText(groupTitle);
} else {
Log.d(TAG, "setLogo: Cannot set Logo for [" + mHelperName + "]");
Log.d(TAG, "setTitleString: Cannot set TitleId for [" + mHelperName + "]");
}
}

View File

@@ -73,6 +73,8 @@ public final class GlobalConstants {
*/
public static final int DEFAULT_MINIMUM_LOCATION_INTERVAL_METERS = 5;
public static final double METER_PER_SECOND_TO_KMH_CONVERTER = 3.6;
/**
* List of available Firebase signIn/Login providers.
*/

View File

@@ -6,12 +6,12 @@ import static com.aldo.apps.familyhelpers.utils.DatabaseConstants.DB_DOC_USER_ID
import android.util.Log;
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.FirebaseFirestore;
import com.google.firebase.firestore.ListenerRegistration;
import java.util.ArrayList;
import java.util.List;
@@ -43,6 +43,13 @@ public class DatabaseHelper {
*/
private final BehaviorSubject<List<User>> mAllUsers = BehaviorSubject.create();
/**
* Local {@link BehaviorSubject} representation holding the {@link LocationObject} that is to be observed.
*/
private BehaviorSubject<LocationObject> mObservedLocation = BehaviorSubject.create();
private ListenerRegistration mLocationUpdateListener;
/**
* C'tor.
*/
@@ -84,6 +91,24 @@ public class DatabaseHelper {
});
}
/**
* Returns the {@link BehaviorSubject} containing the {@link List} of all {@link User} objects.
*
* @return The {@link BehaviorSubject} containing the {@link List} of all {@link User} objects.
*/
public BehaviorSubject<List<User>> getAllUsers() {
return mAllUsers;
}
/**
* Returns the {@link BehaviorSubject} containing the {@link LocationObject} to be observed.
*
* @return The {@link BehaviorSubject} containing the {@link LocationObject} to be observed.
*/
public BehaviorSubject<LocationObject> getLocationSubject() {
return mObservedLocation;
}
/**
* Helper method to insert or update a {@link LocationObject} in the database.
*
@@ -95,6 +120,30 @@ public class DatabaseHelper {
.set(locationObject);
}
/**
* Start listening and publishing {@link LocationObject} updates on {@link #mObservedLocation}.
*
* @param shareId The ID to be observed.
*/
public void startListeningForLocationUpdates(final String shareId) {
mObservedLocation = BehaviorSubject.create();
mLocationUpdateListener = mDatabase.collection(DB_COLL_LOCATION)
.document(shareId)
.addSnapshotListener((value, error) -> {
if (value == null) {
Log.d(TAG, "onEvent: Location was deleted");
mObservedLocation.onComplete();
return;
}
final LocationObject updatedLocation = value.toObject(LocationObject.class);
if (updatedLocation == null) {
Log.w(TAG, "onEvent: Error while parsing, ignore");
return;
}
mObservedLocation.onNext(updatedLocation);
});
}
/**
* Cancels (by deleting) the ongoing {@link LocationObject} sharing in the database.
*/
@@ -103,6 +152,9 @@ public class DatabaseHelper {
Log.w(TAG, "cancelOngoingLocationSharing: No user logged in, cannot stop sharing");
return;
}
if (mLocationUpdateListener != null) {
mLocationUpdateListener.remove();
}
mDatabase.collection(DB_COLL_LOCATION).document(mCurrentUser.getUid()).delete();
}

View File

@@ -67,17 +67,17 @@ public final class LocationHelper implements LocationListener {
* The {@link BehaviorSubject} for the {@link LocationObject} for clients to subscribe to.
* TODO: Check if needed.
*/
private BehaviorSubject<LocationObject> mLocationSubject = BehaviorSubject.create();
private final BehaviorSubject<LocationObject> mLocationSubject = BehaviorSubject.create();
/**
* The {@link DatabaseHelper} for db access.
*/
private DatabaseHelper mDbHelper = new DatabaseHelper();
private final DatabaseHelper mDbHelper = new DatabaseHelper();
/**
* The {@link NotificationHelper} to show and update {@link Notification}s.
*/
private NotificationHelper mNotificationHelper;
private final NotificationHelper mNotificationHelper;
/**
* Private C'Tor for singleton instance.
@@ -93,10 +93,10 @@ public final class LocationHelper implements LocationListener {
}
/**
* Returns the singleton instance of thie {@link LocationHelper}.
* Returns the singleton instance of this {@link LocationHelper}.
*
* @param context The {@link Context) from where this was instantiated.
* @return The singleton instance of thie {@link LocationHelper}.
* @return The singleton instance of this {@link LocationHelper}.
*/
public static LocationHelper getInstance(final Context context) {
if (sInstance == null) {

View File

@@ -8,12 +8,9 @@ import static com.aldo.apps.familyhelpers.utils.GlobalConstants.SLEEP_TIMER_DURA
import static com.aldo.apps.familyhelpers.utils.GlobalConstants.SLEEP_TIMER_NOTIFICATION_ID;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp"
android:height="800dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M8,10C9.105,10 10,9.105 10,8C10,6.895 9.105,6 8,6C6.895,6 6,6.895 6,8C6,9.105 6.895,10 8,10Z"
android:fillColor="#000000"/>
<path
android:pathData="M2.083,7C2.504,4.487 4.487,2.504 7,2.083V0H9V2.083C11.512,2.504 13.495,4.487 13.917,7H16V9H13.917C13.495,11.512 11.512,13.495 9,13.917V16H7V13.917C4.487,13.495 2.504,11.512 2.083,9H0V7H2.083ZM4,8C4,5.791 5.791,4 8,4C10.209,4 12,5.791 12,8C12,10.209 10.209,12 8,12C5.791,12 4,10.209 4,8Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ShareLocationActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/share_location_info_box"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/share_location_info_box"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent">
<ImageView
android:id="@+id/share_location_info_user_icon"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/share_location_info_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:textStyle="bold"
tools:text="@string/share_location_info_title"/>
<TextView
android:id="@+id/share_location_info_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/share_location_info_title"
tools:text="@string/share_location_info_location"/>
<TextView
android:id="@+id/share_location_info_altitude"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/share_location_info_location"
tools:text="@string/share_location_info_altitude"/>
<TextView
android:id="@+id/share_location_info_speed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/share_location_info_altitude"
tools:text="@string/share_location_info_speed"/>
<TextView
android:id="@+id/share_location_info_timestamp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/share_location_info_speed"
tools:text="@string/share_location_info_timestamp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,46 @@
[
{
"featureType": "administrative.land_parcel",
"elementType": "labels",
"stylers": [
{
"visibility": "off"
}
]
},
{
"featureType": "poi",
"elementType": "labels.text",
"stylers": [
{
"visibility": "off"
}
]
},
{
"featureType": "poi.business",
"stylers": [
{
"visibility": "off"
}
]
},
{
"featureType": "poi.park",
"elementType": "labels.text",
"stylers": [
{
"visibility": "off"
}
]
},
{
"featureType": "road.local",
"elementType": "labels",
"stylers": [
{
"visibility": "off"
}
]
}
]

View File

@@ -0,0 +1,205 @@
[
{
"elementType": "geometry",
"stylers": [
{
"color": "#242f3e"
}
]
},
{
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#746855"
}
]
},
{
"elementType": "labels.text.stroke",
"stylers": [
{
"color": "#242f3e"
}
]
},
{
"featureType": "administrative.land_parcel",
"elementType": "labels",
"stylers": [
{
"visibility": "off"
}
]
},
{
"featureType": "administrative.locality",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#d59563"
}
]
},
{
"featureType": "poi",
"elementType": "labels.text",
"stylers": [
{
"visibility": "off"
}
]
},
{
"featureType": "poi",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#d59563"
}
]
},
{
"featureType": "poi.business",
"stylers": [
{
"visibility": "off"
}
]
},
{
"featureType": "poi.park",
"elementType": "geometry",
"stylers": [
{
"color": "#263c3f"
}
]
},
{
"featureType": "poi.park",
"elementType": "labels.text",
"stylers": [
{
"visibility": "off"
}
]
},
{
"featureType": "poi.park",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#6b9a76"
}
]
},
{
"featureType": "road",
"elementType": "geometry",
"stylers": [
{
"color": "#38414e"
}
]
},
{
"featureType": "road",
"elementType": "geometry.stroke",
"stylers": [
{
"color": "#212a37"
}
]
},
{
"featureType": "road",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9ca5b3"
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry",
"stylers": [
{
"color": "#746855"
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry.stroke",
"stylers": [
{
"color": "#1f2835"
}
]
},
{
"featureType": "road.highway",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#f3d19c"
}
]
},
{
"featureType": "road.local",
"elementType": "labels",
"stylers": [
{
"visibility": "off"
}
]
},
{
"featureType": "transit",
"elementType": "geometry",
"stylers": [
{
"color": "#2f3948"
}
]
},
{
"featureType": "transit.station",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#d59563"
}
]
},
{
"featureType": "water",
"elementType": "geometry",
"stylers": [
{
"color": "#17263c"
}
]
},
{
"featureType": "water",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#515c6d"
}
]
},
{
"featureType": "water",
"elementType": "labels.text.stroke",
"stylers": [
{
"color": "#17263c"
}
]
}
]

View File

@@ -19,9 +19,14 @@
<string name="sleep_timer_remaining_time_with_hour">%02d:%02d:%02d"</string>
<string name="sleep_timer_remaining_time_without_hour">%02d:%02d"</string>
<!-- Share Location Funtionality Strings -->
<!-- Share Location Functionality Strings -->
<string name="title_share_location">Share Location</string>
<string name="share_location_notification_channel">ShareLocation</string>
<string name="share_location_notification_content">You are sharing your current location within the family</string>
<string name="share_location_info_title">You are following %s</string>
<string name="share_location_info_location">Last Received Location = %f;%f</string>
<string name="share_location_info_altitude">Last received latitude = %.2f Meters</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>
</resources>