Merge "Add Nearby share entrypoint in DevicePicker" into main
diff --git a/aconfig/settings_bluetooth_declarations.aconfig b/aconfig/settings_bluetooth_declarations.aconfig
index 7aa989b..4d2528a 100644
--- a/aconfig/settings_bluetooth_declarations.aconfig
+++ b/aconfig/settings_bluetooth_declarations.aconfig
@@ -44,3 +44,13 @@
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 0000000..860c553
--- /dev/null
+++ b/res/drawable/ic_bluetooth_share_info.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20"
+ android:tint="?android:attr/colorAccent">
+ <path
+ android:pathData="M9,15H11V9H9V15ZM10,7C10.283,7 10.517,6.908 10.7,6.725C10.9,6.525 11,6.283 11,6C11,5.717 10.9,5.483 10.7,5.3C10.517,5.1 10.283,5 10,5C9.717,5 9.475,5.1 9.275,5.3C9.092,5.483 9,5.717 9,6C9,6.283 9.092,6.525 9.275,6.725C9.475,6.908 9.717,7 10,7ZM10,20C8.617,20 7.317,19.742 6.1,19.225C4.883,18.692 3.825,17.975 2.925,17.075C2.025,16.175 1.308,15.117 0.775,13.9C0.258,12.683 0,11.383 0,10C0,8.617 0.258,7.317 0.775,6.1C1.308,4.883 2.025,3.825 2.925,2.925C3.825,2.025 4.883,1.317 6.1,0.8C7.317,0.267 8.617,-0 10,-0C11.383,-0 12.683,0.267 13.9,0.8C15.117,1.317 16.175,2.025 17.075,2.925C17.975,3.825 18.683,4.883 19.2,6.1C19.733,7.317 20,8.617 20,10C20,11.383 19.733,12.683 19.2,13.9C18.683,15.117 17.975,16.175 17.075,17.075C16.175,17.975 15.117,18.692 13.9,19.225C12.683,19.742 11.383,20 10,20Z"
+ android:fillColor="#ffffff"/>
+</vector>
diff --git a/res/layout/nearby_sharing_suggestion_card.xml b/res/layout/nearby_sharing_suggestion_card.xml
new file mode 100644
index 0000000..6c9d310
--- /dev/null
+++ b/res/layout/nearby_sharing_suggestion_card.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/nearby_sharing_suggestion_card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingVertical="@dimen/settingslib_expressive_space_medium3">
+
+ <LinearLayout
+ android:id="@+id/card_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:baselineAligned="false"
+ android:paddingHorizontal="@dimen/settingslib_expressive_space_small1"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:background="@drawable/settingslib_card_preference_background">
+
+ <FrameLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top|center_horizontal"
+ android:paddingTop="@dimen/settingslib_expressive_space_small1">
+ <ImageView
+ android:layout_width="@dimen/settingslib_expressive_space_medium4"
+ android:layout_height="@dimen/settingslib_expressive_space_medium4"
+ android:layout_gravity="center"
+ android:padding="@dimen/settingslib_expressive_space_extrasmall2"
+ android:scaleType="fitCenter"
+ android:src="@drawable/circle"
+ android:tint="@color/settingslib_materialColorPrimary"
+ android:importantForAccessibility="no"/>
+ <ImageView
+ android:layout_width="@dimen/settingslib_expressive_space_small3"
+ android:layout_height="@dimen/settingslib_expressive_space_small3"
+ android:layout_gravity="center"
+ android:scaleType="fitCenter"
+ android:src="@drawable/ic_bluetooth_share_info"
+ android:tint="@color/settingslib_materialColorPrimaryContainer"
+ android:importantForAccessibility="no"/>
+ </FrameLayout>
+
+ <LinearLayout
+ android:id="@+id/text_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingHorizontal="@dimen/settingslib_expressive_space_small1"
+ android:paddingTop="@dimen/settingslib_expressive_space_small1"
+ android:paddingBottom="@dimen/settingslib_expressive_space_small4"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/nearby_sharing_suggestion_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.CardTitle.SettingsLib" />
+ <TextView
+ android:id="@+id/nearby_sharing_suggestion_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/settingslib_expressive_space_extrasmall2"
+ android:textAppearance="@style/TextAppearance.CardSummary.SettingsLib"
+ android:text="@string/bluetooth_try_nearby_share_summary"/>
+ </LinearLayout>
+
+ <ImageView
+ android:layout_width="@dimen/settingslib_expressive_space_small4"
+ android:layout_height="@dimen/settingslib_expressive_space_small4"
+ android:layout_gravity="center"
+ android:src="@drawable/ic_chevron_right_24dp"
+ android:tint="@color/settingslib_materialColorPrimary"
+ android:importantForAccessibility="no"
+ android:contentDescription="@null"/>
+
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3679ba4..c22ce6c 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -198,6 +198,10 @@
<string name="bluetooth_audio_routing_summary">Route sounds to your hearing device or phone speaker</string>
<!-- Title for related tools section. This section will list related tools below. [CHAR LIMIT=15] -->
<string name="bluetooth_screen_related">Related</string>
+ <!-- Title for trying Nearby Sharing in Bluetooth Sharing screen. -->
+ <string name="bluetooth_try_nearby_share_title">Try sharing with <xliff:g id="Nearby Sharing app label" example="Nearby Sharing">%s</xliff:g></string>
+ <!-- Summary for trying Nearby Sharing in Bluetooth Sharing screen. -->
+ <string name="bluetooth_try_nearby_share_summary">The fastest way to send files to nearby Android devices</string>
<!-- Bluetooth audio output settings. Title of the option managing ringtone and alarms audio path. [CHAR LIMIT=30] -->
<string name="bluetooth_ringtone_title">Ringtone and alarms</string>
diff --git a/res/xml/device_picker.xml b/res/xml/device_picker.xml
index 6f8d267..5e7667d 100644
--- a/res/xml/device_picker.xml
+++ b/res/xml/device_picker.xml
@@ -15,12 +15,21 @@
-->
<PreferenceScreen
- xmlns:android="http://schemas.android.com/apk/res/android">
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto">
<com.android.settings.bluetooth.BluetoothProgressCategory
android:key="bt_device_list"
android:orderingFromXml="false"
android:title="@string/bluetooth_preference_found_media_devices" />
+ <com.android.settingslib.widget.LayoutPreference
+ android:key="nearby_share_key"
+ android:layout="@layout/nearby_sharing_suggestion_card"
+ android:selectable="false"
+ settings:allowDividerBelow="true"
+ settings:searchable="false"
+ settings:controller="com.android.settings.bluetooth.NearbySharePreferenceController" />
+
</PreferenceScreen>
diff --git a/src/com/android/settings/bluetooth/DevicePickerFragment.java b/src/com/android/settings/bluetooth/DevicePickerFragment.java
index 2e81062..3e88c82 100644
--- a/src/com/android/settings/bluetooth/DevicePickerFragment.java
+++ b/src/com/android/settings/bluetooth/DevicePickerFragment.java
@@ -32,9 +32,11 @@
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 @@
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;
@@ -105,6 +109,23 @@
}
@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();
mLocalManager.getCachedDeviceManager().clearNonBondedDevices();
diff --git a/src/com/android/settings/bluetooth/NearbySharePreferenceController.kt b/src/com/android/settings/bluetooth/NearbySharePreferenceController.kt
new file mode 100644
index 0000000..bf70925
--- /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<TextView>(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<View>(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 0000000..2055e88
--- /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<View>(R.id.card_container).performClick()
+
+ verify(context).startActivity(intent)
+ }
+
+ private companion object {
+ const val COMPONENT_NAME = "com.example/.BComponent"
+ const val PREF_KEY = "key"
+ }
+}