diff --git a/aconfig/settings_bluetooth_declarations.aconfig b/aconfig/settings_bluetooth_declarations.aconfig
index 7aa989bb0d3..4d2528a71db 100644
--- a/aconfig/settings_bluetooth_declarations.aconfig
+++ b/aconfig/settings_bluetooth_declarations.aconfig
@@ -44,3 +44,13 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "enable_nearby_share_entrypoint"
+ namespace: "cross_device_experiences"
+ description: "Show Nearby Share entrypoint in Bluetooth Sharing page"
+ bug: "381799866"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/res/drawable/ic_bluetooth_share_info.xml b/res/drawable/ic_bluetooth_share_info.xml
new file mode 100644
index 00000000000..860c5536f0f
--- /dev/null
+++ b/res/drawable/ic_bluetooth_share_info.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/res/layout/nearby_sharing_suggestion_card.xml b/res/layout/nearby_sharing_suggestion_card.xml
new file mode 100644
index 00000000000..6c9d310e439
--- /dev/null
+++ b/res/layout/nearby_sharing_suggestion_card.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ba0c998180b..9282be22982 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -203,6 +203,10 @@
Route sounds to your hearing device or phone speaker
Related
+
+ Try sharing with %s
+
+ The fastest way to send files to nearby Android devices
Ringtone and alarms
diff --git a/res/xml/device_picker.xml b/res/xml/device_picker.xml
index 6f8d267cd66..5e7667dc80e 100644
--- a/res/xml/device_picker.xml
+++ b/res/xml/device_picker.xml
@@ -15,12 +15,21 @@
-->
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto">
+
+
diff --git a/src/com/android/settings/bluetooth/DevicePickerFragment.java b/src/com/android/settings/bluetooth/DevicePickerFragment.java
index 2e810620e7c..3e88c825449 100644
--- a/src/com/android/settings/bluetooth/DevicePickerFragment.java
+++ b/src/com/android/settings/bluetooth/DevicePickerFragment.java
@@ -32,9 +32,11 @@ import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
+import com.android.settings.flags.Flags;
import com.android.settings.password.PasswordUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.core.AbstractPreferenceController;
@@ -48,6 +50,8 @@ import java.util.List;
public final class DevicePickerFragment extends DeviceListPreferenceFragment {
private static final String KEY_BT_DEVICE_LIST = "bt_device_list";
private static final String TAG = "DevicePickerFragment";
+ private static final String EXTRA_ORIGINAL_SEND_INTENT =
+ "android.bluetooth.extra.DEVICE_PICKER_ORIGINAL_SEND_INTENT";
@VisibleForTesting
BluetoothProgressCategory mAvailableDevicesCategory;
@@ -104,6 +108,23 @@ public final class DevicePickerFragment extends DeviceListPreferenceFragment {
setHasOptionsMenu(true);
}
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ if (Flags.enableNearbyShareEntrypoint()) {
+ initNearbySharingController();
+ }
+ }
+
+ private void initNearbySharingController() {
+ Intent sendIntent =
+ getIntent().getParcelableExtra(EXTRA_ORIGINAL_SEND_INTENT, Intent.class);
+ if (sendIntent == null) {
+ return;
+ }
+ use(NearbySharePreferenceController.class).init(sendIntent);
+ }
+
@Override
public void onStart() {
super.onStart();
diff --git a/src/com/android/settings/bluetooth/NearbySharePreferenceController.kt b/src/com/android/settings/bluetooth/NearbySharePreferenceController.kt
new file mode 100644
index 00000000000..bf7092552db
--- /dev/null
+++ b/src/com/android/settings/bluetooth/NearbySharePreferenceController.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth
+
+import android.app.settings.SettingsEnums
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.NameNotFoundException
+import android.provider.Settings
+import android.text.TextUtils
+import android.view.View
+import android.widget.TextView
+import androidx.preference.PreferenceScreen
+import com.android.settings.R
+import com.android.settings.core.BasePreferenceController
+import com.android.settings.overlay.FeatureFactory
+import com.android.settingslib.widget.LayoutPreference
+
+/** Preference controller for Nearby Share. */
+class NearbySharePreferenceController(private val context: Context, key: String) :
+ BasePreferenceController(context, key) {
+ private lateinit var intent: Intent
+ private var nearbyComponentName: ComponentName? = null
+ private var nearbyLabel: CharSequence? = null
+
+ fun init(sendIntent: Intent) {
+ this.intent = sendIntent
+ val componentString =
+ Settings.Secure.getString(
+ context.getContentResolver(),
+ Settings.Secure.NEARBY_SHARING_COMPONENT,
+ )
+ if (TextUtils.isEmpty(componentString)) {
+ return
+ }
+ nearbyComponentName = ComponentName.unflattenFromString(componentString)?.also {
+ intent.setComponent(it)
+ nearbyLabel = getNearbyLabel(it)
+ }
+ }
+
+ override fun getAvailabilityStatus(): Int {
+ if (nearbyLabel == null) {
+ return CONDITIONALLY_UNAVAILABLE
+ }
+ return AVAILABLE
+ }
+
+ override fun displayPreference(screen: PreferenceScreen) {
+ super.displayPreference(screen)
+ val preference: LayoutPreference = screen.findPreference(preferenceKey) ?: return
+
+ preference.findViewById(R.id.nearby_sharing_suggestion_title).text =
+ context.getString(R.string.bluetooth_try_nearby_share_title, nearbyLabel)
+ FeatureFactory.featureFactory.metricsFeatureProvider.action(
+ SettingsEnums.PAGE_UNKNOWN,
+ SettingsEnums.ACTION_NEARBY_SHARE_ENTRYPOINT_SHOWN,
+ SettingsEnums.BLUETOOTH_DEVICE_PICKER,
+ "",
+ 0
+ )
+ preference.findViewById(R.id.card_container).setOnClickListener {
+ FeatureFactory.featureFactory.metricsFeatureProvider.clicked(
+ SettingsEnums.BLUETOOTH_DEVICE_PICKER,
+ preferenceKey
+ )
+ context.startActivity(intent)
+ true
+ }
+ }
+
+ private fun getNearbyLabel(componentName: ComponentName): CharSequence? =
+ try {
+ context.packageManager
+ .getActivityInfo(componentName, PackageManager.GET_META_DATA)
+ .loadLabel(context.packageManager)
+ } catch(_: NameNotFoundException) {
+ null
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/NearbySharePreferenceControllerTest.kt b/tests/robotests/src/com/android/settings/bluetooth/NearbySharePreferenceControllerTest.kt
new file mode 100644
index 00000000000..2055e886c4e
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/NearbySharePreferenceControllerTest.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.provider.Settings
+import android.view.LayoutInflater
+import android.view.View
+import com.android.settings.R
+import com.android.settingslib.widget.LayoutPreference
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class NearbySharePreferenceControllerTest : BluetoothDetailsControllerTestBase() {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var intent: Intent
+ @Mock private lateinit var packageManager: PackageManager
+ @Mock private lateinit var activityInfo: ActivityInfo
+
+ private lateinit var context: Context
+ private lateinit var controller: NearbySharePreferenceController
+
+ override fun setUp() {
+ super.setUp()
+ context = spy(mContext)
+ whenever(context.packageManager).thenReturn(packageManager)
+ whenever(
+ packageManager.getActivityInfo(
+ eq(ComponentName.unflattenFromString(COMPONENT_NAME)!!),
+ eq(PackageManager.GET_META_DATA),
+ )
+ )
+ .thenReturn(activityInfo)
+
+ controller = NearbySharePreferenceController(context, PREF_KEY)
+ }
+
+ @Test
+ fun noIntent_notAvailable() {
+ Settings.Secure.putString(
+ context.contentResolver,
+ Settings.Secure.NEARBY_SHARING_COMPONENT,
+ COMPONENT_NAME,
+ )
+ whenever(activityInfo.loadLabel(any())).thenReturn("App")
+
+ assertThat(controller.isAvailable).isFalse()
+ }
+
+ @Test
+ fun noNearbyComponent_notAvailable() {
+ controller.init(intent)
+
+ assertThat(controller.isAvailable).isFalse()
+ }
+
+ @Test
+ fun hasIntentAndNearbyComponent_available() {
+ Settings.Secure.putString(
+ context.contentResolver,
+ Settings.Secure.NEARBY_SHARING_COMPONENT,
+ COMPONENT_NAME,
+ )
+ whenever(activityInfo.loadLabel(any())).thenReturn("App")
+ controller.init(intent)
+
+ assertThat(controller.isAvailable).isTrue()
+ }
+
+ @Test
+ fun clickPreference_startActivity() {
+ Settings.Secure.putString(
+ context.contentResolver,
+ Settings.Secure.NEARBY_SHARING_COMPONENT,
+ COMPONENT_NAME,
+ )
+ whenever(activityInfo.loadLabel(any())).thenReturn("App")
+ controller.init(intent)
+ doNothing().whenever(context).startActivity(any())
+ val pref =
+ LayoutPreference(
+ context,
+ LayoutInflater.from(context).inflate(R.layout.nearby_sharing_suggestion_card, null),
+ )
+ pref.key = PREF_KEY
+ mScreen.addPreference(pref)
+ controller.displayPreference(mScreen)
+
+ pref.findViewById(R.id.card_container).performClick()
+
+ verify(context).startActivity(intent)
+ }
+
+ private companion object {
+ const val COMPONENT_NAME = "com.example/.BComponent"
+ const val PREF_KEY = "key"
+ }
+}