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" + } +}