Compare commits
8 Commits
yannikblos
...
zy-live-lo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c30f881798 | ||
|
|
93d841609f | ||
|
|
b8ff910c6f | ||
|
|
365c8fad6e | ||
|
|
c34d60a9d9 | ||
|
|
6af3e80f24 | ||
|
|
002477c0bb | ||
|
|
1347324c71 |
@@ -500,6 +500,13 @@
|
||||
android:stopWithTask="false"
|
||||
/>
|
||||
|
||||
<service android:name=".location.LocationSharingService"
|
||||
android:foregroundServiceType="location"
|
||||
android:exported="false"
|
||||
android:enabled="true"
|
||||
android:stopWithTask="false"
|
||||
/>
|
||||
|
||||
<service
|
||||
android:name=".downloader.DownloaderService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
|
||||
@@ -424,19 +424,32 @@ public class MwmActivity extends BaseMwmFragmentActivity
|
||||
private void shareMyLocation()
|
||||
{
|
||||
final Location loc = MwmApplication.from(this).getLocationHelper().getSavedLocation();
|
||||
if (loc != null)
|
||||
if (loc == null)
|
||||
{
|
||||
SharingUtils.shareLocation(this, loc);
|
||||
dismissLocationErrorDialog();
|
||||
mLocationErrorDialog = new MaterialAlertDialogBuilder(MwmActivity.this)
|
||||
.setMessage(R.string.unknown_current_position)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setOnDismissListener(dialog -> mLocationErrorDialog = null)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
dismissLocationErrorDialog();
|
||||
mLocationErrorDialog = new MaterialAlertDialogBuilder(MwmActivity.this)
|
||||
.setMessage(R.string.unknown_current_position)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setOnDismissListener(dialog -> mLocationErrorDialog = null)
|
||||
.show();
|
||||
SharingUtils.shareLocation(this, loc);
|
||||
}
|
||||
|
||||
public void onLocationSharingStateChanged(boolean isSharing)
|
||||
{
|
||||
mMapButtonsViewModel.setLocationSharingState(isSharing);
|
||||
MapButtonsController mapButtonsController =
|
||||
(MapButtonsController) getSupportFragmentManager().findFragmentById(R.id.map_buttons);
|
||||
if (mapButtonsController != null)
|
||||
mapButtonsController.updateMenuBadge();
|
||||
|
||||
// Update share location button color in navigation menu
|
||||
if (mNavigationController != null)
|
||||
mNavigationController.refreshShareLocationColor();
|
||||
}
|
||||
|
||||
private void showDownloader(boolean openDownloaded)
|
||||
@@ -1680,6 +1693,13 @@ public class MwmActivity extends BaseMwmFragmentActivity
|
||||
mMapButtonsViewModel.setLayoutMode(MapButtonsController.LayoutMode.regular);
|
||||
refreshLightStatusBar();
|
||||
Utils.keepScreenOn(Config.isKeepScreenOnEnabled(), getWindow());
|
||||
|
||||
// Stop location sharing when navigation ends
|
||||
if (app.organicmaps.location.LocationSharingManager.getInstance().isSharing())
|
||||
{
|
||||
app.organicmaps.location.LocationSharingManager.getInstance().stopSharing();
|
||||
onLocationSharingStateChanged(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package app.organicmaps.api;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.organicmaps.sdk.util.log.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* HTTP API client for location sharing server.
|
||||
* Sends encrypted location updates to the server.
|
||||
*/
|
||||
public class LocationSharingApiClient
|
||||
{
|
||||
private static final String TAG = LocationSharingApiClient.class.getSimpleName();
|
||||
|
||||
private static final int CONNECT_TIMEOUT_MS = 10000;
|
||||
private static final int READ_TIMEOUT_MS = 10000;
|
||||
|
||||
private final String mServerBaseUrl;
|
||||
private final String mSessionId;
|
||||
private final Executor mExecutor;
|
||||
|
||||
public interface Callback
|
||||
{
|
||||
void onSuccess();
|
||||
void onFailure(@NonNull String error);
|
||||
}
|
||||
|
||||
public LocationSharingApiClient(@NonNull String serverBaseUrl, @NonNull String sessionId)
|
||||
{
|
||||
mServerBaseUrl = serverBaseUrl.endsWith("/") ? serverBaseUrl : serverBaseUrl + "/";
|
||||
mSessionId = sessionId;
|
||||
mExecutor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new session on the server.
|
||||
* @param callback Result callback
|
||||
*/
|
||||
public void createSession(@Nullable Callback callback)
|
||||
{
|
||||
mExecutor.execute(() -> {
|
||||
try
|
||||
{
|
||||
String url = mServerBaseUrl + "api/v1/session";
|
||||
String requestBody = "{\"sessionId\":\"" + mSessionId + "\"}";
|
||||
|
||||
int responseCode = postJson(url, requestBody);
|
||||
|
||||
if (responseCode >= 200 && responseCode < 300)
|
||||
{
|
||||
Logger.d(TAG, "Session created successfully: " + mSessionId);
|
||||
if (callback != null)
|
||||
callback.onSuccess();
|
||||
}
|
||||
else
|
||||
{
|
||||
String error = "Server returned error: " + responseCode;
|
||||
Logger.w(TAG, error);
|
||||
if (callback != null)
|
||||
callback.onFailure(error);
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Logger.e(TAG, "Failed to create session", e);
|
||||
if (callback != null)
|
||||
callback.onFailure(e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update location on the server with encrypted payload.
|
||||
* @param encryptedPayloadJson Encrypted payload JSON (from native code)
|
||||
* @param callback Result callback
|
||||
*/
|
||||
public void updateLocation(@NonNull String encryptedPayloadJson, @Nullable Callback callback)
|
||||
{
|
||||
mExecutor.execute(() -> {
|
||||
try
|
||||
{
|
||||
String url = mServerBaseUrl + "api/v1/location/" + mSessionId;
|
||||
|
||||
int responseCode = postJson(url, encryptedPayloadJson);
|
||||
|
||||
if (responseCode >= 200 && responseCode < 300)
|
||||
{
|
||||
Logger.d(TAG, "Location updated successfully");
|
||||
if (callback != null)
|
||||
callback.onSuccess();
|
||||
}
|
||||
else
|
||||
{
|
||||
String error = "Server returned error: " + responseCode;
|
||||
Logger.w(TAG, error);
|
||||
if (callback != null)
|
||||
callback.onFailure(error);
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Logger.e(TAG, "Failed to update location", e);
|
||||
if (callback != null)
|
||||
callback.onFailure(e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* End the session on the server.
|
||||
*/
|
||||
public void endSession()
|
||||
{
|
||||
mExecutor.execute(() -> {
|
||||
try
|
||||
{
|
||||
String url = mServerBaseUrl + "api/v1/session/" + mSessionId;
|
||||
deleteRequest(url);
|
||||
Logger.d(TAG, "Session ended: " + mSessionId);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Logger.e(TAG, "Failed to end session", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a POST request with JSON body.
|
||||
* @param urlString URL to send request to
|
||||
* @param jsonBody JSON request body
|
||||
* @return HTTP response code
|
||||
* @throws IOException on network error
|
||||
*/
|
||||
private int postJson(@NonNull String urlString, @NonNull String jsonBody) throws IOException
|
||||
{
|
||||
URL url = new URL(urlString);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
|
||||
try
|
||||
{
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||
connection.setReadTimeout(READ_TIMEOUT_MS);
|
||||
connection.setDoOutput(true);
|
||||
|
||||
// Write body
|
||||
byte[] bodyBytes = jsonBody.getBytes(StandardCharsets.UTF_8);
|
||||
connection.setFixedLengthStreamingMode(bodyBytes.length);
|
||||
|
||||
try (OutputStream os = connection.getOutputStream())
|
||||
{
|
||||
os.write(bodyBytes);
|
||||
os.flush();
|
||||
}
|
||||
|
||||
return connection.getResponseCode();
|
||||
}
|
||||
finally
|
||||
{
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a DELETE request.
|
||||
* @param urlString URL to send request to
|
||||
* @return HTTP response code
|
||||
* @throws IOException on network error
|
||||
*/
|
||||
private int deleteRequest(@NonNull String urlString) throws IOException
|
||||
{
|
||||
URL url = new URL(urlString);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
|
||||
try
|
||||
{
|
||||
connection.setRequestMethod("DELETE");
|
||||
connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||
connection.setReadTimeout(READ_TIMEOUT_MS);
|
||||
|
||||
return connection.getResponseCode();
|
||||
}
|
||||
finally
|
||||
{
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package app.organicmaps.location;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* AES-256-GCM encryption/decryption for location data.
|
||||
*/
|
||||
public class LocationCrypto
|
||||
{
|
||||
private static final String ALGORITHM = "AES/GCM/NoPadding";
|
||||
private static final int GCM_IV_LENGTH = 12; // 96 bits
|
||||
private static final int GCM_TAG_LENGTH = 128; // 128 bits
|
||||
|
||||
/**
|
||||
* Encrypt plaintext JSON using AES-256-GCM.
|
||||
* @param base64Key Base64-encoded 256-bit key
|
||||
* @param plaintextJson JSON string to encrypt
|
||||
* @return JSON string with encrypted payload: {"iv":"...","ciphertext":"...","authTag":"..."}
|
||||
*/
|
||||
@Nullable
|
||||
public static String encrypt(@NonNull String base64Key, @NonNull String plaintextJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Decode the base64 key
|
||||
byte[] key = Base64.decode(base64Key, Base64.NO_WRAP);
|
||||
if (key.length != 32) // 256 bits
|
||||
{
|
||||
android.util.Log.e("LocationCrypto", "Invalid key size: " + key.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate random IV
|
||||
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||
new SecureRandom().nextBytes(iv);
|
||||
|
||||
// Create cipher
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);
|
||||
|
||||
// Encrypt
|
||||
byte[] plaintext = plaintextJson.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] ciphertextWithTag = cipher.doFinal(plaintext);
|
||||
|
||||
// Split ciphertext and auth tag
|
||||
// In GCM mode, doFinal() returns ciphertext + tag
|
||||
int ciphertextLength = ciphertextWithTag.length - (GCM_TAG_LENGTH / 8);
|
||||
byte[] ciphertext = new byte[ciphertextLength];
|
||||
byte[] authTag = new byte[GCM_TAG_LENGTH / 8];
|
||||
|
||||
System.arraycopy(ciphertextWithTag, 0, ciphertext, 0, ciphertextLength);
|
||||
System.arraycopy(ciphertextWithTag, ciphertextLength, authTag, 0, authTag.length);
|
||||
|
||||
// Build JSON response
|
||||
JSONObject result = new JSONObject();
|
||||
result.put("iv", Base64.encodeToString(iv, Base64.NO_WRAP));
|
||||
result.put("ciphertext", Base64.encodeToString(ciphertext, Base64.NO_WRAP));
|
||||
result.put("authTag", Base64.encodeToString(authTag, Base64.NO_WRAP));
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
android.util.Log.e("LocationCrypto", "Encryption failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt encrypted payload using AES-256-GCM.
|
||||
* @param base64Key Base64-encoded 256-bit key
|
||||
* @param encryptedPayloadJson JSON string with format: {"iv":"...","ciphertext":"...","authTag":"..."}
|
||||
* @return Decrypted plaintext JSON string
|
||||
*/
|
||||
@Nullable
|
||||
public static String decrypt(@NonNull String base64Key, @NonNull String encryptedPayloadJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse encrypted payload
|
||||
JSONObject payload = new JSONObject(encryptedPayloadJson);
|
||||
byte[] iv = Base64.decode(payload.getString("iv"), Base64.NO_WRAP);
|
||||
byte[] ciphertext = Base64.decode(payload.getString("ciphertext"), Base64.NO_WRAP);
|
||||
byte[] authTag = Base64.decode(payload.getString("authTag"), Base64.NO_WRAP);
|
||||
|
||||
// Decode the base64 key
|
||||
byte[] key = Base64.decode(base64Key, Base64.NO_WRAP);
|
||||
if (key.length != 32) // 256 bits
|
||||
{
|
||||
android.util.Log.e("LocationCrypto", "Invalid key size: " + key.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Combine ciphertext and auth tag for GCM decryption
|
||||
byte[] ciphertextWithTag = new byte[ciphertext.length + authTag.length];
|
||||
System.arraycopy(ciphertext, 0, ciphertextWithTag, 0, ciphertext.length);
|
||||
System.arraycopy(authTag, 0, ciphertextWithTag, ciphertext.length, authTag.length);
|
||||
|
||||
// Create cipher
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
|
||||
|
||||
// Decrypt
|
||||
byte[] plaintext = cipher.doFinal(ciphertextWithTag);
|
||||
|
||||
return new String(plaintext, StandardCharsets.UTF_8);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
android.util.Log.e("LocationCrypto", "Decryption failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package app.organicmaps.location;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import app.organicmaps.R;
|
||||
import app.organicmaps.util.SharingUtils;
|
||||
|
||||
/**
|
||||
* Dialog for starting/stopping live location sharing and managing the share URL.
|
||||
*/
|
||||
public class LocationSharingDialog extends DialogFragment
|
||||
{
|
||||
private static final String TAG = LocationSharingDialog.class.getSimpleName();
|
||||
|
||||
@Nullable
|
||||
private TextView mStatusText;
|
||||
@Nullable
|
||||
private TextView mShareUrlText;
|
||||
@Nullable
|
||||
private Button mStartStopButton;
|
||||
@Nullable
|
||||
private Button mCopyButton;
|
||||
@Nullable
|
||||
private Button mShareButton;
|
||||
|
||||
private LocationSharingManager mManager;
|
||||
|
||||
public static void show(@NonNull FragmentManager fragmentManager)
|
||||
{
|
||||
LocationSharingDialog dialog = new LocationSharingDialog();
|
||||
dialog.show(fragmentManager, TAG);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
mManager = LocationSharingManager.getInstance();
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_location_sharing, null);
|
||||
|
||||
initViews(view);
|
||||
updateUI();
|
||||
|
||||
builder.setView(view);
|
||||
builder.setTitle(R.string.location_sharing_title);
|
||||
builder.setNegativeButton(R.string.close, (dialog, which) -> dismiss());
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
private void initViews(@NonNull View root)
|
||||
{
|
||||
mStatusText = root.findViewById(R.id.status_text);
|
||||
mShareUrlText = root.findViewById(R.id.share_url_text);
|
||||
mStartStopButton = root.findViewById(R.id.start_stop_button);
|
||||
mCopyButton = root.findViewById(R.id.copy_button);
|
||||
mShareButton = root.findViewById(R.id.share_button);
|
||||
|
||||
if (mStartStopButton != null)
|
||||
{
|
||||
mStartStopButton.setOnClickListener(v -> {
|
||||
if (mManager.isSharing())
|
||||
stopSharing();
|
||||
else
|
||||
startSharing();
|
||||
});
|
||||
}
|
||||
|
||||
if (mCopyButton != null)
|
||||
{
|
||||
mCopyButton.setOnClickListener(v -> copyUrl());
|
||||
}
|
||||
|
||||
if (mShareButton != null)
|
||||
{
|
||||
mShareButton.setOnClickListener(v -> shareUrl());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUI()
|
||||
{
|
||||
boolean isSharing = mManager.isSharing();
|
||||
|
||||
if (mStatusText != null)
|
||||
{
|
||||
mStatusText.setText(isSharing
|
||||
? R.string.location_sharing_status_active
|
||||
: R.string.location_sharing_status_inactive);
|
||||
}
|
||||
|
||||
if (mShareUrlText != null)
|
||||
{
|
||||
String url = mManager.getShareUrl();
|
||||
if (url != null && isSharing)
|
||||
{
|
||||
mShareUrlText.setText(url);
|
||||
mShareUrlText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else
|
||||
{
|
||||
mShareUrlText.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
if (mStartStopButton != null)
|
||||
{
|
||||
mStartStopButton.setText(isSharing
|
||||
? R.string.location_sharing_stop
|
||||
: R.string.location_sharing_start);
|
||||
}
|
||||
|
||||
// Show/hide copy and share buttons
|
||||
int visibility = isSharing ? View.VISIBLE : View.GONE;
|
||||
if (mCopyButton != null)
|
||||
mCopyButton.setVisibility(visibility);
|
||||
if (mShareButton != null)
|
||||
mShareButton.setVisibility(visibility);
|
||||
}
|
||||
|
||||
private void startSharing()
|
||||
{
|
||||
String shareUrl = mManager.startSharing();
|
||||
|
||||
if (shareUrl != null)
|
||||
{
|
||||
Toast.makeText(requireContext(),
|
||||
R.string.location_sharing_started,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
updateUI();
|
||||
|
||||
// Notify the activity
|
||||
if (getActivity() instanceof app.organicmaps.MwmActivity)
|
||||
{
|
||||
((app.organicmaps.MwmActivity) getActivity()).onLocationSharingStateChanged(true);
|
||||
}
|
||||
|
||||
// Auto-copy URL to clipboard
|
||||
copyUrlToClipboard(shareUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
Toast.makeText(requireContext(),
|
||||
R.string.location_sharing_failed_to_start,
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopSharing()
|
||||
{
|
||||
mManager.stopSharing();
|
||||
|
||||
Toast.makeText(requireContext(),
|
||||
R.string.location_sharing_stopped,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
updateUI();
|
||||
|
||||
// Notify the activity
|
||||
if (getActivity() instanceof app.organicmaps.MwmActivity)
|
||||
{
|
||||
((app.organicmaps.MwmActivity) getActivity()).onLocationSharingStateChanged(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyUrl()
|
||||
{
|
||||
String url = mManager.getShareUrl();
|
||||
if (url != null)
|
||||
{
|
||||
copyUrlToClipboard(url);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyUrlToClipboard(@NonNull String url)
|
||||
{
|
||||
ClipboardManager clipboard = (ClipboardManager)
|
||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
|
||||
if (clipboard != null)
|
||||
{
|
||||
ClipData clip = ClipData.newPlainText("Location Share URL", url);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
|
||||
Toast.makeText(requireContext(),
|
||||
R.string.location_sharing_url_copied,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void shareUrl()
|
||||
{
|
||||
String url = mManager.getShareUrl();
|
||||
if (url == null)
|
||||
return;
|
||||
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.setType("text/plain");
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.location_sharing_share_message, url));
|
||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.location_sharing_share_url)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package app.organicmaps.location;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.BatteryManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.organicmaps.MwmApplication;
|
||||
import app.organicmaps.sdk.routing.RoutingController;
|
||||
import app.organicmaps.sdk.util.Config;
|
||||
import app.organicmaps.sdk.util.log.Logger;
|
||||
|
||||
/**
|
||||
* Singleton manager for live location sharing functionality.
|
||||
* Coordinates between LocationHelper, RoutingController, and LocationSharingService.
|
||||
*/
|
||||
public class LocationSharingManager
|
||||
{
|
||||
private static final String TAG = LocationSharingManager.class.getSimpleName();
|
||||
|
||||
private static LocationSharingManager sInstance;
|
||||
|
||||
@Nullable
|
||||
private String mSessionId;
|
||||
@Nullable
|
||||
private String mEncryptionKey;
|
||||
@Nullable
|
||||
private String mShareUrl;
|
||||
private boolean mIsSharing = false;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private LocationSharingManager()
|
||||
{
|
||||
mContext = MwmApplication.sInstance;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static synchronized LocationSharingManager getInstance()
|
||||
{
|
||||
if (sInstance == null)
|
||||
sInstance = new LocationSharingManager();
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start live location sharing.
|
||||
* @return Share URL that can be sent to others
|
||||
*/
|
||||
@Nullable
|
||||
public String startSharing()
|
||||
{
|
||||
if (mIsSharing)
|
||||
{
|
||||
Logger.w(TAG, "Location sharing already active");
|
||||
return mShareUrl;
|
||||
}
|
||||
|
||||
// Generate session credentials via native code
|
||||
String[] credentials = nativeGenerateSessionCredentials();
|
||||
if (credentials == null || credentials.length != 2)
|
||||
{
|
||||
Logger.e(TAG, "Failed to generate session credentials");
|
||||
return null;
|
||||
}
|
||||
|
||||
mSessionId = credentials[0];
|
||||
mEncryptionKey = credentials[1];
|
||||
|
||||
// Generate share URL using configured server
|
||||
String serverUrl = Config.LocationSharing.getServerUrl();
|
||||
mShareUrl = nativeGenerateShareUrl(mSessionId, mEncryptionKey, serverUrl);
|
||||
if (mShareUrl == null)
|
||||
{
|
||||
Logger.e(TAG, "Failed to generate share URL");
|
||||
return null;
|
||||
}
|
||||
|
||||
mIsSharing = true;
|
||||
|
||||
// Start foreground service
|
||||
Intent intent = new Intent(mContext, LocationSharingService.class);
|
||||
intent.putExtra(LocationSharingService.EXTRA_SESSION_ID, mSessionId);
|
||||
intent.putExtra(LocationSharingService.EXTRA_ENCRYPTION_KEY, mEncryptionKey);
|
||||
intent.putExtra(LocationSharingService.EXTRA_SERVER_URL, serverUrl);
|
||||
intent.putExtra(LocationSharingService.EXTRA_UPDATE_INTERVAL, Config.LocationSharing.getUpdateInterval());
|
||||
|
||||
mContext.startForegroundService(intent);
|
||||
|
||||
Logger.i(TAG, "Location sharing started, session ID: " + mSessionId);
|
||||
|
||||
return mShareUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop live location sharing.
|
||||
*/
|
||||
public void stopSharing()
|
||||
{
|
||||
if (!mIsSharing)
|
||||
{
|
||||
Logger.w(TAG, "Location sharing not active");
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop foreground service
|
||||
Intent intent = new Intent(mContext, LocationSharingService.class);
|
||||
mContext.stopService(intent);
|
||||
|
||||
mIsSharing = false;
|
||||
mSessionId = null;
|
||||
mEncryptionKey = null;
|
||||
mShareUrl = null;
|
||||
|
||||
Logger.i(TAG, "Location sharing stopped");
|
||||
}
|
||||
|
||||
public boolean isSharing()
|
||||
{
|
||||
return mIsSharing;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getShareUrl()
|
||||
{
|
||||
return mShareUrl;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSessionId()
|
||||
{
|
||||
return mSessionId;
|
||||
}
|
||||
|
||||
public void setUpdateIntervalSeconds(int seconds)
|
||||
{
|
||||
Config.LocationSharing.setUpdateInterval(seconds);
|
||||
}
|
||||
|
||||
public int getUpdateIntervalSeconds()
|
||||
{
|
||||
return Config.LocationSharing.getUpdateInterval();
|
||||
}
|
||||
|
||||
public void setServerBaseUrl(@NonNull String url)
|
||||
{
|
||||
Config.LocationSharing.setServerUrl(url);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getServerBaseUrl()
|
||||
{
|
||||
return Config.LocationSharing.getServerUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current battery level (0-100).
|
||||
*/
|
||||
public int getBatteryLevel()
|
||||
{
|
||||
BatteryManager bm = (BatteryManager) mContext.getSystemService(Context.BATTERY_SERVICE);
|
||||
if (bm == null)
|
||||
return 100;
|
||||
|
||||
return bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if currently navigating with an active route.
|
||||
*/
|
||||
public boolean isNavigating()
|
||||
{
|
||||
return RoutingController.get().isNavigating();
|
||||
}
|
||||
|
||||
// Native methods (implemented in JNI)
|
||||
|
||||
/**
|
||||
* Generate new session credentials (ID and encryption key).
|
||||
* @return Array of [sessionId, encryptionKey]
|
||||
*/
|
||||
@Nullable
|
||||
private static native String[] nativeGenerateSessionCredentials();
|
||||
|
||||
/**
|
||||
* Generate shareable URL from credentials.
|
||||
* @param sessionId Session ID (UUID)
|
||||
* @param encryptionKey Base64-encoded encryption key
|
||||
* @param serverBaseUrl Server base URL
|
||||
* @return Share URL
|
||||
*/
|
||||
@Nullable
|
||||
private static native String nativeGenerateShareUrl(String sessionId, String encryptionKey, String serverBaseUrl);
|
||||
|
||||
/**
|
||||
* Encrypt location payload.
|
||||
* @param encryptionKey Base64-encoded encryption key
|
||||
* @param payloadJson JSON payload to encrypt
|
||||
* @return Encrypted payload JSON (with iv, ciphertext, authTag) or null on failure
|
||||
*/
|
||||
@Nullable
|
||||
public static native String nativeEncryptPayload(String encryptionKey, String payloadJson);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package app.organicmaps.location;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import app.organicmaps.MwmActivity;
|
||||
import app.organicmaps.R;
|
||||
import app.organicmaps.sdk.routing.RoutingInfo;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Helper for creating and updating location sharing notifications.
|
||||
*/
|
||||
public class LocationSharingNotification
|
||||
{
|
||||
public static final String CHANNEL_ID = "LOCATION_SHARING";
|
||||
private static final String CHANNEL_NAME = "Live Location Sharing";
|
||||
|
||||
private final Context mContext;
|
||||
private final NotificationManagerCompat mNotificationManager;
|
||||
|
||||
public LocationSharingNotification(@NonNull Context context)
|
||||
{
|
||||
mContext = context;
|
||||
mNotificationManager = NotificationManagerCompat.from(context);
|
||||
createNotificationChannel();
|
||||
}
|
||||
|
||||
private void createNotificationChannel()
|
||||
{
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
return;
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_LOW); // Low importance = no sound/vibration
|
||||
|
||||
channel.setDescription("Notifications for active live location sharing");
|
||||
channel.setShowBadge(false);
|
||||
channel.enableLights(false);
|
||||
channel.enableVibration(false);
|
||||
|
||||
NotificationManager nm = mContext.getSystemService(NotificationManager.class);
|
||||
if (nm != null)
|
||||
nm.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build notification for location sharing service.
|
||||
* @param stopIntent PendingIntent to stop sharing
|
||||
* @return Notification object
|
||||
*/
|
||||
@NonNull
|
||||
public Notification buildNotification(@NonNull PendingIntent stopIntent)
|
||||
{
|
||||
return buildNotification(stopIntent, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build notification with copy URL action.
|
||||
* @param stopIntent PendingIntent to stop sharing
|
||||
* @param copyUrlIntent PendingIntent to copy URL (optional)
|
||||
* @return Notification object
|
||||
*/
|
||||
@NonNull
|
||||
public Notification buildNotification(
|
||||
@NonNull PendingIntent stopIntent,
|
||||
@Nullable PendingIntent copyUrlIntent)
|
||||
{
|
||||
Intent notificationIntent = new Intent(mContext, MwmActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
mContext,
|
||||
0,
|
||||
notificationIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_share)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setShowWhen(false)
|
||||
.setAutoCancel(false);
|
||||
|
||||
// Title
|
||||
builder.setContentTitle(mContext.getString(R.string.location_sharing_active));
|
||||
|
||||
// No subtitle - keep it simple
|
||||
|
||||
// Copy URL action button (if provided)
|
||||
if (copyUrlIntent != null)
|
||||
{
|
||||
builder.addAction(
|
||||
R.drawable.ic_share,
|
||||
mContext.getString(R.string.location_sharing_copy_url),
|
||||
copyUrlIntent);
|
||||
}
|
||||
|
||||
// Stop action button
|
||||
builder.addAction(
|
||||
R.drawable.ic_close,
|
||||
mContext.getString(R.string.location_sharing_stop),
|
||||
stopIntent);
|
||||
|
||||
// Set foreground service type for Android 10+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
{
|
||||
builder.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update existing notification.
|
||||
* @param notificationId Notification ID
|
||||
* @param notification Updated notification
|
||||
*/
|
||||
public void updateNotification(int notificationId, @NonNull Notification notification)
|
||||
{
|
||||
mNotificationManager.notify(notificationId, notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel notification.
|
||||
* @param notificationId Notification ID
|
||||
*/
|
||||
public void cancelNotification(int notificationId)
|
||||
{
|
||||
mNotificationManager.cancel(notificationId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
package app.organicmaps.location;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import app.organicmaps.MwmActivity;
|
||||
import app.organicmaps.MwmApplication;
|
||||
import app.organicmaps.R;
|
||||
import app.organicmaps.api.LocationSharingApiClient;
|
||||
import app.organicmaps.sdk.location.LocationHelper;
|
||||
import app.organicmaps.sdk.location.LocationListener;
|
||||
import app.organicmaps.sdk.routing.RoutingController;
|
||||
import app.organicmaps.sdk.routing.RoutingInfo;
|
||||
import app.organicmaps.sdk.util.log.Logger;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Foreground service for live GPS location sharing.
|
||||
* Monitors location updates and posts encrypted data to server at regular intervals.
|
||||
*/
|
||||
public class LocationSharingService extends Service implements LocationListener
|
||||
{
|
||||
private static final String TAG = LocationSharingService.class.getSimpleName();
|
||||
private static final int NOTIFICATION_ID = 0x1002; // Unique ID for location sharing
|
||||
|
||||
// Intent extras
|
||||
public static final String EXTRA_SESSION_ID = "session_id";
|
||||
public static final String EXTRA_ENCRYPTION_KEY = "encryption_key";
|
||||
public static final String EXTRA_SERVER_URL = "server_url";
|
||||
public static final String EXTRA_UPDATE_INTERVAL = "update_interval";
|
||||
|
||||
// Actions for notification buttons
|
||||
private static final String ACTION_STOP = "app.organicmaps.ACTION_STOP_LOCATION_SHARING";
|
||||
private static final String ACTION_COPY_URL = "app.organicmaps.ACTION_COPY_LOCATION_URL";
|
||||
|
||||
@Nullable
|
||||
private String mSessionId;
|
||||
@Nullable
|
||||
private String mEncryptionKey;
|
||||
@Nullable
|
||||
private String mServerUrl;
|
||||
private int mUpdateIntervalSeconds = 20;
|
||||
|
||||
@Nullable
|
||||
private Location mLastLocation;
|
||||
private long mLastUpdateTimestamp = 0;
|
||||
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
private final Runnable mUpdateTask = this::processLocationUpdate;
|
||||
|
||||
@Nullable
|
||||
private LocationSharingApiClient mApiClient;
|
||||
@Nullable
|
||||
private LocationSharingNotification mNotificationHelper;
|
||||
|
||||
@Override
|
||||
public void onCreate()
|
||||
{
|
||||
super.onCreate();
|
||||
Logger.i(TAG, "Service created");
|
||||
|
||||
mNotificationHelper = new LocationSharingNotification(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(@Nullable Intent intent, int flags, int startId)
|
||||
{
|
||||
if (intent == null)
|
||||
{
|
||||
Logger.w(TAG, "Null intent, stopping service");
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// Handle stop action from notification
|
||||
if (ACTION_STOP.equals(intent.getAction()))
|
||||
{
|
||||
Logger.i(TAG, "Stop action received from notification");
|
||||
LocationSharingManager.getInstance().stopSharing();
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// Handle copy URL action from notification
|
||||
if (ACTION_COPY_URL.equals(intent.getAction()))
|
||||
{
|
||||
Logger.i(TAG, "Copy URL action received from notification");
|
||||
String shareUrl = LocationSharingManager.getInstance().getShareUrl();
|
||||
if (shareUrl != null)
|
||||
{
|
||||
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
|
||||
android.content.ClipData clip = android.content.ClipData.newPlainText("Location Share URL", shareUrl);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
android.widget.Toast.makeText(this, R.string.location_sharing_url_copied, android.widget.Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
// Extract session info
|
||||
mSessionId = intent.getStringExtra(EXTRA_SESSION_ID);
|
||||
mEncryptionKey = intent.getStringExtra(EXTRA_ENCRYPTION_KEY);
|
||||
mServerUrl = intent.getStringExtra(EXTRA_SERVER_URL);
|
||||
mUpdateIntervalSeconds = intent.getIntExtra(EXTRA_UPDATE_INTERVAL, 20);
|
||||
|
||||
if (mSessionId == null || mEncryptionKey == null || mServerUrl == null)
|
||||
{
|
||||
Logger.e(TAG, "Missing session info, stopping service");
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// Initialize API client
|
||||
mApiClient = new LocationSharingApiClient(mServerUrl, mSessionId);
|
||||
|
||||
// Create session on server
|
||||
mApiClient.createSession(new LocationSharingApiClient.Callback()
|
||||
{
|
||||
@Override
|
||||
public void onSuccess()
|
||||
{
|
||||
Logger.i(TAG, "Session created on server");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull String error)
|
||||
{
|
||||
Logger.w(TAG, "Failed to create session on server: " + error);
|
||||
}
|
||||
});
|
||||
|
||||
// Start foreground with notification
|
||||
Notification notification = mNotificationHelper != null
|
||||
? mNotificationHelper.buildNotification(getStopIntent(), getCopyUrlIntent())
|
||||
: buildFallbackNotification();
|
||||
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
|
||||
// Register for location updates
|
||||
LocationHelper locationHelper = MwmApplication.sInstance.getLocationHelper();
|
||||
locationHelper.addListener(this);
|
||||
|
||||
Logger.i(TAG, "Service started for session: " + mSessionId);
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy()
|
||||
{
|
||||
Logger.i(TAG, "Service destroyed");
|
||||
|
||||
// Unregister location listener
|
||||
LocationHelper locationHelper = MwmApplication.sInstance.getLocationHelper();
|
||||
locationHelper.removeListener(this);
|
||||
|
||||
// Cancel pending updates
|
||||
mHandler.removeCallbacks(mUpdateTask);
|
||||
|
||||
// Send session end to server (optional)
|
||||
if (mApiClient != null && mSessionId != null)
|
||||
mApiClient.endSession();
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent)
|
||||
{
|
||||
return null; // Not a bound service
|
||||
}
|
||||
|
||||
// LocationHelper.LocationListener implementation
|
||||
|
||||
@Override
|
||||
public void onLocationUpdated(@NonNull Location location)
|
||||
{
|
||||
mLastLocation = location;
|
||||
|
||||
// No need to update notification - it's simple and static now
|
||||
|
||||
// Schedule update if needed
|
||||
scheduleUpdate();
|
||||
}
|
||||
|
||||
|
||||
// Private methods
|
||||
|
||||
private void scheduleUpdate()
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
long timeSinceLastUpdate = (now - mLastUpdateTimestamp) / 1000; // Convert to seconds
|
||||
|
||||
if (timeSinceLastUpdate >= mUpdateIntervalSeconds)
|
||||
{
|
||||
// Remove any pending updates
|
||||
mHandler.removeCallbacks(mUpdateTask);
|
||||
// Execute immediately
|
||||
mHandler.post(mUpdateTask);
|
||||
}
|
||||
}
|
||||
|
||||
private void processLocationUpdate()
|
||||
{
|
||||
if (mLastLocation == null || mEncryptionKey == null || mApiClient == null)
|
||||
return;
|
||||
|
||||
// Check battery level
|
||||
int batteryLevel = getBatteryLevel();
|
||||
if (batteryLevel < 10)
|
||||
{
|
||||
Logger.w(TAG, "Battery level too low (" + batteryLevel + "%), stopping sharing");
|
||||
LocationSharingManager.getInstance().stopSharing();
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
|
||||
// Build payload JSON
|
||||
JSONObject payload = buildPayloadJson(mLastLocation, batteryLevel);
|
||||
if (payload == null)
|
||||
return;
|
||||
|
||||
// Encrypt payload
|
||||
String encryptedJson = LocationCrypto.encrypt(mEncryptionKey, payload.toString());
|
||||
if (encryptedJson == null)
|
||||
{
|
||||
Logger.e(TAG, "Failed to encrypt payload");
|
||||
return;
|
||||
}
|
||||
|
||||
// Send to server
|
||||
mApiClient.updateLocation(encryptedJson, new LocationSharingApiClient.Callback()
|
||||
{
|
||||
@Override
|
||||
public void onSuccess()
|
||||
{
|
||||
Logger.d(TAG, "Location update sent successfully");
|
||||
mLastUpdateTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull String error)
|
||||
{
|
||||
Logger.w(TAG, "Failed to send location update: " + error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private JSONObject buildPayloadJson(@NonNull Location location, int batteryLevel)
|
||||
{
|
||||
try
|
||||
{
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("timestamp", System.currentTimeMillis() / 1000); // Unix timestamp
|
||||
json.put("lat", location.getLatitude());
|
||||
json.put("lon", location.getLongitude());
|
||||
json.put("accuracy", location.getAccuracy());
|
||||
|
||||
if (location.hasSpeed())
|
||||
json.put("speed", location.getSpeed());
|
||||
|
||||
if (location.hasBearing())
|
||||
json.put("bearing", location.getBearing());
|
||||
|
||||
// Check if navigating
|
||||
RoutingInfo routingInfo = getNavigationInfo();
|
||||
if (routingInfo != null && routingInfo.distToTarget != null)
|
||||
{
|
||||
json.put("mode", "navigation");
|
||||
|
||||
// Calculate ETA (current time + time remaining)
|
||||
if (routingInfo.totalTimeInSeconds > 0)
|
||||
{
|
||||
long etaTimestamp = (System.currentTimeMillis() / 1000) + routingInfo.totalTimeInSeconds;
|
||||
json.put("eta", etaTimestamp);
|
||||
}
|
||||
|
||||
// Distance remaining in meters
|
||||
if (routingInfo.distToTarget != null)
|
||||
{
|
||||
json.put("distanceRemaining", routingInfo.distToTarget.mDistance);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
json.put("mode", "standalone");
|
||||
}
|
||||
|
||||
json.put("batteryLevel", batteryLevel);
|
||||
|
||||
return json;
|
||||
}
|
||||
catch (JSONException e)
|
||||
{
|
||||
Logger.e(TAG, "Failed to build payload JSON", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private RoutingInfo getNavigationInfo()
|
||||
{
|
||||
if (!RoutingController.get().isNavigating())
|
||||
return null;
|
||||
|
||||
return RoutingController.get().getCachedRoutingInfo();
|
||||
}
|
||||
|
||||
private int getBatteryLevel()
|
||||
{
|
||||
BatteryManager bm = (BatteryManager) getSystemService(BATTERY_SERVICE);
|
||||
if (bm == null)
|
||||
return 100;
|
||||
|
||||
return bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private PendingIntent getStopIntent()
|
||||
{
|
||||
Intent stopIntent = new Intent(this, LocationSharingService.class);
|
||||
stopIntent.setAction(ACTION_STOP);
|
||||
return PendingIntent.getService(this, 0, stopIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private PendingIntent getCopyUrlIntent()
|
||||
{
|
||||
Intent copyIntent = new Intent(this, LocationSharingService.class);
|
||||
copyIntent.setAction(ACTION_COPY_URL);
|
||||
return PendingIntent.getService(this, 1, copyIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Notification buildFallbackNotification()
|
||||
{
|
||||
Intent notificationIntent = new Intent(this, MwmActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
return new NotificationCompat.Builder(this, LocationSharingNotification.CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.location_sharing_active))
|
||||
.setSmallIcon(R.drawable.ic_share)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -296,7 +296,7 @@ public class MapButtonsController extends Fragment
|
||||
mBadgeDrawable.setMaxCharacterCount(0);
|
||||
mBadgeDrawable.setHorizontalOffset(verticalOffset);
|
||||
mBadgeDrawable.setVerticalOffset(dpToPx(9, context));
|
||||
mBadgeDrawable.setBackgroundColor(ContextCompat.getColor(context, R.color.base_accent));
|
||||
mBadgeDrawable.setBackgroundColor(ContextCompat.getColor(context, R.color.active_track_recording));
|
||||
mBadgeDrawable.setVisible(enable);
|
||||
BadgeUtils.attachBadgeDrawable(mBadgeDrawable, menuButton);
|
||||
}
|
||||
@@ -322,7 +322,8 @@ public class MapButtonsController extends Fragment
|
||||
mBadgeDrawable.setVisible(count > 0);
|
||||
BadgeUtils.attachBadgeDrawable(mBadgeDrawable, menuButton);
|
||||
|
||||
updateMenuBadge(TrackRecorder.nativeIsTrackRecordingEnabled());
|
||||
final boolean isTrackRecording = TrackRecorder.nativeIsTrackRecordingEnabled();
|
||||
updateMenuBadge(isTrackRecording);
|
||||
}
|
||||
|
||||
public void updateLayerButton()
|
||||
|
||||
@@ -16,6 +16,7 @@ public class MapButtonsViewModel extends ViewModel
|
||||
private final MutableLiveData<SearchWheel.SearchOption> mSearchOption = new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> mTrackRecorderState =
|
||||
new MutableLiveData<>(TrackRecorder.nativeIsTrackRecordingEnabled());
|
||||
private final MutableLiveData<Boolean> mLocationSharingState = new MutableLiveData<>(false);
|
||||
|
||||
public MutableLiveData<Boolean> getButtonsHidden()
|
||||
{
|
||||
@@ -86,4 +87,14 @@ public class MapButtonsViewModel extends ViewModel
|
||||
{
|
||||
return mTrackRecorderState;
|
||||
}
|
||||
|
||||
public void setLocationSharingState(boolean state)
|
||||
{
|
||||
mLocationSharingState.setValue(state);
|
||||
}
|
||||
|
||||
public MutableLiveData<Boolean> getLocationSharingState()
|
||||
{
|
||||
return mLocationSharingState;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +210,11 @@ public class NavigationController implements TrafficManager.TrafficCallback, Nav
|
||||
mNavMenu.refreshTts();
|
||||
}
|
||||
|
||||
public void refreshShareLocationColor()
|
||||
{
|
||||
mNavMenu.updateShareLocationColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnabled()
|
||||
{
|
||||
|
||||
@@ -26,8 +26,10 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import app.organicmaps.MwmActivity;
|
||||
import app.organicmaps.MwmApplication;
|
||||
import app.organicmaps.R;
|
||||
import app.organicmaps.location.LocationSharingDialog;
|
||||
import app.organicmaps.sdk.Framework;
|
||||
import app.organicmaps.sdk.bookmarks.data.DistanceAndAzimut;
|
||||
import app.organicmaps.sdk.routing.RouteMarkData;
|
||||
@@ -144,6 +146,9 @@ final class RoutingBottomMenuController implements View.OnClickListener
|
||||
mActionButton.setOnClickListener(this);
|
||||
View actionSearchButton = actionFrame.findViewById(R.id.btn__search_point);
|
||||
actionSearchButton.setOnClickListener(this);
|
||||
View shareLocationButton = actionFrame.findViewById(R.id.btn__share_location);
|
||||
if (shareLocationButton != null)
|
||||
shareLocationButton.setOnClickListener(this);
|
||||
mActionIcon = mActionButton.findViewById(R.id.iv__icon);
|
||||
UiUtils.hide(mAltitudeChartFrame, mActionFrame);
|
||||
mListener = listener;
|
||||
@@ -472,6 +477,11 @@ final class RoutingBottomMenuController implements View.OnClickListener
|
||||
final RouteMarkType pointType = (RouteMarkType) mActionMessage.getTag();
|
||||
mListener.onSearchRoutePoint(pointType);
|
||||
}
|
||||
else if (id == R.id.btn__share_location)
|
||||
{
|
||||
if (mContext instanceof MwmActivity)
|
||||
LocationSharingDialog.show(((MwmActivity) mContext).getSupportFragmentManager());
|
||||
}
|
||||
else if (id == R.id.btn__manage_route)
|
||||
mListener.onManageRouteOpen();
|
||||
else if (id == R.id.btn__save)
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.EditTextPreference;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
@@ -82,6 +83,7 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La
|
||||
initShowOnLockScreenPrefsCallbacks();
|
||||
initLeftButtonPrefs();
|
||||
initCustomMapDownloadUrlPrefsCallbacks();
|
||||
initLocationSharingPrefsCallbacks();
|
||||
}
|
||||
|
||||
private void initLeftButtonPrefs()
|
||||
@@ -584,6 +586,29 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La
|
||||
category.removePreference(preference);
|
||||
}
|
||||
|
||||
private void initLocationSharingPrefsCallbacks()
|
||||
{
|
||||
// Server URL preference
|
||||
final EditTextPreference serverUrlPref = getPreference(getString(R.string.pref_location_sharing_server_url));
|
||||
serverUrlPref.setText(Config.LocationSharing.getServerUrl());
|
||||
serverUrlPref.setSummary(Config.LocationSharing.getServerUrl());
|
||||
serverUrlPref.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
String url = (String) newValue;
|
||||
Config.LocationSharing.setServerUrl(url);
|
||||
serverUrlPref.setSummary(url);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Update interval preference
|
||||
final ListPreference intervalPref = getPreference(getString(R.string.pref_location_sharing_update_interval));
|
||||
intervalPref.setValue(String.valueOf(Config.LocationSharing.getUpdateInterval()));
|
||||
intervalPref.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
int seconds = Integer.parseInt((String) newValue);
|
||||
Config.LocationSharing.setUpdateInterval(seconds);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLanguageSelected(Language language)
|
||||
{
|
||||
|
||||
@@ -65,8 +65,6 @@ public class MenuAdapter extends RecyclerView.Adapter<MenuAdapter.ViewHolder>
|
||||
iv.setImageResource(R.drawable.ic_track_recording_on);
|
||||
iv.setImageTintMode(null);
|
||||
viewHolder.getTitleTextView().setText(R.string.stop_track_recording);
|
||||
badge.setBackgroundResource(R.drawable.track_recorder_badge);
|
||||
badge.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import app.organicmaps.R;
|
||||
import app.organicmaps.location.LocationSharingDialog;
|
||||
import app.organicmaps.sdk.routing.RoutingInfo;
|
||||
import app.organicmaps.sdk.sound.TtsPlayer;
|
||||
import app.organicmaps.sdk.util.DateUtils;
|
||||
@@ -26,6 +27,7 @@ public class NavMenu
|
||||
private final View mHeaderFrame;
|
||||
|
||||
private final ShapeableImageView mTts;
|
||||
private final ShapeableImageView mShareLocation;
|
||||
private final MaterialTextView mEtaValue;
|
||||
private final MaterialTextView mEtaAmPm;
|
||||
private final MaterialTextView mTimeHourValue;
|
||||
@@ -97,12 +99,16 @@ public class NavMenu
|
||||
mRouteProgress = bottomFrame.findViewById(R.id.navigation_progress);
|
||||
|
||||
// Bottom frame buttons
|
||||
mShareLocation = bottomFrame.findViewById(R.id.share_location);
|
||||
mShareLocation.setOnClickListener(v -> onShareLocationClicked());
|
||||
ShapeableImageView mSettings = bottomFrame.findViewById(R.id.settings);
|
||||
mSettings.setOnClickListener(v -> onSettingsClicked());
|
||||
mTts = bottomFrame.findViewById(R.id.tts_volume);
|
||||
mTts.setOnClickListener(v -> onTtsClicked());
|
||||
MaterialButton stop = bottomFrame.findViewById(R.id.stop);
|
||||
stop.setOnClickListener(v -> onStopClicked());
|
||||
|
||||
updateShareLocationColor();
|
||||
}
|
||||
|
||||
private void onStopClicked()
|
||||
@@ -110,6 +116,22 @@ public class NavMenu
|
||||
mNavMenuListener.onStopClicked();
|
||||
}
|
||||
|
||||
private void onShareLocationClicked()
|
||||
{
|
||||
LocationSharingDialog.show(mActivity.getSupportFragmentManager());
|
||||
// Update color after dialog is shown (in case state changes)
|
||||
mShareLocation.postDelayed(this::updateShareLocationColor, 500);
|
||||
}
|
||||
|
||||
public void updateShareLocationColor()
|
||||
{
|
||||
final boolean isLocationSharing = app.organicmaps.location.LocationSharingManager.getInstance().isSharing();
|
||||
final int color = isLocationSharing
|
||||
? androidx.core.content.ContextCompat.getColor(mActivity, R.color.active_location_sharing)
|
||||
: app.organicmaps.util.ThemeUtils.getColor(mActivity, R.attr.iconTint);
|
||||
mShareLocation.setImageTintList(android.content.res.ColorStateList.valueOf(color));
|
||||
}
|
||||
|
||||
private void onSettingsClicked()
|
||||
{
|
||||
mNavMenuListener.onSettingsClicked();
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#51585E" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M20,8 C20.82725,8 21.5,8.67275 21.5,9.5 L21.5,18.5 C21.5,19.32725 20.82725,20 20,20 L8,20 C7.17275,20 6.5,19.32725 6.5,18.5 L6.5,9.5 C6.5,8.67275 7.17275,8 8,8 L20,8 Z M20.00075,14 L8,14 L8,18.5 L20.0015,18.5 L20.00075,14 Z M14,15.5 L14,17 L9.5,17 L9.5,15.5 L14,15.5 Z M20,9.5 L8,9.5 L8,11 L20,11 L20,9.5 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#51585E" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M7,9.53073333 L14,6.3 L21,9.53073333 L21,10.60766 L19.9230733,10.60766 L19.9230733,18.68461 L21,18.68461 L21,20.2999767 L7,20.2999767 L7,18.68461 L8.07692667,18.68461 L8.07692667,10.60766 L7,10.60766 L7,9.53073333 Z M16.6923167,15.2756333 C16.6923167,14.0852833 15.4814333,13.1208 13.9976667,13.1208 C13.50468,13.1208 13.09784,12.7993017 13.09784,12.4012583 C13.09784,12.003215 13.4998967,11.6817167 13.9976667,11.6817167 C14.4954483,11.6817167 14.8974933,12.0070417 14.8974933,12.4012583 L16.69241,12.4012583 C16.69241,11.4673883 15.94096,10.6789083 14.8974933,10.3803583 L14.8974933,9.53068667 L13.1025767,9.53068667 L13.1025767,10.3803583 C12.0591567,10.6788967 11.30766,11.46733 11.30766,12.4012583 C11.30766,13.5916083 12.5185433,14.5560917 14.00231,14.5560917 C14.5000917,14.5560917 14.9021367,14.87759 14.9021367,15.2756333 C14.9021367,15.6736767 14.50008,15.995175 14.00231,15.995175 C13.5045283,15.995175 13.1024833,15.66985 13.1024833,15.2756333 L11.3075667,15.2756333 C11.3075667,16.2095033 12.0590167,16.9979833 13.1024833,17.2965333 L13.1024833,18.146205 L14.8974,18.146205 L14.8974,17.2965333 C15.94082,16.997995 16.6923167,16.2095617 16.6923167,15.2756333 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#802D19" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:pathData="M10.7398398,14.5988166 C11.2766539,14.5988166 11.7741396,14.7668787 12.1823664,15.0532612 C11.7724801,15.4914625 11.5235582,16.0775925 11.5235582,16.7240712 L11.529303,16.8892426 L11.5491056,17.0705775 C11.6012609,17.429281 11.7280184,17.7632492 11.9167798,18.0556079 L11.9546499,18.1104694 L11.9142567,18.13405 C11.1417478,18.5947243 10.6376499,19.4380664 10.6376499,20.3887564 L10.637,21.073 L5.952,21.073 L5.95232411,17.1079171 C5.95232411,15.7221656 7.06098553,14.5988166 8.46443574,14.5988166 L10.7398398,14.5988166 Z M14.7363802,18.7647355 C15.6343829,18.7647355 16.3623501,19.4913492 16.3623501,20.3887564 L16.362,21.073 L11.637,21.073 L11.6376499,20.3887564 C11.6376499,19.4918257 12.3552335,18.7647355 13.2636198,18.7647355 L14.7363802,18.7647355 Z M19.4916069,12.4068343 C20.8792516,12.4068343 22.0041478,13.5296391 22.0041478,14.9163636 L22.004,21.073 L17.362,21.073 L17.3623501,20.3887564 L17.3567032,20.2194334 L17.3385166,20.043376 C17.2273964,19.2299207 16.7526866,18.5346787 16.0784713,18.1328785 L16.0416499,18.1124694 L16.0806774,18.0575117 C16.336328,17.665886 16.4764418,17.2070581 16.4764418,16.7240712 C16.4764418,15.6254025 15.757471,14.7010059 14.7609634,14.3838791 C15.0006762,13.2537372 15.9971232,12.4068343 17.215814,12.4068343 L19.4916069,12.4068343 Z M14,15.2664694 C14.8194057,15.2664694 15.4764418,15.9151249 15.4764418,16.7240712 C15.4764418,17.5330608 14.819449,18.1816731 14,18.1816731 C13.1805943,18.1816731 12.5235582,17.5330175 12.5235582,16.7240712 C12.5235582,15.9150816 13.1805943,15.2664694 14,15.2664694 Z M9.60213775,9.19402109 C10.8681135,9.19402109 11.8832295,10.1961892 11.8832295,11.4460053 C11.8832295,12.6958883 10.8681805,13.6979895 9.60213775,13.6979895 C8.33616197,13.6979895 7.32104605,12.6958214 7.32104605,11.4460053 C7.32104605,10.1961223 8.33616197,9.19402109 9.60213775,9.19402109 Z M18.3537105,7.00111521 C19.6199026,7.00111521 20.635192,8.00345456 20.635192,9.25348422 C20.635192,10.5035808 19.6199695,11.5058532 18.3537105,11.5058532 C17.0875184,11.5058532 16.072229,10.5035139 16.072229,9.25348422 C16.072229,8.00338763 17.0875184,7.00111521 18.3537105,7.00111521 Z"
|
||||
android:fillColor="#000" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#8C491C" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:pathData="M12.5416667,11.1416783 L12.5416667,7.70257833 C12.5416667,7.31455667 12.232395,7 11.8466717,7 L11.7783342,7 C11.3945008,7 11.0833392,7.32498667 11.0833392,7.70257833 L11.0833392,11.1416783 L9.62500583,11.1416783 L9.62500583,7.70257833 C9.62500583,7.31455667 9.31573417,7 8.93001083,7 L8.86167333,7 C8.47784,7 8.16667833,7.32498667 8.16667833,7.70257833 L8.16667833,11.1416783 C8.16667833,12.749345 9.62501167,14.0000117 11.0104283,14.0000117 L10.8211833,20.055945 C10.8048897,20.5773283 11.2048417,21 11.7169383,21 L11.9080967,21 C12.4190967,21 12.815985,20.58308 12.7940167,20.055945 L12.54169,14.0000117 C14.0000233,14.0000117 15.7500233,12.749345 15.4583567,11.1416783 L15.4583567,7.70257833 C15.4583567,7.31455667 15.149085,7 14.7633617,7 L14.6950242,7 C14.3111908,7 14.0000292,7.32498667 14.0000292,7.70257833 L14.0000292,11.1416783 L12.5416958,11.1416783 L12.5416667,11.1416783 Z M16.3333333,14.933345 L18.0833333,14.933345 L18.0833333,20.130845 C18.0833333,20.6108933 18.4717167,21.0000467 18.9583333,21.0000467 C19.4415783,21.0000467 19.8333333,20.6109983 19.8333333,20.1242883 L19.8333333,7.000455 C17.3903333,7.000455 16.275,8.69912167 16.3333333,10.0337883 L16.3333333,14.9337883 L16.3333333,14.933345 Z"
|
||||
android:fillColor="#000" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#802D19" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:pathData="M14.8166667,12.2166667 L14.8166667,17.525 C14.8166667,20.0076667 12.8076667,22.0166667 10.325,22.0166667 C7.84233333,22.0166667 5.83333333,20.0076667 5.83333333,17.525 L5.83333333,12.2166667 L14.8166667,12.2166667 Z M12.3666667,17.9333333 L8.28333333,17.9333333 C8.28333333,18.6111667 9.198,19.1583333 10.325,19.1583333 C11.452,19.1583333 12.3666667,18.6111667 12.3666667,17.9333333 Z M22.1666667,6.5 L22.1666667,11.8083333 C22.1666667,14.291 20.1576667,16.3 17.675,16.3 C16.94,16.3 16.2458333,16.1121667 15.6333333,15.8018333 L15.6333333,11.4 L13.1833333,11.4 L13.1833333,6.5 L22.1666667,6.5 Z M12.3666667,14.6666667 C11.9175,14.6666667 11.55,15.0341667 11.55,15.4833333 C11.55,15.9325 11.9175,16.3 12.3666667,16.3 C12.8158333,16.3 13.1833333,15.9325 13.1833333,15.4833333 C13.1833333,15.0341667 12.8158333,14.6666667 12.3666667,14.6666667 Z M8.28333333,14.6666667 C7.83416667,14.6666667 7.46666667,15.0341667 7.46666667,15.4833333 C7.46666667,15.9325 7.83416667,16.3 8.28333333,16.3 C8.7325,16.3 9.1,15.9325 9.1,15.4833333 C9.1,15.0341667 8.7325,14.6666667 8.28333333,14.6666667 Z M17.675,12.2166667 C16.548,12.2166667 15.6333333,12.7638333 15.6333333,13.4416667 L19.7166667,13.4416667 C19.7166667,12.7638333 18.802,12.2166667 17.675,12.2166667 Z M15.6333333,9.01533333 C15.1841667,9.01533333 14.8166667,9.38283333 14.8166667,9.832 C14.8166667,10.2811667 15.1841667,10.6486667 15.6333333,10.6486667 C16.0825,10.6486667 16.45,10.2893333 16.45,9.832 C16.45,9.38283333 16.0825,9.01533333 15.6333333,9.01533333 Z M19.7166667,9.01533333 C19.2675,9.01533333 18.9,9.38283333 18.9,9.832 C18.9,10.2811667 19.2675,10.6486667 19.7166667,10.6486667 C20.1658333,10.6486667 20.5333333,10.2893333 20.5333333,9.832 C20.5333333,9.38283333 20.1658333,9.01533333 19.7166667,9.01533333 Z"
|
||||
android:fillColor="#000" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#6B425C" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M11.3735667,18.307716 C10.6313217,18.307716 10.0307333,18.9134843 10.0307333,19.653816 C10.0307333,20.394206 10.631275,20.999916 11.3735667,20.999916 C12.1158117,20.999916 12.72305,20.3941477 12.72305,19.653816 C12.72305,18.913426 12.115765,18.307716 11.3735667,18.307716 Z M6.8,7.48804933 C6.8,7.813281 7.06993167,8.07692433 7.38562,8.07692433 L8.2,8.07692433 L10.3,14.5385077 C9.55654167,14.5385077 8.9539,15.3205827 8.9539,16.286291 L8.9539,16.0215277 C8.9539,16.9867927 9.739055,17.769311 10.7023833,17.769311 L18.8085,17.769311 C19.1352133,17.769311 19.4000583,17.5195393 19.4000583,17.180436 L19.4000583,17.2812663 C19.4000583,16.9560347 19.129765,16.6923913 18.8139717,16.6923913 L10.6169717,16.6923913 C10.29328,16.6923913 10.030885,16.4426197 10.030885,16.1035163 L10.030885,16.2043467 C10.030885,15.879115 10.2980983,15.6154717 10.6084667,15.6154717 L18.0620667,15.6154717 C19.2169033,15.6154717 19.113315,15.3197567 19.2870667,14.969325 L20.7161167,10.1360583 C20.7721167,10.0380583 20.8001167,9.91905833 20.8001167,9.80005833 C20.8001167,9.41505833 20.4851167,9.10005833 20.2616533,9.1539 L9.74707,9.1539 L8.90007,7 L7.37395333,7 C7.05702833,7 6.8,7.24977167 6.8,7.588875 L6.8,7.48804467 L6.8,7.48804933 Z M17.8350333,18.307716 C17.0927883,18.307716 16.4922,18.9134843 16.4922,19.653816 C16.4922,20.394206 17.0927417,20.999916 17.8350333,20.999916 C18.577325,20.999916 19.1845167,20.3941477 19.1845167,19.653816 C19.1845167,18.913426 18.5772317,18.307716 17.8350333,18.307716 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#574469" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M17.846514,4.676 C17.5575662,4.7216586 17.3186367,4.92551065 17.2280519,5.20366514 C17.1374671,5.48181964 17.2105329,5.78727723 17.4171807,5.99433333 L19.428514,8.00566667 C19.799514,8.37666667 20.062014,8.83516667 20.1973474,9.33333333 L19.523014,9.33333333 C18.2431807,9.33333333 17.1896807,10.3868333 17.1896807,11.6666667 C17.1896807,12.9465 18.2431807,14 19.523014,14 L20.300007,14 L20.300007,19.4448333 C20.300007,19.8835 19.9616807,20.2218333 19.523014,20.2218333 C19.315878,20.2250062 19.1162779,20.1442015 18.9696836,19.997827 C18.8230893,19.8514525 18.7419852,19.6519738 18.7448474,19.4448333 L18.7448474,17.8885 C18.7448474,16.6098333 17.6913474,15.5551667 16.411514,15.5551667 L15.6333474,15.5551667 L15.6333474,7.77816667 C15.6345903,7.36522389 15.4711795,6.96881891 15.1792931,6.67671349 C14.8874066,6.38460807 14.4911243,6.22090001 14.0781807,6.22183333 L7.85634735,6.22183333 C7.44320185,6.22058891 7.04661893,6.38415963 6.75447962,6.67629894 C6.46234031,6.96843825 6.2987696,7.36502117 6.30000697,7.77816667 L6.30000697,20.2218333 C6.2987696,20.6349788 6.46234031,21.0315618 6.75447962,21.3237011 C7.04661893,21.6158404 7.44320185,21.7794111 7.85634735,21.7781737 L14.0781807,21.7781737 C14.4911243,21.7791 14.8874066,21.6153919 15.1792931,21.3232865 C15.4711795,21.0311811 15.6345903,20.6347761 15.6333474,20.2218333 L15.6333474,17.1115 L16.411514,17.1115 C16.8501807,17.1115 17.1896807,17.4498333 17.1896807,17.8885 L17.1896807,19.4448333 C17.1896807,20.7235 18.2431807,21.7781737 19.523014,21.7781737 C20.8016807,21.7781737 21.8563474,20.7235 21.8563474,19.4448333 L21.8563474,10.1115 C21.8563474,8.90940677 21.3788136,7.75655029 20.5286807,6.90666667 L18.5173474,4.89416667 C18.3411731,4.71808596 18.0913541,4.63698153 17.8453474,4.676 L17.846514,4.676 Z M8.63218069,7.77816667 L13.2988474,7.77816667 C13.730514,7.77816667 14.077014,8.12466667 14.077014,8.55516667 L14.077014,12.4448333 L7.85634735,12.4448333 L7.85634735,8.55516667 C7.85634735,8.12466667 8.20284735,7.77816667 8.63334735,7.77816667 L8.63218069,7.77816667 Z M19.523014,10.5 C20.1673462,10.5 20.6896807,11.0223345 20.6896807,11.6666667 C20.6896807,12.3109989 20.1673462,12.8333333 19.523014,12.8333333 C18.8786818,12.8333333 18.3563474,12.3109989 18.3563474,11.6666667 C18.3563474,11.0223345 18.8786818,10.5 19.523014,10.5 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#983E44" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M21.1291111,17.1111111 L17.1106444,17.1111111 L17.1106444,21.1295778 C17.1106444,21.9138422 16.4689778,22.5555089 15.6847133,22.5555089 L12.3142911,22.5555089 C11.5300267,22.5555089 10.88836,21.9138422 10.88836,21.1295778 L10.88836,17.1111111 L6.86973778,17.1111111 C6.08547333,17.1111111 5.44383778,16.4694444 5.44383778,15.68518 L5.44383778,12.3147578 C5.44383778,11.5304933 6.08550444,10.8888267 6.86973778,10.8888267 L10.88836,10.8888267 L10.88836,6.87036 C10.88836,6.08609556 11.5300267,5.44442889 12.3142911,5.44442889 L15.6847133,5.44442889 C16.4689778,5.44442889 17.1106444,6.08609556 17.1106444,6.87036 L17.1106444,10.8888267 L21.1291111,10.8888267 C21.9133756,10.8888267 22.5550422,11.5304933 22.5550422,12.3147578 L22.5550422,15.68518 C22.5550422,16.4694444 21.9133756,17.1111111 21.1291111,17.1111111 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#614A43" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M10.2879,14.1061667 C11.5203667,14.1061667 12.5151833,13.111315 12.5151833,11.8788833 C12.5151833,10.6464167 11.5203317,9.6516 10.2879,9.6516 C9.05543333,9.6516 8.06061667,10.6464517 8.06061667,11.8788833 C8.06061667,13.11135 9.05546833,14.1061667 10.2879,14.1061667 Z M19.1970333,9.6516 L14.7424667,9.6516 C13.9258,9.6516 13.25765,10.319785 13.25765,11.1364167 L13.25765,14.8485167 L7.31826667,14.8485167 L7.31826667,8.90913333 C7.31826667,8.5008 6.98418,8.16671333 6.57584667,8.16671333 C6.16751333,8.16671333 5.83342667,8.5008 5.83342667,8.90913333 L5.83342667,18.5606167 C5.83342667,18.96895 6.16751333,19.3030367 6.57584667,19.3030367 C6.98418,19.3030367 7.31826667,18.96895 7.31826667,18.5606167 L7.31826667,17.0758 L20.6824333,17.0758 L20.6824333,18.5606167 C20.6824333,18.96895 21.01652,19.3030367 21.4248533,19.3030367 C21.8331867,19.3030367 22.1672733,18.96895 22.1672733,18.5606167 L22.1672733,12.6212333 C22.1672733,10.9804333 20.8383233,9.65148333 19.1975233,9.65148333 L19.1970333,9.6516 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,28 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#851F03" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:alpha="0.5"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12"
|
||||
android:scaleX="0.5"
|
||||
android:scaleY="0.5">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M8.5714,5.1429l6.8571,0l0,-1.7143l-6.8571,0ZM24,13.7143l0,6.4286c0,0.5893 -0.2098,1.0938 -0.6295,1.5134 -0.4196,0.4196 -0.9241,0.6295 -1.5134,0.6295l-19.7143,0c-0.5893,0 -1.0938,-0.2098 -1.5134,-0.6295 -0.4196,-0.4196 -0.6295,-0.9241 -0.6295,-1.5134l0,-6.4286l9,0l0,2.1429c0,0.2321 0.0848,0.433 0.2545,0.6027 0.1696,0.1696 0.3705,0.2545 0.6027,0.2545l4.2857,0c0.2321,0 0.433,-0.0848 0.6027,-0.2545 0.1696,-0.1696 0.2545,-0.3705 0.2545,-0.6027l0,-2.1429ZM13.7143,13.7143l0,1.7143l-3.4286,0l0,-1.7143ZM24,7.2857l0,5.1429l-24,0l0,-5.1429c0,-0.5893 0.2098,-1.0938 0.6295,-1.5134 0.4196,-0.4196 0.9241,-0.6295 1.5134,-0.6295l4.7143,0l0,-2.1429c0,-0.3571 0.125,-0.6607 0.375,-0.9107 0.25,-0.25 0.5536,-0.375 0.9107,-0.375l7.7143,0c0.3571,0 0.6607,0.125 0.9107,0.375 0.25,0.25 0.375,0.5536 0.375,0.9107l0,2.1429l4.7143,0c0.5893,0 1.0938,0.2098 1.5134,0.6295 0.4196,0.4196 0.6295,0.9241 0.6295,1.5134Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,26 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#8C491C" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M21,8.42778167 C21,7.80556333 20.4944483,7.3 19.8722183,7.3 L8.127385,7.3 C7.50516667,7.3 6.99960333,7.80555167 6.99960333,8.42778167 C6.99960333,8.7 7.10071483,8.97223 7.287385,9.17444833 L13.2218683,15.8555983 L13.2218683,19.7444483 L10.1107183,19.7444483 C9.68293667,19.7444483 9.33293667,20.0944483 9.33293667,20.52223 C9.33293667,20.9500117 9.68293667,21.3000117 10.1107183,21.3000117 L17.888535,21.3000117 C18.3163167,21.3000117 18.6663167,20.9500117 18.6663167,20.52223 C18.6663167,20.0944483 18.3163167,19.7444483 17.888535,19.7444483 L14.777385,19.7444483 L14.777385,15.8555983 L20.7118683,9.17444833 C20.898535,8.97223 20.99965,8.7 20.99965,8.42778167 L21,8.42778167 Z M10.4455167,10.411115 L9.06885,8.85559833 L18.93885,8.85559833 L17.5543667,10.411115 L10.4455167,10.411115 L10.4455167,10.411115 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#20607C" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M9.35,22.3 L13.1840042,22.3 L13.1840042,16.379733 L14.4487167,16.379733 C16.4953333,16.379733 18.0936417,15.9233039 19.1939042,14.9897598 C20.3178375,14.0448193 20.85,12.6687324 20.85,10.8925278 C20.85,9.17330503 20.3522033,7.86253173 19.3111083,6.99727004 C18.2581875,6.12060241 16.709425,5.7 14.6982667,5.7 L9.3502875,5.7002873 L9.35,22.3 Z M13.1840042,8.60425301 L14.5565292,8.60425301 C15.5029792,8.60425301 16.17755,8.78662355 16.6154125,9.17371683 C17.0649667,9.57220647 17.2884404,10.1637639 17.2884404,10.9949323 C17.2884404,11.8032888 17.0518663,12.4290544 16.5076479,12.8616374 C15.9752746,13.282891 15.1586979,13.5329885 14.0820104,13.5329885 L13.1840042,13.5329885 L13.1840042,8.60425301 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#983E44" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M16.961,6.99999417 C15.9026933,6.99999417 14.84875,7.41071333 14.0726833,8.17973333 L8.2299,13.9907833 C7.02531667,15.1842833 6.69806667,16.9171333 7.28183167,18.3600667 C7.28183167,18.3600667 7.71838667,19.382475 8.14171167,19.80195 C8.54021,20.1968083 9.021355,20.4748717 9.508695,20.675795 C9.64324667,20.7452082 9.744875,20.770512 9.86146,20.80687 C9.877112,20.8116985 9.88986133,20.8240783 9.9055565,20.8287158 C11.3082398,21.2432442 12.8732065,20.907362 13.9403565,19.8019442 L17.4680065,16.3064942 L19.7830232,13.9907775 C21.4057398,12.3828775 21.4057398,9.80501083 19.7830232,8.26711083 C19.0774815,7.42821917 18.0191398,6.99999417 16.9608565,6.99999417 L16.961,6.99999417 Z M16.961,8.39813333 C17.6665417,8.39813333 18.3852667,8.67339667 18.8792333,9.162755 C19.3732,9.65211333 19.6509133,10.364305 19.6509133,11.0633717 C19.6509133,11.76245 19.3731067,12.452755 18.8792333,12.9421717 L16.4760167,15.323455 L12.6658,11.523155 L14.9987833,9.22832167 C15.5632167,8.66905667 16.1849333,8.39813333 16.9611167,8.39813333 L16.961,8.39813333 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#51585E" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M14,5.833275 L7.31815,8.803025 L7.31815,13.2575917 C7.31815,17.378025 10.1690167,21.231175 14,22.166725 C17.8308667,21.2312683 20.68185,17.3781417 20.68185,13.2575917 L20.68185,8.803025 L14,5.833275 L14,5.833275 Z M15.85605,14.438025 L16.524235,17.3186417 L14.000035,15.796725 L11.475835,17.3186417 L12.14402,14.4454917 L9.91673667,12.5225917 L12.8567367,12.2701717 L14.00007,9.560355 L15.1434033,12.2628217 L18.0834033,12.5152417 L15.85612,14.4381417 L15.85605,14.438025 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#51585E" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M20.5333333,7 C21.4316667,7 22.1666667,7.735 22.1666667,8.63333333 L22.1666667,18.4333333 C22.1666667,19.3316667 21.4316667,20.0666667 20.5333333,20.0666667 L7.46666667,20.0666667 C6.56833333,20.0666667 5.83333333,19.3316667 5.83333333,18.4333333 L5.8415,8.63333333 C5.8415,7.735 6.56833333,7 7.46666667,7 L20.5333333,7 Z M20.5333333,9.88283333 C20.5333333,9.33566667 19.9371667,9.009 19.4716667,9.29483333 L14,12.7166667 L8.52833333,9.29483333 C8.06283333,9.009 7.46666667,9.33566667 7.46666667,9.88283333 C7.46666667,10.1196667 7.58916667,10.3401667 7.79333333,10.4708333 L13.5671667,14.0805 C13.8285,14.2438333 14.1715,14.2438333 14.4328333,14.0805 L20.2066667,10.4708333 C20.4108333,10.3401667 20.5333333,10.1196667 20.5333333,9.88283333 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,26 +0,0 @@
|
||||
<layer-list
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#51585E" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M9.26178239,9.86916295 L10.4193833,7.95061481 L11.2531868,6.56634591 C11.5688972,6.04016587 12.3298444,6.04016587 12.6455913,6.56634591 L13.843676,8.5577503 L12.8479738,10.2253086 L12.0384558,11.5367213 L9.26178239,9.86916295 Z M22.1731256,14.5805343 L20.8778628,12.4272676 L18.0769039,14.0462551 L19.8658893,17.0333617 L20.7806361,17.0333617 C21.3958708,17.0333617 21.9544354,16.6852666 22.2296256,16.1348011 C22.3429572,15.9081332 22.3996236,15.665279 22.3996236,15.4143378 C22.3996236,15.1229127 22.3186718,14.8395869 22.1729556,14.5805343 L22.1731256,14.5805343 Z M17.5427461,21.0808911 L18.7570171,21.0808911 C19.3722518,21.0808911 19.9308164,20.732796 20.2060066,20.1823306 L21.379806,17.8427947 L17.5427097,17.8427947 L17.5427097,16.2238072 L14.3046133,19.4619036 L17.5427097,22.7 L17.5427097,21.0810125 L17.5427461,21.0808911 Z M12.6856622,17.8427947 L9.20471166,17.8427947 L8.52471991,18.9842094 C8.28186572,19.3889623 8.26567949,19.8908691 8.47614908,20.3199075 C8.70281704,20.7813304 9.18042624,21.0808547 9.70656985,21.0808547 L12.6855408,21.0808547 L12.6855408,17.8427583 L12.6856622,17.8427947 Z M9.54470753,15.6975422 L10.9451262,16.5394327 L9.83609613,12.0952009 L5.4,13.204231 L6.77613329,14.0299353 L6.44423661,14.5884999 C6.16091076,15.0661091 6.13662534,15.6489592 6.38757022,16.1427668 L7.70711848,18.7817419 L9.54467474,15.6974936 L9.54470753,15.6975422 Z M18.3684504,8.24191842 L17.3160782,6.48523261 C17.0165539,5.99952422 16.4903739,5.7 15.9236736,5.7 L13.0661298,5.7 L15.5918134,9.90951317 L14.1994089,10.7433166 L18.6436406,11.8523467 L19.7526707,7.40811497 L18.3684018,8.24191842 L18.3684504,8.24191842 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#51585E" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M8.33758333,7.43983333 C6.95648333,7.43983333 5.83333333,8.590085 5.83333333,10.0056833 L5.83333333,16.9397667 C5.83333333,18.0495583 6.52384833,18.99765 7.48766667,19.3551167 C7.45779982,19.1965237 7.44027578,19.0357487 7.43526,18.87431 C7.43522971,18.0654358 7.74879599,17.2897014 8.30693531,16.7178646 C8.86507462,16.1460278 9.62203578,15.8249626 10.4111933,15.8253433 C12.0537082,15.8258056 13.3849729,17.1907579 13.38491,18.87431 C13.3830282,19.0874192 13.3593515,19.2997388 13.3142683,19.5077867 L23.3312683,19.5077867 C23.6679333,19.5077867 23.9419483,19.2262233 23.9419483,18.8811467 C23.9419483,18.53607 23.6679217,18.2567933 23.3312683,18.2567933 L21.4741683,18.2567933 C21.6999883,17.87135 21.82964,17.4210517 21.82964,16.9397433 L21.82964,13.3690433 C21.82964,10.09841 19.2349733,7.43983333 16.04414,7.43983333 L8.33772333,7.43983333 L8.33758333,7.43983333 Z M8.88445833,9.60225 L10.9944917,9.60225 C11.3312033,9.60225 11.6051833,9.88155 11.6051833,10.226615 L11.6051833,12.391365 C11.6051833,12.7364417 11.3311567,13.0157183 10.9944917,13.0157183 L8.88445833,13.0157183 C8.54779333,13.0157183 8.27376667,12.7364183 8.27376667,12.391365 L8.27376667,10.226615 C8.27376667,9.88153833 8.54779333,9.60225 8.88445833,9.60225 Z M16.647925,10.684625 L16.8097067,10.684625 C18.2658233,10.684625 19.4484733,11.8992417 19.4484733,13.3916417 L19.4484733,18.2566417 L18.2294233,18.2566417 L18.2294233,13.3916417 C18.2294233,12.5883683 17.5935433,11.9333083 16.8098233,11.9333083 L16.6480417,11.9333083 C15.8643567,11.9333083 15.2284417,12.5883683 15.2284417,13.3916417 L15.2284417,18.2566417 L14.0070583,18.2566417 L14.0070583,13.3916417 C14.0070583,11.8992417 15.191925,10.684625 16.6480417,10.684625 L16.647925,10.684625 Z M10.4111583,17.0581247 C9.94104172,17.0578394 9.49008616,17.2490609 9.15757567,17.5896894 C8.82506518,17.9303179 8.63829167,18.3924263 8.63829167,18.874275 C8.63829167,19.3561237 8.82506518,19.8182321 9.15757567,20.1588606 C9.49008616,20.4994891 9.94104172,20.6907106 10.4111583,20.6904253 C11.3892489,20.6896989 12.1817548,19.8767746 12.1816917,18.874275 C12.1817548,17.8717754 11.3892489,17.0588511 10.4111583,17.0581247 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#6B425C" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M13.9999526,5 C11.853286,5 10.110286,6.743 10.110286,8.88966667 L8.55628597,8.88966667 C7.7007343,8.88966667 7.00683597,9.58815 7.00683597,10.4436667 L7,19.777 C7,20.6325517 7.70075833,21.3333333 8.55633333,21.3333333 L19.4436667,21.3333333 C20.2992183,21.3333333 21,20.632575 21,19.777 L21,10.4436667 C21,9.588115 20.2992417,8.88966667 19.4436667,8.88966667 L17.8896667,8.88966667 C17.8896667,6.743 16.1466667,5 14,5 L13.9999526,5 Z M13.9999526,6.55633333 C15.2911026,6.55633333 16.333286,7.59855167 16.333286,8.88966667 L11.6666193,8.88966667 C11.6666193,7.59851667 12.7088376,6.55633333 13.9999526,6.55633333 Z M13.4348426,10.5098167 C13.4948839,10.5028588 13.5551031,10.5212898 13.6009647,10.5606609 C13.6468264,10.6000319 13.6741635,10.6567658 13.6763776,10.7171683 L13.7151145,11.8291533 C13.7184994,11.938957 13.6391949,12.0339269 13.5305478,12.0501783 C12.9058678,12.1446293 12.3049645,12.4314333 11.8238311,12.9160667 C10.7022678,14.04575 10.6473645,15.8238667 11.6255911,17.0426833 L12.6851578,15.97628 C12.7471434,15.914964 12.8398366,15.8967615 12.9204066,15.9300834 C13.0009766,15.9634054 13.0537346,16.0417631 13.0543028,16.12895 L13.0611383,19.2483833 C13.0611463,19.307801 13.0367599,19.3646163 12.9936828,19.4055408 C12.9506056,19.4464653 12.8926159,19.4679097 12.8332766,19.4648583 L9.88697663,19.3053517 C9.80143518,19.3012871 9.72630824,19.2472336 9.69526871,19.1674188 C9.66422918,19.0876039 9.68309816,18.996996 9.7434183,18.9362067 L10.54094,18.1295617 C8.97060663,16.3068783 9.02648997,13.5555283 10.7414666,11.829095 C11.495705,11.069245 12.4494666,10.629295 13.4348333,10.5097117 L13.4348426,10.5098167 Z M15.1711926,10.7559133 L18.1174926,10.917695 C18.2026243,10.9225973 18.2769698,10.9769641 18.3074479,11.056604 C18.3379261,11.1362439 18.3188721,11.2263546 18.2587643,11.28684 L17.4612426,12.0911983 C19.032976,13.9128317 18.980126,16.6660483 17.2652776,18.3938817 C17.2645181,18.3938777 17.2637586,18.3938777 17.2629991,18.3938817 C16.5080141,19.1533467 15.5524325,19.5913483 14.5674158,19.7109317 C14.5073745,19.7178895 14.4471553,19.6994585 14.4012937,19.6600875 C14.3554321,19.6207164 14.3280949,19.5639825 14.3258808,19.50358 L14.2894225,18.3938817 C14.2860376,18.284078 14.365342,18.1891081 14.4739891,18.1728567 C15.0982141,18.0784745 15.6977058,17.7894083 16.1783725,17.3046933 C16.179132,17.3046973 16.1798915,17.3046973 16.180651,17.3046933 C17.3022143,16.1759783 17.357351,14.4000433 16.378891,13.1802933 L15.3193243,14.2466967 C15.2573387,14.3080127 15.1646455,14.3262151 15.0840755,14.2928932 C15.0035055,14.2595713 14.9507475,14.1812135 14.9501793,14.0940267 L14.9433438,10.97226 C14.9433358,10.9128424 14.9677222,10.8560271 15.0107993,10.8151026 C15.0538765,10.7741781 15.1118662,10.7527337 15.1712055,10.755785 L15.1711926,10.7559133 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#6B425C" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M19.4443861,8.88885 L17.8888695,8.88885 C17.8888695,6.74218333 16.1466861,5 14.0000195,5 C11.8533528,5 10.1111695,6.74218333 10.1111695,8.88885 L8.55565282,8.88885 C7.70010115,8.88885 7.00783615,9.58885 7.00783615,10.4443667 L7.00005833,19.7777 C7.00005833,20.6332517 7.70005833,21.3332167 8.555575,21.3332167 L19.444425,21.3332167 C20.2999767,21.3332167 20.9999417,20.6332167 20.9999417,19.7777 L20.9999417,10.4443667 C20.9999417,9.588815 20.2999417,8.88885 19.444425,8.88885 L19.4443861,8.88885 Z M13.9999028,6.55551667 C15.2910528,6.55551667 16.3332362,7.597735 16.3332362,8.88885 L11.6665695,8.88885 C11.6665695,7.5977 12.7087878,6.55551667 13.9999028,6.55551667 Z M13.9999028,14.3333333 C11.8532362,14.3333333 10.1110528,12.59115 10.1110528,10.4444833 L11.6665695,10.4444833 C11.6665695,11.7356333 12.7087878,12.7778167 13.9999028,12.7778167 C15.2910178,12.7778167 16.3332362,11.7355983 16.3332362,10.4444833 L17.8887528,10.4444833 C17.8887528,12.59115 16.1465695,14.3333333 13.9999028,14.3333333 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#51585E" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M10.3454333,8.64113333 C11.22839,8.64113333 11.9363,7.94225333 11.9363,7.07056667 C11.9363,6.19892667 11.2283433,5.5 10.3454333,5.5 C9.46252333,5.5 8.75456667,6.19888 8.75456667,7.07056667 C8.75456667,7.94220667 9.46252333,8.64113333 10.3454333,8.64113333 L10.3454333,8.64113333 Z M19.2545667,17.43605 L19.2545667,21.83345 L16.7091333,21.83345 L16.7091333,17.43605 L14.1637,17.43605 L16.3909833,10.5257667 C16.3909833,10.5257667 16.7091683,9.26938333 17.98185,9.26938333 C19.2545317,9.26938333 19.5727167,10.5257667 19.5727167,10.5257667 L21.8,17.43605 L19.2545667,17.43605 L19.2545667,17.43605 Z M11.61815,15.5514167 L11.61815,21.83345 L9.07271667,21.83345 L9.07271667,15.5514167 L7.8,15.5514167 L7.8,11.0192667 C7.8,10.0528233 8.57319667,9.26938333 9.55198333,9.26938333 L11.1388833,9.26938333 C12.1064817,9.26938333 12.8908667,10.05231 12.8908667,11.0192667 L12.8908667,15.5514167 L11.61815,15.5514167 Z M19.7362833,7.07068333 C19.7362833,7.94232333 19.00161,8.64125 18.0853333,8.64125 C17.1690567,8.64125 16.4343833,7.94237 16.4343833,7.07068333 C16.4343833,6.19904333 17.1690567,5.50011667 18.0853333,5.50011667 C19.00161,5.50011667 19.7362833,6.19899667 19.7362833,7.07068333 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#6E4426" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M22.167,11.328l-5.717,-0.816l-2.45,-5.712l-2.45,5.712l-5.717,0.816l4.083,4.08l-0.817,5.726l4.9,-2.463l4.9,2.448l-0.817,-5.711z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#2F6499" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M17.7652425,5.6 C19.3205789,5.6 20.6007172,6.82330936 20.6712621,8.37700229 L21.1340668,18.5587623 C21.2018095,20.0490603 20.1357825,21.3284073 18.6981339,21.5620297 L18.2940892,21.5992497 L9.77120086,21.6 C8.27928213,21.6 7.04973517,20.4769811 6.88169744,19.0301853 L6.86287684,18.6248613 L7.3279318,8.37707196 C7.39855249,6.8233354 8.67873133,5.6 10.2339515,5.6 L17.7652425,5.6 Z M9.92695997,17.2362972 C9.28430184,17.2362972 8.76333027,17.7572687 8.76333027,18.3999268 C8.76333027,19.042585 9.28430184,19.5635566 9.92695997,19.5635566 C10.5696181,19.5635566 11.0905897,19.042585 11.0905897,18.3999268 C11.0905897,17.7572687 10.5696181,17.2362972 9.92695997,17.2362972 Z M18.0723679,17.2362972 C17.4297098,17.2362972 16.9087383,17.7572687 16.9087383,18.3999268 C16.9087383,19.042585 17.4297098,19.5635566 18.0723679,19.5635566 C18.7150261,19.5635566 19.2359977,19.042585 19.2359977,18.3999268 C19.2359977,17.7572687 18.7150261,17.2362972 18.0723679,17.2362972 Z M17.2628087,7.78180569 L10.736446,7.78180569 C9.95826161,7.78180569 9.31791619,8.39432582 9.28337093,9.17176139 L9.12032285,12.8726871 C9.12032285,13.675999 9.77154822,14.3272243 10.57486,14.3272243 L17.4890037,14.3257899 C18.2915299,14.2901216 18.9131848,13.6106375 18.8775195,12.8081258 L18.7159058,9.17178293 C18.6813533,8.39434738 18.0410005,7.78180569 17.2628087,7.78180569 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,26 +0,0 @@
|
||||
<layer-list
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#0A6074" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M14,21.8749 C11.1749,21.8749 8.5482,19.4518 8.5482,16.4231 C8.5482,13.3606 12.6876,7.8747 14,6.1251 C15.3125,7.8751 19.4518,13.3606 19.4518,16.4231 C19.4518,19.4519 16.825,21.8749 14,21.8749 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#51585E" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M14,16.98 C15.1045695,16.98 16.1045695,17.4277153 16.8284271,18.1515729 L14,20.98 L11.1715729,18.1515729 C11.8954305,17.4277153 12.8954305,16.98 14,16.98 Z M14,12.48 C16.3472102,12.48 18.4722102,13.4313949 20.0104076,14.9695924 L18.2426407,16.7373593 C17.1568542,15.6515729 15.6568542,14.98 14,14.98 C12.3431458,14.98 10.8431458,15.6515729 9.75735931,16.7373593 L7.98959236,14.9695924 C9.52778981,13.4313949 11.6527898,12.48 14,12.48 Z M14,7.98 C17.5898509,7.98 20.8398509,9.43507456 23.1923882,11.7876118 L21.4251209,13.5558785 C19.5249552,11.6554586 16.8997491,10.48 14,10.48 C11.1002509,10.48 8.47504482,11.6554586 6.57487909,13.5558785 L4.80761184,11.7876118 C7.16014913,9.43507456 10.4101491,7.98 14,7.98 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#747E86" />
|
||||
<solid android:color="@color/amenity" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M20,8 C20.82725,8 21.5,8.67275 21.5,9.5 L21.5,18.5 C21.5,19.32725 20.82725,20 20,20 L8,20 C7.17275,20 6.5,19.32725 6.5,18.5 L6.5,9.5 C6.5,8.67275 7.17275,8 8,8 L20,8 Z M20.00075,14 L8,14 L8,18.5 L20.0015,18.5 L20.00075,14 Z M14,15.5 L14,17 L9.5,17 L9.5,15.5 L14,15.5 Z M20,9.5 L8,9.5 L8,11 L20,11 L20,9.5 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#747E86" />
|
||||
<solid android:color="@color/amenity" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M7,9.53073333 L14,6.3 L21,9.53073333 L21,10.60766 L19.9230733,10.60766 L19.9230733,18.68461 L21,18.68461 L21,20.2999767 L7,20.2999767 L7,18.68461 L8.07692667,18.68461 L8.07692667,10.60766 L7,10.60766 L7,9.53073333 Z M16.6923167,15.2756333 C16.6923167,14.0852833 15.4814333,13.1208 13.9976667,13.1208 C13.50468,13.1208 13.09784,12.7993017 13.09784,12.4012583 C13.09784,12.003215 13.4998967,11.6817167 13.9976667,11.6817167 C14.4954483,11.6817167 14.8974933,12.0070417 14.8974933,12.4012583 L16.69241,12.4012583 C16.69241,11.4673883 15.94096,10.6789083 14.8974933,10.3803583 L14.8974933,9.53068667 L13.1025767,9.53068667 L13.1025767,10.3803583 C12.0591567,10.6788967 11.30766,11.46733 11.30766,12.4012583 C11.30766,13.5916083 12.5185433,14.5560917 14.00231,14.5560917 C14.5000917,14.5560917 14.9021367,14.87759 14.9021367,15.2756333 C14.9021367,15.6736767 14.50008,15.995175 14.00231,15.995175 C13.5045283,15.995175 13.1024833,15.66985 13.1024833,15.2756333 L11.3075667,15.2756333 C11.3075667,16.2095033 12.0590167,16.9979833 13.1024833,17.2965333 L13.1024833,18.146205 L14.8974,18.146205 L14.8974,17.2965333 C15.94082,16.997995 16.6923167,16.2095617 16.6923167,15.2756333 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#EB785D" />
|
||||
<solid android:color="@color/entertainment" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -18,7 +18,7 @@
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:pathData="M10.7398398,14.5988166 C11.2766539,14.5988166 11.7741396,14.7668787 12.1823664,15.0532612 C11.7724801,15.4914625 11.5235582,16.0775925 11.5235582,16.7240712 L11.529303,16.8892426 L11.5491056,17.0705775 C11.6012609,17.429281 11.7280184,17.7632492 11.9167798,18.0556079 L11.9546499,18.1104694 L11.9142567,18.13405 C11.1417478,18.5947243 10.6376499,19.4380664 10.6376499,20.3887564 L10.637,21.073 L5.952,21.073 L5.95232411,17.1079171 C5.95232411,15.7221656 7.06098553,14.5988166 8.46443574,14.5988166 L10.7398398,14.5988166 Z M14.7363802,18.7647355 C15.6343829,18.7647355 16.3623501,19.4913492 16.3623501,20.3887564 L16.362,21.073 L11.637,21.073 L11.6376499,20.3887564 C11.6376499,19.4918257 12.3552335,18.7647355 13.2636198,18.7647355 L14.7363802,18.7647355 Z M19.4916069,12.4068343 C20.8792516,12.4068343 22.0041478,13.5296391 22.0041478,14.9163636 L22.004,21.073 L17.362,21.073 L17.3623501,20.3887564 L17.3567032,20.2194334 L17.3385166,20.043376 C17.2273964,19.2299207 16.7526866,18.5346787 16.0784713,18.1328785 L16.0416499,18.1124694 L16.0806774,18.0575117 C16.336328,17.665886 16.4764418,17.2070581 16.4764418,16.7240712 C16.4764418,15.6254025 15.757471,14.7010059 14.7609634,14.3838791 C15.0006762,13.2537372 15.9971232,12.4068343 17.215814,12.4068343 L19.4916069,12.4068343 Z M14,15.2664694 C14.8194057,15.2664694 15.4764418,15.9151249 15.4764418,16.7240712 C15.4764418,17.5330608 14.819449,18.1816731 14,18.1816731 C13.1805943,18.1816731 12.5235582,17.5330175 12.5235582,16.7240712 C12.5235582,15.9150816 13.1805943,15.2664694 14,15.2664694 Z M9.60213775,9.19402109 C10.8681135,9.19402109 11.8832295,10.1961892 11.8832295,11.4460053 C11.8832295,12.6958883 10.8681805,13.6979895 9.60213775,13.6979895 C8.33616197,13.6979895 7.32104605,12.6958214 7.32104605,11.4460053 C7.32104605,10.1961223 8.33616197,9.19402109 9.60213775,9.19402109 Z M18.3537105,7.00111521 C19.6199026,7.00111521 20.635192,8.00345456 20.635192,9.25348422 C20.635192,10.5035808 19.6199695,11.5058532 18.3537105,11.5058532 C17.0875184,11.5058532 16.072229,10.5035139 16.072229,9.25348422 C16.072229,8.00338763 17.0875184,7.00111521 18.3537105,7.00111521 Z"
|
||||
android:fillColor="#FFF" />
|
||||
android:fillColor="@color/foreground_color" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#EC955E" />
|
||||
<solid android:color="@color/food" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -18,7 +18,7 @@
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:pathData="M12.5416667,11.1416783 L12.5416667,7.70257833 C12.5416667,7.31455667 12.232395,7 11.8466717,7 L11.7783342,7 C11.3945008,7 11.0833392,7.32498667 11.0833392,7.70257833 L11.0833392,11.1416783 L9.62500583,11.1416783 L9.62500583,7.70257833 C9.62500583,7.31455667 9.31573417,7 8.93001083,7 L8.86167333,7 C8.47784,7 8.16667833,7.32498667 8.16667833,7.70257833 L8.16667833,11.1416783 C8.16667833,12.749345 9.62501167,14.0000117 11.0104283,14.0000117 L10.8211833,20.055945 C10.8048897,20.5773283 11.2048417,21 11.7169383,21 L11.9080967,21 C12.4190967,21 12.815985,20.58308 12.7940167,20.055945 L12.54169,14.0000117 C14.0000233,14.0000117 15.7500233,12.749345 15.4583567,11.1416783 L15.4583567,7.70257833 C15.4583567,7.31455667 15.149085,7 14.7633617,7 L14.6950242,7 C14.3111908,7 14.0000292,7.32498667 14.0000292,7.70257833 L14.0000292,11.1416783 L12.5416958,11.1416783 L12.5416667,11.1416783 Z M16.3333333,14.933345 L18.0833333,14.933345 L18.0833333,20.130845 C18.0833333,20.6108933 18.4717167,21.0000467 18.9583333,21.0000467 C19.4415783,21.0000467 19.8333333,20.6109983 19.8333333,20.1242883 L19.8333333,7.000455 C17.3903333,7.000455 16.275,8.69912167 16.3333333,10.0337883 L16.3333333,14.9337883 L16.3333333,14.933345 Z"
|
||||
android:fillColor="#FFF" />
|
||||
android:fillColor="@color/foreground_color" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#EB785D" />
|
||||
<solid android:color="@color/entertainment" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -18,7 +18,7 @@
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:pathData="M14.8166667,12.2166667 L14.8166667,17.525 C14.8166667,20.0076667 12.8076667,22.0166667 10.325,22.0166667 C7.84233333,22.0166667 5.83333333,20.0076667 5.83333333,17.525 L5.83333333,12.2166667 L14.8166667,12.2166667 Z M12.3666667,17.9333333 L8.28333333,17.9333333 C8.28333333,18.6111667 9.198,19.1583333 10.325,19.1583333 C11.452,19.1583333 12.3666667,18.6111667 12.3666667,17.9333333 Z M22.1666667,6.5 L22.1666667,11.8083333 C22.1666667,14.291 20.1576667,16.3 17.675,16.3 C16.94,16.3 16.2458333,16.1121667 15.6333333,15.8018333 L15.6333333,11.4 L13.1833333,11.4 L13.1833333,6.5 L22.1666667,6.5 Z M12.3666667,14.6666667 C11.9175,14.6666667 11.55,15.0341667 11.55,15.4833333 C11.55,15.9325 11.9175,16.3 12.3666667,16.3 C12.8158333,16.3 13.1833333,15.9325 13.1833333,15.4833333 C13.1833333,15.0341667 12.8158333,14.6666667 12.3666667,14.6666667 Z M8.28333333,14.6666667 C7.83416667,14.6666667 7.46666667,15.0341667 7.46666667,15.4833333 C7.46666667,15.9325 7.83416667,16.3 8.28333333,16.3 C8.7325,16.3 9.1,15.9325 9.1,15.4833333 C9.1,15.0341667 8.7325,14.6666667 8.28333333,14.6666667 Z M17.675,12.2166667 C16.548,12.2166667 15.6333333,12.7638333 15.6333333,13.4416667 L19.7166667,13.4416667 C19.7166667,12.7638333 18.802,12.2166667 17.675,12.2166667 Z M15.6333333,9.01533333 C15.1841667,9.01533333 14.8166667,9.38283333 14.8166667,9.832 C14.8166667,10.2811667 15.1841667,10.6486667 15.6333333,10.6486667 C16.0825,10.6486667 16.45,10.2893333 16.45,9.832 C16.45,9.38283333 16.0825,9.01533333 15.6333333,9.01533333 Z M19.7166667,9.01533333 C19.2675,9.01533333 18.9,9.38283333 18.9,9.832 C18.9,10.2811667 19.2675,10.6486667 19.7166667,10.6486667 C20.1658333,10.6486667 20.5333333,10.2893333 20.5333333,9.832 C20.5333333,9.38283333 20.1658333,9.01533333 19.7166667,9.01533333 Z"
|
||||
android:fillColor="#FFF" />
|
||||
android:fillColor="@color/foreground_color" />
|
||||
</group>
|
||||
</vector>
|
||||
</item>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#BE75A3" />
|
||||
<solid android:color="@color/shop" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M11.3735667,18.307716 C10.6313217,18.307716 10.0307333,18.9134843 10.0307333,19.653816 C10.0307333,20.394206 10.631275,20.999916 11.3735667,20.999916 C12.1158117,20.999916 12.72305,20.3941477 12.72305,19.653816 C12.72305,18.913426 12.115765,18.307716 11.3735667,18.307716 Z M6.8,7.48804933 C6.8,7.813281 7.06993167,8.07692433 7.38562,8.07692433 L8.2,8.07692433 L10.3,14.5385077 C9.55654167,14.5385077 8.9539,15.3205827 8.9539,16.286291 L8.9539,16.0215277 C8.9539,16.9867927 9.739055,17.769311 10.7023833,17.769311 L18.8085,17.769311 C19.1352133,17.769311 19.4000583,17.5195393 19.4000583,17.180436 L19.4000583,17.2812663 C19.4000583,16.9560347 19.129765,16.6923913 18.8139717,16.6923913 L10.6169717,16.6923913 C10.29328,16.6923913 10.030885,16.4426197 10.030885,16.1035163 L10.030885,16.2043467 C10.030885,15.879115 10.2980983,15.6154717 10.6084667,15.6154717 L18.0620667,15.6154717 C19.2169033,15.6154717 19.113315,15.3197567 19.2870667,14.969325 L20.7161167,10.1360583 C20.7721167,10.0380583 20.8001167,9.91905833 20.8001167,9.80005833 C20.8001167,9.41505833 20.4851167,9.10005833 20.2616533,9.1539 L9.74707,9.1539 L8.90007,7 L7.37395333,7 C7.05702833,7 6.8,7.24977167 6.8,7.588875 L6.8,7.48804467 L6.8,7.48804933 Z M17.8350333,18.307716 C17.0927883,18.307716 16.4922,18.9134843 16.4922,19.653816 C16.4922,20.394206 17.0927417,20.999916 17.8350333,20.999916 C18.577325,20.999916 19.1845167,20.3941477 19.1845167,19.653816 C19.1845167,18.913426 18.5772317,18.307716 17.8350333,18.307716 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#AA96BC" />
|
||||
<solid android:color="@color/cm_services" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M17.846514,4.676 C17.5575662,4.7216586 17.3186367,4.92551065 17.2280519,5.20366514 C17.1374671,5.48181964 17.2105329,5.78727723 17.4171807,5.99433333 L19.428514,8.00566667 C19.799514,8.37666667 20.062014,8.83516667 20.1973474,9.33333333 L19.523014,9.33333333 C18.2431807,9.33333333 17.1896807,10.3868333 17.1896807,11.6666667 C17.1896807,12.9465 18.2431807,14 19.523014,14 L20.300007,14 L20.300007,19.4448333 C20.300007,19.8835 19.9616807,20.2218333 19.523014,20.2218333 C19.315878,20.2250062 19.1162779,20.1442015 18.9696836,19.997827 C18.8230893,19.8514525 18.7419852,19.6519738 18.7448474,19.4448333 L18.7448474,17.8885 C18.7448474,16.6098333 17.6913474,15.5551667 16.411514,15.5551667 L15.6333474,15.5551667 L15.6333474,7.77816667 C15.6345903,7.36522389 15.4711795,6.96881891 15.1792931,6.67671349 C14.8874066,6.38460807 14.4911243,6.22090001 14.0781807,6.22183333 L7.85634735,6.22183333 C7.44320185,6.22058891 7.04661893,6.38415963 6.75447962,6.67629894 C6.46234031,6.96843825 6.2987696,7.36502117 6.30000697,7.77816667 L6.30000697,20.2218333 C6.2987696,20.6349788 6.46234031,21.0315618 6.75447962,21.3237011 C7.04661893,21.6158404 7.44320185,21.7794111 7.85634735,21.7781737 L14.0781807,21.7781737 C14.4911243,21.7791 14.8874066,21.6153919 15.1792931,21.3232865 C15.4711795,21.0311811 15.6345903,20.6347761 15.6333474,20.2218333 L15.6333474,17.1115 L16.411514,17.1115 C16.8501807,17.1115 17.1896807,17.4498333 17.1896807,17.8885 L17.1896807,19.4448333 C17.1896807,20.7235 18.2431807,21.7781737 19.523014,21.7781737 C20.8016807,21.7781737 21.8563474,20.7235 21.8563474,19.4448333 L21.8563474,10.1115 C21.8563474,8.90940677 21.3788136,7.75655029 20.5286807,6.90666667 L18.5173474,4.89416667 C18.3411731,4.71808596 18.0913541,4.63698153 17.8453474,4.676 L17.846514,4.676 Z M8.63218069,7.77816667 L13.2988474,7.77816667 C13.730514,7.77816667 14.077014,8.12466667 14.077014,8.55516667 L14.077014,12.4448333 L7.85634735,12.4448333 L7.85634735,8.55516667 C7.85634735,8.12466667 8.20284735,7.77816667 8.63334735,7.77816667 L8.63218069,7.77816667 Z M19.523014,10.5 C20.1673462,10.5 20.6896807,11.0223345 20.6896807,11.6666667 C20.6896807,12.3109989 20.1673462,12.8333333 19.523014,12.8333333 C18.8786818,12.8333333 18.3563474,12.3109989 18.3563474,11.6666667 C18.3563474,11.0223345 18.8786818,10.5 19.523014,10.5 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#D85961" />
|
||||
<solid android:color="@color/medical" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M21.1291111,17.1111111 L17.1106444,17.1111111 L17.1106444,21.1295778 C17.1106444,21.9138422 16.4689778,22.5555089 15.6847133,22.5555089 L12.3142911,22.5555089 C11.5300267,22.5555089 10.88836,21.9138422 10.88836,21.1295778 L10.88836,17.1111111 L6.86973778,17.1111111 C6.08547333,17.1111111 5.44383778,16.4694444 5.44383778,15.68518 L5.44383778,12.3147578 C5.44383778,11.5304933 6.08550444,10.8888267 6.86973778,10.8888267 L10.88836,10.8888267 L10.88836,6.87036 C10.88836,6.08609556 11.5300267,5.44442889 12.3142911,5.44442889 L15.6847133,5.44442889 C16.4689778,5.44442889 17.1106444,6.08609556 17.1106444,6.87036 L17.1106444,10.8888267 L21.1291111,10.8888267 C21.9133756,10.8888267 22.5550422,11.5304933 22.5550422,12.3147578 L22.5550422,15.68518 C22.5550422,16.4694444 21.9133756,17.1111111 21.1291111,17.1111111 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#664E42" />
|
||||
<solid android:color="@color/hotel" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M10.2879,14.1061667 C11.5203667,14.1061667 12.5151833,13.111315 12.5151833,11.8788833 C12.5151833,10.6464167 11.5203317,9.6516 10.2879,9.6516 C9.05543333,9.6516 8.06061667,10.6464517 8.06061667,11.8788833 C8.06061667,13.11135 9.05546833,14.1061667 10.2879,14.1061667 Z M19.1970333,9.6516 L14.7424667,9.6516 C13.9258,9.6516 13.25765,10.319785 13.25765,11.1364167 L13.25765,14.8485167 L7.31826667,14.8485167 L7.31826667,8.90913333 C7.31826667,8.5008 6.98418,8.16671333 6.57584667,8.16671333 C6.16751333,8.16671333 5.83342667,8.5008 5.83342667,8.90913333 L5.83342667,18.5606167 C5.83342667,18.96895 6.16751333,19.3030367 6.57584667,19.3030367 C6.98418,19.3030367 7.31826667,18.96895 7.31826667,18.5606167 L7.31826667,17.0758 L20.6824333,17.0758 L20.6824333,18.5606167 C20.6824333,18.96895 21.01652,19.3030367 21.4248533,19.3030367 C21.8331867,19.3030367 22.1672733,18.96895 22.1672733,18.5606167 L22.1672733,12.6212333 C22.1672733,10.9804333 20.8383233,9.65148333 19.1975233,9.65148333 L19.1970333,9.6516 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#FB3207" />
|
||||
<solid android:color="@color/luggagehero" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -20,7 +20,7 @@
|
||||
android:scaleX="0.5"
|
||||
android:scaleY="0.5">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M8.5714,5.1429l6.8571,0l0,-1.7143l-6.8571,0ZM24,13.7143l0,6.4286c0,0.5893 -0.2098,1.0938 -0.6295,1.5134 -0.4196,0.4196 -0.9241,0.6295 -1.5134,0.6295l-19.7143,0c-0.5893,0 -1.0938,-0.2098 -1.5134,-0.6295 -0.4196,-0.4196 -0.6295,-0.9241 -0.6295,-1.5134l0,-6.4286l9,0l0,2.1429c0,0.2321 0.0848,0.433 0.2545,0.6027 0.1696,0.1696 0.3705,0.2545 0.6027,0.2545l4.2857,0c0.2321,0 0.433,-0.0848 0.6027,-0.2545 0.1696,-0.1696 0.2545,-0.3705 0.2545,-0.6027l0,-2.1429ZM13.7143,13.7143l0,1.7143l-3.4286,0l0,-1.7143ZM24,7.2857l0,5.1429l-24,0l0,-5.1429c0,-0.5893 0.2098,-1.0938 0.6295,-1.5134 0.4196,-0.4196 0.9241,-0.6295 1.5134,-0.6295l4.7143,0l0,-2.1429c0,-0.3571 0.125,-0.6607 0.375,-0.9107 0.25,-0.25 0.5536,-0.375 0.9107,-0.375l7.7143,0c0.3571,0 0.6607,0.125 0.9107,0.375 0.25,0.25 0.375,0.5536 0.375,0.9107l0,2.1429l4.7143,0c0.5893,0 1.0938,0.2098 1.5134,0.6295 0.4196,0.4196 0.6295,0.9241 0.6295,1.5134Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#EC955E" />
|
||||
<solid android:color="@color/food" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M21,8.42778167 C21,7.80556333 20.4944483,7.3 19.8722183,7.3 L8.127385,7.3 C7.50516667,7.3 6.99960333,7.80555167 6.99960333,8.42778167 C6.99960333,8.7 7.10071483,8.97223 7.287385,9.17444833 L13.2218683,15.8555983 L13.2218683,19.7444483 L10.1107183,19.7444483 C9.68293667,19.7444483 9.33293667,20.0944483 9.33293667,20.52223 C9.33293667,20.9500117 9.68293667,21.3000117 10.1107183,21.3000117 L17.888535,21.3000117 C18.3163167,21.3000117 18.6663167,20.9500117 18.6663167,20.52223 C18.6663167,20.0944483 18.3163167,19.7444483 17.888535,19.7444483 L14.777385,19.7444483 L14.777385,15.8555983 L20.7118683,9.17444833 C20.898535,8.97223 20.99965,8.7 20.99965,8.42778167 L21,8.42778167 Z M10.4455167,10.411115 L9.06885,8.85559833 L18.93885,8.85559833 L17.5543667,10.411115 L10.4455167,10.411115 L10.4455167,10.411115 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#427BB8" />
|
||||
<solid android:color="@color/parking" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M9.35,22.3 L13.1840042,22.3 L13.1840042,16.379733 L14.4487167,16.379733 C16.4953333,16.379733 18.0936417,15.9233039 19.1939042,14.9897598 C20.3178375,14.0448193 20.85,12.6687324 20.85,10.8925278 C20.85,9.17330503 20.3522033,7.86253173 19.3111083,6.99727004 C18.2581875,6.12060241 16.709425,5.7 14.6982667,5.7 L9.3502875,5.7002873 L9.35,22.3 Z M13.1840042,8.60425301 L14.5565292,8.60425301 C15.5029792,8.60425301 16.17755,8.78662355 16.6154125,9.17371683 C17.0649667,9.57220647 17.2884404,10.1637639 17.2884404,10.9949323 C17.2884404,11.8032888 17.0518663,12.4290544 16.5076479,12.8616374 C15.9752746,13.282891 15.1586979,13.5329885 14.0820104,13.5329885 L13.1840042,13.5329885 L13.1840042,8.60425301 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#D85961" />
|
||||
<solid android:color="@color/medical" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M16.961,6.99999417 C15.9026933,6.99999417 14.84875,7.41071333 14.0726833,8.17973333 L8.2299,13.9907833 C7.02531667,15.1842833 6.69806667,16.9171333 7.28183167,18.3600667 C7.28183167,18.3600667 7.71838667,19.382475 8.14171167,19.80195 C8.54021,20.1968083 9.021355,20.4748717 9.508695,20.675795 C9.64324667,20.7452082 9.744875,20.770512 9.86146,20.80687 C9.877112,20.8116985 9.88986133,20.8240783 9.9055565,20.8287158 C11.3082398,21.2432442 12.8732065,20.907362 13.9403565,19.8019442 L17.4680065,16.3064942 L19.7830232,13.9907775 C21.4057398,12.3828775 21.4057398,9.80501083 19.7830232,8.26711083 C19.0774815,7.42821917 18.0191398,6.99999417 16.9608565,6.99999417 L16.961,6.99999417 Z M16.961,8.39813333 C17.6665417,8.39813333 18.3852667,8.67339667 18.8792333,9.162755 C19.3732,9.65211333 19.6509133,10.364305 19.6509133,11.0633717 C19.6509133,11.76245 19.3731067,12.452755 18.8792333,12.9421717 L16.4760167,15.323455 L12.6658,11.523155 L14.9987833,9.22832167 C15.5632167,8.66905667 16.1849333,8.39813333 16.9611167,8.39813333 L16.961,8.39813333 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#747E86" />
|
||||
<solid android:color="@color/amenity" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M14,5.833275 L7.31815,8.803025 L7.31815,13.2575917 C7.31815,17.378025 10.1690167,21.231175 14,22.166725 C17.8308667,21.2312683 20.68185,17.3781417 20.68185,13.2575917 L20.68185,8.803025 L14,5.833275 L14,5.833275 Z M15.85605,14.438025 L16.524235,17.3186417 L14.000035,15.796725 L11.475835,17.3186417 L12.14402,14.4454917 L9.91673667,12.5225917 L12.8567367,12.2701717 L14.00007,9.560355 L15.1434033,12.2628217 L18.0834033,12.5152417 L15.85612,14.4381417 L15.85605,14.438025 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#747E86" />
|
||||
<solid android:color="@color/amenity" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M20.5333333,7 C21.4316667,7 22.1666667,7.735 22.1666667,8.63333333 L22.1666667,18.4333333 C22.1666667,19.3316667 21.4316667,20.0666667 20.5333333,20.0666667 L7.46666667,20.0666667 C6.56833333,20.0666667 5.83333333,19.3316667 5.83333333,18.4333333 L5.8415,8.63333333 C5.8415,7.735 6.56833333,7 7.46666667,7 L20.5333333,7 Z M20.5333333,9.88283333 C20.5333333,9.33566667 19.9371667,9.009 19.4716667,9.29483333 L14,12.7166667 L8.52833333,9.29483333 C8.06283333,9.009 7.46666667,9.33566667 7.46666667,9.88283333 C7.46666667,10.1196667 7.58916667,10.3401667 7.79333333,10.4708333 L13.5671667,14.0805 C13.8285,14.2438333 14.1715,14.2438333 14.4328333,14.0805 L20.2066667,10.4708333 C20.4108333,10.3401667 20.5333333,10.1196667 20.5333333,9.88283333 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#747E86" />
|
||||
<solid android:color="@color/amenity" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -18,7 +18,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M9.26178239,9.86916295 L10.4193833,7.95061481 L11.2531868,6.56634591 C11.5688972,6.04016587 12.3298444,6.04016587 12.6455913,6.56634591 L13.843676,8.5577503 L12.8479738,10.2253086 L12.0384558,11.5367213 L9.26178239,9.86916295 Z M22.1731256,14.5805343 L20.8778628,12.4272676 L18.0769039,14.0462551 L19.8658893,17.0333617 L20.7806361,17.0333617 C21.3958708,17.0333617 21.9544354,16.6852666 22.2296256,16.1348011 C22.3429572,15.9081332 22.3996236,15.665279 22.3996236,15.4143378 C22.3996236,15.1229127 22.3186718,14.8395869 22.1729556,14.5805343 L22.1731256,14.5805343 Z M17.5427461,21.0808911 L18.7570171,21.0808911 C19.3722518,21.0808911 19.9308164,20.732796 20.2060066,20.1823306 L21.379806,17.8427947 L17.5427097,17.8427947 L17.5427097,16.2238072 L14.3046133,19.4619036 L17.5427097,22.7 L17.5427097,21.0810125 L17.5427461,21.0808911 Z M12.6856622,17.8427947 L9.20471166,17.8427947 L8.52471991,18.9842094 C8.28186572,19.3889623 8.26567949,19.8908691 8.47614908,20.3199075 C8.70281704,20.7813304 9.18042624,21.0808547 9.70656985,21.0808547 L12.6855408,21.0808547 L12.6855408,17.8427583 L12.6856622,17.8427947 Z M9.54470753,15.6975422 L10.9451262,16.5394327 L9.83609613,12.0952009 L5.4,13.204231 L6.77613329,14.0299353 L6.44423661,14.5884999 C6.16091076,15.0661091 6.13662534,15.6489592 6.38757022,16.1427668 L7.70711848,18.7817419 L9.54467474,15.6974936 L9.54470753,15.6975422 Z M18.3684504,8.24191842 L17.3160782,6.48523261 C17.0165539,5.99952422 16.4903739,5.7 15.9236736,5.7 L13.0661298,5.7 L15.5918134,9.90951317 L14.1994089,10.7433166 L18.6436406,11.8523467 L19.7526707,7.40811497 L18.3684018,8.24191842 L18.3684504,8.24191842 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#747E86" />
|
||||
<solid android:color="@color/amenity" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M8.33758333,7.43983333 C6.95648333,7.43983333 5.83333333,8.590085 5.83333333,10.0056833 L5.83333333,16.9397667 C5.83333333,18.0495583 6.52384833,18.99765 7.48766667,19.3551167 C7.45779982,19.1965237 7.44027578,19.0357487 7.43526,18.87431 C7.43522971,18.0654358 7.74879599,17.2897014 8.30693531,16.7178646 C8.86507462,16.1460278 9.62203578,15.8249626 10.4111933,15.8253433 C12.0537082,15.8258056 13.3849729,17.1907579 13.38491,18.87431 C13.3830282,19.0874192 13.3593515,19.2997388 13.3142683,19.5077867 L23.3312683,19.5077867 C23.6679333,19.5077867 23.9419483,19.2262233 23.9419483,18.8811467 C23.9419483,18.53607 23.6679217,18.2567933 23.3312683,18.2567933 L21.4741683,18.2567933 C21.6999883,17.87135 21.82964,17.4210517 21.82964,16.9397433 L21.82964,13.3690433 C21.82964,10.09841 19.2349733,7.43983333 16.04414,7.43983333 L8.33772333,7.43983333 L8.33758333,7.43983333 Z M8.88445833,9.60225 L10.9944917,9.60225 C11.3312033,9.60225 11.6051833,9.88155 11.6051833,10.226615 L11.6051833,12.391365 C11.6051833,12.7364417 11.3311567,13.0157183 10.9944917,13.0157183 L8.88445833,13.0157183 C8.54779333,13.0157183 8.27376667,12.7364183 8.27376667,12.391365 L8.27376667,10.226615 C8.27376667,9.88153833 8.54779333,9.60225 8.88445833,9.60225 Z M16.647925,10.684625 L16.8097067,10.684625 C18.2658233,10.684625 19.4484733,11.8992417 19.4484733,13.3916417 L19.4484733,18.2566417 L18.2294233,18.2566417 L18.2294233,13.3916417 C18.2294233,12.5883683 17.5935433,11.9333083 16.8098233,11.9333083 L16.6480417,11.9333083 C15.8643567,11.9333083 15.2284417,12.5883683 15.2284417,13.3916417 L15.2284417,18.2566417 L14.0070583,18.2566417 L14.0070583,13.3916417 C14.0070583,11.8992417 15.191925,10.684625 16.6480417,10.684625 L16.647925,10.684625 Z M10.4111583,17.0581247 C9.94104172,17.0578394 9.49008616,17.2490609 9.15757567,17.5896894 C8.82506518,17.9303179 8.63829167,18.3924263 8.63829167,18.874275 C8.63829167,19.3561237 8.82506518,19.8182321 9.15757567,20.1588606 C9.49008616,20.4994891 9.94104172,20.6907106 10.4111583,20.6904253 C11.3892489,20.6896989 12.1817548,19.8767746 12.1816917,18.874275 C12.1817548,17.8717754 11.3892489,17.0588511 10.4111583,17.0581247 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#BE75A3" />
|
||||
<solid android:color="@color/shop" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M13.9999526,5 C11.853286,5 10.110286,6.743 10.110286,8.88966667 L8.55628597,8.88966667 C7.7007343,8.88966667 7.00683597,9.58815 7.00683597,10.4436667 L7,19.777 C7,20.6325517 7.70075833,21.3333333 8.55633333,21.3333333 L19.4436667,21.3333333 C20.2992183,21.3333333 21,20.632575 21,19.777 L21,10.4436667 C21,9.588115 20.2992417,8.88966667 19.4436667,8.88966667 L17.8896667,8.88966667 C17.8896667,6.743 16.1466667,5 14,5 L13.9999526,5 Z M13.9999526,6.55633333 C15.2911026,6.55633333 16.333286,7.59855167 16.333286,8.88966667 L11.6666193,8.88966667 C11.6666193,7.59851667 12.7088376,6.55633333 13.9999526,6.55633333 Z M13.4348426,10.5098167 C13.4948839,10.5028588 13.5551031,10.5212898 13.6009647,10.5606609 C13.6468264,10.6000319 13.6741635,10.6567658 13.6763776,10.7171683 L13.7151145,11.8291533 C13.7184994,11.938957 13.6391949,12.0339269 13.5305478,12.0501783 C12.9058678,12.1446293 12.3049645,12.4314333 11.8238311,12.9160667 C10.7022678,14.04575 10.6473645,15.8238667 11.6255911,17.0426833 L12.6851578,15.97628 C12.7471434,15.914964 12.8398366,15.8967615 12.9204066,15.9300834 C13.0009766,15.9634054 13.0537346,16.0417631 13.0543028,16.12895 L13.0611383,19.2483833 C13.0611463,19.307801 13.0367599,19.3646163 12.9936828,19.4055408 C12.9506056,19.4464653 12.8926159,19.4679097 12.8332766,19.4648583 L9.88697663,19.3053517 C9.80143518,19.3012871 9.72630824,19.2472336 9.69526871,19.1674188 C9.66422918,19.0876039 9.68309816,18.996996 9.7434183,18.9362067 L10.54094,18.1295617 C8.97060663,16.3068783 9.02648997,13.5555283 10.7414666,11.829095 C11.495705,11.069245 12.4494666,10.629295 13.4348333,10.5097117 L13.4348426,10.5098167 Z M15.1711926,10.7559133 L18.1174926,10.917695 C18.2026243,10.9225973 18.2769698,10.9769641 18.3074479,11.056604 C18.3379261,11.1362439 18.3188721,11.2263546 18.2587643,11.28684 L17.4612426,12.0911983 C19.032976,13.9128317 18.980126,16.6660483 17.2652776,18.3938817 C17.2645181,18.3938777 17.2637586,18.3938777 17.2629991,18.3938817 C16.5080141,19.1533467 15.5524325,19.5913483 14.5674158,19.7109317 C14.5073745,19.7178895 14.4471553,19.6994585 14.4012937,19.6600875 C14.3554321,19.6207164 14.3280949,19.5639825 14.3258808,19.50358 L14.2894225,18.3938817 C14.2860376,18.284078 14.365342,18.1891081 14.4739891,18.1728567 C15.0982141,18.0784745 15.6977058,17.7894083 16.1783725,17.3046933 C16.179132,17.3046973 16.1798915,17.3046973 16.180651,17.3046933 C17.3022143,16.1759783 17.357351,14.4000433 16.378891,13.1802933 L15.3193243,14.2466967 C15.2573387,14.3080127 15.1646455,14.3262151 15.0840755,14.2928932 C15.0035055,14.2595713 14.9507475,14.1812135 14.9501793,14.0940267 L14.9433438,10.97226 C14.9433358,10.9128424 14.9677222,10.8560271 15.0107993,10.8151026 C15.0538765,10.7741781 15.1118662,10.7527337 15.1712055,10.755785 L15.1711926,10.7559133 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#BE75A3" />
|
||||
<solid android:color="@color/shop" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M19.4443861,8.88885 L17.8888695,8.88885 C17.8888695,6.74218333 16.1466861,5 14.0000195,5 C11.8533528,5 10.1111695,6.74218333 10.1111695,8.88885 L8.55565282,8.88885 C7.70010115,8.88885 7.00783615,9.58885 7.00783615,10.4443667 L7.00005833,19.7777 C7.00005833,20.6332517 7.70005833,21.3332167 8.555575,21.3332167 L19.444425,21.3332167 C20.2999767,21.3332167 20.9999417,20.6332167 20.9999417,19.7777 L20.9999417,10.4443667 C20.9999417,9.588815 20.2999417,8.88885 19.444425,8.88885 L19.4443861,8.88885 Z M13.9999028,6.55551667 C15.2910528,6.55551667 16.3332362,7.597735 16.3332362,8.88885 L11.6665695,8.88885 C11.6665695,7.5977 12.7087878,6.55551667 13.9999028,6.55551667 Z M13.9999028,14.3333333 C11.8532362,14.3333333 10.1110528,12.59115 10.1110528,10.4444833 L11.6665695,10.4444833 C11.6665695,11.7356333 12.7087878,12.7778167 13.9999028,12.7778167 C15.2910178,12.7778167 16.3332362,11.7355983 16.3332362,10.4444833 L17.8887528,10.4444833 C17.8887528,12.59115 16.1465695,14.3333333 13.9999028,14.3333333 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#747E86" />
|
||||
<solid android:color="@color/amenity" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M10.3454333,8.64113333 C11.22839,8.64113333 11.9363,7.94225333 11.9363,7.07056667 C11.9363,6.19892667 11.2283433,5.5 10.3454333,5.5 C9.46252333,5.5 8.75456667,6.19888 8.75456667,7.07056667 C8.75456667,7.94220667 9.46252333,8.64113333 10.3454333,8.64113333 L10.3454333,8.64113333 Z M19.2545667,17.43605 L19.2545667,21.83345 L16.7091333,21.83345 L16.7091333,17.43605 L14.1637,17.43605 L16.3909833,10.5257667 C16.3909833,10.5257667 16.7091683,9.26938333 17.98185,9.26938333 C19.2545317,9.26938333 19.5727167,10.5257667 19.5727167,10.5257667 L21.8,17.43605 L19.2545667,17.43605 L19.2545667,17.43605 Z M11.61815,15.5514167 L11.61815,21.83345 L9.07271667,21.83345 L9.07271667,15.5514167 L7.8,15.5514167 L7.8,11.0192667 C7.8,10.0528233 8.57319667,9.26938333 9.55198333,9.26938333 L11.1388833,9.26938333 C12.1064817,9.26938333 12.8908667,10.05231 12.8908667,11.0192667 L12.8908667,15.5514167 L11.61815,15.5514167 Z M19.7362833,7.07068333 C19.7362833,7.94232333 19.00161,8.64125 18.0853333,8.64125 C17.1690567,8.64125 16.4343833,7.94237 16.4343833,7.07068333 C16.4343833,6.19904333 17.1690567,5.50011667 18.0853333,5.50011667 C19.00161,5.50011667 19.7362833,6.19899667 19.7362833,7.07068333 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#9C6136" />
|
||||
<solid android:color="@color/tourism" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M22.167,11.328l-5.717,-0.816l-2.45,-5.712l-2.45,5.712l-5.717,0.816l4.083,4.08l-0.817,5.726l4.9,-2.463l4.9,2.448l-0.817,-5.711z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#2F6499" />
|
||||
<solid android:color="@color/transport" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M17.7652425,5.6 C19.3205789,5.6 20.6007172,6.82330936 20.6712621,8.37700229 L21.1340668,18.5587623 C21.2018095,20.0490603 20.1357825,21.3284073 18.6981339,21.5620297 L18.2940892,21.5992497 L9.77120086,21.6 C8.27928213,21.6 7.04973517,20.4769811 6.88169744,19.0301853 L6.86287684,18.6248613 L7.3279318,8.37707196 C7.39855249,6.8233354 8.67873133,5.6 10.2339515,5.6 L17.7652425,5.6 Z M9.92695997,17.2362972 C9.28430184,17.2362972 8.76333027,17.7572687 8.76333027,18.3999268 C8.76333027,19.042585 9.28430184,19.5635566 9.92695997,19.5635566 C10.5696181,19.5635566 11.0905897,19.042585 11.0905897,18.3999268 C11.0905897,17.7572687 10.5696181,17.2362972 9.92695997,17.2362972 Z M18.0723679,17.2362972 C17.4297098,17.2362972 16.9087383,17.7572687 16.9087383,18.3999268 C16.9087383,19.042585 17.4297098,19.5635566 18.0723679,19.5635566 C18.7150261,19.5635566 19.2359977,19.042585 19.2359977,18.3999268 C19.2359977,17.7572687 18.7150261,17.2362972 18.0723679,17.2362972 Z M17.2628087,7.78180569 L10.736446,7.78180569 C9.95826161,7.78180569 9.31791619,8.39432582 9.28337093,9.17176139 L9.12032285,12.8726871 C9.12032285,13.675999 9.77154822,14.3272243 10.57486,14.3272243 L17.4890037,14.3257899 C18.2915299,14.2901216 18.9131848,13.6106375 18.8775195,12.8081258 L18.7159058,9.17178293 C18.6813533,8.39434738 18.0410005,7.78180569 17.2628087,7.78180569 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#54A3B6" />
|
||||
<solid android:color="@color/water" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -18,7 +18,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M14,21.8749 C11.1749,21.8749 8.5482,19.4518 8.5482,16.4231 C8.5482,13.3606 12.6876,7.8747 14,6.1251 C15.3125,7.8751 19.4518,13.3606 19.4518,16.4231 C19.4518,19.4519 16.825,21.8749 14,21.8749 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
<solid android:color="#747E86" />
|
||||
<solid android:color="@color/amenity" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:fillColor="@color/foreground_color"
|
||||
android:pathData="M14,16.98 C15.1045695,16.98 16.1045695,17.4277153 16.8284271,18.1515729 L14,20.98 L11.1715729,18.1515729 C11.8954305,17.4277153 12.8954305,16.98 14,16.98 Z M14,12.48 C16.3472102,12.48 18.4722102,13.4313949 20.0104076,14.9695924 L18.2426407,16.7373593 C17.1568542,15.6515729 15.6568542,14.98 14,14.98 C12.3431458,14.98 10.8431458,15.6515729 9.75735931,16.7373593 L7.98959236,14.9695924 C9.52778981,13.4313949 11.6527898,12.48 14,12.48 Z M14,7.98 C17.5898509,7.98 20.8398509,9.43507456 23.1923882,11.7876118 L21.4251209,13.5558785 C19.5249552,11.6554586 16.8997491,10.48 14,10.48 C11.1002509,10.48 8.47504482,11.6554586 6.57487909,13.5558785 L4.80761184,11.7876118 C7.16014913,9.43507456 10.4101491,7.98 14,7.98 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
16
android/app/src/main/res/drawable/ic_location_sharing.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- Location pin with share icon -->
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z"/>
|
||||
<!-- Share arrows (smaller, overlaid) -->
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,16l-3,3v-2H9v-2h6v-2z"
|
||||
android:fillAlpha="0.8"/>
|
||||
</vector>
|
||||
@@ -3,5 +3,5 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="12dp" />
|
||||
<solid android:color="@color/active_track_recording" />
|
||||
</shape>
|
||||
<solid android:color="@color/active_location_sharing" />
|
||||
</shape>
|
||||
83
android/app/src/main/res/layout/dialog_location_sharing.xml
Normal file
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- Status Text -->
|
||||
<TextView
|
||||
android:id="@+id/status_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:paddingBottom="8dp"
|
||||
tools:text="Location sharing is not active" />
|
||||
|
||||
<!-- Description -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/location_sharing_description"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:paddingBottom="16dp" />
|
||||
|
||||
<!-- Share URL (visible only when sharing) -->
|
||||
<TextView
|
||||
android:id="@+id/share_url_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:fontFamily="monospace"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:padding="12dp"
|
||||
android:textIsSelectable="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
tools:text="https://live.organicmaps.app/live/abc123def456" />
|
||||
|
||||
<!-- Button Row -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="end"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<!-- Copy Button -->
|
||||
<Button
|
||||
android:id="@+id/copy_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/location_sharing_copy_url"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!-- Share Button -->
|
||||
<Button
|
||||
android:id="@+id/share_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/location_sharing_share_url"
|
||||
android:visibility="gone"
|
||||
android:layout_marginStart="8dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!-- Start/Stop Button -->
|
||||
<Button
|
||||
android:id="@+id/start_stop_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
tools:text="Start Sharing" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -43,6 +43,17 @@
|
||||
android:paddingEnd="@dimen/nav_bottom_gap"
|
||||
tools:background="#300000FF">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/share_location"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/nav_icon_size"
|
||||
android:layout_weight="0.2"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:scaleType="center"
|
||||
android:contentDescription="@string/location_sharing_title"
|
||||
app:srcCompat="@drawable/ic_share"
|
||||
app:tint="?iconTint" />
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/tts_volume"
|
||||
android:layout_width="0dp"
|
||||
|
||||
@@ -57,4 +57,27 @@
|
||||
app:srcCompat="@drawable/ic_location_crosshair"
|
||||
tools:tint="?colorSecondary"/>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/btn__share_location"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:background="?clickableBackground"
|
||||
tools:visibility="visible">
|
||||
<View
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="@dimen/margin_half"
|
||||
android:layout_marginTop="@dimen/margin_half"
|
||||
android:background="?dividerHorizontal"/>
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/iv__share_location_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_base"
|
||||
app:srcCompat="@drawable/ic_share"
|
||||
app:tint="?colorSecondary"
|
||||
android:contentDescription="@string/location_sharing_title"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -42,4 +42,20 @@
|
||||
<!-- Downloader -->
|
||||
<color name="dl_done">#54575b</color>
|
||||
<color name="dl_download">#ffe68c</color>
|
||||
|
||||
<!-- Categories -->
|
||||
<color name="foreground_color">#000</color>
|
||||
|
||||
<color name="amenity">#51585E</color>
|
||||
<color name="entertainment">#802D19</color>
|
||||
<color name="food">#8C491C</color>
|
||||
<color name="shop">#6B425C</color>
|
||||
<color name="cm_services">#574469</color>
|
||||
<color name="medical">#983E44</color>
|
||||
<color name="hotel">#614A43</color>
|
||||
<color name="luggagehero">#851F03</color>
|
||||
<color name="parking">#20607C</color>
|
||||
<color name="tourism">#6E4426</color>
|
||||
<color name="transport">#2F6499</color>
|
||||
<color name="water">#0A6074</color>
|
||||
</resources>
|
||||
|
||||
@@ -51,4 +51,21 @@
|
||||
<item>8</item>
|
||||
<item>10</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Location Sharing Update Intervals -->
|
||||
<string-array name="location_sharing_intervals">
|
||||
<item>5 seconds</item>
|
||||
<item>10 seconds</item>
|
||||
<item>20 seconds</item>
|
||||
<item>30 seconds</item>
|
||||
<item>60 seconds</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="location_sharing_interval_values">
|
||||
<item>5</item>
|
||||
<item>10</item>
|
||||
<item>20</item>
|
||||
<item>30</item>
|
||||
<item>60</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
||||
@@ -124,7 +124,8 @@
|
||||
<color name="elevation_profile_dot_enabled">#FF9600</color>
|
||||
<color name="elevation_profile">@color/base_accent</color>
|
||||
|
||||
<color name="active_track_recording">#0057ff</color>
|
||||
<color name="active_track_recording">#FFFF0000</color>
|
||||
<color name="active_location_sharing">#FF9500</color>
|
||||
|
||||
<color name="material_calendar_surface_dark">#929292</color>
|
||||
<color name="notification_warning">#FFC22219</color>
|
||||
@@ -133,5 +134,21 @@
|
||||
<color name="dl_done">#ebebeb</color>
|
||||
<color name="dl_download">#FF37653F</color>
|
||||
<color name="dl_update">#ffc30a</color>
|
||||
|
||||
<!-- Categories -->
|
||||
<color name="foreground_color">#FFF</color>
|
||||
|
||||
<color name="amenity">#747E86</color>
|
||||
<color name="entertainment">#EB785D</color>
|
||||
<color name="food">#EC955E</color>
|
||||
<color name="shop">#BE75A3</color>
|
||||
<color name="cm_services">#AA96BC</color>
|
||||
<color name="medical">#D85961</color>
|
||||
<color name="hotel">#664E42</color>
|
||||
<color name="luggagehero">#FB3207</color>
|
||||
<color name="parking">#427BB8</color>
|
||||
<color name="tourism">#9C6136</color>
|
||||
<color name="transport">#2F6499</color>
|
||||
<color name="water">#54A3B6</color>
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -210,6 +210,9 @@
|
||||
<!-- Length of track in cell that describes route -->
|
||||
<string name="length">Length</string>
|
||||
<string name="share_my_location">Share My Location</string>
|
||||
<string name="stop_sharing_my_location">Stop Sharing My Location</string>
|
||||
<string name="share_location_coordinates">Share Current Coordinates</string>
|
||||
<string name="share_location_live">Start Live Location Sharing</string>
|
||||
<!-- Settings general group in settings screen -->
|
||||
<string name="prefs_group_general">General settings</string>
|
||||
<!-- Settings information group in settings screen -->
|
||||
|
||||
41
android/app/src/main/res/values/strings_location_sharing.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Location Sharing -->
|
||||
<string name="location_sharing_title">Live Location Sharing</string>
|
||||
<string name="location_sharing_start">Start Sharing</string>
|
||||
<string name="location_sharing_stop">Stop Sharing</string>
|
||||
<string name="location_sharing_active">Sharing live location</string>
|
||||
<string name="location_sharing_status_active">Your location is being shared</string>
|
||||
<string name="location_sharing_status_inactive">Location sharing is not active</string>
|
||||
|
||||
<!-- Notification -->
|
||||
<string name="location_sharing_notification_text">Your live location is being shared</string>
|
||||
<string name="location_sharing_tap_to_view">Tap to view in app</string>
|
||||
<string name="location_sharing_eta">ETA: %s</string>
|
||||
<string name="location_sharing_remaining">remaining</string>
|
||||
<string name="location_sharing_accuracy">Accuracy: %s</string>
|
||||
<string name="location_sharing_accuracy_high">high</string>
|
||||
<string name="location_sharing_accuracy_medium">medium</string>
|
||||
<string name="location_sharing_accuracy_low">low</string>
|
||||
<string name="location_sharing_waiting_for_location">Waiting for location…</string>
|
||||
|
||||
<!-- Messages -->
|
||||
<string name="location_sharing_started">Location sharing started</string>
|
||||
<string name="location_sharing_stopped">Location sharing stopped</string>
|
||||
<string name="location_sharing_failed_to_start">Failed to start location sharing</string>
|
||||
<string name="location_sharing_url_copied">Share URL copied to clipboard</string>
|
||||
<string name="location_sharing_share_message">Follow my live location: %s</string>
|
||||
|
||||
<!-- Dialog -->
|
||||
<string name="location_sharing_description">Share your real-time location with end-to-end encryption. Only people with the link can view your location.</string>
|
||||
<string name="location_sharing_copy_url">Copy Link</string>
|
||||
<string name="location_sharing_share_url">Share Link</string>
|
||||
<!-- Using existing close string from main strings.xml -->
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="pref_location_sharing_category">Live Location Sharing</string>
|
||||
<string name="pref_location_sharing_update_interval">Update interval</string>
|
||||
<string name="pref_location_sharing_update_interval_summary">How often to send location updates</string>
|
||||
<string name="pref_location_sharing_server_url">Server URL</string>
|
||||
<string name="pref_location_sharing_server_url_summary">Location sharing server endpoint</string>
|
||||
</resources>
|
||||
@@ -193,6 +193,27 @@
|
||||
android:order="5"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<androidx.preference.PreferenceCategory
|
||||
android:key="@string/pref_location_sharing_category"
|
||||
android:title="@string/location_sharing_title"
|
||||
android:order="5">
|
||||
<EditTextPreference
|
||||
android:key="@string/pref_location_sharing_server_url"
|
||||
android:title="@string/pref_location_sharing_server_url"
|
||||
app:singleLineTitle="false"
|
||||
android:summary="@string/pref_location_sharing_server_url_summary"
|
||||
android:defaultValue="https://live.organicmaps.app"
|
||||
android:order="1"/>
|
||||
<ListPreference
|
||||
android:key="@string/pref_location_sharing_update_interval"
|
||||
android:title="@string/pref_location_sharing_update_interval"
|
||||
app:singleLineTitle="false"
|
||||
android:summary="@string/pref_location_sharing_update_interval_summary"
|
||||
android:entries="@array/location_sharing_intervals"
|
||||
android:entryValues="@array/location_sharing_interval_values"
|
||||
android:defaultValue="20"
|
||||
android:order="2"/>
|
||||
</androidx.preference.PreferenceCategory>
|
||||
<androidx.preference.PreferenceCategory
|
||||
android:key="@string/pref_privacy"
|
||||
android:title="@string/privacy"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../../../../data/subtypes.csv
|
||||
|
@@ -53,6 +53,7 @@ set(SRC
|
||||
app/organicmaps/sdk/editor/OpeningHours.cpp
|
||||
app/organicmaps/sdk/editor/OsmOAuth.cpp
|
||||
app/organicmaps/sdk/Framework.cpp
|
||||
app/organicmaps/location/LocationSharingJni.cpp
|
||||
app/organicmaps/sdk/isolines/IsolinesManager.cpp
|
||||
app/organicmaps/sdk/LocationState.cpp
|
||||
app/organicmaps/sdk/Map.cpp
|
||||
@@ -94,6 +95,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE .)
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
# CoMaps libs
|
||||
map
|
||||
location_sharing
|
||||
# ge0
|
||||
# tracking
|
||||
# routing
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "app/organicmaps/sdk/core/jni_helper.hpp"
|
||||
|
||||
#include "location_sharing/location_sharing_types.hpp"
|
||||
#include "location_sharing/crypto_util.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
using namespace location_sharing;
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
// Generate session credentials
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_app_organicmaps_location_LocationSharingManager_nativeGenerateSessionCredentials(
|
||||
JNIEnv * env, jclass)
|
||||
{
|
||||
SessionCredentials creds = SessionCredentials::Generate();
|
||||
|
||||
// Create String array [sessionId, encryptionKey]
|
||||
jobjectArray result = env->NewObjectArray(2, env->FindClass("java/lang/String"), nullptr);
|
||||
if (!result)
|
||||
{
|
||||
LOG(LERROR, ("Failed to create result array"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jstring sessionId = jni::ToJavaString(env, creds.sessionId);
|
||||
jstring encryptionKey = jni::ToJavaString(env, creds.encryptionKey);
|
||||
|
||||
env->SetObjectArrayElement(result, 0, sessionId);
|
||||
env->SetObjectArrayElement(result, 1, encryptionKey);
|
||||
|
||||
env->DeleteLocalRef(sessionId);
|
||||
env->DeleteLocalRef(encryptionKey);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Generate share URL
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_app_organicmaps_location_LocationSharingManager_nativeGenerateShareUrl(
|
||||
JNIEnv * env, jclass, jstring jSessionId, jstring jEncryptionKey, jstring jServerBaseUrl)
|
||||
{
|
||||
std::string sessionId = jni::ToNativeString(env, jSessionId);
|
||||
std::string encryptionKey = jni::ToNativeString(env, jEncryptionKey);
|
||||
std::string serverBaseUrl = jni::ToNativeString(env, jServerBaseUrl);
|
||||
|
||||
SessionCredentials creds(sessionId, encryptionKey);
|
||||
std::string shareUrl = creds.GenerateShareUrl(serverBaseUrl);
|
||||
|
||||
return jni::ToJavaString(env, shareUrl);
|
||||
}
|
||||
|
||||
// Encrypt payload
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_app_organicmaps_location_LocationSharingManager_nativeEncryptPayload(
|
||||
JNIEnv * env, jclass, jstring jEncryptionKey, jstring jPayloadJson)
|
||||
{
|
||||
std::string encryptionKey = jni::ToNativeString(env, jEncryptionKey);
|
||||
std::string payloadJson = jni::ToNativeString(env, jPayloadJson);
|
||||
|
||||
auto encryptedOpt = crypto::EncryptAes256Gcm(encryptionKey, payloadJson);
|
||||
if (!encryptedOpt.has_value())
|
||||
{
|
||||
LOG(LERROR, ("Encryption failed"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string resultJson = encryptedOpt->ToJson();
|
||||
return jni::ToJavaString(env, resultJson);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -520,6 +520,46 @@ public final class Config
|
||||
}
|
||||
}
|
||||
|
||||
public static class LocationSharing
|
||||
{
|
||||
interface Keys
|
||||
{
|
||||
String SERVER_URL = "LocationSharingServerUrl";
|
||||
String UPDATE_INTERVAL = "LocationSharingUpdateInterval";
|
||||
}
|
||||
|
||||
public interface Defaults
|
||||
{
|
||||
String SERVER_URL = "https://live.organicmaps.app";
|
||||
int UPDATE_INTERVAL = 20; // seconds
|
||||
int UPDATE_INTERVAL_MIN = 5;
|
||||
int UPDATE_INTERVAL_MAX = 60;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getServerUrl()
|
||||
{
|
||||
return getString(Keys.SERVER_URL, Defaults.SERVER_URL);
|
||||
}
|
||||
|
||||
public static void setServerUrl(@NonNull String url)
|
||||
{
|
||||
setString(Keys.SERVER_URL, url);
|
||||
}
|
||||
|
||||
public static int getUpdateInterval()
|
||||
{
|
||||
return getInt(Keys.UPDATE_INTERVAL, Defaults.UPDATE_INTERVAL);
|
||||
}
|
||||
|
||||
public static void setUpdateInterval(int seconds)
|
||||
{
|
||||
if (seconds < Defaults.UPDATE_INTERVAL_MIN || seconds > Defaults.UPDATE_INTERVAL_MAX)
|
||||
seconds = Defaults.UPDATE_INTERVAL;
|
||||
setInt(Keys.UPDATE_INTERVAL, seconds);
|
||||
}
|
||||
}
|
||||
|
||||
private static native boolean nativeHasConfigValue(String name);
|
||||
private static native boolean nativeDeleteConfigValue(String name);
|
||||
private static native boolean nativeGetBoolean(String name, boolean defaultValue);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<circle cx="12" cy="12" r="12" fill="#FFF" opacity=".4"/>
|
||||
<circle cx="12" cy="12" r="11" fill="#0D3C73"/>
|
||||
<path d="m12.7 19.1c-3.6 0.4-6.7-2.5-6.7-6.1v-8h4v7.9c0 0.8 0.5 1.6 1.3 1.9 1.4 0.4 2.7-0.5 2.7-1.8v-8h4v7.8c0 3.1-2.2 5.9-5.3 6.3z" fill="#FFF" opacity=".6"/>
|
||||
</g>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="18" height="18" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="12" fill="#fff" opacity=".4"/>
|
||||
<circle cx="12" cy="12" r="11" fill="#0d3c73"/>
|
||||
<path d="m12 19.141c-2.6667 0-6-1.8076-6-6.141v-7.0295h4v6.9295c0 0.66086 0.66667 1.9 2 1.9 1.3333 0 2-1.0119 2-1.8v-7.0295h4v6.8295c0 4.5333-3.3333 6.341-6 6.341z" fill="#e7ecf1"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 374 B After Width: | Height: | Size: 435 B |
@@ -1,7 +1,8 @@
|
||||
<svg viewBox="0 0 18 18" width="14" height="14" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<circle cx="9" cy="9" r="9" fill="#FFF" opacity=".4"/>
|
||||
<circle cx="9" cy="9" r="8" fill="#0D3C73"/>
|
||||
<path d="m5 4h3v6c0 0.57157 0.42843 1 1 1s1-0.42843 1-1v-6h3v6c0 2.2284-1.7716 4-4 4-2.2284 0-4-1.7716-4-4v-6z" fill="#FFF" opacity=".6"/>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14" height="14" version="1.1" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<g stroke-width=".75">
|
||||
<circle cx="9" cy="9" r="9" fill="#fff" opacity=".4"/>
|
||||
<circle cx="9" cy="9" r="8.25" fill="#0d3c73"/>
|
||||
<path d="m9 14.356c-2 0-4.5-1.3557-4.5-4.6057v-5.2722h3v5.1972c0 0.49565 0.5 1.425 1.5 1.425 1 0 1.5-0.75896 1.5-1.35v-5.2722h3v5.1222c0 3.4-2.5 4.7557-4.5 4.7557z" fill="#e7ecf1"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 347 B After Width: | Height: | Size: 465 B |
@@ -1,7 +1,2 @@
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<circle cx="12" cy="12" r="12" fill="#FFF" opacity=".6"/>
|
||||
<circle cx="12" cy="12" r="11" fill="#1565C0"/>
|
||||
<path d="m12.7 19.1c-3.6 0.4-6.7-2.5-6.7-6.1v-8h4v7.9c0 0.8 0.5 1.6 1.3 1.9 1.4 0.4 2.7-0.5 2.7-1.8v-8h4v7.8c0 3.1-2.2 5.9-5.3 6.3z" fill="#FFF"/>
|
||||
</g>
|
||||
</svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="18" height="18" version="1.1" viewBox="0 0 24 24" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="#fff" opacity=".6"/><circle cx="12" cy="12" r="11" fill="#3b87c9"/><path d="m12 19.141c-2.6667 0-6-1.8076-6-6.141v-7.0295h4v6.9295c0 0.66086 0.66667 1.9 2 1.9 1.3333 0 2-1.0119 2-1.8v-7.0295h4v6.8295c0 4.5333-3.3333 6.341-6 6.341z" fill="#fff"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 361 B After Width: | Height: | Size: 446 B |
@@ -1,7 +1,8 @@
|
||||
<svg viewBox="0 0 18 18" width="14" height="14" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<circle cx="9" cy="9" r="9" fill="#FFF" opacity=".6"/>
|
||||
<circle cx="9" cy="9" r="8" fill="#1565C0"/>
|
||||
<path d="m5 4h3v6c0 0.57157 0.42843 1 1 1s1-0.42843 1-1v-6h3v6c0 2.2284-1.7716 4-4 4-2.2284 0-4-1.7716-4-4v-6z" fill="#FFF"/>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14" height="14" version="1.1" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<g stroke-width=".75">
|
||||
<circle cx="9" cy="9" r="9" fill="#fff" opacity=".6"/>
|
||||
<circle cx="9" cy="9" r="8.25" fill="#3b87c9"/>
|
||||
<path d="m9 14.356c-2 0-4.5-1.3557-4.5-4.6057v-5.2722h3v5.1972c0 0.49565 0.5 1.425 1.5 1.425 1 0 1.5-0.75896 1.5-1.35v-5.2722h3v5.1222c0 3.4-2.5 4.7557-4.5 4.7557z" fill="#fff"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 334 B After Width: | Height: | Size: 462 B |
@@ -1,2 +0,0 @@
|
||||
Types;Related Subtypes
|
||||
amenity|charging_station;amenity|charging_station|motorcar,amenity|charging_station|motorcycle,amenity|charging_station|bicycle,amenity|charging_station|small,amenity|charging_station|carless
|
||||
|
@@ -11,7 +11,6 @@
|
||||
#include "indexer/classificator.hpp"
|
||||
#include "indexer/feature_impl.hpp"
|
||||
#include "indexer/ftypes_matcher.hpp"
|
||||
#include "indexer/ftypes_subtypes.hpp"
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
@@ -352,11 +351,9 @@ private:
|
||||
// - both amenity-charging_station-motorcar and amenity-charging_station-bicycle are left;
|
||||
void LeaveLongestTypes(std::vector<generator::TypeStrings> & matchedTypes)
|
||||
{
|
||||
// Prevents types, that either have subtypes or are subtypes, from being removed
|
||||
auto subtypes = ftypes::Subtypes::Instance();
|
||||
auto const hasSubtypeRelatedTypes = [subtypes](auto const & lhs, auto const & rhs)
|
||||
auto const isChargingStation = [](auto const & lhs, auto const & rhs)
|
||||
{
|
||||
return subtypes.IsPathOfTypeWithSubtypesOrSubtype(lhs) || subtypes.IsPathOfTypeWithSubtypesOrSubtype(rhs);
|
||||
return lhs.size() > 1 && rhs.size() > 1 && lhs.at(1) == "charging_station" && rhs.at(1) == "charging_station" && lhs.at(0) == "amenity" && rhs.at(0) == "amenity";
|
||||
};
|
||||
|
||||
auto const equalPrefix = [](auto const & lhs, auto const & rhs)
|
||||
@@ -377,10 +374,9 @@ void LeaveLongestTypes(std::vector<generator::TypeStrings> & matchedTypes)
|
||||
return lhs < rhs;
|
||||
};
|
||||
|
||||
// `true` means it will be deleted, because being equal means it isn't unique
|
||||
auto const isEqual = [&equalPrefix, &hasSubtypeRelatedTypes](auto const & lhs, auto const & rhs)
|
||||
auto const isEqual = [&equalPrefix, &isChargingStation](auto const & lhs, auto const & rhs)
|
||||
{
|
||||
if (hasSubtypeRelatedTypes(lhs, rhs))
|
||||
if (isChargingStation(lhs, rhs))
|
||||
return false;
|
||||
|
||||
if (equalPrefix(lhs, rhs))
|
||||
|
||||
136
iphone/Maps/Core/LocationSharing/LocationSharingApiClient.swift
Normal file
@@ -0,0 +1,136 @@
|
||||
import Foundation
|
||||
|
||||
/// API client for location sharing server
|
||||
class LocationSharingApiClient {
|
||||
|
||||
private let serverBaseUrl: String
|
||||
private let sessionId: String
|
||||
private let session: URLSession
|
||||
|
||||
private static let connectTimeout: TimeInterval = 10
|
||||
private static let requestTimeout: TimeInterval = 10
|
||||
|
||||
init(serverBaseUrl: String, sessionId: String) {
|
||||
self.serverBaseUrl = serverBaseUrl.hasSuffix("/") ? serverBaseUrl : serverBaseUrl + "/"
|
||||
self.sessionId = sessionId
|
||||
|
||||
let config = URLSessionConfiguration.default
|
||||
config.timeoutIntervalForRequest = Self.requestTimeout
|
||||
config.timeoutIntervalForResource = Self.connectTimeout
|
||||
self.session = URLSession(configuration: config)
|
||||
}
|
||||
|
||||
// MARK: - API Methods
|
||||
|
||||
/// Create session on server
|
||||
func createSession(completion: ((Bool, String?) -> Void)? = nil) {
|
||||
let urlString = "\(serverBaseUrl)api/v1/session"
|
||||
guard let url = URL(string: urlString) else {
|
||||
completion?(false, "Invalid URL")
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let body: [String: Any] = ["sessionId": sessionId]
|
||||
guard let jsonData = try? JSONSerialization.data(withJSONObject: body) else {
|
||||
completion?(false, "Failed to serialize JSON")
|
||||
return
|
||||
}
|
||||
|
||||
request.httpBody = jsonData
|
||||
|
||||
let task = session.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
NSLog("Create session error: \(error.localizedDescription)")
|
||||
completion?(false, error.localizedDescription)
|
||||
return
|
||||
}
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
completion?(false, "Invalid response")
|
||||
return
|
||||
}
|
||||
|
||||
if (200..<300).contains(httpResponse.statusCode) {
|
||||
NSLog("Session created: \(self.sessionId)")
|
||||
completion?(true, nil)
|
||||
} else {
|
||||
completion?(false, "Server error: \(httpResponse.statusCode)")
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
/// Update location on server with encrypted payload
|
||||
func updateLocation(encryptedPayload: String, completion: ((Bool, String?) -> Void)? = nil) {
|
||||
let urlString = "\(serverBaseUrl)api/v1/location/\(sessionId)"
|
||||
guard let url = URL(string: urlString) else {
|
||||
completion?(false, "Invalid URL")
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = encryptedPayload.data(using: .utf8)
|
||||
|
||||
let task = session.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
NSLog("Update location error: \(error.localizedDescription)")
|
||||
completion?(false, error.localizedDescription)
|
||||
return
|
||||
}
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
completion?(false, "Invalid response")
|
||||
return
|
||||
}
|
||||
|
||||
if (200..<300).contains(httpResponse.statusCode) {
|
||||
completion?(true, nil)
|
||||
} else {
|
||||
completion?(false, "Server error: \(httpResponse.statusCode)")
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
/// End session on server
|
||||
func endSession(completion: ((Bool, String?) -> Void)? = nil) {
|
||||
let urlString = "\(serverBaseUrl)api/v1/session/\(sessionId)"
|
||||
guard let url = URL(string: urlString) else {
|
||||
completion?(false, "Invalid URL")
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "DELETE"
|
||||
|
||||
let task = session.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
NSLog("End session error: \(error.localizedDescription)")
|
||||
completion?(false, error.localizedDescription)
|
||||
return
|
||||
}
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
completion?(false, "Invalid response")
|
||||
return
|
||||
}
|
||||
|
||||
if (200..<300).contains(httpResponse.statusCode) {
|
||||
NSLog("Session ended: \(self.sessionId)")
|
||||
completion?(true, nil)
|
||||
} else {
|
||||
completion?(false, "Server error: \(httpResponse.statusCode)")
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
31
iphone/Maps/Core/LocationSharing/LocationSharingBridge.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Objective-C++ bridge to C++ location sharing functionality
|
||||
@interface LocationSharingBridgeObjC : NSObject
|
||||
|
||||
/// Generate new session credentials
|
||||
/// @return Array of [sessionId, encryptionKey]
|
||||
+ (NSArray<NSString *> *)generateSessionCredentials;
|
||||
|
||||
/// Generate share URL from credentials
|
||||
+ (nullable NSString *)generateShareUrlWithSessionId:(NSString *)sessionId
|
||||
encryptionKey:(NSString *)encryptionKey
|
||||
serverBaseUrl:(NSString *)serverBaseUrl;
|
||||
|
||||
/// Encrypt payload using AES-256-GCM
|
||||
/// @param key Base64-encoded encryption key
|
||||
/// @param plaintext JSON payload to encrypt
|
||||
/// @return Encrypted JSON (with iv, ciphertext, authTag) or nil on failure
|
||||
+ (nullable NSString *)encryptPayloadWithKey:(NSString *)key plaintext:(NSString *)plaintext;
|
||||
|
||||
/// Decrypt payload using AES-256-GCM
|
||||
/// @param key Base64-encoded encryption key
|
||||
/// @param encryptedJson Encrypted JSON (with iv, ciphertext, authTag)
|
||||
/// @return Decrypted plaintext or nil on failure
|
||||
+ (nullable NSString *)decryptPayloadWithKey:(NSString *)key encryptedJson:(NSString *)encryptedJson;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
77
iphone/Maps/Core/LocationSharing/LocationSharingBridge.mm
Normal file
@@ -0,0 +1,77 @@
|
||||
#import "LocationSharingBridge.h"
|
||||
|
||||
#include "location_sharing/location_sharing_types.hpp"
|
||||
#include "location_sharing/crypto_util.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
using namespace location_sharing;
|
||||
|
||||
@implementation LocationSharingBridgeObjC
|
||||
|
||||
+ (NSArray<NSString *> *)generateSessionCredentials
|
||||
{
|
||||
SessionCredentials creds = SessionCredentials::Generate();
|
||||
|
||||
NSString * sessionId = [NSString stringWithUTF8String:creds.sessionId.c_str()];
|
||||
NSString * encryptionKey = [NSString stringWithUTF8String:creds.encryptionKey.c_str()];
|
||||
|
||||
return @[sessionId, encryptionKey];
|
||||
}
|
||||
|
||||
+ (NSString *)generateShareUrlWithSessionId:(NSString *)sessionId
|
||||
encryptionKey:(NSString *)encryptionKey
|
||||
serverBaseUrl:(NSString *)serverBaseUrl
|
||||
{
|
||||
std::string sessionIdStr = [sessionId UTF8String];
|
||||
std::string encryptionKeyStr = [encryptionKey UTF8String];
|
||||
std::string serverBaseUrlStr = [serverBaseUrl UTF8String];
|
||||
|
||||
SessionCredentials creds(sessionIdStr, encryptionKeyStr);
|
||||
std::string shareUrl = creds.GenerateShareUrl(serverBaseUrlStr);
|
||||
|
||||
return [NSString stringWithUTF8String:shareUrl.c_str()];
|
||||
}
|
||||
|
||||
+ (NSString *)encryptPayloadWithKey:(NSString *)key plaintext:(NSString *)plaintext
|
||||
{
|
||||
std::string keyStr = [key UTF8String];
|
||||
std::string plaintextStr = [plaintext UTF8String];
|
||||
|
||||
auto encryptedOpt = crypto::EncryptAes256Gcm(keyStr, plaintextStr);
|
||||
if (!encryptedOpt.has_value())
|
||||
{
|
||||
LOG(LERROR, ("Encryption failed"));
|
||||
return nil;
|
||||
}
|
||||
|
||||
std::string resultJson = encryptedOpt->ToJson();
|
||||
return [NSString stringWithUTF8String:resultJson.c_str()];
|
||||
}
|
||||
|
||||
+ (NSString *)decryptPayloadWithKey:(NSString *)key encryptedJson:(NSString *)encryptedJson
|
||||
{
|
||||
std::string keyStr = [key UTF8String];
|
||||
std::string encryptedJsonStr = [encryptedJson UTF8String];
|
||||
|
||||
// Parse encrypted JSON
|
||||
auto encryptedPayloadOpt = EncryptedPayload::FromJson(encryptedJsonStr);
|
||||
if (!encryptedPayloadOpt.has_value())
|
||||
{
|
||||
LOG(LERROR, ("Failed to parse encrypted JSON"));
|
||||
return nil;
|
||||
}
|
||||
|
||||
auto decryptedOpt = crypto::DecryptAes256Gcm(keyStr, *encryptedPayloadOpt);
|
||||
if (!decryptedOpt.has_value())
|
||||
{
|
||||
LOG(LERROR, ("Decryption failed"));
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSString stringWithUTF8String:decryptedOpt->c_str()];
|
||||
}
|
||||
|
||||
@end
|
||||
200
iphone/Maps/Core/LocationSharing/LocationSharingNotifier.swift
Normal file
@@ -0,0 +1,200 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
import CoreLocation
|
||||
|
||||
/// Manages notifications for location sharing
|
||||
@objc class LocationSharingNotifier: NSObject {
|
||||
|
||||
@objc static let shared = LocationSharingNotifier()
|
||||
|
||||
private let notificationCenter = UNUserNotificationCenter.current()
|
||||
private var reminderTimer: Timer?
|
||||
|
||||
private static let activeNotificationId = "location_sharing_active"
|
||||
private static let reminderNotificationId = "location_sharing_reminder"
|
||||
private static let categoryId = "LOCATION_SHARING"
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
setupNotificationCategories()
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupNotificationCategories() {
|
||||
// Define stop action
|
||||
let stopAction = UNNotificationAction(
|
||||
identifier: "STOP_SHARING",
|
||||
title: "Stop Sharing",
|
||||
options: [.destructive])
|
||||
|
||||
// Define category
|
||||
let category = UNNotificationCategory(
|
||||
identifier: Self.categoryId,
|
||||
actions: [stopAction],
|
||||
intentIdentifiers: [],
|
||||
options: [])
|
||||
|
||||
notificationCenter.setNotificationCategories([category])
|
||||
}
|
||||
|
||||
// MARK: - Authorization
|
||||
|
||||
func requestAuthorization(completion: @escaping (Bool) -> Void) {
|
||||
notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
|
||||
if let error = error {
|
||||
NSLog("Notification authorization error: \(error)")
|
||||
}
|
||||
completion(granted)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Active Notification
|
||||
|
||||
/// Schedule persistent notification while sharing is active
|
||||
func scheduleActiveNotification() {
|
||||
requestAuthorization { [weak self] granted in
|
||||
guard granted else { return }
|
||||
self?.showActiveNotification()
|
||||
}
|
||||
|
||||
// Schedule reminder notifications every 10 minutes
|
||||
scheduleReminderNotifications()
|
||||
}
|
||||
|
||||
private func showActiveNotification() {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "Location Sharing Active"
|
||||
content.body = "Your live location is being shared. Tap to view or stop."
|
||||
content.sound = nil // Silent notification
|
||||
content.categoryIdentifier = Self.categoryId
|
||||
|
||||
// Add badge
|
||||
content.badge = 1
|
||||
|
||||
let request = UNNotificationRequest(
|
||||
identifier: Self.activeNotificationId,
|
||||
content: content,
|
||||
trigger: nil) // Immediate
|
||||
|
||||
notificationCenter.add(request) { error in
|
||||
if let error = error {
|
||||
NSLog("Failed to schedule active notification: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update notification with current location info
|
||||
func updateNotification(location: CLLocation) {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "Location Sharing Active"
|
||||
|
||||
// Format accuracy
|
||||
let accuracyText = formatAccuracy(location.horizontalAccuracy)
|
||||
content.body = "Sharing your location (accuracy: \(accuracyText))"
|
||||
|
||||
content.sound = nil
|
||||
content.categoryIdentifier = Self.categoryId
|
||||
content.badge = 1
|
||||
|
||||
let request = UNNotificationRequest(
|
||||
identifier: Self.activeNotificationId,
|
||||
content: content,
|
||||
trigger: nil)
|
||||
|
||||
notificationCenter.add(request)
|
||||
}
|
||||
|
||||
// MARK: - Reminder Notifications
|
||||
|
||||
private func scheduleReminderNotifications() {
|
||||
// Cancel existing reminders
|
||||
notificationCenter.removePendingNotificationRequests(withIdentifiers: [Self.reminderNotificationId])
|
||||
|
||||
// Schedule timer to show reminder every 10 minutes
|
||||
reminderTimer?.invalidate()
|
||||
reminderTimer = Timer.scheduledTimer(
|
||||
withTimeInterval: 600, // 10 minutes
|
||||
repeats: true) { [weak self] _ in
|
||||
self?.showReminderNotification()
|
||||
}
|
||||
}
|
||||
|
||||
private func showReminderNotification() {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "Location Sharing Reminder"
|
||||
content.body = "Your location is still being shared. Tap to stop if needed."
|
||||
content.sound = .default
|
||||
content.categoryIdentifier = Self.categoryId
|
||||
|
||||
let request = UNNotificationRequest(
|
||||
identifier: Self.reminderNotificationId,
|
||||
content: content,
|
||||
trigger: nil)
|
||||
|
||||
notificationCenter.add(request) { error in
|
||||
if let error = error {
|
||||
NSLog("Failed to schedule reminder notification: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cancel
|
||||
|
||||
func cancelNotifications() {
|
||||
// Cancel all location sharing notifications
|
||||
notificationCenter.removePendingNotificationRequests(
|
||||
withIdentifiers: [Self.activeNotificationId, Self.reminderNotificationId])
|
||||
|
||||
notificationCenter.removeDeliveredNotifications(
|
||||
withIdentifiers: [Self.activeNotificationId, Self.reminderNotificationId])
|
||||
|
||||
// Stop reminder timer
|
||||
reminderTimer?.invalidate()
|
||||
reminderTimer = nil
|
||||
|
||||
// Clear badge
|
||||
UIApplication.shared.applicationIconBadgeNumber = 0
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func formatAccuracy(_ accuracy: Double) -> String {
|
||||
if accuracy < 10 {
|
||||
return "high"
|
||||
} else if accuracy < 50 {
|
||||
return "medium"
|
||||
} else {
|
||||
return "low"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Live Activities (iOS 16.1+)
|
||||
|
||||
#if canImport(ActivityKit)
|
||||
import ActivityKit
|
||||
|
||||
@available(iOS 16.1, *)
|
||||
extension LocationSharingNotifier {
|
||||
|
||||
/// Start Live Activity for location sharing
|
||||
func startLiveActivity() {
|
||||
// TODO: Implement Live Activity
|
||||
// This would show persistent UI on lock screen and Dynamic Island
|
||||
// Requires defining ActivityAttributes and ActivityConfiguration
|
||||
NSLog("Live Activity support would be implemented here for iOS 16.1+")
|
||||
}
|
||||
|
||||
/// Update Live Activity with current location
|
||||
func updateLiveActivity(location: CLLocation, eta: TimeInterval?, distance: Int?) {
|
||||
// TODO: Update Live Activity content
|
||||
}
|
||||
|
||||
/// End Live Activity
|
||||
func endLiveActivity() {
|
||||
// TODO: End Live Activity
|
||||
}
|
||||
}
|
||||
#endif
|
||||
252
iphone/Maps/Core/LocationSharing/LocationSharingService.swift
Normal file
@@ -0,0 +1,252 @@
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
import UIKit
|
||||
|
||||
/// Service managing the location sharing lifecycle
|
||||
@objc class LocationSharingService: NSObject {
|
||||
|
||||
@objc static let shared = LocationSharingService()
|
||||
|
||||
private var locationManager: CLLocationManager?
|
||||
private var apiClient: LocationSharingApiClient?
|
||||
private let session = LocationSharingSession.shared
|
||||
private var updateTimer: Timer?
|
||||
|
||||
// Battery monitoring
|
||||
private var batteryLevelObserver: NSObjectProtocol?
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
setupSession()
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopSharing()
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
/// Start sharing location
|
||||
@objc func startSharing() -> String? {
|
||||
let config = LocationSharingConfig()
|
||||
|
||||
guard let shareUrl = session.start(with: config) else {
|
||||
NSLog("Failed to start location sharing session")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize API client
|
||||
guard let credentials = session.credentials else {
|
||||
return nil
|
||||
}
|
||||
|
||||
apiClient = LocationSharingApiClient(
|
||||
serverBaseUrl: config.serverBaseUrl,
|
||||
sessionId: credentials.sessionId)
|
||||
|
||||
// Request location authorization if needed
|
||||
setupLocationManager()
|
||||
requestLocationAuthorization()
|
||||
|
||||
// Start location updates
|
||||
startLocationUpdates()
|
||||
|
||||
// Setup battery monitoring
|
||||
setupBatteryMonitoring()
|
||||
|
||||
// Send session creation to server
|
||||
apiClient?.createSession()
|
||||
|
||||
NSLog("Location sharing service started")
|
||||
|
||||
return shareUrl
|
||||
}
|
||||
|
||||
/// Stop sharing location
|
||||
@objc func stopSharing() {
|
||||
// Stop location updates
|
||||
stopLocationUpdates()
|
||||
|
||||
// Stop battery monitoring
|
||||
stopBatteryMonitoring()
|
||||
|
||||
// End session on server
|
||||
apiClient?.endSession()
|
||||
apiClient = nil
|
||||
|
||||
// Stop session
|
||||
session.stop()
|
||||
|
||||
NSLog("Location sharing service stopped")
|
||||
}
|
||||
|
||||
/// Check if currently sharing
|
||||
@objc var isSharing: Bool {
|
||||
return session.state == .active
|
||||
}
|
||||
|
||||
/// Get current share URL
|
||||
@objc var shareUrl: String? {
|
||||
return session.shareUrl
|
||||
}
|
||||
|
||||
// MARK: - Location Management
|
||||
|
||||
private func setupLocationManager() {
|
||||
if locationManager == nil {
|
||||
locationManager = CLLocationManager()
|
||||
locationManager?.delegate = self
|
||||
locationManager?.desiredAccuracy = kCLLocationAccuracyBest
|
||||
locationManager?.allowsBackgroundLocationUpdates = true
|
||||
locationManager?.pausesLocationUpdatesAutomatically = false
|
||||
locationManager?.showsBackgroundLocationIndicator = true
|
||||
}
|
||||
}
|
||||
|
||||
private func requestLocationAuthorization() {
|
||||
guard let manager = locationManager else { return }
|
||||
|
||||
let status = CLLocationManager.authorizationStatus()
|
||||
|
||||
switch status {
|
||||
case .notDetermined:
|
||||
manager.requestWhenInUseAuthorization()
|
||||
case .authorizedWhenInUse:
|
||||
manager.requestAlwaysAuthorization()
|
||||
case .authorizedAlways:
|
||||
break
|
||||
case .denied, .restricted:
|
||||
NSLog("Location authorization denied or restricted")
|
||||
session.onError?("Location permission required")
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func startLocationUpdates() {
|
||||
locationManager?.startUpdatingLocation()
|
||||
|
||||
// Also monitor significant location changes for battery efficiency
|
||||
locationManager?.startMonitoringSignificantLocationChanges()
|
||||
}
|
||||
|
||||
private func stopLocationUpdates() {
|
||||
locationManager?.stopUpdatingLocation()
|
||||
locationManager?.stopMonitoringSignificantLocationChanges()
|
||||
}
|
||||
|
||||
// MARK: - Battery Monitoring
|
||||
|
||||
private func setupBatteryMonitoring() {
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
|
||||
batteryLevelObserver = NotificationCenter.default.addObserver(
|
||||
forName: UIDevice.batteryLevelDidChangeNotification,
|
||||
object: nil,
|
||||
queue: .main) { [weak self] _ in
|
||||
self?.updateBatteryLevel()
|
||||
}
|
||||
|
||||
// Initial battery level
|
||||
updateBatteryLevel()
|
||||
}
|
||||
|
||||
private func stopBatteryMonitoring() {
|
||||
if let observer = batteryLevelObserver {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
batteryLevelObserver = nil
|
||||
}
|
||||
|
||||
UIDevice.current.isBatteryMonitoringEnabled = false
|
||||
}
|
||||
|
||||
private func updateBatteryLevel() {
|
||||
let level = Int(UIDevice.current.batteryLevel * 100)
|
||||
if level >= 0 {
|
||||
session.updateBatteryLevel(level)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Session Setup
|
||||
|
||||
private func setupSession() {
|
||||
session.onStateChange = { [weak self] state in
|
||||
self?.handleStateChange(state)
|
||||
}
|
||||
|
||||
session.onError = { error in
|
||||
NSLog("Location sharing error: \(error)")
|
||||
}
|
||||
|
||||
session.onPayloadReady = { [weak self] data in
|
||||
self?.sendPayloadToServer(data)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleStateChange(_ state: LocationSharingState) {
|
||||
switch state {
|
||||
case .active:
|
||||
LocationSharingNotifier.shared.scheduleActiveNotification()
|
||||
case .inactive:
|
||||
LocationSharingNotifier.shared.cancelNotifications()
|
||||
case .error:
|
||||
LocationSharingNotifier.shared.cancelNotifications()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Server Communication
|
||||
|
||||
private func sendPayloadToServer(_ data: Data) {
|
||||
guard let jsonString = String(data: data, encoding: .utf8) else {
|
||||
NSLog("Failed to convert payload to string")
|
||||
return
|
||||
}
|
||||
|
||||
apiClient?.updateLocation(encryptedPayload: jsonString) { success, error in
|
||||
if success {
|
||||
NSLog("Location update sent successfully")
|
||||
} else {
|
||||
NSLog("Failed to send location update: \(error ?? "unknown error")")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CLLocationManagerDelegate
|
||||
|
||||
extension LocationSharingService: CLLocationManagerDelegate {
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let location = locations.last else { return }
|
||||
|
||||
// Update session with new location
|
||||
session.updateLocation(location)
|
||||
|
||||
// Update notification with current location
|
||||
LocationSharingNotifier.shared.updateNotification(location: location)
|
||||
|
||||
// TODO: Check if navigation is active and update navigation info
|
||||
// This would integrate with the routing framework
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
NSLog("Location manager error: \(error.localizedDescription)")
|
||||
session.onError?(error.localizedDescription)
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
|
||||
switch status {
|
||||
case .authorizedAlways, .authorizedWhenInUse:
|
||||
if isSharing {
|
||||
startLocationUpdates()
|
||||
}
|
||||
case .denied, .restricted:
|
||||
session.onError?("Location permission denied")
|
||||
stopSharing()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
317
iphone/Maps/Core/LocationSharing/LocationSharingSession.swift
Normal file
@@ -0,0 +1,317 @@
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
|
||||
/// Session state for location sharing
|
||||
@objc enum LocationSharingState: Int {
|
||||
case inactive
|
||||
case starting
|
||||
case active
|
||||
case paused
|
||||
case stopping
|
||||
case error
|
||||
}
|
||||
|
||||
/// Mode for location sharing
|
||||
enum LocationSharingMode {
|
||||
case standalone // GPS only
|
||||
case navigation // GPS + ETA + distance
|
||||
}
|
||||
|
||||
/// Configuration for location sharing session
|
||||
struct LocationSharingConfig {
|
||||
var updateIntervalSeconds: Int = 20
|
||||
var includeDestinationName: Bool = true
|
||||
var includeBatteryLevel: Bool = true
|
||||
var lowBatteryThreshold: Int = 10
|
||||
// Default from LOCATION_SHARING_SERVER_URL in private.h
|
||||
// Can be overridden by user settings
|
||||
var serverBaseUrl: String = "https://live.organicmaps.app"
|
||||
}
|
||||
|
||||
/// Session credentials
|
||||
struct LocationSharingCredentials {
|
||||
let sessionId: String
|
||||
let encryptionKey: String
|
||||
|
||||
/// Generate share URL from credentials
|
||||
func generateShareUrl(serverBaseUrl: String) -> String {
|
||||
let combined = "\(sessionId):\(encryptionKey)"
|
||||
guard let data = combined.data(using: .utf8) else { return "" }
|
||||
let base64 = data.base64EncodedString()
|
||||
.replacingOccurrences(of: "+", with: "-")
|
||||
.replacingOccurrences(of: "/", with: "_")
|
||||
.replacingOccurrences(of: "=", with: "")
|
||||
|
||||
var url = serverBaseUrl
|
||||
if !url.hasSuffix("/") {
|
||||
url += "/"
|
||||
}
|
||||
url += "live/\(base64)"
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
/// Generate new random credentials
|
||||
static func generate() -> LocationSharingCredentials {
|
||||
let sessionId = UUID().uuidString
|
||||
let encryptionKey = Self.generateRandomKey()
|
||||
return LocationSharingCredentials(sessionId: sessionId, encryptionKey: encryptionKey)
|
||||
}
|
||||
|
||||
private static func generateRandomKey() -> String {
|
||||
var bytes = [UInt8](repeating: 0, count: 32)
|
||||
_ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
|
||||
return Data(bytes).base64EncodedString()
|
||||
}
|
||||
}
|
||||
|
||||
/// Location payload structure
|
||||
struct LocationPayload {
|
||||
var timestamp: TimeInterval
|
||||
var latitude: Double
|
||||
var longitude: Double
|
||||
var accuracy: Double
|
||||
var speed: Double?
|
||||
var bearing: Double?
|
||||
var mode: LocationSharingMode = .standalone
|
||||
var eta: TimeInterval?
|
||||
var distanceRemaining: Int?
|
||||
var destinationName: String?
|
||||
var batteryLevel: Int?
|
||||
|
||||
init(location: CLLocation) {
|
||||
self.timestamp = Date().timeIntervalSince1970
|
||||
self.latitude = location.coordinate.latitude
|
||||
self.longitude = location.coordinate.longitude
|
||||
self.accuracy = location.horizontalAccuracy
|
||||
|
||||
if location.speed >= 0 {
|
||||
self.speed = location.speed
|
||||
}
|
||||
|
||||
if location.course >= 0 {
|
||||
self.bearing = location.course
|
||||
}
|
||||
}
|
||||
|
||||
func toJSON() -> [String: Any] {
|
||||
var json: [String: Any] = [
|
||||
"timestamp": Int(timestamp),
|
||||
"lat": latitude,
|
||||
"lon": longitude,
|
||||
"accuracy": accuracy
|
||||
]
|
||||
|
||||
if let speed = speed {
|
||||
json["speed"] = speed
|
||||
}
|
||||
|
||||
if let bearing = bearing {
|
||||
json["bearing"] = bearing
|
||||
}
|
||||
|
||||
json["mode"] = mode == .navigation ? "navigation" : "standalone"
|
||||
|
||||
if mode == .navigation {
|
||||
if let eta = eta {
|
||||
json["eta"] = Int(eta)
|
||||
}
|
||||
if let distanceRemaining = distanceRemaining {
|
||||
json["distanceRemaining"] = distanceRemaining
|
||||
}
|
||||
if let destinationName = destinationName {
|
||||
json["destinationName"] = destinationName
|
||||
}
|
||||
}
|
||||
|
||||
if let batteryLevel = batteryLevel {
|
||||
json["batteryLevel"] = batteryLevel
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
/// Main location sharing session manager
|
||||
@objc class LocationSharingSession: NSObject {
|
||||
|
||||
// Singleton instance
|
||||
@objc static let shared = LocationSharingSession()
|
||||
|
||||
// State
|
||||
private(set) var state: LocationSharingState = .inactive
|
||||
private(set) var credentials: LocationSharingCredentials?
|
||||
private(set) var config: LocationSharingConfig = LocationSharingConfig()
|
||||
private(set) var shareUrl: String?
|
||||
|
||||
// Current payload
|
||||
private var currentPayload: LocationPayload?
|
||||
private var lastUpdateTimestamp: TimeInterval = 0
|
||||
|
||||
// Callbacks
|
||||
var onStateChange: ((LocationSharingState) -> Void)?
|
||||
var onError: ((String) -> Void)?
|
||||
var onPayloadReady: ((Data) -> Void)?
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
/// Start location sharing session
|
||||
@objc func start(with config: LocationSharingConfig) -> String? {
|
||||
if state != .inactive {
|
||||
NSLog("Location sharing already active, stopping previous session")
|
||||
stop()
|
||||
}
|
||||
|
||||
setState(.starting)
|
||||
|
||||
self.config = config
|
||||
self.credentials = LocationSharingCredentials.generate()
|
||||
self.lastUpdateTimestamp = 0
|
||||
|
||||
guard let credentials = self.credentials else {
|
||||
onError?("Failed to generate credentials")
|
||||
setState(.error)
|
||||
return nil
|
||||
}
|
||||
|
||||
self.shareUrl = credentials.generateShareUrl(serverBaseUrl: config.serverBaseUrl)
|
||||
|
||||
NSLog("Location sharing session started: \(credentials.sessionId)")
|
||||
setState(.active)
|
||||
|
||||
return shareUrl
|
||||
}
|
||||
|
||||
/// Stop location sharing session
|
||||
@objc func stop() {
|
||||
if state == .inactive {
|
||||
return
|
||||
}
|
||||
|
||||
setState(.stopping)
|
||||
|
||||
NSLog("Location sharing session stopped")
|
||||
|
||||
currentPayload = nil
|
||||
credentials = nil
|
||||
shareUrl = nil
|
||||
lastUpdateTimestamp = 0
|
||||
|
||||
setState(.inactive)
|
||||
}
|
||||
|
||||
/// Update location
|
||||
func updateLocation(_ location: CLLocation) {
|
||||
guard state == .active else {
|
||||
NSLog("Cannot update location - session not active")
|
||||
return
|
||||
}
|
||||
|
||||
currentPayload = LocationPayload(location: location)
|
||||
processLocationUpdate()
|
||||
}
|
||||
|
||||
/// Update navigation info
|
||||
func updateNavigationInfo(eta: TimeInterval, distanceRemaining: Int, destinationName: String?) {
|
||||
guard state == .active, currentPayload != nil else { return }
|
||||
|
||||
currentPayload?.mode = .navigation
|
||||
currentPayload?.eta = eta
|
||||
currentPayload?.distanceRemaining = distanceRemaining
|
||||
|
||||
if config.includeDestinationName, let name = destinationName {
|
||||
currentPayload?.destinationName = name
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear navigation info
|
||||
func clearNavigationInfo() {
|
||||
currentPayload?.mode = .standalone
|
||||
currentPayload?.eta = nil
|
||||
currentPayload?.distanceRemaining = nil
|
||||
currentPayload?.destinationName = nil
|
||||
}
|
||||
|
||||
/// Update battery level
|
||||
func updateBatteryLevel(_ level: Int) {
|
||||
guard state == .active else { return }
|
||||
|
||||
if config.includeBatteryLevel {
|
||||
currentPayload?.batteryLevel = level
|
||||
}
|
||||
|
||||
// Stop if battery too low
|
||||
if level < config.lowBatteryThreshold {
|
||||
NSLog("Battery level too low (\(level)%), stopping location sharing")
|
||||
onError?("Battery level too low")
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
private func setState(_ newState: LocationSharingState) {
|
||||
if state == newState {
|
||||
return
|
||||
}
|
||||
|
||||
NSLog("Location sharing state: \(state.rawValue) -> \(newState.rawValue)")
|
||||
state = newState
|
||||
onStateChange?(newState)
|
||||
}
|
||||
|
||||
private func processLocationUpdate() {
|
||||
guard shouldSendUpdate() else { return }
|
||||
|
||||
guard let encryptedData = createEncryptedPayload() else {
|
||||
onError?("Failed to create encrypted payload")
|
||||
return
|
||||
}
|
||||
|
||||
lastUpdateTimestamp = Date().timeIntervalSince1970
|
||||
onPayloadReady?(encryptedData)
|
||||
}
|
||||
|
||||
private func shouldSendUpdate() -> Bool {
|
||||
guard currentPayload != nil else { return false }
|
||||
|
||||
let now = Date().timeIntervalSince1970
|
||||
let timeSinceLastUpdate = now - lastUpdateTimestamp
|
||||
|
||||
return timeSinceLastUpdate >= Double(config.updateIntervalSeconds)
|
||||
}
|
||||
|
||||
private func createEncryptedPayload() -> Data? {
|
||||
guard let payload = currentPayload,
|
||||
let credentials = credentials else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let json = payload.toJSON()
|
||||
|
||||
guard let jsonData = try? JSONSerialization.data(withJSONObject: json),
|
||||
let jsonString = String(data: jsonData, encoding: .utf8) else {
|
||||
NSLog("Failed to serialize payload to JSON")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Call native encryption (via bridge)
|
||||
guard let encryptedJson = LocationSharingBridge.encryptPayload(
|
||||
key: credentials.encryptionKey,
|
||||
plaintext: jsonString) else {
|
||||
NSLog("Encryption failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
return encryptedJson.data(using: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
/// Swift wrapper for LocationSharingBridgeObjC
|
||||
extension LocationSharingBridge {
|
||||
static func encryptPayload(key: String, plaintext: String) -> String? {
|
||||
return LocationSharingBridgeObjC.encryptPayload(withKey: key, plaintext: plaintext)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/* Location Sharing */
|
||||
|
||||
/* Title for location sharing feature */
|
||||
"location_sharing_title" = "Live Location Sharing";
|
||||
|
||||
/* Start sharing button */
|
||||
"location_sharing_start" = "Start Sharing";
|
||||
|
||||
/* Stop sharing button */
|
||||
"location_sharing_stop" = "Stop Sharing";
|
||||
|
||||
/* Location sharing active status */
|
||||
"location_sharing_active" = "Sharing location";
|
||||
|
||||
/* Status message when sharing is active */
|
||||
"location_sharing_status_active" = "Your location is being shared";
|
||||
|
||||
/* Status message when sharing is inactive */
|
||||
"location_sharing_status_inactive" = "Location sharing is not active";
|
||||
|
||||
/* Description text explaining the feature */
|
||||
"location_sharing_description" = "Share your real-time location with end-to-end encryption. Only people with the link can view your location.";
|
||||
|
||||
/* Copy link button */
|
||||
"location_sharing_copy_url" = "Copy Link";
|
||||
|
||||
/* Share link button */
|
||||
"location_sharing_share_url" = "Share Link";
|
||||
|
||||
/* Close button */
|
||||
"close" = "Close";
|
||||
|
||||
/* Success message when sharing started */
|
||||
"location_sharing_started" = "Location sharing started";
|
||||
|
||||
/* Success message when sharing stopped */
|
||||
"location_sharing_stopped" = "Location sharing stopped";
|
||||
|
||||
/* Error message when failed to start */
|
||||
"location_sharing_failed_to_start" = "Failed to start location sharing";
|
||||
|
||||
/* Success message when URL copied */
|
||||
"location_sharing_url_copied" = "Share URL copied to clipboard";
|
||||
|
||||
/* Share message template */
|
||||
"location_sharing_share_message" = "Follow my live location: %@";
|
||||
|
||||
/* Notification title */
|
||||
"location_sharing_notification_title" = "Location Sharing Active";
|
||||
|
||||
/* Notification body */
|
||||
"location_sharing_notification_body" = "Your live location is being shared. Tap to view or stop.";
|
||||
|
||||
/* Reminder notification title */
|
||||
"location_sharing_reminder_title" = "Location Sharing Reminder";
|
||||
|
||||
/* Reminder notification body */
|
||||
"location_sharing_reminder_body" = "Your location is still being shared. Tap to stop if needed.";
|
||||
@@ -14,7 +14,6 @@
|
||||
270C9C282E16AB6F00ABA688 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 270C9C252E16AB6300ABA688 /* Profile.swift */; };
|
||||
27176A862E65B0150015F25F /* Icon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 27176A852E65B0150015F25F /* Icon.icon */; };
|
||||
27176A8A2E65B01B0015F25F /* Debug Icon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 27176A892E65B01B0015F25F /* Debug Icon.icon */; };
|
||||
272CA4492F127221005A3F5B /* subtypes.csv in Resources */ = {isa = PBXBuildFile; fileRef = 272CA4482F127221005A3F5B /* subtypes.csv */; };
|
||||
272F1F392E0EE09000FA52EF /* ExistingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272F1F382E0EE08A00FA52EF /* ExistingProfileView.swift */; };
|
||||
272F1F3B2E0EE0A300FA52EF /* NoExistingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272F1F3A2E0EE09500FA52EF /* NoExistingProfileView.swift */; };
|
||||
272F1F3D2E0EE0C800FA52EF /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272F1F3C2E0EE0C400FA52EF /* ProfileView.swift */; };
|
||||
@@ -765,7 +764,6 @@
|
||||
270C9C252E16AB6300ABA688 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = "<group>"; };
|
||||
27176A852E65B0150015F25F /* Icon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = Icon.icon; sourceTree = "<group>"; };
|
||||
27176A892E65B01B0015F25F /* Debug Icon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = "Debug Icon.icon"; sourceTree = "<group>"; };
|
||||
272CA4482F127221005A3F5B /* subtypes.csv */ = {isa = PBXFileReference; lastKnownFileType = text; name = subtypes.csv; path = ../../data/subtypes.csv; sourceTree = SOURCE_ROOT; };
|
||||
272F1F382E0EE08A00FA52EF /* ExistingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExistingProfileView.swift; sourceTree = "<group>"; };
|
||||
272F1F3A2E0EE09500FA52EF /* NoExistingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoExistingProfileView.swift; sourceTree = "<group>"; };
|
||||
272F1F3C2E0EE0C400FA52EF /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
|
||||
@@ -3956,7 +3954,6 @@
|
||||
F623DA6A1C9C2731006A3436 /* opening_hours_how_to_edit.html */,
|
||||
FA85F632145DDDC20090E1A0 /* packed_polygons.bin */,
|
||||
451950391B7A3E070085DA05 /* patterns.txt */,
|
||||
272CA4482F127221005A3F5B /* subtypes.csv */,
|
||||
FAAEA7D0161BD26600CCD661 /* synonyms.txt */,
|
||||
BB25B1A51FB32767007276FA /* transit_colors.txt */,
|
||||
FA64D9A813F975AD00350ECF /* types.txt */,
|
||||
@@ -4284,7 +4281,6 @@
|
||||
34AB66681FC5AA330078E451 /* TransportTransitPedestrian.xib in Resources */,
|
||||
F6D67CDE2062BBA60032FD38 /* MWMBCCreateCategoryAlert.xib in Resources */,
|
||||
3490D2E31CE9DD2500D0B838 /* MWMSideButtonsView.xib in Resources */,
|
||||
272CA4492F127221005A3F5B /* subtypes.csv in Resources */,
|
||||
F6E2FE2E1E097BA00083EBEC /* MWMStreetEditorEditTableViewCell.xib in Resources */,
|
||||
3463BA691DE81DB90082417F /* MWMTrafficButtonViewController.xib in Resources */,
|
||||
F623DA6C1C9C2731006A3436 /* opening_hours_how_to_edit.html in Resources */,
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
import UIKit
|
||||
|
||||
/// View controller for managing live location sharing
|
||||
@objc class LocationSharingViewController: UIViewController {
|
||||
|
||||
private let service = LocationSharingService.shared
|
||||
|
||||
// UI Components
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentView = UIView()
|
||||
private let statusLabel = UILabel()
|
||||
private let descriptionLabel = UILabel()
|
||||
private let urlTextView = UITextView()
|
||||
private let startStopButton = UIButton(type: .system)
|
||||
private let copyButton = UIButton(type: .system)
|
||||
private let shareButton = UIButton(type: .system)
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = NSLocalizedString("location_sharing_title", comment: "")
|
||||
view.backgroundColor = .systemBackground
|
||||
|
||||
setupUI()
|
||||
updateUI()
|
||||
}
|
||||
|
||||
// MARK: - UI Setup
|
||||
|
||||
private func setupUI() {
|
||||
// Setup scroll view
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(scrollView)
|
||||
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
scrollView.addSubview(contentView)
|
||||
|
||||
// Status label
|
||||
statusLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
statusLabel.textColor = .label
|
||||
statusLabel.numberOfLines = 0
|
||||
statusLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(statusLabel)
|
||||
|
||||
// Description label
|
||||
descriptionLabel.font = .systemFont(ofSize: 14)
|
||||
descriptionLabel.textColor = .secondaryLabel
|
||||
descriptionLabel.numberOfLines = 0
|
||||
descriptionLabel.text = NSLocalizedString("location_sharing_description", comment: "")
|
||||
descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(descriptionLabel)
|
||||
|
||||
// URL text view
|
||||
urlTextView.font = .monospacedSystemFont(ofSize: 12, weight: .regular)
|
||||
urlTextView.textColor = .label
|
||||
urlTextView.backgroundColor = .secondarySystemBackground
|
||||
urlTextView.layer.cornerRadius = 8
|
||||
urlTextView.isEditable = false
|
||||
urlTextView.isScrollEnabled = false
|
||||
urlTextView.textContainerInset = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
|
||||
urlTextView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(urlTextView)
|
||||
|
||||
// Buttons
|
||||
setupButtons()
|
||||
|
||||
// Constraints
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
private func setupButtons() {
|
||||
// Start/Stop button
|
||||
startStopButton.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
startStopButton.addTarget(self, action: #selector(startStopTapped), for: .touchUpInside)
|
||||
startStopButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(startStopButton)
|
||||
|
||||
// Copy button
|
||||
copyButton.setTitle(NSLocalizedString("location_sharing_copy_url", comment: ""), for: .normal)
|
||||
copyButton.addTarget(self, action: #selector(copyTapped), for: .touchUpInside)
|
||||
copyButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(copyButton)
|
||||
|
||||
// Share button
|
||||
shareButton.setTitle(NSLocalizedString("location_sharing_share_url", comment: ""), for: .normal)
|
||||
shareButton.addTarget(self, action: #selector(shareTapped), for: .touchUpInside)
|
||||
shareButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(shareButton)
|
||||
}
|
||||
|
||||
private func setupConstraints() {
|
||||
NSLayoutConstraint.activate([
|
||||
// Scroll view
|
||||
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
|
||||
// Content view
|
||||
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
|
||||
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
|
||||
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
|
||||
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
|
||||
|
||||
// Status label
|
||||
statusLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
|
||||
statusLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
statusLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
|
||||
// Description label
|
||||
descriptionLabel.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 8),
|
||||
descriptionLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
descriptionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
|
||||
// URL text view
|
||||
urlTextView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 16),
|
||||
urlTextView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
urlTextView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
|
||||
// Start/Stop button
|
||||
startStopButton.topAnchor.constraint(equalTo: urlTextView.bottomAnchor, constant: 24),
|
||||
startStopButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
startStopButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
startStopButton.heightAnchor.constraint(equalToConstant: 50),
|
||||
|
||||
// Copy button
|
||||
copyButton.topAnchor.constraint(equalTo: startStopButton.bottomAnchor, constant: 12),
|
||||
copyButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
copyButton.heightAnchor.constraint(equalToConstant: 44),
|
||||
|
||||
// Share button
|
||||
shareButton.topAnchor.constraint(equalTo: startStopButton.bottomAnchor, constant: 12),
|
||||
shareButton.leadingAnchor.constraint(equalTo: copyButton.trailingAnchor, constant: 12),
|
||||
shareButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
shareButton.widthAnchor.constraint(equalTo: copyButton.widthAnchor),
|
||||
shareButton.heightAnchor.constraint(equalToConstant: 44),
|
||||
shareButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16),
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Update UI
|
||||
|
||||
private func updateUI() {
|
||||
let isSharing = service.isSharing
|
||||
|
||||
// Update status
|
||||
statusLabel.text = isSharing
|
||||
? NSLocalizedString("location_sharing_status_active", comment: "")
|
||||
: NSLocalizedString("location_sharing_status_inactive", comment: "")
|
||||
|
||||
// Update URL
|
||||
if let shareUrl = service.shareUrl, isSharing {
|
||||
urlTextView.text = shareUrl
|
||||
urlTextView.isHidden = false
|
||||
} else {
|
||||
urlTextView.isHidden = true
|
||||
}
|
||||
|
||||
// Update buttons
|
||||
let startStopTitle = isSharing
|
||||
? NSLocalizedString("location_sharing_stop", comment: "")
|
||||
: NSLocalizedString("location_sharing_start", comment: "")
|
||||
startStopButton.setTitle(startStopTitle, for: .normal)
|
||||
startStopButton.backgroundColor = isSharing ? .systemRed : .systemBlue
|
||||
startStopButton.setTitleColor(.white, for: .normal)
|
||||
startStopButton.layer.cornerRadius = 8
|
||||
|
||||
copyButton.isHidden = !isSharing
|
||||
shareButton.isHidden = !isSharing
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func startStopTapped() {
|
||||
if service.isSharing {
|
||||
stopSharing()
|
||||
} else {
|
||||
startSharing()
|
||||
}
|
||||
}
|
||||
|
||||
private func startSharing() {
|
||||
guard let shareUrl = service.startSharing() else {
|
||||
showAlert(message: NSLocalizedString("location_sharing_failed_to_start", comment: ""))
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-copy URL to clipboard
|
||||
UIPasteboard.general.string = shareUrl
|
||||
|
||||
showAlert(message: NSLocalizedString("location_sharing_started", comment: ""))
|
||||
updateUI()
|
||||
}
|
||||
|
||||
private func stopSharing() {
|
||||
service.stopSharing()
|
||||
showAlert(message: NSLocalizedString("location_sharing_stopped", comment: ""))
|
||||
updateUI()
|
||||
}
|
||||
|
||||
@objc private func copyTapped() {
|
||||
guard let shareUrl = service.shareUrl else { return }
|
||||
|
||||
UIPasteboard.general.string = shareUrl
|
||||
showAlert(message: NSLocalizedString("location_sharing_url_copied", comment: ""))
|
||||
}
|
||||
|
||||
@objc private func shareTapped() {
|
||||
guard let shareUrl = service.shareUrl else { return }
|
||||
|
||||
let message = String(format: NSLocalizedString("location_sharing_share_message", comment: ""), shareUrl)
|
||||
let activityVC = UIActivityViewController(activityItems: [message], applicationActivities: nil)
|
||||
activityVC.popoverPresentationController?.sourceView = shareButton
|
||||
present(activityVC, animated: true)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func showAlert(message: String) {
|
||||
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("close", comment: ""), style: .default))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ add_subdirectory(mwm_diff)
|
||||
add_subdirectory(geometry)
|
||||
add_subdirectory(indexer)
|
||||
add_subdirectory(kml)
|
||||
add_subdirectory(location_sharing)
|
||||
add_subdirectory(map)
|
||||
add_subdirectory(cppjansson)
|
||||
add_subdirectory(platform)
|
||||
|
||||
@@ -88,8 +88,6 @@ set(SRC
|
||||
ftraits.hpp
|
||||
ftypes_matcher.cpp
|
||||
ftypes_matcher.hpp
|
||||
ftypes_subtypes.cpp
|
||||
ftypes_subtypes.hpp
|
||||
house_to_street_iface.hpp
|
||||
index_builder.cpp
|
||||
index_builder.hpp
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "indexer/classificator.hpp"
|
||||
#include "indexer/feature.hpp"
|
||||
#include "indexer/ftypes_matcher.hpp"
|
||||
#include "indexer/ftypes_subtypes.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "base/macros.hpp"
|
||||
@@ -192,13 +191,25 @@ void TypesHolder::SortBySpec()
|
||||
auto const getPriority = [&cl](uint32_t type) { return cl.GetObject(type)->GetMaxOverlaysPriority(); };
|
||||
|
||||
auto const & checker = UselessTypesChecker::Instance();
|
||||
auto const & subtypes = ftypes::Subtypes::Instance();
|
||||
auto const & isChargingStationChecker = ftypes::IsCharingStationChecker::Instance();
|
||||
auto const & isChargingStationSmallChecker = ftypes::IsCharingStationSmallChecker::Instance();
|
||||
|
||||
std::stable_sort(begin(), end(), [&checker, &getPriority, &subtypes](uint32_t t1, uint32_t t2)
|
||||
std::stable_sort(begin(), end(), [&checker, &getPriority, &isChargingStationChecker, &isChargingStationSmallChecker](uint32_t t1, uint32_t t2)
|
||||
{
|
||||
std::optional<bool> const comaprisonResultBasedOnTypeRelation = subtypes.ComaprisonResultBasedOnTypeRelation(t1, t2);
|
||||
if (comaprisonResultBasedOnTypeRelation.has_value())
|
||||
return comaprisonResultBasedOnTypeRelation.value();
|
||||
if (isChargingStationChecker(t1) && isChargingStationChecker(t2))
|
||||
{
|
||||
if (isChargingStationSmallChecker(t1) && !isChargingStationSmallChecker(t2))
|
||||
return false;
|
||||
else if (!isChargingStationSmallChecker(t1) && isChargingStationSmallChecker(t2))
|
||||
return true;
|
||||
|
||||
uint8_t const t1Level = ftype::GetLevel(t1);
|
||||
uint8_t const t2Level = ftype::GetLevel(t2);
|
||||
if (t1Level == 2 && t2Level != 2)
|
||||
return true;
|
||||
else if (t1Level != 2 && t2Level == 2)
|
||||
return false;
|
||||
}
|
||||
|
||||
int const p1 = getPriority(t1);
|
||||
int const p2 = getPriority(t2);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "indexer/feature.hpp"
|
||||
#include "indexer/feature_data.hpp"
|
||||
#include "indexer/ftypes_matcher.hpp"
|
||||
#include "indexer/ftypes_subtypes.hpp"
|
||||
#include "indexer/scales.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
@@ -136,9 +135,6 @@ bool TypeAlwaysExists(uint32_t type, GeomType geomType = GeomType::Undefined)
|
||||
if (IsUsefulStandaloneType(type, geomType))
|
||||
return true;
|
||||
|
||||
if (ftypes::Subtypes::Instance().IsTypeWithSubtypesOrSubtype(type))
|
||||
return true;
|
||||
|
||||
uint8_t const typeLevel = ftype::GetLevel(type);
|
||||
ftype::TruncValue(type, 1);
|
||||
|
||||
|
||||
@@ -753,6 +753,12 @@ IsDirectionalChecker::IsDirectionalChecker() : ftypes::BaseChecker(1 /* level */
|
||||
m_types.push_back(c.GetTypeByPath({"lateral"}));
|
||||
}
|
||||
|
||||
IsCharingStationChecker::IsCharingStationChecker() : ftypes::BaseChecker(2 /* level */)
|
||||
{
|
||||
Classificator const & c = classif();
|
||||
m_types.push_back(c.GetTypeByPath({"amenity", "charging_station"}));
|
||||
}
|
||||
|
||||
IsCharingStationCarChecker::IsCharingStationCarChecker() : ftypes::BaseChecker(3 /* level */)
|
||||
{
|
||||
Classificator const & c = classif();
|
||||
|
||||
@@ -519,6 +519,14 @@ public:
|
||||
DECLARE_CHECKER_INSTANCE(IsDirectionalChecker);
|
||||
};
|
||||
|
||||
class IsCharingStationChecker : public ftypes::BaseChecker
|
||||
{
|
||||
IsCharingStationChecker();
|
||||
|
||||
public:
|
||||
DECLARE_CHECKER_INSTANCE(IsCharingStationChecker);
|
||||
};
|
||||
|
||||
class IsCharingStationCarChecker : public ftypes::BaseChecker
|
||||
{
|
||||
IsCharingStationCarChecker();
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
#include "indexer/ftypes_subtypes.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "coding/reader_streambuf.hpp"
|
||||
#include "indexer/classificator.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
namespace ftypes
|
||||
{
|
||||
/// Constructor
|
||||
Subtypes::Subtypes()
|
||||
{
|
||||
auto const & classificator = classif();
|
||||
|
||||
// Load the CSV file
|
||||
Platform & platform = GetPlatform();
|
||||
unique_ptr<ModelReader> reader = platform.GetReader("subtypes.csv");
|
||||
ReaderStreamBuf buffer(std::move(reader));
|
||||
istream stream(&buffer);
|
||||
|
||||
// Go trought the lines of the CSV file one by one
|
||||
string line;
|
||||
while (stream.good())
|
||||
{
|
||||
getline(stream, line);
|
||||
strings::Trim(line);
|
||||
|
||||
// Skip empty lines and the column headers
|
||||
if (line.empty() || line == "Types;Related Subtypes")
|
||||
continue;
|
||||
|
||||
// Separate the columns by the `;`. There only should be two columns.
|
||||
vector<string_view> const columns = strings::Tokenize(line, ";");
|
||||
if (columns.size() != 2)
|
||||
{
|
||||
ASSERT(false, ("Parsing of subtypes file: Invalid line \"", line, "\""));
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse the column. The first column has the type definitions(s) and the second one has the associated subtype definitions(s).
|
||||
vector<uint32_t> types;
|
||||
vector<uint32_t> subtypes;
|
||||
for (int columnIndex = 0; columnIndex < 2; columnIndex++) {
|
||||
string_view const column = columns[columnIndex];
|
||||
|
||||
// Separate the different type definitions by the `,`. There needs to be at least one.
|
||||
vector<string_view> const typeDefinitions = strings::Tokenize(column, ",");
|
||||
if (typeDefinitions.size() < 1)
|
||||
{
|
||||
if (columnIndex == 0)
|
||||
ASSERT(false, ("Parsing of subtypes file: Invalid or missing types definition \"", column, "\""));
|
||||
else
|
||||
ASSERT(false, ("Parsing of subtypes file: Invalid or missing subtypes definition \"", column, "\""));
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse the type definitions and convert them to actual types. Invalid types are getting skipped.
|
||||
vector<uint32_t> typesInColumn;
|
||||
for (auto typeDefinition : typeDefinitions)
|
||||
{
|
||||
vector<string_view> const typePath = strings::Tokenize(typeDefinition, "|");
|
||||
uint32_t const type = classificator.GetTypeByPathSafe(typePath);
|
||||
if (type != IndexAndTypeMapping::INVALID_TYPE)
|
||||
{
|
||||
typesInColumn.push_back(type);
|
||||
|
||||
vector<string> typesAndSubtypesPath(typePath.begin(), typePath.end());
|
||||
if (find(m_typesAndSubtypesPaths.begin(), m_typesAndSubtypesPaths.end(), typesAndSubtypesPath) == m_typesAndSubtypesPaths.end())
|
||||
m_typesAndSubtypesPaths.push_back(typesAndSubtypesPath);
|
||||
}
|
||||
else if (columnIndex == 0)
|
||||
{
|
||||
ASSERT(false, ("Parsing of subtypes file: Invalid type \"", typeDefinition, "\""));
|
||||
}
|
||||
else
|
||||
{
|
||||
ASSERT(false, ("Parsing of subtypes file: Invalid subtype \"", typeDefinition, "\""));
|
||||
}
|
||||
}
|
||||
|
||||
if (columnIndex == 0)
|
||||
types = typesInColumn;
|
||||
else
|
||||
subtypes = typesInColumn;
|
||||
}
|
||||
|
||||
for (auto type : types)
|
||||
{
|
||||
m_types.insert(type);
|
||||
m_typesWithSubtypes[type] = subtypes;
|
||||
}
|
||||
|
||||
for (auto subtype : subtypes)
|
||||
{
|
||||
m_subtypes.insert(subtype);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Static instance
|
||||
Subtypes const & Subtypes::Instance()
|
||||
{
|
||||
static Subtypes instance;
|
||||
return instance;
|
||||
}
|
||||
} // namespace ftypes
|
||||