Skip to content

Commit 23a4bb5

Browse files
authored
feat(demo): Add StreetViewJavaHelper and demo and enable edge-to-edge (#1595)
* feat(demo): Add Java demo for Street View utility This commit adds a new demo activity written in Java to demonstrate the usage of the Street View utility. To facilitate this, a Kotlin helper has been introduced to bridge the gap between suspend functions and Java's callback-based asynchronous model. The key changes are: - **`StreetViewDemoActivityJava`**: A new demo activity that shows how to check for Street View availability from Java. - **`StreetViewHelper`**: A Kotlin object that wraps the `suspend` function `StreetViewUtils.fetchStreetViewData` and exposes it to Java consumers via a callback interface. - **`ApiKeyValidator`**: A new utility class to check for a valid Maps API key within the demo application. - **Unit Tests**: Added tests for `StreetViewHelper` using Robolectric, MockK, and coroutine testing libraries to verify its behavior. - **Dependencies**: Added `mockk`, `kotlinx-coroutines-test`, and `robolectric` to the demo module's test dependencies. * feat(demo): Enable edge-to-edge for StreetView demo activities This commit refactors the `StreetViewDemoActivity` in both its Kotlin and Java versions to support edge-to-edge display. The key changes include: - Changing the base class from `Activity` to `AppCompatActivity`. - Using `WindowCompat.setDecorFitsSystemWindows` to allow the app to draw behind the system bars. - Adding an `OnApplyWindowInsetsListener` to the root view to apply padding for the system bars, preventing content from being obscured. - Updating the `street_view_demo.xml` layout with an ID for the root view to facilitate the insets listener. - Updating the copyright year. * refactor(library): Move StreetViewJavaHelper to library module This commit moves the `StreetViewHelper` from the `demo` module to the `library` module, making it an officially supported utility for Java consumers. The key changes include: - Renaming `StreetViewHelper` to `StreetViewJavaHelper` for better clarity and moving it to the `com.google.maps.android` package within the library. - Migrating the corresponding unit tests from the `demo` to the `library` module and updating them to reflect the class rename. - Adjusting `build.gradle.kts` files to move test dependencies (`mockk`, `coroutines-test`, `robolectric`) from the `demo` to the `library` module. - Renaming `StreetViewDemoActivityJava` to `StreetViewDemoJavaActivity` and updating it to use the new helper from the library. - Improving the warning message for invalid or missing API keys in the demo app. * refactor: Remove redundant Javadoc in StreetViewDemoJavaActivity
1 parent db84504 commit 23a4bb5

File tree

11 files changed

+369
-8
lines changed

11 files changed

+369
-8
lines changed

demo/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@
124124
<activity
125125
android:name=".StreetViewDemoActivity"
126126
android:exported="true" />
127+
<activity
128+
android:name=".StreetViewDemoJavaActivity"
129+
android:exported="true" />
127130
<activity
128131
android:name=".ClusterAlgorithmsDemoActivity"
129132
android:exported="true" />
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES, OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.maps.android.utils.demo;
18+
19+
import android.content.Context;
20+
import android.content.pm.PackageManager;
21+
import android.os.Bundle;
22+
23+
import java.util.regex.Pattern;
24+
25+
/**
26+
* A utility class to validate the Maps API key. The purpose of this check is to ensure that a
27+
* developer has set a valid-looking key. This is not a definitive check, and there is a
28+
* possibility of a false negative. If you are sure your key is correct, you can remove this
29+
* check.
30+
*/
31+
class ApiKeyValidator {
32+
private static final String REGEX = "^AIza[0-9A-Za-z-_]{35}$";
33+
private static final Pattern PATTERN = Pattern.compile(REGEX);
34+
35+
/**
36+
* Checks if the provided context has a valid Google Maps API key in its metadata.
37+
*
38+
* @param context The context to check for the API key.
39+
* @return `true` if the context has a valid API key, `false` otherwise.
40+
*/
41+
static boolean hasMapsApiKey(Context context) {
42+
String mapsApiKey = getMapsApiKey(context);
43+
return mapsApiKey != null && PATTERN.matcher(mapsApiKey).matches();
44+
}
45+
46+
/**
47+
* Retrieves the Google Maps API key from the application metadata.
48+
*
49+
* @param context The context to retrieve the API key from.
50+
* @return The API key if found, `null` otherwise.
51+
*/
52+
private static String getMapsApiKey(Context context) {
53+
try {
54+
Bundle bundle = context.getPackageManager()
55+
.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA)
56+
.metaData;
57+
return bundle.getString("com.google.android.geo.API_KEY");
58+
} catch (PackageManager.NameNotFoundException e) {
59+
e.printStackTrace();
60+
return null;
61+
}
62+
}
63+
}

demo/src/main/java/com/google/maps/android/utils/demo/MainActivity.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ protected void onCreate(Bundle savedInstanceState) {
8787
addDemo("Multi Layer", MultiLayerDemoActivity.class);
8888
addDemo("AnimationUtil sample", AnimationUtilDemoActivity.class);
8989
addDemo("Street View Demo", StreetViewDemoActivity.class);
90+
addDemo("Street View Demo (Java)", StreetViewDemoJavaActivity.class);
9091
}
9192

9293
private void addDemo(String demoName, Class<? extends Activity> activityClass) {

demo/src/main/java/com/google/maps/android/utils/demo/StreetViewDemoActivity.kt

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 Google LLC
2+
* Copyright 2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,35 +17,61 @@
1717
package com.google.maps.android.utils.demo
1818

1919
import android.annotation.SuppressLint
20-
import android.app.Activity
2120
import android.os.Bundle
2221
import android.widget.TextView
2322
import android.widget.Toast
23+
import androidx.appcompat.app.AppCompatActivity
24+
import androidx.core.view.ViewCompat
25+
import androidx.core.view.WindowCompat
26+
import androidx.core.view.WindowInsetsCompat
27+
import androidx.lifecycle.lifecycleScope
2428
import com.google.android.gms.maps.model.LatLng
2529
import com.google.maps.android.StreetViewUtils
26-
import kotlinx.coroutines.DelicateCoroutinesApi
2730
import kotlinx.coroutines.Dispatchers
28-
import kotlinx.coroutines.GlobalScope
2931
import kotlinx.coroutines.launch
3032

31-
class StreetViewDemoActivity : Activity() {
33+
/**
34+
* An activity that demonstrates how to use the [StreetViewUtils] to check for Street View
35+
* availability at different locations.
36+
*
37+
* This activity performs the following actions:
38+
* 1. Sets up the layout to fit system windows.
39+
* 2. Checks if a valid Google Maps API key is present.
40+
* 3. Launches a coroutine to fetch Street View data for two predefined locations.
41+
* 4. Displays the results of the Street View data fetch on the screen.
42+
*/
43+
class StreetViewDemoActivity : AppCompatActivity() {
3244

3345
@SuppressLint("SetTextI18n")
34-
@OptIn(DelicateCoroutinesApi::class)
3546
override fun onCreate(savedInstanceState: Bundle?) {
3647
super.onCreate(savedInstanceState)
48+
// Make the activity content fit behind the system bars.
49+
WindowCompat.setDecorFitsSystemWindows(window, false)
3750
setContentView(R.layout.street_view_demo)
3851

52+
// Apply window insets to the main view to avoid content overlapping with system bars.
53+
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
54+
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
55+
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
56+
insets
57+
}
58+
59+
// Check for a valid Maps API key before proceeding.
3960
if (!hasMapsApiKey(this)) {
4061
Toast.makeText(this, R.string.bad_maps_api_key, Toast.LENGTH_LONG).show()
4162
finish()
63+
return // Return early to prevent further execution
4264
}
4365

44-
GlobalScope.launch(Dispatchers.Main) {
66+
// Launch a coroutine in the Main dispatcher to fetch Street View data and update the UI.
67+
lifecycleScope.launch(Dispatchers.Main) {
68+
// Fetch Street View data for the first location (which is expected to be supported).
4569
val response1 =
4670
StreetViewUtils.fetchStreetViewData(LatLng(48.1425918, 11.5386121), BuildConfig.MAPS_API_KEY)
71+
// Fetch Street View data for the second location (which is expected to be unsupported).
4772
val response2 = StreetViewUtils.fetchStreetViewData(LatLng(8.1425918, 11.5386121), BuildConfig.MAPS_API_KEY)
4873

74+
// Update the UI with the results.
4975
findViewById<TextView>(R.id.textViewFirstLocation).text = "Location 1 is supported in StreetView: $response1"
5076
findViewById<TextView>(R.id.textViewSecondLocation).text = "Location 2 is supported in StreetView: $response2"
5177
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.maps.android.utils.demo;
18+
19+
import android.annotation.SuppressLint;
20+
import android.os.Bundle;
21+
import android.util.Log;
22+
import android.widget.TextView;
23+
import android.widget.Toast;
24+
25+
import androidx.annotation.NonNull;
26+
import androidx.appcompat.app.AppCompatActivity;
27+
import androidx.core.graphics.Insets;
28+
import androidx.core.view.ViewCompat;
29+
import androidx.core.view.WindowCompat;
30+
import androidx.core.view.WindowInsetsCompat;
31+
32+
import com.google.android.gms.maps.model.LatLng;
33+
import com.google.maps.android.Status;
34+
import com.google.maps.android.StreetViewJavaHelper;
35+
36+
/**
37+
* An activity that demonstrates how to use the Street View utility in Java to check for Street View
38+
* availability at different locations.
39+
* <p>
40+
* This activity performs the following actions:
41+
* 1. Sets up the layout to fit system windows.
42+
* 2. Checks if a valid Google Maps API key is present.
43+
* 3. Fetches Street View data for two predefined locations using asynchronous callbacks.
44+
* 4. Displays the results of the Street View data fetch on the screen.
45+
*/
46+
public class StreetViewDemoJavaActivity extends AppCompatActivity {
47+
48+
@SuppressLint("SetTextI18n")
49+
@Override
50+
protected void onCreate(Bundle savedInstanceState) {
51+
super.onCreate(savedInstanceState);
52+
// Make the activity content fit behind the system bars.
53+
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
54+
setContentView(R.layout.street_view_demo);
55+
56+
// Apply window insets to the main view to avoid content overlapping with system bars.
57+
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insetsCompat) -> {
58+
Insets insets = insetsCompat.getInsets(WindowInsetsCompat.Type.systemBars());
59+
v.setPadding(insets.left, insets.top, insets.right, insets.bottom);
60+
return insetsCompat;
61+
});
62+
63+
// Check for a valid Maps API key before proceeding.
64+
if (!ApiKeyValidator.hasMapsApiKey(this)) {
65+
Toast.makeText(this, R.string.bad_maps_api_key, Toast.LENGTH_LONG).show();
66+
finish();
67+
return; // Return early to prevent further execution
68+
}
69+
70+
// Fetches Street View data for the first location (expected to be supported).
71+
StreetViewJavaHelper.fetchStreetViewData(
72+
new LatLng(48.1425918, 11.5386121),
73+
BuildConfig.MAPS_API_KEY, new StreetViewJavaHelper.StreetViewCallback() {
74+
@Override
75+
public void onStreetViewResult(@NonNull Status status) {
76+
// Updates the UI with the result on the UI thread.
77+
runOnUiThread(() -> ((TextView) findViewById(R.id.textViewFirstLocation)).setText("Location 1 is supported in StreetView: " + status));
78+
}
79+
80+
@Override
81+
public void onStreetViewError(@NonNull Exception e) {
82+
// Handles the error by printing stack trace and showing a toast.
83+
Log.w("SVJDemo", "Error fetching Street View data: " + e.getMessage());
84+
Toast.makeText(StreetViewDemoJavaActivity.this, "Error fetching Street View data: " + e.getMessage(), Toast.LENGTH_SHORT).show();
85+
}
86+
});
87+
88+
// Fetches Street View data for the second location (expected to be unsupported).
89+
StreetViewJavaHelper.fetchStreetViewData(new LatLng(8.1425918, 11.5386121), BuildConfig.MAPS_API_KEY, new StreetViewJavaHelper.StreetViewCallback() {
90+
@Override
91+
public void onStreetViewResult(@NonNull Status status) {
92+
// Updates the UI with the result on the UI thread.
93+
runOnUiThread(() -> ((TextView) findViewById(R.id.textViewSecondLocation)).setText("Location 2 is supported in StreetView: " + status));
94+
}
95+
96+
@Override
97+
public void onStreetViewError(@NonNull Exception e) {
98+
// Handles the error by printing stack trace and showing a toast.
99+
Log.w("SVJDemo", "Error fetching Street View data: " + e.getMessage());
100+
Toast.makeText(StreetViewDemoJavaActivity.this, "Error fetching Street View data: " + e.getMessage(), Toast.LENGTH_SHORT).show();
101+
}
102+
});
103+
}
104+
}

demo/src/main/res/layout/street_view_demo.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
-->
1717

1818
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
19+
android:id="@+id/main"
1920
android:orientation="vertical"
2021
android:layout_width="match_parent"
2122
android:layout_height="match_parent">

demo/src/main/res/values/strings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@
3333
<string name="button_radius">Radius</string>
3434
<string name="button_gradient">Gradient</string>
3535
<string name="button_opacity">Opacity</string>
36-
<string name="bad_maps_api_key">Invalid or missing Google Maps API key</string>
36+
<string name="bad_maps_api_key">The Google Maps API key looks invalid or is missing. If you are sure your key is correct, you can remove this check from the demo.</string>
3737
</resources>

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx
3535
kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
3636
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
3737
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
38+
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
3839
junit = { module = "junit:junit", version.ref = "junit" }
3940
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito-core" }
4041
secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secrets-gradle-plugin" }

library/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ dependencies {
7171
testImplementation(libs.kotlin.test)
7272
testImplementation(libs.truth)
7373
implementation(libs.kotlin.stdlib.jdk8)
74+
75+
testImplementation(libs.mockk)
76+
testImplementation(libs.kotlinx.coroutines.test)
77+
testImplementation(libs.robolectric)
7478
}
7579

7680
tasks.register("instrumentTest") {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.maps.android
17+
18+
import com.google.android.gms.maps.model.LatLng
19+
import kotlinx.coroutines.CoroutineScope
20+
import kotlinx.coroutines.Dispatchers
21+
import kotlinx.coroutines.launch
22+
23+
/**
24+
* A helper object to call the suspend function `fetchStreetViewData` from Java.
25+
*/
26+
object StreetViewJavaHelper {
27+
/**
28+
* A callback interface to receive the result of the Street View data fetch.
29+
*/
30+
interface StreetViewCallback {
31+
/**
32+
* Called when the Street View data is fetched successfully.
33+
*
34+
* @param status The status of the Street View data.
35+
*/
36+
fun onStreetViewResult(status: Status)
37+
38+
/**
39+
* Called when there is an error fetching the Street View data.
40+
*
41+
* @param e The exception that occurred.
42+
*/
43+
fun onStreetViewError(e: Exception)
44+
}
45+
46+
/**
47+
* Fetches Street View data for the given location and returns the result via a callback.
48+
*
49+
* @param latLng The location to fetch Street View data for.
50+
* @param apiKey The API key to use for the request.
51+
* @param callback The callback to receive the result.
52+
*/
53+
@JvmStatic
54+
fun fetchStreetViewData(latLng: LatLng, apiKey: String, callback: StreetViewCallback) {
55+
CoroutineScope(Dispatchers.Main).launch {
56+
try {
57+
val status = StreetViewUtils.fetchStreetViewData(latLng, apiKey)
58+
callback.onStreetViewResult(status)
59+
} catch (e: Exception) {
60+
callback.onStreetViewError(e)
61+
}
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)