From 974662936eff47d655e1f913aecd5d5bba4d242f Mon Sep 17 00:00:00 2001 From: Manish Singh Date: Thu, 24 Dec 2020 05:07:21 +0000 Subject: [PATCH] Add Settings UI for MTP transcoding over USB. Enabling "Transcode exported media" means that the media transferred from the device via MTP over USB would be transcoded, if necessary. This switch also sets / unsets the sys.fuse.transcode_mtp system property. "Transcode exported media" category would be accessible only when the "File transfer / Audio Auto" or "PTP" radio button has been selected. We are including PTP also since PTP supports transfer of videos. Adding UsbDetailsTranscodeMtpController in a separate preference category than "Charge connected device" primarily because they seemed to be different in their own rights. Here are a few screenshots: https://screenshot.googleplex.com/8jeMstnSFsTtVCS.png https://screenshot.googleplex.com/76hNz4iXp5dcX4M.png https://screenshot.googleplex.com/AkTngE5hDDJCovv.png https://screenshot.googleplex.com/4uQYGXuuSQLoz3w.png BUG: 158466651 Test: manual testing. Also added unit test. Change-Id: I2603a9bffed3320c193cc08f867bd67d9848da18 --- res/values/strings.xml | 7 + res/xml/usb_details_fragment.xml | 5 +- .../usb/UsbDetailsFragment.java | 1 + .../usb/UsbDetailsTranscodeMtpController.java | 95 ++++++++++ .../UsbDetailsTranscodeMtpControllerTest.java | 178 ++++++++++++++++++ 5 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 src/com/android/settings/connecteddevice/usb/UsbDetailsTranscodeMtpController.java create mode 100644 tests/robotests/src/com/android/settings/connecteddevice/usb/UsbDetailsTranscodeMtpControllerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index d72a7bd87af..9e9b3ac9a85 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9956,6 +9956,10 @@ select what the USB connection for this device should be used for. This choice is for transferring photos via PTP. --> PTP + + Transcode exported media @@ -9984,6 +9988,9 @@ Power options + + File transfer options USB diff --git a/res/xml/usb_details_fragment.xml b/res/xml/usb_details_fragment.xml index 62ccf087a09..96a449b5dd4 100644 --- a/res/xml/usb_details_fragment.xml +++ b/res/xml/usb_details_fragment.xml @@ -33,8 +33,11 @@ android:key="usb_details_functions" android:title="@string/usb_use"/> + + - diff --git a/src/com/android/settings/connecteddevice/usb/UsbDetailsFragment.java b/src/com/android/settings/connecteddevice/usb/UsbDetailsFragment.java index 4b83b06a260..bc7656815b0 100644 --- a/src/com/android/settings/connecteddevice/usb/UsbDetailsFragment.java +++ b/src/com/android/settings/connecteddevice/usb/UsbDetailsFragment.java @@ -92,6 +92,7 @@ public class UsbDetailsFragment extends DashboardFragment { ret.add(new UsbDetailsDataRoleController(context, fragment, usbBackend)); ret.add(new UsbDetailsFunctionsController(context, fragment, usbBackend)); ret.add(new UsbDetailsPowerRoleController(context, fragment, usbBackend)); + ret.add(new UsbDetailsTranscodeMtpController(context, fragment, usbBackend)); return ret; } diff --git a/src/com/android/settings/connecteddevice/usb/UsbDetailsTranscodeMtpController.java b/src/com/android/settings/connecteddevice/usb/UsbDetailsTranscodeMtpController.java new file mode 100644 index 00000000000..bb91930f073 --- /dev/null +++ b/src/com/android/settings/connecteddevice/usb/UsbDetailsTranscodeMtpController.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2021 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.connecteddevice.usb; + +import static android.hardware.usb.UsbPortStatus.DATA_ROLE_DEVICE; + +import android.content.Context; +import android.hardware.usb.UsbManager; +import android.os.SystemProperties; + +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.settings.R; +import com.android.settings.Utils; + +/** + * This class controls the switch for setting if we should transcode files transferred via MTP over + * USB. + */ +public class UsbDetailsTranscodeMtpController extends UsbDetailsController + implements Preference.OnPreferenceClickListener { + private static final String TRANSCODE_MTP_SYS_PROP_KEY = "sys.fuse.transcode_mtp"; + private static final String PREFERENCE_KEY = "usb_transcode_mtp"; + + private PreferenceCategory mPreferenceCategory; + private SwitchPreference mSwitchPreference; + + public UsbDetailsTranscodeMtpController(Context context, UsbDetailsFragment fragment, + UsbBackend backend) { + super(context, fragment, backend); + } + + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreferenceCategory = screen.findPreference(getPreferenceKey()); + mSwitchPreference = new SwitchPreference(mPreferenceCategory.getContext()); + mSwitchPreference.setTitle(R.string.usb_transcode_files); + mSwitchPreference.setOnPreferenceClickListener(this); + mPreferenceCategory.addPreference(mSwitchPreference); + } + + @Override + protected void refresh(boolean connected, long functions, int powerRole, int dataRole) { + if (mUsbBackend.areFunctionsSupported(UsbManager.FUNCTION_MTP | UsbManager.FUNCTION_PTP)) { + mFragment.getPreferenceScreen().addPreference(mPreferenceCategory); + } else { + mFragment.getPreferenceScreen().removePreference(mPreferenceCategory); + } + + mSwitchPreference.setChecked(SystemProperties.getBoolean(TRANSCODE_MTP_SYS_PROP_KEY, true)); + mPreferenceCategory.setEnabled( + connected && isDeviceInFileTransferMode(functions, dataRole)); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + SystemProperties.set(TRANSCODE_MTP_SYS_PROP_KEY, + Boolean.toString(mSwitchPreference.isChecked())); + return true; + } + + @Override + public boolean isAvailable() { + return !Utils.isMonkeyRunning(); + } + + @Override + public String getPreferenceKey() { + return PREFERENCE_KEY; + } + + private static boolean isDeviceInFileTransferMode(long functions, int dataRole) { + return dataRole == DATA_ROLE_DEVICE && ((functions & UsbManager.FUNCTION_MTP) != 0 + || (functions & UsbManager.FUNCTION_PTP) != 0); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/usb/UsbDetailsTranscodeMtpControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/usb/UsbDetailsTranscodeMtpControllerTest.java new file mode 100644 index 00000000000..31fd13e064a --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/usb/UsbDetailsTranscodeMtpControllerTest.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2021 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.connecteddevice.usb; + +import static android.hardware.usb.UsbPortStatus.DATA_ROLE_NONE; +import static android.hardware.usb.UsbPortStatus.POWER_ROLE_NONE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.hardware.usb.UsbManager; +import android.os.SystemProperties; + +import androidx.fragment.app.FragmentActivity; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.settings.testutils.shadow.ShadowUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +public class UsbDetailsTranscodeMtpControllerTest { + private static final String TRANSCODE_MTP_SYS_PROP_KEY = "sys.fuse.transcode_mtp"; + + private Context mContext; + private PreferenceCategory mPreference; + private PreferenceManager mPreferenceManager; + private PreferenceScreen mScreen; + private UsbDetailsTranscodeMtpController mUnderTest; + + @Mock + private UsbBackend mUsbBackend; + @Mock + private UsbDetailsFragment mFragment; + @Mock + private FragmentActivity mActivity; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + mPreferenceManager = new PreferenceManager(mContext); + mScreen = mPreferenceManager.createPreferenceScreen(mContext); + + when(mFragment.getActivity()).thenReturn(mActivity); + when(mActivity.getApplicationContext()).thenReturn(mContext); + when(mFragment.getContext()).thenReturn(mContext); + when(mFragment.getPreferenceManager()).thenReturn(mPreferenceManager); + when(mFragment.getPreferenceScreen()).thenReturn(mScreen); + + mUnderTest = new UsbDetailsTranscodeMtpController(mContext, mFragment, mUsbBackend); + + mPreference = new PreferenceCategory(mContext); + mPreference.setKey(mUnderTest.getPreferenceKey()); + mScreen.addPreference(mPreference); + } + + @Test + public void displayRefresh_noUsbConnection_shouldDisablePrefCategory() { + mUnderTest.displayPreference(mScreen); + when(mUsbBackend.areAllRolesSupported()).thenReturn(true); + + mUnderTest.refresh(false /* connected */, UsbManager.FUNCTION_MTP, POWER_ROLE_NONE, + DATA_ROLE_NONE); + + assertThat(mPreference.isEnabled()).isFalse(); + } + + @Test + public void displayRefresh_noDataTransfer_shouldDisablePrefCategory() { + mUnderTest.displayPreference(mScreen); + when(mUsbBackend.areAllRolesSupported()).thenReturn(true); + + mUnderTest.refresh(true /* connected */, UsbManager.FUNCTION_NONE, POWER_ROLE_NONE, + DATA_ROLE_NONE); + + assertThat(mPreference.isEnabled()).isFalse(); + } + + @Test + public void displayRefresh_noDataRole_shouldDisablePrefCategory() throws InterruptedException { + mUnderTest.displayPreference(mScreen); + when(mUsbBackend.areAllRolesSupported()).thenReturn(true); + + mUnderTest.refresh(true /* connected */, UsbManager.FUNCTION_MTP, POWER_ROLE_NONE, + DATA_ROLE_NONE); + + assertThat(mPreference.isEnabled()).isFalse(); + } + + @Test + public void displayRefresh_fileTransfer_withAbsentProp_shouldCheck() { + mUnderTest.displayPreference(mScreen); + when(mUsbBackend.areAllRolesSupported()).thenReturn(true); + + mUnderTest.refresh(true /* connected */, UsbManager.FUNCTION_MTP, POWER_ROLE_NONE, + DATA_ROLE_NONE); + + assertThat(getSwitchPreference().isChecked()).isTrue(); + } + + @Test + public void displayRefresh_fileTransfer_withUnsetProp_shouldUncheck() { + mUnderTest.displayPreference(mScreen); + SystemProperties.set(TRANSCODE_MTP_SYS_PROP_KEY, Boolean.toString(false)); + when(mUsbBackend.areAllRolesSupported()).thenReturn(true); + + mUnderTest.refresh(true /* connected */, UsbManager.FUNCTION_MTP, POWER_ROLE_NONE, + DATA_ROLE_NONE); + + assertThat(getSwitchPreference().isChecked()).isFalse(); + } + + @Test + public void displayRefresh_fileTransfer_withSetProp_shouldCheck() { + mUnderTest.displayPreference(mScreen); + SystemProperties.set(TRANSCODE_MTP_SYS_PROP_KEY, Boolean.toString(true)); + when(mUsbBackend.areAllRolesSupported()).thenReturn(true); + + mUnderTest.refresh(true /* connected */, UsbManager.FUNCTION_MTP, POWER_ROLE_NONE, + DATA_ROLE_NONE); + + assertThat(getSwitchPreference().isChecked()).isTrue(); + } + + @Test + public void click_checked_shouldSetSystemProperty() { + mUnderTest.displayPreference(mScreen); + getSwitchPreference().performClick(); + assertThat(SystemProperties.getBoolean(TRANSCODE_MTP_SYS_PROP_KEY, false)).isTrue(); + } + + @Test + public void click_unChecked_shouldUnsetSystemProperty() { + mUnderTest.displayPreference(mScreen); + getSwitchPreference().performClick(); + getSwitchPreference().performClick(); + assertThat(SystemProperties.getBoolean(TRANSCODE_MTP_SYS_PROP_KEY, false)).isFalse(); + } + + @Test + @Config(shadows = ShadowUtils.class) + public void isAvailable_isMonkey_shouldReturnFalse() { + ShadowUtils.setIsUserAMonkey(true); + assertThat(mUnderTest.isAvailable()).isFalse(); + } + + private SwitchPreference getSwitchPreference() { + return (SwitchPreference) mPreference.getPreference(0); + } +}