capacitor-google-navigation 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -20
- package/android/src/main/java/com/attributeai/navigation/NavigationFragment.java +59 -37
- package/ios/Sources/GoogleNavigationPlugin/GoogleNavigationPlugin.swift +9 -3
- package/ios/Sources/GoogleNavigationPlugin/NavigationViewController.swift +45 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,30 +57,47 @@ The `GoogleNavigation ~> 9.0` pod is declared in the plugin's podspec and is pul
|
|
|
57
57
|
<string>This app uses your location for navigation, including in the background.</string>
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
### 3. Register your API key
|
|
60
|
+
### 3. Register your API key
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
#### Production (recommended)
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
import UIKit
|
|
66
|
-
import Capacitor
|
|
67
|
-
import CapacitorGoogleNavigation
|
|
68
|
-
import GoogleNavigation // required for GMSServices
|
|
64
|
+
Store the key in `Info.plist` under the key `GoogleNavigationAPIKey`. The plugin reads it automatically — no key needs to be passed from JS.
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
```xml
|
|
67
|
+
<key>GoogleNavigationAPIKey</key>
|
|
68
|
+
<string>YOUR_IOS_API_KEY</string>
|
|
69
|
+
```
|
|
72
70
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
Then call `initialize()` with no key:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
await GoogleNavigation.initialize({});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Keep the key out of source control by injecting it at build time via an `.xcconfig` file:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
// Config.xcconfig (gitignored)
|
|
81
|
+
GOOGLE_NAV_API_KEY = AIzaSy...
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```xml
|
|
85
|
+
<!-- Info.plist -->
|
|
86
|
+
<key>GoogleNavigationAPIKey</key>
|
|
87
|
+
<string>$(GOOGLE_NAV_API_KEY)</string>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Development only
|
|
91
|
+
|
|
92
|
+
You can pass the key directly from JS for quick local testing. **Do not ship this in production** — the key will be visible in your compiled JS bundle.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
await GoogleNavigation.initialize({ apiKey: 'YOUR_IOS_API_KEY' });
|
|
81
96
|
```
|
|
82
97
|
|
|
83
|
-
|
|
98
|
+
#### Restrict your API key
|
|
99
|
+
|
|
100
|
+
In Google Cloud Console → APIs & Services → Credentials, add an **iOS app restriction** with your app's bundle ID. This ensures the key is rejected if extracted and used outside your app.
|
|
84
101
|
|
|
85
102
|
---
|
|
86
103
|
|
|
@@ -88,6 +105,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
|
88
105
|
|
|
89
106
|
### 1. Add the API key and permissions to `android/app/src/main/AndroidManifest.xml`
|
|
90
107
|
|
|
108
|
+
The Android Navigation SDK reads the key directly from the manifest at startup — it is never passed from JS.
|
|
109
|
+
|
|
91
110
|
```xml
|
|
92
111
|
<manifest>
|
|
93
112
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
@@ -95,14 +114,32 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
|
95
114
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
96
115
|
|
|
97
116
|
<application>
|
|
98
|
-
<!-- Required: Navigation SDK reads this at startup -->
|
|
99
117
|
<meta-data
|
|
100
118
|
android:name="com.google.android.geo.API_KEY"
|
|
101
|
-
android:value="
|
|
119
|
+
android:value="${GOOGLE_NAV_API_KEY}" />
|
|
102
120
|
</application>
|
|
103
121
|
</manifest>
|
|
104
122
|
```
|
|
105
123
|
|
|
124
|
+
Keep the key out of source control using `gradle.properties` (gitignored):
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
# android/gradle.properties (gitignored)
|
|
128
|
+
GOOGLE_NAV_API_KEY=AIzaSy...
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Then in `android/app/build.gradle`, expose it to the manifest:
|
|
132
|
+
|
|
133
|
+
```groovy
|
|
134
|
+
android {
|
|
135
|
+
defaultConfig {
|
|
136
|
+
manifestPlaceholders = [GOOGLE_NAV_API_KEY: project.findProperty("GOOGLE_NAV_API_KEY") ?: ""]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Restrict the key in Google Cloud Console by adding your app's **SHA-1 certificate fingerprint** and **package name** under Android app restrictions.
|
|
142
|
+
|
|
106
143
|
> **Important:** On Android the Navigation SDK reads the API key from `AndroidManifest.xml` — not from the `apiKey` parameter passed to `initialize()`. The `apiKey` parameter is used on iOS only.
|
|
107
144
|
|
|
108
145
|
### 2. Request location permission at runtime
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.attributeai.navigation;
|
|
2
2
|
|
|
3
|
+
import android.app.Activity;
|
|
3
4
|
import android.graphics.Color;
|
|
4
5
|
import android.graphics.Typeface;
|
|
5
6
|
import android.os.Bundle;
|
|
@@ -25,6 +26,7 @@ import com.google.android.libraries.navigation.NavigationView;
|
|
|
25
26
|
public class NavigationFragment extends Fragment {
|
|
26
27
|
|
|
27
28
|
private NavigationView navigationView;
|
|
29
|
+
private Button closeButton;
|
|
28
30
|
private Runnable onCloseListener;
|
|
29
31
|
|
|
30
32
|
public static NavigationFragment newInstance() {
|
|
@@ -44,57 +46,29 @@ public class NavigationFragment extends Fragment {
|
|
|
44
46
|
) {
|
|
45
47
|
navigationView = new NavigationView(requireContext());
|
|
46
48
|
navigationView.onCreate(savedInstanceState);
|
|
47
|
-
|
|
48
|
-
FrameLayout root = new FrameLayout(requireContext());
|
|
49
|
-
root.addView(navigationView, new FrameLayout.LayoutParams(
|
|
50
|
-
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
51
|
-
FrameLayout.LayoutParams.MATCH_PARENT
|
|
52
|
-
));
|
|
53
|
-
|
|
54
|
-
Button closeButton = new Button(requireContext());
|
|
55
|
-
closeButton.setText("✕");
|
|
56
|
-
closeButton.setTextColor(Color.WHITE);
|
|
57
|
-
closeButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
|
|
58
|
-
closeButton.setTypeface(null, Typeface.BOLD);
|
|
59
|
-
closeButton.setBackgroundColor(Color.argb(153, 0, 0, 0)); // 60% black
|
|
60
|
-
closeButton.setOnClickListener(v -> {
|
|
61
|
-
if (onCloseListener != null) onCloseListener.run();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
int sizePx = (int) TypedValue.applyDimension(
|
|
65
|
-
TypedValue.COMPLEX_UNIT_DIP, 40, getResources().getDisplayMetrics()
|
|
66
|
-
);
|
|
67
|
-
int marginPx = (int) TypedValue.applyDimension(
|
|
68
|
-
TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics()
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
FrameLayout.LayoutParams btnParams = new FrameLayout.LayoutParams(sizePx, sizePx);
|
|
72
|
-
btnParams.gravity = Gravity.TOP | Gravity.START;
|
|
73
|
-
btnParams.topMargin = marginPx;
|
|
74
|
-
btnParams.leftMargin = marginPx;
|
|
75
|
-
root.addView(closeButton, btnParams);
|
|
76
|
-
|
|
77
|
-
return root;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
@Override
|
|
81
|
-
public void onStart() {
|
|
82
|
-
super.onStart();
|
|
83
|
-
if (navigationView != null) navigationView.onStart();
|
|
49
|
+
return navigationView;
|
|
84
50
|
}
|
|
85
51
|
|
|
86
52
|
@Override
|
|
87
53
|
public void onResume() {
|
|
88
54
|
super.onResume();
|
|
89
55
|
if (navigationView != null) navigationView.onResume();
|
|
56
|
+
attachCloseButtonToDecorView();
|
|
90
57
|
}
|
|
91
58
|
|
|
92
59
|
@Override
|
|
93
60
|
public void onPause() {
|
|
61
|
+
detachCloseButtonFromDecorView();
|
|
94
62
|
if (navigationView != null) navigationView.onPause();
|
|
95
63
|
super.onPause();
|
|
96
64
|
}
|
|
97
65
|
|
|
66
|
+
@Override
|
|
67
|
+
public void onStart() {
|
|
68
|
+
super.onStart();
|
|
69
|
+
if (navigationView != null) navigationView.onStart();
|
|
70
|
+
}
|
|
71
|
+
|
|
98
72
|
@Override
|
|
99
73
|
public void onStop() {
|
|
100
74
|
if (navigationView != null) navigationView.onStop();
|
|
@@ -103,6 +77,7 @@ public class NavigationFragment extends Fragment {
|
|
|
103
77
|
|
|
104
78
|
@Override
|
|
105
79
|
public void onDestroyView() {
|
|
80
|
+
detachCloseButtonFromDecorView();
|
|
106
81
|
if (navigationView != null) navigationView.onDestroy();
|
|
107
82
|
super.onDestroyView();
|
|
108
83
|
}
|
|
@@ -112,4 +87,51 @@ public class NavigationFragment extends Fragment {
|
|
|
112
87
|
super.onSaveInstanceState(outState);
|
|
113
88
|
if (navigationView != null) navigationView.onSaveInstanceState(outState);
|
|
114
89
|
}
|
|
90
|
+
|
|
91
|
+
// MARK: - Close button attached to the Activity decor view, above all SDK UI
|
|
92
|
+
|
|
93
|
+
private void attachCloseButtonToDecorView() {
|
|
94
|
+
Activity activity = getActivity();
|
|
95
|
+
if (activity == null || closeButton != null) return;
|
|
96
|
+
|
|
97
|
+
int sizePx = (int) TypedValue.applyDimension(
|
|
98
|
+
TypedValue.COMPLEX_UNIT_DIP, 40, getResources().getDisplayMetrics()
|
|
99
|
+
);
|
|
100
|
+
int marginPx = (int) TypedValue.applyDimension(
|
|
101
|
+
TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics()
|
|
102
|
+
);
|
|
103
|
+
int statusBarHeight = getStatusBarHeight(activity);
|
|
104
|
+
|
|
105
|
+
Button button = new Button(requireContext());
|
|
106
|
+
button.setText("✕");
|
|
107
|
+
button.setTextColor(Color.WHITE);
|
|
108
|
+
button.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
|
|
109
|
+
button.setTypeface(null, Typeface.BOLD);
|
|
110
|
+
button.setBackgroundColor(Color.argb(153, 0, 0, 0));
|
|
111
|
+
button.setOnClickListener(v -> {
|
|
112
|
+
if (onCloseListener != null) onCloseListener.run();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(sizePx, sizePx);
|
|
116
|
+
params.gravity = Gravity.TOP | Gravity.START;
|
|
117
|
+
params.topMargin = statusBarHeight + marginPx;
|
|
118
|
+
params.leftMargin = marginPx;
|
|
119
|
+
|
|
120
|
+
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
|
|
121
|
+
decorView.addView(button, params);
|
|
122
|
+
this.closeButton = button;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private void detachCloseButtonFromDecorView() {
|
|
126
|
+
Activity activity = getActivity();
|
|
127
|
+
if (activity == null || closeButton == null) return;
|
|
128
|
+
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
|
|
129
|
+
decorView.removeView(closeButton);
|
|
130
|
+
closeButton = null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private int getStatusBarHeight(Activity activity) {
|
|
134
|
+
int resourceId = activity.getResources().getIdentifier("status_bar_height", "dimen", "android");
|
|
135
|
+
return resourceId > 0 ? activity.getResources().getDimensionPixelSize(resourceId) : 0;
|
|
136
|
+
}
|
|
115
137
|
}
|
|
@@ -14,11 +14,17 @@ public class GoogleNavigationPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
14
14
|
private lazy var implementation = GoogleNavigation(plugin: self)
|
|
15
15
|
|
|
16
16
|
@objc func initialize(_ call: CAPPluginCall) {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
let jsKey = call.getString("apiKey") ?? ""
|
|
18
|
+
let resolvedKey = !jsKey.isEmpty
|
|
19
|
+
? jsKey
|
|
20
|
+
: Bundle.main.object(forInfoDictionaryKey: "GoogleNavigationAPIKey") as? String ?? ""
|
|
21
|
+
|
|
22
|
+
guard !resolvedKey.isEmpty else {
|
|
23
|
+
call.reject("No API key found. Add GoogleNavigationAPIKey to Info.plist (recommended) or pass apiKey to initialize().")
|
|
19
24
|
return
|
|
20
25
|
}
|
|
21
|
-
|
|
26
|
+
|
|
27
|
+
implementation.initialize(apiKey: resolvedKey) { success, error in
|
|
22
28
|
if let error = error {
|
|
23
29
|
call.reject(error)
|
|
24
30
|
} else {
|
|
@@ -5,6 +5,7 @@ import GoogleNavigation
|
|
|
5
5
|
class NavigationMapViewController: UIViewController {
|
|
6
6
|
private let session: GMSNavigationSession
|
|
7
7
|
private var mapView: GMSMapView?
|
|
8
|
+
private var overlayWindow: UIWindow?
|
|
8
9
|
var onDismiss: (() -> Void)?
|
|
9
10
|
|
|
10
11
|
init(session: GMSNavigationSession) {
|
|
@@ -26,18 +27,41 @@ class NavigationMapViewController: UIViewController {
|
|
|
26
27
|
container.addSubview(mapView)
|
|
27
28
|
self.mapView = mapView
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
self.view = container
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
override func viewDidLoad() {
|
|
34
|
+
super.viewDidLoad()
|
|
35
|
+
guard let mapView = mapView else { return }
|
|
36
|
+
let enabled = mapView.enableNavigation(with: session)
|
|
37
|
+
if enabled {
|
|
38
|
+
mapView.cameraMode = .following
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override func viewDidAppear(_ animated: Bool) {
|
|
43
|
+
super.viewDidAppear(animated)
|
|
44
|
+
addCloseButtonWindow()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override func viewDidDisappear(_ animated: Bool) {
|
|
48
|
+
super.viewDidDisappear(animated)
|
|
49
|
+
tearDownCloseButtonWindow()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// MARK: - Close button in a top-level UIWindow above all SDK UI
|
|
53
|
+
|
|
54
|
+
private func addCloseButtonWindow() {
|
|
55
|
+
guard let scene = view.window?.windowScene else { return }
|
|
56
|
+
|
|
57
|
+
let window = UIWindow(windowScene: scene)
|
|
58
|
+
window.windowLevel = .alert + 1
|
|
59
|
+
window.backgroundColor = .clear
|
|
60
|
+
window.isHidden = false
|
|
61
|
+
|
|
62
|
+
let overlayVC = UIViewController()
|
|
63
|
+
overlayVC.view.backgroundColor = .clear
|
|
64
|
+
window.rootViewController = overlayVC
|
|
41
65
|
|
|
42
66
|
let button = UIButton(type: .system)
|
|
43
67
|
button.setImage(UIImage(systemName: "xmark"), for: .normal)
|
|
@@ -46,27 +70,25 @@ class NavigationMapViewController: UIViewController {
|
|
|
46
70
|
button.layer.cornerRadius = 20
|
|
47
71
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
48
72
|
button.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
|
|
49
|
-
|
|
73
|
+
overlayVC.view.addSubview(button)
|
|
74
|
+
|
|
50
75
|
NSLayoutConstraint.activate([
|
|
51
|
-
button.topAnchor.constraint(equalTo:
|
|
52
|
-
button.leadingAnchor.constraint(equalTo:
|
|
76
|
+
button.topAnchor.constraint(equalTo: overlayVC.view.safeAreaLayoutGuide.topAnchor, constant: 16),
|
|
77
|
+
button.leadingAnchor.constraint(equalTo: overlayVC.view.leadingAnchor, constant: 16),
|
|
53
78
|
button.widthAnchor.constraint(equalToConstant: 40),
|
|
54
79
|
button.heightAnchor.constraint(equalToConstant: 40),
|
|
55
80
|
])
|
|
56
81
|
|
|
57
|
-
self.
|
|
82
|
+
self.overlayWindow = window
|
|
58
83
|
}
|
|
59
84
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
let enabled = mapView.enableNavigation(with: session)
|
|
64
|
-
if enabled {
|
|
65
|
-
mapView.cameraMode = .following
|
|
66
|
-
}
|
|
85
|
+
private func tearDownCloseButtonWindow() {
|
|
86
|
+
overlayWindow?.isHidden = true
|
|
87
|
+
overlayWindow = nil
|
|
67
88
|
}
|
|
68
89
|
|
|
69
90
|
@objc private func closeTapped() {
|
|
91
|
+
tearDownCloseButtonWindow()
|
|
70
92
|
dismiss(animated: true) { [weak self] in
|
|
71
93
|
self?.onDismiss?()
|
|
72
94
|
}
|