diff --git a/desktop/flipper-common/src/settings.tsx b/desktop/flipper-common/src/settings.tsx index a1fd3f6ad66..bd9fcb05d7d 100644 --- a/desktop/flipper-common/src/settings.tsx +++ b/desktop/flipper-common/src/settings.tsx @@ -19,6 +19,11 @@ export enum Tristate { */ export type Settings = { androidHome: string; + /** + * If unset, this will be set to '0', as it's the default profile on android devices. + * Enterprises using Work Profile might changed it to the Work Profile user id. + */ + androidUserId: string; enableAndroid: boolean; enableIOS: boolean; enablePhysicalIOS: boolean; diff --git a/desktop/flipper-server/src/devices/android/AndroidCertificateProvider.tsx b/desktop/flipper-server/src/devices/android/AndroidCertificateProvider.tsx index 0d2ebaa0fcb..adaf7067da4 100644 --- a/desktop/flipper-server/src/devices/android/AndroidCertificateProvider.tsx +++ b/desktop/flipper-server/src/devices/android/AndroidCertificateProvider.tsx @@ -16,12 +16,13 @@ import { } from '../../app-connectivity/certificate-exchange/certificate-utils'; import {ClientQuery} from 'flipper-common'; import {recorder} from '../../recorder'; +import {FlipperServerImpl} from '../../FlipperServerImpl'; export default class AndroidCertificateProvider extends CertificateProvider { name = 'AndroidCertificateProvider'; medium = 'FS_ACCESS' as const; - constructor(private adb: Client) { + constructor(private flipperServer: FlipperServerImpl, private adb: Client) { super(); } @@ -109,6 +110,7 @@ export default class AndroidCertificateProvider extends CertificateProvider { this.adb, deviceId, appName, + this.flipperServer.config.settings.androidUserId, destination + filename, contents, clientQuery, @@ -126,6 +128,7 @@ export default class AndroidCertificateProvider extends CertificateProvider { this.adb, deviceId, processName, + this.flipperServer.config.settings.androidUserId, directory + csrFileName, clientQuery, ); diff --git a/desktop/flipper-server/src/devices/android/AndroidDevice.tsx b/desktop/flipper-server/src/devices/android/AndroidDevice.tsx index dee90d1e210..34a8c44e5ae 100644 --- a/desktop/flipper-server/src/devices/android/AndroidDevice.tsx +++ b/desktop/flipper-server/src/devices/android/AndroidDevice.tsx @@ -323,6 +323,7 @@ export default class AndroidDevice appIds.map(async (appId): Promise => { const sonarDirFilePaths = await executeCommandAsApp( this.adb, + this.flipperServer.config.settings.androidUserId, this.info.serial, appId, `find /data/data/${appId}/files/sonar -type f`, @@ -370,6 +371,7 @@ export default class AndroidDevice path: filePath, data: await pull( this.adb, + this.flipperServer.config.settings.androidUserId, this.info.serial, appId, filePath, @@ -380,6 +382,7 @@ export default class AndroidDevice const sonarDirContentWithStatsCommandPromise = executeCommandAsApp( this.adb, + this.flipperServer.config.settings.androidUserId, this.info.serial, appId, `ls -al /data/data/${appId}/files/sonar`, diff --git a/desktop/flipper-server/src/devices/android/androidContainerUtility.tsx b/desktop/flipper-server/src/devices/android/androidContainerUtility.tsx index 3ddac60d979..e5a52619836 100644 --- a/desktop/flipper-server/src/devices/android/androidContainerUtility.tsx +++ b/desktop/flipper-server/src/devices/android/androidContainerUtility.tsx @@ -27,6 +27,7 @@ export async function push( adbClient: Client, deviceId: string, app: string, + user: string, filepath: string, contents: string, clientQuery?: ClientQuery, @@ -34,19 +35,28 @@ export async function push( validateAppName(app); validateFilePath(filepath); validateFileContent(contents); - return await _push(adbClient, deviceId, app, filepath, contents, clientQuery); + return await _push( + adbClient, + deviceId, + app, + user, + filepath, + contents, + clientQuery, + ); } export async function pull( adbClient: Client, deviceId: string, app: string, + user: string, path: string, clientQuery?: ClientQuery, ): Promise { validateAppName(app); validateFilePath(path); - return await _pull(adbClient, deviceId, app, path, clientQuery); + return await _pull(adbClient, deviceId, app, user, path, clientQuery); } function validateAppName(app: string): void { @@ -87,6 +97,7 @@ async function _push( adbClient: Client, deviceId: string, app: AppName, + user: string, filename: FilePath, contents: FileContent, clientQuery?: ClientQuery, @@ -118,14 +129,14 @@ async function _push( }; try { - await executeCommandAsApp(adbClient, deviceId, app, cmd); + await executeCommandAsApp(adbClient, deviceId, app, user, cmd); reportSuccess(); } catch (error) { if (error instanceof RunAsError) { // Fall back to running the command directly. // This will work if adb is running as root. try { - await executeCommandWithSu(adbClient, deviceId, app, cmd, error); + await executeCommandWithSu(adbClient, deviceId, app, user, cmd, error); reportSuccess(); return; } catch (suError) { @@ -142,6 +153,7 @@ async function _pull( adbClient: Client, deviceId: string, app: AppName, + user: string, path: FilePath, clientQuery?: ClientQuery, ): Promise { @@ -170,7 +182,13 @@ async function _pull( }; try { - const content = await executeCommandAsApp(adbClient, deviceId, app, cmd); + const content = await executeCommandAsApp( + adbClient, + deviceId, + app, + user, + cmd, + ); reportSuccess(); return content; } catch (error) { @@ -182,6 +200,7 @@ async function _pull( adbClient, deviceId, app, + user, cmd, error, ); @@ -202,6 +221,7 @@ export function executeCommandAsApp( adbClient: Client, deviceId: string, app: string, + user: string, command: string, ): Promise { return _executeCommandWithRunner( @@ -209,7 +229,7 @@ export function executeCommandAsApp( deviceId, app, command, - `run-as '${app}'`, + `run-as '${app}' --user ${user}`, ); } @@ -217,11 +237,18 @@ async function executeCommandWithSu( adbClient: Client, deviceId: string, app: string, + user: string, command: string, originalErrorToThrow: RunAsError, ): Promise { try { - return _executeCommandWithRunner(adbClient, deviceId, app, command, 'su'); + return _executeCommandWithRunner( + adbClient, + deviceId, + app, + command, + `su --user ${user}`, + ); } catch (e) { throw originalErrorToThrow; } diff --git a/desktop/flipper-server/src/devices/android/androidDeviceManager.tsx b/desktop/flipper-server/src/devices/android/androidDeviceManager.tsx index 337f98672a0..5ff433c5efa 100644 --- a/desktop/flipper-server/src/devices/android/androidDeviceManager.tsx +++ b/desktop/flipper-server/src/devices/android/androidDeviceManager.tsx @@ -23,7 +23,10 @@ export class AndroidDeviceManager { private readonly flipperServer: FlipperServerImpl, private readonly adbClient: ADBClient, ) { - this.certificateProvider = new AndroidCertificateProvider(this.adbClient); + this.certificateProvider = new AndroidCertificateProvider( + this.flipperServer, + this.adbClient, + ); } private createDevice( diff --git a/desktop/flipper-server/src/utils/settings.tsx b/desktop/flipper-server/src/utils/settings.tsx index 47a808dd672..1afe875365d 100644 --- a/desktop/flipper-server/src/utils/settings.tsx +++ b/desktop/flipper-server/src/utils/settings.tsx @@ -52,6 +52,7 @@ export const DEFAULT_ANDROID_SDK_PATH = getDefaultAndroidSdkPath(); async function getDefaultSettings(): Promise { return { androidHome: await getDefaultAndroidSdkPath(), + androidUserId: '0', enableAndroid: true, enableIOS: os.platform() === 'darwin', enablePhysicalIOS: os.platform() === 'darwin', diff --git a/desktop/flipper-ui/src/chrome/SettingsSheet.tsx b/desktop/flipper-ui/src/chrome/SettingsSheet.tsx index 44180efe95d..474dd4fbd40 100644 --- a/desktop/flipper-ui/src/chrome/SettingsSheet.tsx +++ b/desktop/flipper-ui/src/chrome/SettingsSheet.tsx @@ -7,14 +7,14 @@ * @format */ -import React, {Component, useContext} from 'react'; +import React, {Component, useContext, useState} from 'react'; import {Radio} from 'antd'; import {updateSettings, Action} from '../reducers/settings'; import { Action as LauncherAction, updateLauncherSettings, } from '../reducers/launcherSettings'; -import {connect} from 'react-redux'; +import {connect, useSelector} from 'react-redux'; import {State as Store} from '../reducers'; import {flush} from '../utils/persistor'; import ToggledSection from './settings/ToggledSection'; @@ -22,6 +22,7 @@ import { FilePathConfigField, ConfigText, URLConfigField, + ComboBoxConfigField, } from './settings/configFields'; import {isEqual, isMatch, isEmpty} from 'lodash'; import LauncherSettingsPanel from '../fb-stubs/LauncherSettingsPanel'; @@ -40,7 +41,9 @@ import { NUX, } from 'flipper-plugin'; import {loadTheme} from '../utils/loadTheme'; +import {getActiveDevice} from '../selectors/connections'; import {getFlipperServer, getFlipperServerConfig} from '../flipperServer'; +import BaseDevice from '../devices/BaseDevice'; type OwnProps = { onHide: () => void; @@ -119,6 +122,7 @@ class SettingsSheet extends Component { const { enableAndroid, androidHome, + androidUserId, enableIOS, enablePhysicalIOS, enablePrefetching, @@ -169,6 +173,17 @@ class SettingsSheet extends Component { }); }} /> + { + this.setState({ + updatedSettings: { + ...this.state.updatedSettings, + androidUserId: v, + }, + }); + }} + /> ( {updateSettings, updateLauncherSettings}, )(withTrackingScope(SettingsSheet)); +function AndroidUserIdField(props: { + defaultValue: string; + onChange: (path: string) => void; +}) { + const activeDevice: BaseDevice = useSelector(getActiveDevice); + const [users, setUsers] = useState([] as {id: string; name: string}[]); + + activeDevice + .executeShell('pm list users') + .then((result: string) => { + const users = result + .match(/(?<=UserInfo{)(.*?)(?=})/g) + ?.map((userInfo) => { + const infos = userInfo.split(':'); + return {id: infos[0], name: `${infos[0]} (${infos[1]})`}; + }); + setUsers(users || []); + }) + .catch((error: Error) => console.error(error)); + + if (users.length === 0) { + return ( + + ); + } + + return ( + + ); +} + function ResetTooltips() { const nuxManager = useContext(_NuxManagerContext); diff --git a/desktop/flipper-ui/src/chrome/settings/configFields.tsx b/desktop/flipper-ui/src/chrome/settings/configFields.tsx index 9e295ec40a3..76810d2571e 100644 --- a/desktop/flipper-ui/src/chrome/settings/configFields.tsx +++ b/desktop/flipper-ui/src/chrome/settings/configFields.tsx @@ -41,6 +41,16 @@ const FileInputBox = styled(Input)<{isValid: boolean}>(({isValid}) => ({ marginBottom: 'auto', })); +const SelectBox = styled.select<{isValid: boolean}>(({isValid}) => ({ + marginRight: 0, + flexGrow: 1, + fontFamily: 'monospace', + color: isValid ? undefined : colors.red, + marginLeft: 10, + marginTop: 'auto', + marginBottom: 'auto', +})); + const CenteredGlyph = styled(Glyph)({ margin: 'auto', marginLeft: 10, @@ -56,6 +66,66 @@ const GrayedOutOverlay = styled.div({ right: 0, }); +export function ComboBoxConfigField(props: { + label: string; + resetValue?: string; + options: {id: string; name: string}[]; + defaultValue: string; + onChange: (path: string) => void; +}) { + let defaultOption = props.options.find( + (opt) => opt.id === props.defaultValue, + ); + const resetOption = props.options.find((opt) => opt.id === props.resetValue); + const [value, setValue] = useState(defaultOption?.id); + + // If there is no valid default value, force setting the value to the first one + if (!value) { + defaultOption = props.options[0]; + props.onChange(defaultOption.id); + setValue(defaultOption.id); + } + + return ( + + {props.label} + opt.id === value)} + value={value} + onChange={(event) => { + props.onChange(event.target.value); + setValue(props.options[event.target.selectedIndex].id); + }}> + {props.options.map((option) => { + return ( + + ); + })} + + {resetOption && ( + { + props.onChange(props.resetValue!); + setValue(props.resetValue!); + }}> + + + )} + {props.options.some((opt) => opt.id === value) ? null : ( + + )} + + ); +} + export function FilePathConfigField(props: { label: string; resetValue?: string; diff --git a/desktop/scripts/jest-setup-after.tsx b/desktop/scripts/jest-setup-after.tsx index 704ec2c6366..fda6bc56e86 100644 --- a/desktop/scripts/jest-setup-after.tsx +++ b/desktop/scripts/jest-setup-after.tsx @@ -189,6 +189,7 @@ export function createStubFlipperServerConfig(): FlipperServerConfig { }, settings: { androidHome: `/dev/null`, + androidUserId: '0', darkMode: 'light', enableAndroid: false, enableIOS: false,