Add Nearby share entrypoint in DevicePicker

Bug: 381799866
Test: local build
Flag: com.android.settings.flags.enable_nearby_share_entrypoint
Change-Id: Ied79b9eebe7eaf669e45922787c5b01e504822d8
This commit is contained in:
Haijie Hong
2025-02-19 13:19:21 +08:00
parent 7ab462f1d7
commit a9e67c0327
8 changed files with 389 additions and 1 deletions

View File

@@ -44,3 +44,13 @@ flag {
purpose: PURPOSE_BUGFIX 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
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -203,6 +203,10 @@
<string name="bluetooth_audio_routing_summary">Route sounds to your hearing device or phone speaker</string> <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] --> <!-- Title for related tools section. This section will list related tools below. [CHAR LIMIT=15] -->
<string name="bluetooth_screen_related">Related</string> <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] --> <!-- 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> <string name="bluetooth_ringtone_title">Ringtone and alarms</string>

View File

@@ -15,12 +15,21 @@
--> -->
<PreferenceScreen <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 <com.android.settings.bluetooth.BluetoothProgressCategory
android:key="bt_device_list" android:key="bt_device_list"
android:orderingFromXml="false" android:orderingFromXml="false"
android:title="@string/bluetooth_preference_found_media_devices" /> 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> </PreferenceScreen>

View File

@@ -32,9 +32,11 @@ import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.flags.Flags;
import com.android.settings.password.PasswordUtils; import com.android.settings.password.PasswordUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.AbstractPreferenceController;
@@ -48,6 +50,8 @@ import java.util.List;
public final class DevicePickerFragment extends DeviceListPreferenceFragment { public final class DevicePickerFragment extends DeviceListPreferenceFragment {
private static final String KEY_BT_DEVICE_LIST = "bt_device_list"; private static final String KEY_BT_DEVICE_LIST = "bt_device_list";
private static final String TAG = "DevicePickerFragment"; private static final String TAG = "DevicePickerFragment";
private static final String EXTRA_ORIGINAL_SEND_INTENT =
"android.bluetooth.extra.DEVICE_PICKER_ORIGINAL_SEND_INTENT";
@VisibleForTesting @VisibleForTesting
BluetoothProgressCategory mAvailableDevicesCategory; BluetoothProgressCategory mAvailableDevicesCategory;
@@ -104,6 +108,23 @@ public final class DevicePickerFragment extends DeviceListPreferenceFragment {
setHasOptionsMenu(true); 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 @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();

View File

@@ -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
}
}

View File

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