diff --git a/lib/tools/adb-commands.js b/lib/tools/adb-commands.js index 940e59d9..12cee5d4 100644 --- a/lib/tools/adb-commands.js +++ b/lib/tools/adb-commands.js @@ -69,15 +69,71 @@ methods.clear = async function (pkg) { return await this.shell(['pm', 'clear', pkg]); }; +methods.grantAllPermissions = async function (pkg) { + let permissions = await this.getReqPermissions(pkg); + for (let permission of permissions) { + await this.grantPermission(pkg, permission); + } +}; + +methods.grantPermission = async function (pkg, permission) { + try { + await this.shell(['pm', 'grant', pkg, permission]); + } catch (error) { + if (!error.message.includes("not a changeable permission type")) { + throw error; + } + } +}; + +methods.revokePermission = async function (pkg, permission) { + try { + await this.shell(['pm', 'revoke', pkg, permission]); + } catch (error) { + if (!error.message.includes("not a changeable permission type")) { + throw error; + } + } +}; + +methods.getGrantedPermissions = async function (pkg) { + let stdout = await this.shell(['pm', 'dump', pkg]); + let reqPermissions = new RegExp(/install permissions:([\s\S]*?)DUMP OF SERVICE activity:/g).exec(stdout)[0].split("\n"); + return await cleanUpReqPermissions(reqPermissions); +}; + +methods.getReqPermissions = async function (pkg) { + let stdout = await this.shell(['pm', 'dump', pkg]); + let reqPermissions = new RegExp(/requested permissions:([\s\S]*?)install permissions:/g).exec(stdout)[0].split("\n"); + return await cleanUpReqPermissions(reqPermissions); +}; + +async function cleanUpReqPermissions (reqPermissions) { + return reqPermissions + .filter(p => p.trim().startsWith('android.permission')) + .map(p => p.trim().replace(': granted=true', '')); +} + methods.stopAndClear = async function (pkg) { try { await this.forceStop(pkg); await this.clear(pkg); + let apiLevel = await this.getApiLevel(); + let targetSdk = await this.getTargetSdkUsingPKG(pkg); + if (apiLevel >= 23 && targetSdk >= 23) { + await this.grantAllPermissions(pkg); + } } catch (e) { log.errorAndThrow(`Cannot stop and clear ${pkg}. Original error: ${e.message}`); } }; +methods.getTargetSdkUsingPKG = async function (pkg) { + let stdout = await this.shell(['pm', 'dump', pkg]); + let targetSdk = new RegExp(/targetSdk=([^\s\s]+)/g).exec(stdout)[1]; + return targetSdk; +}; + methods.availableIMEs = async function () { try { return getIMEListFromOutput(await this.shell(['ime', 'list', '-a'])); diff --git a/lib/tools/android-manifest.js b/lib/tools/android-manifest.js index 199045e2..11b76086 100644 --- a/lib/tools/android-manifest.js +++ b/lib/tools/android-manifest.js @@ -89,6 +89,19 @@ manifestMethods.packageAndLaunchActivityFromManifest = async function (localApk) } }; +manifestMethods.targetSdkVersionFromManifest = async function (localApk) { + try { + await this.initAapt(); + log.info("Extracting package and launch activity from manifest"); + let args = ['dump', 'badging', localApk]; + let {stdout} = await exec(this.binaries.aapt, args); + let targetSdkVersion = new RegExp(/targetSdkVersion:'([^']+)'/g).exec(stdout); + return parseInt(targetSdkVersion[1], 10); + } catch (e) { + log.errorAndThrow(`targetSdkVersionFromManifest failed. Original error: ${e.message}`); + } +}; + manifestMethods.compileManifest = async function (manifest, manifestPackage, targetPackage) { log.debug(`Compiling manifest ${manifest}`); let {platform, platformPath} = await getAndroidPlatformAndPath(); diff --git a/lib/tools/apk-utils.js b/lib/tools/apk-utils.js index 6562b4e0..92d25ce8 100644 --- a/lib/tools/apk-utils.js +++ b/lib/tools/apk-utils.js @@ -191,6 +191,13 @@ apkUtilsMethods.install = async function (apk, replace = true, timeout = 60000) log.debug(`Application '${apk}' already installed. Continuing.`); } } + if (apk.includes('.apk')) { + let apiLevel = await this.getApiLevel(); + let targetSdk = await this.targetSdkVersionFromManifest(apk); + if (apiLevel >= 23 && targetSdk >= 23) { + await this.grantAllPermissions(await this.getPackageName(apk)); + } + } }; apkUtilsMethods.extractStringsFromApk = async function (apk, language, out) { @@ -274,4 +281,17 @@ apkUtilsMethods.setDeviceLocale = async function (locale) { await this.setDeviceSysLocale(locale); }; +apkUtilsMethods.getPackageName = async function (apk) { + let args = ['dump', 'badging', apk]; + await this.initAapt(); + let {stdout} = await exec(this.binaries.aapt, args); + let apkPackage = new RegExp(/package: name='([^']+)'/g).exec(stdout); + if (apkPackage && apkPackage.length >= 2) { + apkPackage = apkPackage[1]; + } else { + apkPackage = null; + } + return apkPackage; +}; + export default apkUtilsMethods; diff --git a/test/fixtures/ApiDemos-debug.apk b/test/fixtures/ApiDemos-debug.apk new file mode 100644 index 00000000..565cc7c4 Binary files /dev/null and b/test/fixtures/ApiDemos-debug.apk differ diff --git a/test/functional/adb-commands-e2e-specs.js b/test/functional/adb-commands-e2e-specs.js index 9bf4511a..f52ee80c 100644 --- a/test/functional/adb-commands-e2e-specs.js +++ b/test/functional/adb-commands-e2e-specs.js @@ -16,6 +16,7 @@ const apiLevel = '18', 'fixtures', 'ContactManager.apk'), pkg = 'com.example.android.contactmanager', activity = 'ContactManager'; +let expect = chai.expect; describe('adb commands', function () { let adb; @@ -98,4 +99,34 @@ describe('adb commands', function () { logs.should.have.length.above(0); await adb.stopLogcat(); }); + describe('app permissions', async () => { + before(async function () { + let deviceApiLevel = await adb.getApiLevel(); + if (deviceApiLevel < 23) { + //test should skip if the device API < 23 + this.skip(); + } + let isInstalled = await adb.isAppInstalled('io.appium.android.apis'); + if (isInstalled) { + await adb.uninstallApk('io.appium.android.apis'); + } + }); + it('should install and grant all permission', async () => { + let apiDemos = path.resolve(rootDir, 'test', + 'fixtures', 'ApiDemos-debug.apk'); + await adb.install(apiDemos); + (await adb.isAppInstalled('io.appium.android.apis')).should.be.true; + let requestedPermissions = await adb.getReqPermissions('io.appium.android.apis'); + expect(await adb.getGrantedPermissions('io.appium.android.apis')).to.have.members(requestedPermissions); + }); + it('should revoke permission', async () => { + await adb.revokePermission('io.appium.android.apis', 'android.permission.RECEIVE_SMS'); + expect(await adb.getGrantedPermissions('io.appium.android.apis')).to.not.have.members(['android.permission.RECEIVE_SMS']); + }); + it('should grant permission', async () => { + await adb.grantPermission('io.appium.android.apis', 'android.permission.RECEIVE_SMS'); + expect(await adb.getGrantedPermissions('io.appium.android.apis')).to.include.members(['android.permission.RECEIVE_SMS']); + }); + }); }); + diff --git a/test/unit/adb-commands-specs.js b/test/unit/adb-commands-specs.js index b2949e81..23245d55 100644 --- a/test/unit/adb-commands-specs.js +++ b/test/unit/adb-commands-specs.js @@ -576,6 +576,20 @@ describe('adb commands', () => { }); })); }); + describe('app permission', withMocks({adb}, (mocks) => { + it('should grant requested permission', async () => { + mocks.adb.expects("shell") + .once().withArgs(['pm', 'grant', 'io.appium.android.apis', 'android.permission.READ_EXTERNAL_STORAGE']); + await adb.grantPermission('io.appium.android.apis', 'android.permission.READ_EXTERNAL_STORAGE'); + mocks.adb.verify(); + }); + it('should revoke requested permission', async () => { + mocks.adb.expects("shell") + .once().withArgs(['pm', 'revoke', 'io.appium.android.apis', 'android.permission.READ_EXTERNAL_STORAGE']); + await adb.revokePermission('io.appium.android.apis', 'android.permission.READ_EXTERNAL_STORAGE'); + mocks.adb.verify(); + }); + })); describe('sendTelnetCommand', withMocks({adb, net}, (mocks) => { it('should call shell with correct args', async () => { const port = 54321;