From 135c7b1393a944af29d9fe21c6e52b77ca2388e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20BATEZAT?= Date: Thu, 16 Mar 2023 11:03:25 +0100 Subject: [PATCH 1/2] feat(android-work-profile): support user flag on run-as command to use Android Work Profile --- desktop/flipper-common/src/settings.tsx | 5 ++ .../android/AndroidCertificateProvider.tsx | 5 +- .../src/devices/android/AndroidDevice.tsx | 3 + .../android/androidContainerUtility.tsx | 29 +++++--- .../devices/android/androidDeviceManager.tsx | 5 +- .../src/utils/settings.tsx | 1 + .../src/chrome/SettingsSheet.tsx | 60 +++++++++++++++- .../src/chrome/settings/configFields.tsx | 70 +++++++++++++++++++ desktop/scripts/jest-setup-after.tsx | 1 + 9 files changed, 163 insertions(+), 16 deletions(-) diff --git a/desktop/flipper-common/src/settings.tsx b/desktop/flipper-common/src/settings.tsx index 9568d768079..556dc483329 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-core/src/devices/android/AndroidCertificateProvider.tsx b/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx index 18fc654c2cf..8cfa4a8d598 100644 --- a/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx +++ b/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx @@ -11,6 +11,7 @@ import CertificateProvider from '../../utils/CertificateProvider'; import {Client} from 'adbkit'; import * as androidUtil from './androidContainerUtility'; import {csrFileName, extractAppNameFromCSR} from '../../utils/certificateUtils'; +import {FlipperServerImpl} from '../../FlipperServerImpl'; const logTag = 'AndroidCertificateProvider'; @@ -18,7 +19,7 @@ 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(); } @@ -101,6 +102,7 @@ export default class AndroidCertificateProvider extends CertificateProvider { this.adb, deviceId, appName, + this.flipperServer.config.settings.androidUserId, destination + filename, contents, ); @@ -116,6 +118,7 @@ export default class AndroidCertificateProvider extends CertificateProvider { this.adb, deviceId, processName, + this.flipperServer.config.settings.androidUserId, directory + csrFileName, ); // Santitize both of the string before comparation diff --git a/desktop/flipper-server-core/src/devices/android/AndroidDevice.tsx b/desktop/flipper-server-core/src/devices/android/AndroidDevice.tsx index 92c20214fe6..605c81c79cf 100644 --- a/desktop/flipper-server-core/src/devices/android/AndroidDevice.tsx +++ b/desktop/flipper-server-core/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-core/src/devices/android/androidContainerUtility.tsx b/desktop/flipper-server-core/src/devices/android/androidContainerUtility.tsx index ae5464c10b9..d2eed04c208 100644 --- a/desktop/flipper-server-core/src/devices/android/androidContainerUtility.tsx +++ b/desktop/flipper-server-core/src/devices/android/androidContainerUtility.tsx @@ -26,24 +26,26 @@ export async function push( client: Client, deviceId: string, app: string, + user: string, filepath: string, contents: string, ): Promise { validateAppName(app); validateFilePath(filepath); validateFileContent(contents); - return await _push(client, deviceId, app, filepath, contents); + return await _push(client, deviceId, app, user, filepath, contents); } export async function pull( client: Client, deviceId: string, app: string, + user: string, path: string, ): Promise { validateAppName(app); validateFilePath(path); - return await _pull(client, deviceId, app, path); + return await _pull(client, deviceId, app, user, path); } function validateAppName(app: string): void { @@ -84,6 +86,7 @@ function _push( client: Client, deviceId: string, app: AppName, + user: string, filename: FilePath, contents: FileContent, ): Promise { @@ -91,7 +94,7 @@ function _push( // TODO: this is sensitive to escaping issues, can we leverage client.push instead? // https://www.npmjs.com/package/adbkit#pushing-a-file-to-all-connected-devices const command = `echo "${contents}" > '${filename}' && chmod 644 '${filename}'`; - return executeCommandAsApp(client, deviceId, app, command) + return executeCommandAsApp(client, deviceId, app, user, command) .then((_) => undefined) .catch((error) => { if (error instanceof RunAsError) { @@ -107,16 +110,19 @@ function _pull( client: Client, deviceId: string, app: AppName, + user: string, path: FilePath, ): Promise { const command = `cat '${path}'`; - return executeCommandAsApp(client, deviceId, app, command).catch((error) => { - if (error instanceof RunAsError) { - // Fall back to running the command directly. This will work if adb is running as root. - return executeCommandWithSu(client, deviceId, app, command, error); - } - throw error; - }); + return executeCommandAsApp(client, deviceId, app, user, command).catch( + (error) => { + if (error instanceof RunAsError) { + // Fall back to running the command directly. This will work if adb is running as root. + return executeCommandWithSu(client, deviceId, app, command, error); + } + throw error; + }, + ); } // Keep this method private since it relies on pre-validated arguments @@ -124,6 +130,7 @@ export function executeCommandAsApp( client: Client, deviceId: string, app: string, + user: string, command: string, ): Promise { return _executeCommandWithRunner( @@ -131,7 +138,7 @@ export function executeCommandAsApp( deviceId, app, command, - `run-as '${app}'`, + `run-as '${app}' --user ${user}`, ); } diff --git a/desktop/flipper-server-core/src/devices/android/androidDeviceManager.tsx b/desktop/flipper-server-core/src/devices/android/androidDeviceManager.tsx index 1589f3ffae4..ba3b9db5666 100644 --- a/desktop/flipper-server-core/src/devices/android/androidDeviceManager.tsx +++ b/desktop/flipper-server-core/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-core/src/utils/settings.tsx b/desktop/flipper-server-core/src/utils/settings.tsx index 5c4a9b3ba73..32c03b397bb 100644 --- a/desktop/flipper-server-core/src/utils/settings.tsx +++ b/desktop/flipper-server-core/src/utils/settings.tsx @@ -51,6 +51,7 @@ export const DEFAULT_ANDROID_SDK_PATH = getDefaultAndroidSdkPath(); function getDefaultSettings(): Settings { return { androidHome: getDefaultAndroidSdkPath(), + androidUserId: '0', enableAndroid: true, enableIOS: os.platform() === 'darwin', enablePhysicalIOS: os.platform() === 'darwin', diff --git a/desktop/flipper-ui-core/src/chrome/SettingsSheet.tsx b/desktop/flipper-ui-core/src/chrome/SettingsSheet.tsx index 5ab70a30062..99c1c330d6c 100644 --- a/desktop/flipper-ui-core/src/chrome/SettingsSheet.tsx +++ b/desktop/flipper-ui-core/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 KeyboardShortcutInput from './settings/KeyboardShortcutInput'; import {isEqual, isMatch, isEmpty} from 'lodash'; @@ -40,8 +41,9 @@ import { _NuxManagerContext, NUX, } from 'flipper-plugin'; -import {getRenderHostInstance} from 'flipper-frontend-core'; +import {BaseDevice, getRenderHostInstance} from 'flipper-frontend-core'; import {loadTheme} from '../utils/loadTheme'; +import {getActiveDevice} from '../selectors/connections'; type OwnProps = { onHide: () => void; @@ -120,6 +122,7 @@ class SettingsSheet extends Component { const { enableAndroid, androidHome, + androidUserId, enableIOS, enablePhysicalIOS, enablePrefetching, @@ -180,6 +183,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) => { + 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) => console.error(error)); + + if (users.length === 0) { + return ( + + ); + } + + return ( + + ); +} + function ResetTooltips() { const nuxManager = useContext(_NuxManagerContext); diff --git a/desktop/flipper-ui-core/src/chrome/settings/configFields.tsx b/desktop/flipper-ui-core/src/chrome/settings/configFields.tsx index 216383bf2e1..4303f710bb4 100644 --- a/desktop/flipper-ui-core/src/chrome/settings/configFields.tsx +++ b/desktop/flipper-ui-core/src/chrome/settings/configFields.tsx @@ -42,6 +42,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, @@ -57,6 +67,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 1fd69331ffe..abfbe78498e 100644 --- a/desktop/scripts/jest-setup-after.tsx +++ b/desktop/scripts/jest-setup-after.tsx @@ -142,6 +142,7 @@ function createStubRenderHost(): RenderHost { }, settings: { androidHome: `/dev/null`, + androidUserId: '0', darkMode: 'light', enableAndroid: false, enableIOS: false, From 5d1f6b1b76844866f84ebee2cd1fe70d9474a7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20BATEZAT?= Date: Wed, 29 Nov 2023 16:25:23 +0100 Subject: [PATCH 2/2] fix: duplicate import after last merge --- .../src/devices/android/AndroidCertificateProvider.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx b/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx index 4aea3301417..dee3570ece3 100644 --- a/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx +++ b/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx @@ -10,16 +10,13 @@ import CertificateProvider from '../../app-connectivity/certificate-exchange/CertificateProvider'; import {Client} from 'adbkit'; import * as androidUtil from './androidContainerUtility'; -import {csrFileName, extractAppNameFromCSR} from '../../utils/certificateUtils'; -import {FlipperServerImpl} from '../../FlipperServerImpl'; - -const logTag = 'AndroidCertificateProvider'; import { csrFileName, extractBundleIdFromCSR, } 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';