diff --git a/docs/commands.md b/docs/commands.md index b4ac40f04..fbd365879 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -119,4 +119,4 @@ Upgrade your app's template files to the specified or latest npm version using [ 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. -_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`._ +_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`._ \ No newline at end of file diff --git a/packages/cli-platform-android/README.md b/packages/cli-platform-android/README.md index b24fa44d9..d2b324b44 100644 --- a/packages/cli-platform-android/README.md +++ b/packages/cli-platform-android/README.md @@ -126,6 +126,9 @@ react-native build-android --extra-params "-x lint -x test" Installs passed binary instead of building a fresh one. This command is not compatible with `--tasks`. +#### `--user` + +Id of the User Profile you want to install the app on. ### `log-android` Usage: diff --git a/packages/cli-platform-android/src/commands/runAndroid/__tests__/checkUsers.test.ts b/packages/cli-platform-android/src/commands/runAndroid/__tests__/checkUsers.test.ts new file mode 100644 index 000000000..8c137de04 --- /dev/null +++ b/packages/cli-platform-android/src/commands/runAndroid/__tests__/checkUsers.test.ts @@ -0,0 +1,25 @@ +import execa from 'execa'; +import {checkUsers} from '../listAndroidUsers'; + +// output of "adb -s ... shell pm users list" command +const gradleOutput = ` +Users: + UserInfo{0:Homersimpsons:c13} running + UserInfo{10:Guest:404} +`; + +jest.mock('execa', () => { + return {sync: jest.fn()}; +}); + +describe('check android users', () => { + it('should correctly parse recieved users', () => { + (execa.sync as jest.Mock).mockReturnValueOnce({stdout: gradleOutput}); + const users = checkUsers('device', 'adbPath'); + + expect(users).toStrictEqual([ + {id: '0', name: 'Homersimpsons'}, + {id: '10', name: 'Guest'}, + ]); + }); +}); diff --git a/packages/cli-platform-android/src/commands/runAndroid/getTaskNames.ts b/packages/cli-platform-android/src/commands/runAndroid/getTaskNames.ts index d994e2621..26b71dac4 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/getTaskNames.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/getTaskNames.ts @@ -10,10 +10,11 @@ export function getTaskNames( taskPrefix: 'assemble' | 'install' | 'bundle', sourceDir: string, ): Array { - let appTasks = tasks || [taskPrefix + toPascalCase(mode)]; + let appTasks = + tasks && tasks.length ? tasks : [taskPrefix + toPascalCase(mode)]; // Check against build flavors for "install" task ("assemble" don't care about it so much and will build all) - if (!tasks && taskPrefix === 'install') { + if (!tasks?.length && taskPrefix === 'install') { const actionableInstallTasks = getGradleTasks('install', sourceDir); if (!actionableInstallTasks.find((t) => t.task.includes(appTasks[0]))) { const installTasksForMode = actionableInstallTasks.filter((t) => diff --git a/packages/cli-platform-android/src/commands/runAndroid/index.ts b/packages/cli-platform-android/src/commands/runAndroid/index.ts index 68ba317b2..a269cb555 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/index.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/index.ts @@ -22,6 +22,7 @@ import path from 'path'; import {build, runPackager, BuildFlags, options} from '../buildAndroid'; import {promptForTaskSelection} from './listAndroidTasks'; import {getTaskNames} from './getTaskNames'; +import {checkUsers, promptForUser} from './listAndroidUsers'; export interface Flags extends BuildFlags { appId: string; @@ -30,6 +31,7 @@ export interface Flags extends BuildFlags { deviceId?: string; listDevices?: boolean; binaryPath?: string; + user?: number | string; } export type AndroidProject = NonNullable; @@ -121,6 +123,17 @@ async function buildAndRun(args: Flags, androidProject: AndroidProject) { ); } + if (args.interactive) { + const users = checkUsers(device.deviceId as string, adbPath); + if (users && users.length > 1) { + const user = await promptForUser(users); + + if (user) { + args.user = user.id; + } + } + } + if (device.connected) { return runOnSpecificDevice( {...args, deviceId: device.deviceId}, @@ -167,14 +180,16 @@ function runOnSpecificDevice( // if coming from run-android command and we have selected task // from interactive mode we need to create appropriate build task // eg 'installRelease' -> 'assembleRelease' - const buildTask = selectedTask?.replace('install', 'assemble') ?? 'build'; + const buildTask = selectedTask + ? [selectedTask.replace('install', 'assemble')] + : []; if (devices.length > 0 && deviceId) { if (devices.indexOf(deviceId) !== -1) { let gradleArgs = getTaskNames( androidProject.appName, args.mode || args.variant, - args.tasks ?? [buildTask], + args.tasks ?? buildTask, 'install', androidProject.sourceDir, ); @@ -287,6 +302,11 @@ export default { description: 'Path relative to project root where pre-built .apk binary lives.', }, + { + name: '--user ', + description: 'Id of the User Profile you want to install the app on.', + parse: Number, + }, ], }; diff --git a/packages/cli-platform-android/src/commands/runAndroid/listAndroidUsers.ts b/packages/cli-platform-android/src/commands/runAndroid/listAndroidUsers.ts new file mode 100644 index 000000000..cb0b38321 --- /dev/null +++ b/packages/cli-platform-android/src/commands/runAndroid/listAndroidUsers.ts @@ -0,0 +1,57 @@ +import {logger} from '@react-native-community/cli-tools'; +import execa from 'execa'; +import prompts from 'prompts'; + +type User = { + id: string; + name: string; +}; + +export function checkUsers(device: string, adbPath: string) { + try { + const adbArgs = ['-s', device, 'shell', 'pm', 'list', 'users']; + + logger.debug(`Checking users on "${device}"...`); + const {stdout} = execa.sync(adbPath, adbArgs, {encoding: 'utf-8'}); + const regex = new RegExp( + /^\s*UserInfo\{(?\d+):(?.*):(?[0-9a-f]*)}/, + ); + const users: User[] = []; + + const lines = stdout.split('\n'); + for (const line of lines) { + const res = regex.exec(line); + if (res?.groups) { + users.push({id: res.groups.userId, name: res.groups.userName}); + } + } + + if (users.length > 1) { + logger.debug( + `Available users are:\n${users + .map((user) => `${user.name} - ${user.id}`) + .join('\n')}`, + ); + } + + return users; + } catch (error) { + logger.error('Failed to check users of device.', error as any); + return []; + } +} + +export async function promptForUser(users: User[]) { + const {selectedUser}: {selectedUser: User} = await prompts({ + type: 'select', + name: 'selectedUser', + message: 'Which profile would you like to launch your app into?', + choices: users.map((user: User) => ({ + title: user.name, + value: user, + })), + min: 1, + }); + + return selectedUser; +} diff --git a/packages/cli-platform-android/src/commands/runAndroid/tryInstallAppOnDevice.ts b/packages/cli-platform-android/src/commands/runAndroid/tryInstallAppOnDevice.ts index eeef15a7d..b0a387ae4 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/tryInstallAppOnDevice.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/tryInstallAppOnDevice.ts @@ -45,11 +45,13 @@ function tryInstallAppOnDevice( pathToApk = args.binaryPath; } - const adbArgs = ['-s', device, 'install', '-r', '-d', pathToApk]; + const installArgs = ['-s', device, 'install', '-r', '-d']; + if (args.user !== undefined) { + installArgs.push('--user', `${args.user}`); + } + const adbArgs = [...installArgs, pathToApk]; logger.info(`Installing the app on the device "${device}"...`); - logger.debug( - `Running command "cd android && adb -s ${device} install -r -d ${pathToApk}"`, - ); + logger.debug(`Running command "cd android && adb ${adbArgs.join(' ')}"`); execa.sync(adbPath, adbArgs, {stdio: 'inherit'}); } catch (error) { throw new CLIError( @@ -82,7 +84,7 @@ function getInstallApkName( return apkName; } - throw new CLIError('Could not find the correct install APK file.'); + throw new Error('Could not find the correct install APK file.'); } export default tryInstallAppOnDevice;