Skip to content
This repository has been archived by the owner on Nov 22, 2024. It is now read-only.

[WIP] Add a way to select Android User to let Flipper working on devices with Work Profile #4606

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
135c7b1
feat(android-work-profile): support user flag on run-as command to us…
sbatezat Mar 16, 2023
9f9c2cc
Merge branch 'facebook:main' into main
sbatezat Mar 16, 2023
38792c6
Merge branch 'facebook:main' into main
sbatezat Mar 16, 2023
7afef0b
Merge branch 'facebook:main' into main
sbatezat Mar 17, 2023
1effdd5
Merge branch 'facebook:main' into main
sbatezat Mar 21, 2023
b1a24a0
Merge branch 'main' into main
sbatezat Mar 23, 2023
0068dbb
Merge branch 'facebook:main' into main
sbatezat Mar 24, 2023
95d3830
Merge branch 'facebook:main' into main
sbatezat Mar 27, 2023
67e0353
Merge branch 'facebook:main' into main
sbatezat Mar 30, 2023
99497c4
Merge branch 'facebook:main' into main
sbatezat Apr 13, 2023
8f823ec
Merge branch 'facebook:main' into main
sbatezat Apr 23, 2023
a49ff37
Merge branch 'facebook:main' into main
sbatezat Apr 27, 2023
3bd1b6f
Merge branch 'facebook:main' into main
sbatezat May 4, 2023
a152f29
Merge branch 'facebook:main' into main
sbatezat May 15, 2023
375d749
Merge branch 'facebook:main' into main
sbatezat May 17, 2023
7d5a978
Merge branch 'main' into main
sbatezat May 18, 2023
ea4c687
chore: merge changes from facebook's main branch
sbatezat Nov 29, 2023
ca85b69
Merge branch 'fb_main'
sbatezat Nov 29, 2023
5d1f6b1
fix: duplicate import after last merge
sbatezat Nov 29, 2023
920ba5c
Merge branch 'fb_main'
sbatezat Nov 29, 2023
2a02952
Merge branch 'fb_main'
sbatezat Nov 29, 2023
45186b0
Merge branch 'fb_main'
sbatezat Dec 2, 2023
70e2414
Merge branch 'main' into main
sbatezat Dec 4, 2023
78c378c
Merge branch 'main' into main
sbatezat Dec 4, 2023
6b26be3
chore: merge latest changes
sbatezat Dec 13, 2023
33295bd
Merge branch 'facebook:main' into main
sbatezat Dec 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions desktop/flipper-common/src/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ 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';

export default class AndroidCertificateProvider extends CertificateProvider {
name = 'AndroidCertificateProvider';
medium = 'FS_ACCESS' as const;

constructor(private adb: Client) {
constructor(private flipperServer: FlipperServerImpl, private adb: Client) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we pass in less here than the entire flipperServer? For example just the settings object?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do that yes 👍
What's your though about the more suitable thing to pass in?
You are suggesting the settings object, but the minimal one would be the userId. Indeed, the only reason why flipperServer is pass in is to read "this.flipperServer.config.settings.androidUserId"

super();
}

Expand Down Expand Up @@ -101,6 +102,7 @@ export default class AndroidCertificateProvider extends CertificateProvider {
this.adb,
deviceId,
appName,
this.flipperServer.config.settings.androidUserId,
destination + filename,
contents,
);
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ export default class AndroidDevice
appIds.map(async (appId): Promise<DeviceDebugData | undefined> => {
const sonarDirFilePaths = await executeCommandAsApp(
this.adb,
this.flipperServer.config.settings.androidUserId,
this.info.serial,
appId,
`find /data/data/${appId}/files/sonar -type f`,
Expand Down Expand Up @@ -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,
Expand All @@ -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`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,26 @@ export async function push(
client: Client,
deviceId: string,
app: string,
user: string,
filepath: string,
contents: string,
): Promise<void> {
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<string> {
validateAppName(app);
validateFilePath(path);
return await _pull(client, deviceId, app, path);
return await _pull(client, deviceId, app, user, path);
}

function validateAppName(app: string): void {
Expand Down Expand Up @@ -84,14 +86,15 @@ function _push(
client: Client,
deviceId: string,
app: AppName,
user: string,
filename: FilePath,
contents: FileContent,
): Promise<void> {
console.debug(`Deploying ${filename} to ${deviceId}:${app}`, logTag);
// 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) {
Expand All @@ -107,31 +110,35 @@ function _pull(
client: Client,
deviceId: string,
app: AppName,
user: string,
path: FilePath,
): Promise<string> {
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
export function executeCommandAsApp(
client: Client,
deviceId: string,
app: string,
user: string,
command: string,
): Promise<string> {
return _executeCommandWithRunner(
client,
deviceId,
app,
command,
`run-as '${app}'`,
`run-as '${app}' --user ${user}`,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions desktop/flipper-server-core/src/utils/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
60 changes: 57 additions & 3 deletions desktop/flipper-ui-core/src/chrome/SettingsSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@
* @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';
import {
FilePathConfigField,
ConfigText,
URLConfigField,
ComboBoxConfigField,
} from './settings/configFields';
import KeyboardShortcutInput from './settings/KeyboardShortcutInput';
import {isEqual, isMatch, isEmpty} from 'lodash';
Expand All @@ -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;
Expand Down Expand Up @@ -120,6 +122,7 @@ class SettingsSheet extends Component<Props, State> {
const {
enableAndroid,
androidHome,
androidUserId,
enableIOS,
enablePhysicalIOS,
enablePrefetching,
Expand Down Expand Up @@ -180,6 +183,17 @@ class SettingsSheet extends Component<Props, State> {
});
}}
/>
<AndroidUserIdField
defaultValue={androidUserId}
onChange={(v) => {
this.setState({
updatedSettings: {
...this.state.updatedSettings,
androidUserId: v,
},
});
}}
/>
</ToggledSection>
<ToggledSection
label="iOS Developer"
Expand Down Expand Up @@ -499,6 +513,46 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
{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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best run shell commands in an effect, with the current setup the command is fired on every render

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As said on comments, I don't know React. Please feel free to edit that part, as I don't have knowledge about these "useEffect" things. Just a warn: when you say "the command is fired on every render", it may be fine. Best would be to run this command each time selected device change (and user is opening settings sheet) or you could have bad users ids shown.

.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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some more details to the error message? Also let's start with .warn, as this is typically a local setup issue

Copy link
Author

@sbatezat sbatezat Mar 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This command should not fail. If this command fails there is chances that every shell command will. If I have a look on Flipper source code where executeShell is called, errors are not even managed. For example, on plugins/public/cpi/index.tsx

const chipname = await executeShell('getprop ro.chipname');

There is no try/catch.
The only reason why I've got a catch it's because I don't know how to await in a React context so linters are compelling me to consume the promise in the good old way (.then/.catch) with a compelled catch block (as per linters rules).

With these informations in mind, what do you think about changes I should work on this? Just a warn instead of an error?


if (users.length === 0) {
return (
<ConfigText
content={
'Loading... Please be sure to connect a device to show available users.'
}></ConfigText>
);
}

return (
<ComboBoxConfigField
label="Android User"
resetValue={getRenderHostInstance().serverConfig.settings.androidUserId}
options={users}
defaultValue={props.defaultValue}
onChange={props.onChange}
/>
);
}

function ResetTooltips() {
const nuxManager = useContext(_NuxManagerContext);

Expand Down
70 changes: 70 additions & 0 deletions desktop/flipper-ui-core/src/chrome/settings/configFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 (
<ConfigFieldContainer>
<InfoText>{props.label}</InfoText>
<SelectBox
name="select"
isValid={props.options.some((opt) => opt.id === value)}
value={value}
onChange={(event) => {
props.onChange(event.target.value);
setValue(props.options[event.target.selectedIndex].id);
}}>
{props.options.map((option) => {
return (
<option key={option.id} value={option.id}>
{option.name}
</option>
);
})}
</SelectBox>
{resetOption && (
<FlexColumn
title={`Reset to default ${props.resetValue}`}
onClick={() => {
props.onChange(props.resetValue!);
setValue(props.resetValue!);
}}>
<CenteredGlyph
color={theme.primaryColor}
name="undo"
variant="outline"
/>
</FlexColumn>
)}
{props.options.some((opt) => opt.id === value) ? null : (
<CenteredGlyph name="caution-triangle" color={colors.yellow} />
)}
</ConfigFieldContainer>
);
}

export function FilePathConfigField(props: {
label: string;
resetValue?: string;
Expand Down
1 change: 1 addition & 0 deletions desktop/scripts/jest-setup-after.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ function createStubRenderHost(): RenderHost {
},
settings: {
androidHome: `/dev/null`,
androidUserId: '0',
darkMode: 'light',
enableAndroid: false,
enableIOS: false,
Expand Down