Skip to content

Commit a73cecc

Browse files
adamTrzszymonrybczakhomersimpsons
authored andcommitted
feat: added --user flag to run-android command (#1869)
* feat: added userId to run-android command * Update packages/cli-platform-android/src/commands/runAndroid/listAndroidUsers.ts Co-authored-by: Szymon Rybczak <[email protected]> * Update docs/commands.md Co-authored-by: homersimpsons <[email protected]> * fix: code review fixes * chore: update docs * fix: ts and tests --------- Co-authored-by: Szymon Rybczak <[email protected]> Co-authored-by: homersimpsons <[email protected]>
1 parent a00a896 commit a73cecc

File tree

7 files changed

+118
-10
lines changed

7 files changed

+118
-10
lines changed

docs/commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,4 @@ Upgrade your app's template files to the specified or latest npm version using [
119119

120120
Using this command is a recommended way of upgrading relatively simple React Native apps with not too many native libraries linked. The more iOS and Android build files are modified, the higher chance for a conflicts. The command will guide you on how to continue upgrade process manually in case of failure.
121121

122-
_Note: If you'd like to upgrade using this method from React Native version lower than 0.59.0, you may use a standalone version of this CLI: `npx @react-native-community/cli upgrade`._
122+
_Note: If you'd like to upgrade using this method from React Native version lower than 0.59.0, you may use a standalone version of this CLI: `npx @react-native-community/cli upgrade`._

packages/cli-platform-android/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ react-native build-android --extra-params "-x lint -x test"
126126

127127
Installs passed binary instead of building a fresh one. This command is not compatible with `--tasks`.
128128

129+
#### `--user` <number | string>
130+
131+
Id of the User Profile you want to install the app on.
129132
### `log-android`
130133

131134
Usage:
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import execa from 'execa';
2+
import {checkUsers} from '../listAndroidUsers';
3+
4+
// output of "adb -s ... shell pm users list" command
5+
const gradleOutput = `
6+
Users:
7+
UserInfo{0:Homersimpsons:c13} running
8+
UserInfo{10:Guest:404}
9+
`;
10+
11+
jest.mock('execa', () => {
12+
return {sync: jest.fn()};
13+
});
14+
15+
describe('check android users', () => {
16+
it('should correctly parse recieved users', () => {
17+
(execa.sync as jest.Mock).mockReturnValueOnce({stdout: gradleOutput});
18+
const users = checkUsers('device', 'adbPath');
19+
20+
expect(users).toStrictEqual([
21+
{id: '0', name: 'Homersimpsons'},
22+
{id: '10', name: 'Guest'},
23+
]);
24+
});
25+
});

packages/cli-platform-android/src/commands/runAndroid/getTaskNames.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ export function getTaskNames(
1010
taskPrefix: 'assemble' | 'install' | 'bundle',
1111
sourceDir: string,
1212
): Array<string> {
13-
let appTasks = tasks || [taskPrefix + toPascalCase(mode)];
13+
let appTasks =
14+
tasks && tasks.length ? tasks : [taskPrefix + toPascalCase(mode)];
1415

1516
// Check against build flavors for "install" task ("assemble" don't care about it so much and will build all)
16-
if (!tasks && taskPrefix === 'install') {
17+
if (!tasks?.length && taskPrefix === 'install') {
1718
const actionableInstallTasks = getGradleTasks('install', sourceDir);
1819
if (!actionableInstallTasks.find((t) => t.task.includes(appTasks[0]))) {
1920
const installTasksForMode = actionableInstallTasks.filter((t) =>

packages/cli-platform-android/src/commands/runAndroid/index.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import path from 'path';
2222
import {build, runPackager, BuildFlags, options} from '../buildAndroid';
2323
import {promptForTaskSelection} from './listAndroidTasks';
2424
import {getTaskNames} from './getTaskNames';
25+
import {checkUsers, promptForUser} from './listAndroidUsers';
2526

2627
export interface Flags extends BuildFlags {
2728
appId: string;
@@ -30,6 +31,7 @@ export interface Flags extends BuildFlags {
3031
deviceId?: string;
3132
listDevices?: boolean;
3233
binaryPath?: string;
34+
user?: number | string;
3335
}
3436

3537
export type AndroidProject = NonNullable<Config['project']['android']>;
@@ -121,6 +123,17 @@ async function buildAndRun(args: Flags, androidProject: AndroidProject) {
121123
);
122124
}
123125

126+
if (args.interactive) {
127+
const users = checkUsers(device.deviceId as string, adbPath);
128+
if (users && users.length > 1) {
129+
const user = await promptForUser(users);
130+
131+
if (user) {
132+
args.user = user.id;
133+
}
134+
}
135+
}
136+
124137
if (device.connected) {
125138
return runOnSpecificDevice(
126139
{...args, deviceId: device.deviceId},
@@ -167,14 +180,16 @@ function runOnSpecificDevice(
167180
// if coming from run-android command and we have selected task
168181
// from interactive mode we need to create appropriate build task
169182
// eg 'installRelease' -> 'assembleRelease'
170-
const buildTask = selectedTask?.replace('install', 'assemble') ?? 'build';
183+
const buildTask = selectedTask
184+
? [selectedTask.replace('install', 'assemble')]
185+
: [];
171186

172187
if (devices.length > 0 && deviceId) {
173188
if (devices.indexOf(deviceId) !== -1) {
174189
let gradleArgs = getTaskNames(
175190
androidProject.appName,
176191
args.mode || args.variant,
177-
args.tasks ?? [buildTask],
192+
args.tasks ?? buildTask,
178193
'install',
179194
androidProject.sourceDir,
180195
);
@@ -287,6 +302,11 @@ export default {
287302
description:
288303
'Path relative to project root where pre-built .apk binary lives.',
289304
},
305+
{
306+
name: '--user <number>',
307+
description: 'Id of the User Profile you want to install the app on.',
308+
parse: Number,
309+
},
290310
],
291311
};
292312

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {logger} from '@react-native-community/cli-tools';
2+
import execa from 'execa';
3+
import prompts from 'prompts';
4+
5+
type User = {
6+
id: string;
7+
name: string;
8+
};
9+
10+
export function checkUsers(device: string, adbPath: string) {
11+
try {
12+
const adbArgs = ['-s', device, 'shell', 'pm', 'list', 'users'];
13+
14+
logger.debug(`Checking users on "${device}"...`);
15+
const {stdout} = execa.sync(adbPath, adbArgs, {encoding: 'utf-8'});
16+
const regex = new RegExp(
17+
/^\s*UserInfo\{(?<userId>\d+):(?<userName>.*):(?<userFlags>[0-9a-f]*)}/,
18+
);
19+
const users: User[] = [];
20+
21+
const lines = stdout.split('\n');
22+
for (const line of lines) {
23+
const res = regex.exec(line);
24+
if (res?.groups) {
25+
users.push({id: res.groups.userId, name: res.groups.userName});
26+
}
27+
}
28+
29+
if (users.length > 1) {
30+
logger.debug(
31+
`Available users are:\n${users
32+
.map((user) => `${user.name} - ${user.id}`)
33+
.join('\n')}`,
34+
);
35+
}
36+
37+
return users;
38+
} catch (error) {
39+
logger.error('Failed to check users of device.', error as any);
40+
return [];
41+
}
42+
}
43+
44+
export async function promptForUser(users: User[]) {
45+
const {selectedUser}: {selectedUser: User} = await prompts({
46+
type: 'select',
47+
name: 'selectedUser',
48+
message: 'Which profile would you like to launch your app into?',
49+
choices: users.map((user: User) => ({
50+
title: user.name,
51+
value: user,
52+
})),
53+
min: 1,
54+
});
55+
56+
return selectedUser;
57+
}

packages/cli-platform-android/src/commands/runAndroid/tryInstallAppOnDevice.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@ function tryInstallAppOnDevice(
4545
pathToApk = args.binaryPath;
4646
}
4747

48-
const adbArgs = ['-s', device, 'install', '-r', '-d', pathToApk];
48+
const installArgs = ['-s', device, 'install', '-r', '-d'];
49+
if (args.user !== undefined) {
50+
installArgs.push('--user', `${args.user}`);
51+
}
52+
const adbArgs = [...installArgs, pathToApk];
4953
logger.info(`Installing the app on the device "${device}"...`);
50-
logger.debug(
51-
`Running command "cd android && adb -s ${device} install -r -d ${pathToApk}"`,
52-
);
54+
logger.debug(`Running command "cd android && adb ${adbArgs.join(' ')}"`);
5355
execa.sync(adbPath, adbArgs, {stdio: 'inherit'});
5456
} catch (error) {
5557
throw new CLIError(
@@ -82,7 +84,7 @@ function getInstallApkName(
8284
return apkName;
8385
}
8486

85-
throw new CLIError('Could not find the correct install APK file.');
87+
throw new Error('Could not find the correct install APK file.');
8688
}
8789

8890
export default tryInstallAppOnDevice;

0 commit comments

Comments
 (0)