Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions samples/java_layout/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- network security config set to allow proxy tools to view HTTP communication for app debugging.
tools:replace is needed because SDK declares a network security config as well for automated tests.
Expand Down Expand Up @@ -126,6 +127,10 @@
android:name=".ui.settings.InternalSettingsActivity"
android:exported="false"
android:label="@string/label_internal_settings_activity" />
<activity
android:name=".ui.location.LocationTestActivity"
android:exported="false"
android:label="@string/label_location_test_activity" />
<activity
android:name=".ui.inline.InlineExamplesActivity"
android:exported="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.customer.android.sample.java_layout.support.Optional;
import io.customer.messaginginapp.MessagingInAppModuleConfig;
import io.customer.messaginginapp.ModuleMessagingInApp;
import io.customer.location.ModuleLocation;
import io.customer.messagingpush.ModuleMessagingPushFCM;
import io.customer.sdk.CustomerIO;
import io.customer.sdk.CustomerIOConfig;
Expand All @@ -38,6 +39,9 @@ public void initializeSdk(SampleApplication application) {
// Enables push notification
builder.addCustomerIOModule(new ModuleMessagingPushFCM());

// Enables location tracking
builder.addCustomerIOModule(new ModuleLocation());

// Enables in-app messages
if (sdkConfig.isInAppMessagingEnabled()) {
builder.addCustomerIOModule(new ModuleMessagingInApp(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.customer.android.sample.java_layout.sdk.CustomerIORepository;
import io.customer.android.sample.java_layout.ui.core.BaseActivity;
import io.customer.android.sample.java_layout.ui.inline.InlineExamplesActivity;
import io.customer.android.sample.java_layout.ui.location.LocationTestActivity;
import io.customer.android.sample.java_layout.ui.login.LoginActivity;
import io.customer.android.sample.java_layout.ui.settings.InternalSettingsActivity;
import io.customer.android.sample.java_layout.ui.settings.SettingsActivity;
Expand Down Expand Up @@ -149,6 +150,9 @@ private void setupViews() {
binding.showPushPromptButton.setOnClickListener(view -> {
requestNotificationPermission();
});
binding.locationTestButton.setOnClickListener(view -> {
startActivity(new Intent(DashboardActivity.this, LocationTestActivity.class));
});
binding.inlineExamplesButton.setOnClickListener(view -> {
startInlineExamplesActivity();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package io.customer.android.sample.java_layout.ui.location;

import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.provider.Settings;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;

import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;

import io.customer.android.sample.java_layout.R;
import io.customer.android.sample.java_layout.databinding.ActivityLocationTestBinding;
import io.customer.android.sample.java_layout.ui.core.BaseActivity;
import io.customer.location.ModuleLocation;
import io.customer.sdk.CustomerIO;

public class LocationTestActivity extends BaseActivity<ActivityLocationTestBinding> {

private static final double[][] PRESET_COORDS = {
{40.7128, -74.0060}, // New York
{51.5074, -0.1278}, // London
{35.6762, 139.6503}, // Tokyo
{-33.8688, 151.2093}, // Sydney
{-23.5505, -46.6333}, // Sao Paulo
{0.0, 0.0} // 0, 0
};
private static final String[] PRESET_NAMES = {
"New York", "London", "Tokyo", "Sydney", "Sao Paulo", "0, 0"
};

private LocationManager locationManager;
private LocationListener locationListener;
private boolean userRequestedCurrentLocation = false;
private boolean userRequestedSdkLocation = false;

private final ActivityResultLauncher<String[]> locationPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), permissions -> {
boolean fineGranted = Boolean.TRUE.equals(permissions.get(Manifest.permission.ACCESS_FINE_LOCATION));
boolean coarseGranted = Boolean.TRUE.equals(permissions.get(Manifest.permission.ACCESS_COARSE_LOCATION));
if (fineGranted || coarseGranted) {
if (userRequestedCurrentLocation) {
userRequestedCurrentLocation = false;
fetchDeviceLocation();
} else if (userRequestedSdkLocation) {
userRequestedSdkLocation = false;
performSdkLocationRequest();
}
} else {
showPermissionDeniedAlert();
}
});

@Override
protected ActivityLocationTestBinding inflateViewBinding() {
return ActivityLocationTestBinding.inflate(getLayoutInflater());
}

@Override
protected void setupContent() {
binding.topAppBar.setNavigationOnClickListener(v -> onBackPressed());

locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
CustomerIO.instance().screen(getString(R.string.location_test_title));

setupPresetButtons();
setupSdkLocationButtons();
setupDeviceLocationButton();
setupManualEntrySection();
}

private void setupPresetButtons() {
int[] buttonIds = {
R.id.preset_new_york, R.id.preset_london, R.id.preset_tokyo,
R.id.preset_sydney, R.id.preset_sao_paulo, R.id.preset_zero
};
for (int i = 0; i < buttonIds.length; i++) {
final int index = i;
findViewById(buttonIds[i]).setOnClickListener(v ->
setLocation(PRESET_COORDS[index][0], PRESET_COORDS[index][1], PRESET_NAMES[index])
);
}
}

private void setupSdkLocationButtons() {
binding.requestSdkLocationOnce.setOnClickListener(v -> requestSdkLocationOnce());
binding.stopLocationUpdates.setOnClickListener(v -> stopSdkLocationUpdates());
}

private void setupDeviceLocationButton() {
binding.useCurrentLocation.setOnClickListener(v -> requestCurrentLocation());
}

private void setupManualEntrySection() {
binding.setManualLocation.setOnClickListener(v -> setManualLocation());
}

// --- Location Actions ---

private void setLocation(double latitude, double longitude, String sourceName) {
ModuleLocation.instance().getLocationServices().setLastKnownLocation(latitude, longitude);
String sourceText = sourceName != null ? " (" + sourceName + ")" : "";
binding.lastSetLocationLabel.setText(
getString(R.string.last_set_format, latitude, longitude, sourceText)
);
showSnackbar(getString(R.string.location_set_success, sourceText));
}

private void setManualLocation() {
String latText = binding.latitudeInput.getText() != null
? binding.latitudeInput.getText().toString().trim() : "";
String lonText = binding.longitudeInput.getText() != null
? binding.longitudeInput.getText().toString().trim() : "";

if (latText.isEmpty() || lonText.isEmpty()) {
showSnackbar(getString(R.string.enter_valid_coordinates));
return;
}

try {
double latitude = Double.parseDouble(latText);
double longitude = Double.parseDouble(lonText);
setLocation(latitude, longitude, "Manual");
} catch (NumberFormatException e) {
showSnackbar(getString(R.string.enter_valid_coordinates));
}
}

private void requestSdkLocationOnce() {
if (isLocationPermissionGranted()) {
performSdkLocationRequest();
} else {
userRequestedSdkLocation = true;
locationPermissionLauncher.launch(new String[]{
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
});
}
}

private void performSdkLocationRequest() {
binding.lastSetLocationLabel.setText(R.string.requesting_location_sdk);
ModuleLocation.instance().getLocationServices().requestLocationUpdate();
showSnackbar(getString(R.string.sdk_requested_location));
}

private void stopSdkLocationUpdates() {
ModuleLocation.instance().getLocationServices().stopLocationUpdates();
binding.lastSetLocationLabel.setText(R.string.location_updates_stopped);
showSnackbar(getString(R.string.stopped_location_updates));
}

private void requestCurrentLocation() {
if (isLocationPermissionGranted()) {
fetchDeviceLocation();
} else {
userRequestedCurrentLocation = true;
locationPermissionLauncher.launch(new String[]{
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
});
}
}

@SuppressWarnings("MissingPermission")
private void fetchDeviceLocation() {
binding.useCurrentLocation.setEnabled(false);
binding.useCurrentLocation.setText(R.string.fetching_location);

locationListener = new LocationListener() {
@Override
public void onLocationChanged(@NonNull Location location) {
locationManager.removeUpdates(this);
setLocation(location.getLatitude(), location.getLongitude(), "Device");
binding.useCurrentLocation.setEnabled(true);
binding.useCurrentLocation.setText(R.string.use_current_location);
}

@Override
public void onProviderDisabled(@NonNull String provider) {
showSnackbar(getString(R.string.location_not_available));
binding.useCurrentLocation.setEnabled(true);
binding.useCurrentLocation.setText(R.string.use_current_location);
}
};

String provider = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
? LocationManager.GPS_PROVIDER
: LocationManager.NETWORK_PROVIDER;

locationManager.requestSingleUpdate(provider, locationListener, null);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GPS provider crashes when only coarse permission granted

Medium Severity

isLocationPermissionGranted() returns true when only ACCESS_COARSE_LOCATION is granted, but fetchDeviceLocation() may select GPS_PROVIDER which requires ACCESS_FINE_LOCATION. On Android 12+, users can choose "Approximate" location, granting only coarse permission. In that scenario, requestSingleUpdate with GPS_PROVIDER throws a SecurityException, crashing the app. The provider selection needs to account for which specific permission was actually granted.

Additional Locations (1)

Fix in Cursor Fix in Web

}

// --- Permission Helpers ---

private boolean isLocationPermissionGranted() {
return ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|| ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
}

private void showPermissionDeniedAlert() {
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.location_permission_required)
.setMessage(R.string.location_permission_failure)
.setNeutralButton(R.string.open_settings, (dialog, which) -> {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
})
.show();
}

private void showSnackbar(String message) {
Snackbar.make(binding.getRoot(), message, Snackbar.LENGTH_SHORT).show();
}

@Override
protected void onDestroy() {
super.onDestroy();
if (locationListener != null && locationManager != null) {
locationManager.removeUpdates(locationListener);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F7F7F7" />
<corners android:radius="10dp" />
<stroke
android:width="1dp"
android:color="#E6E6E6" />
</shape>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F0F0F0" />
<corners android:radius="8dp" />
</shape>
17 changes: 15 additions & 2 deletions samples/java_layout/src/main/res/layout/activity_dashboard.xml
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,25 @@
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_default"
android:text="@string/show_push_prompt"
app:layout_constraintBottom_toTopOf="@id/view_logs_button"
app:layout_constraintBottom_toTopOf="@id/location_test_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/set_profile_attributes_button"
app:layout_constraintWidth_max="@dimen/material_button_max_width" />

<Button
android:id="@+id/location_test_button"
style="?attr/materialButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_default"
android:text="@string/location_test"
app:layout_constraintBottom_toTopOf="@id/view_logs_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/show_push_prompt_button"
app:layout_constraintWidth_max="@dimen/material_button_max_width" />

<Button
android:id="@+id/view_logs_button"
style="?attr/materialButtonStyle"
Expand All @@ -196,7 +209,7 @@
app:layout_constraintBottom_toTopOf="@id/inline_examples_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/show_push_prompt_button"
app:layout_constraintTop_toBottomOf="@id/location_test_button"
app:layout_constraintWidth_max="@dimen/material_button_max_width"
tools:visibility="visible" />

Expand Down
Loading
Loading