Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add isSettingsAppServiceRunningInForeground to check the settings' service existence better #715

Merged
merged 10 commits into from
Jan 9, 2024
10 changes: 10 additions & 0 deletions lib/tools/adb-commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -1953,6 +1953,16 @@ methods.takeScreenshot = async function takeScreenshot (displayId) {
return stdout;
};

/**
* Return the result of 'adb shell dumpsys activity services <packagename>'
*
* @this {import('../adb.js').ADB}
* @returns {Promise<string>} the result of 'adb shell dumpsys activity services <packagename>'
*/
methods.getActivityService = async function getActivityService (pkgName) {
return await this.shell(['dumpsys', 'activity', 'services', pkgName]);
mykola-mokhnach marked this conversation as resolved.
Show resolved Hide resolved
};

export default methods;

/**
Expand Down
36 changes: 33 additions & 3 deletions lib/tools/settings-client-commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,34 @@ const GPS_CACHE_REFRESHED_LOGS = [

const GPS_COORDINATES_PATTERN = /data="(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)"/;

const FORE_GROUND_APP_KEYWORD = 'isForeground=true';
KazuCocoa marked this conversation as resolved.
Show resolved Hide resolved

const commands = {};

/**
* If the io.appium.settings package has running foreground service.
* It returns the io.appium.settings's process existence for api level 25 and lower
* since it does not have the foreground service.
*
* @this {import('../adb.js').ADB}
* @returns {Promise<boolean>} Return true if the device has running settings app foreground service.
*/
commands.hasRunningSettingsAppForegroundService = async function hasRunningSettingsAppForegroundService () {
KazuCocoa marked this conversation as resolved.
Show resolved Hide resolved
KazuCocoa marked this conversation as resolved.
Show resolved Hide resolved
if (await this.getApiLevel() < 26) {
return await this.processExists(SETTINGS_HELPER_ID);
}

// The foreground service is available since api level 26,
// thus this method works only for api level 26+.
try {
const output = await this.getActivityService(SETTINGS_HELPER_ID);
return (output.includes(FORE_GROUND_APP_KEYWORD));
KazuCocoa marked this conversation as resolved.
Show resolved Hide resolved
} catch (e) {
log.warn(`Got an error in getting the foreground service state: ${e.message}`);
KazuCocoa marked this conversation as resolved.
Show resolved Hide resolved
}
return false;
};

/**
* @typedef {Object} SettingsAppStartupOptions
* @property {number} [timeout=5000] The maximum number of milliseconds
Expand All @@ -51,15 +77,19 @@ const commands = {};

/**
* Ensures that Appium Settings helper application is running
* and starts it if necessary
* and starts it if necessary.
*
* The settings app process could keep working by 'android.service.notification.NotificationListenerService cmp=io.appium.settings/.NLService'
* while the app process was killed, or forcefully stopped, or the app was just installed.
* In the case, the io.appium.settings process exists but the foreground service hasn't been started yet.
*
* @this {import('../adb.js').ADB}
* @param {SettingsAppStartupOptions} [opts={}]
* @throws {Error} If Appium Settings has failed to start
* @returns {Promise<import('../adb.js').ADB>} self instance for chaining
*/
commands.requireRunningSettingsApp = async function requireRunningSettingsApp (opts = {}) {
if (await this.processExists(SETTINGS_HELPER_ID)) {
if (await this.hasRunningSettingsAppForegroundService()) {
return this;
}

Expand All @@ -85,7 +115,7 @@ commands.requireRunningSettingsApp = async function requireRunningSettingsApp (o
waitForLaunch: false,
});
try {
await waitForCondition(async () => await this.processExists(SETTINGS_HELPER_ID), {
await waitForCondition(async () => await this.hasRunningSettingsAppForegroundService(), {
waitMs: timeout,
intervalMs: 300,
});
Expand Down
115 changes: 115 additions & 0 deletions test/unit/adb-commands-specs.js
Original file line number Diff line number Diff line change
Expand Up @@ -1244,4 +1244,119 @@ describe('adb commands', withMocks({adb, logcat, teen_process, net}, function (m
await adb.setDefaultHiddenApiPolicy();
});
});

describe('hasRunningSettingsAppForegroundService', function () {
KazuCocoa marked this conversation as resolved.
Show resolved Hide resolved
it('should return true if the output includes isForeground=true', async function () {
// this case is when 'io.appium.settings/.NLService' was started AND
// the settings app is running as a foreground service.
// This case could happen when only 'shell cmd notification allow_listener io.appium.settings/.NLService' is
// called but the process hasn't been started from io.appium.settings/.ForegroundService,
// or the app process was stopped by "Force Stop" via the system settings app.
const getActivityServiceOutput = `
ACTIVITY MANAGER SERVICES (dumpsys activity services)
User 0 active services:
* ServiceRecord{f0ad90b u0 io.appium.settings/.NLService}
intent={act=android.service.notification.NotificationListenerService cmp=io.appium.settings/.NLService}
packageName=io.appium.settings
processName=io.appium.settings
permission=android.permission.BIND_NOTIFICATION_LISTENER_SERVICE
baseDir=/data/app/~~fHuRc6u9ehtAcXvuXy-fiw==/io.appium.settings-wJRwd1HrrbVG5ZINWuHi5Q==/base.apk
dataDir=/data/user/0/io.appium.settings
app=ProcessRecord{1d61746 18302:io.appium.settings/u0a320}
whitelistManager=true
allowWhileInUsePermissionInFgs=true
startForegroundCount=0
recentCallingPackage=android
createTime=-6m21s859ms startingBgTimeout=--
lastActivity=-6m21s783ms restartTime=-6m21s783ms createdFromFg=true
Bindings:
* IntentBindRecord{a5d675f CREATE}:
intent={act=android.service.notification.NotificationListenerService cmp=io.appium.settings/.NLService}
binder=android.os.BinderProxy@1be25ac
requested=true received=true hasBound=true doRebind=false
* Client AppBindRecord{c78a275 ProcessRecord{3f853b1 1847:system/1000}}
Per-process Connections:
ConnectionRecord{fbf1188 u0 CR FGS !PRCP io.appium.settings/.NLService:@339692b}
All Connections:
ConnectionRecord{fbf1188 u0 CR FGS !PRCP io.appium.settings/.NLService:@339692b}

* ServiceRecord{e7a180b u0 io.appium.settings/.ForegroundService}
intent={act=start cmp=io.appium.settings/.ForegroundService}
packageName=io.appium.settings
processName=io.appium.settings
permission=android.permission.FOREGROUND_SERVICE
baseDir=/data/app/~~fHuRc6u9ehtAcXvuXy-fiw==/io.appium.settings-wJRwd1HrrbVG5ZINWuHi5Q==/base.apk
dataDir=/data/user/0/io.appium.settings
app=ProcessRecord{1d61746 18302:io.appium.settings/u0a320}
allowWhileInUsePermissionInFgs=true
startForegroundCount=1
recentCallingPackage=io.appium.settings
isForeground=true foregroundId=1 foregroundNoti=Notification(channel=main_channel shortcut=null contentView=null vibrate=null sound=null defaults=0x0 flags=0x62 color=0x00000000 vis=PRIVATE)
createTime=-5m1s703ms startingBgTimeout=--
lastActivity=-5m1s702ms restartTime=-5m1s702ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

Connection bindings to services:
* ConnectionRecord{fbf1188 u0 CR FGS !PRCP io.appium.settings/.NLService:@339692b}
binding=AppBindRecord{c78a275 io.appium.settings/.NLService:system}
conn=android.app.LoadedApk$ServiceDispatcher$InnerConnection@339692b flags=0x5000101`;
mocks.adb.expects('getApiLevel').once().returns(26);
mocks.adb.expects('processExists').never();
mocks.adb.expects('getActivityService').once().returns(getActivityServiceOutput);
await adb.hasRunningSettingsAppForegroundService().should.eventually.true;
});
it('should return false if the output does not include isForeground=true', async function () {
// this case is when 'io.appium.settings/.NLService' was started but
// the settings app hasn't been started as a foreground service yet.
const getActivityServiceOutput = `
ACTIVITY MANAGER SERVICES (dumpsys activity services)
User 0 active services:
* ServiceRecord{41dde04 u0 io.appium.settings/.NLService}
intent={act=android.service.notification.NotificationListenerService cmp=io.appium.settings/.NLService}
packageName=io.appium.settings
processName=io.appium.settings
permission=android.permission.BIND_NOTIFICATION_LISTENER_SERVICE
baseDir=/data/app/~~fHuRc6u9ehtAcXvuXy-fiw==/io.appium.settings-wJRwd1HrrbVG5ZINWuHi5Q==/base.apk
dataDir=/data/user/0/io.appium.settings
app=ProcessRecord{d3b2ed1 18588:io.appium.settings/u0a320}
whitelistManager=true
allowWhileInUsePermissionInFgs=true
startForegroundCount=0
recentCallingPackage=android
createTime=-2s362ms startingBgTimeout=--
lastActivity=-2s283ms restartTime=-2s283ms createdFromFg=true
Bindings:
* IntentBindRecord{26ce8cd CREATE}:
intent={act=android.service.notification.NotificationListenerService cmp=io.appium.settings/.NLService}
binder=android.os.BinderProxy@2dbc582
requested=true received=true hasBound=true doRebind=false
* Client AppBindRecord{24ce493 ProcessRecord{3f853b1 1847:system/1000}}
Per-process Connections:
ConnectionRecord{8f3e709 u0 CR FGS !PRCP io.appium.settings/.NLService:@d481010}
ConnectionRecord{bd3f9f8 u0 CR FGS !PRCP io.appium.settings/.NLService:@1c7ed5b}
All Connections:
ConnectionRecord{bd3f9f8 u0 CR FGS !PRCP io.appium.settings/.NLService:@1c7ed5b}
ConnectionRecord{8f3e709 u0 CR FGS !PRCP io.appium.settings/.NLService:@d481010}

Connection bindings to services:
* ConnectionRecord{bd3f9f8 u0 CR FGS !PRCP io.appium.settings/.NLService:@1c7ed5b}
binding=AppBindRecord{24ce493 io.appium.settings/.NLService:system}
conn=android.app.LoadedApk$ServiceDispatcher$InnerConnection@1c7ed5b flags=0x5000101
* ConnectionRecord{8f3e709 u0 CR FGS !PRCP io.appium.settings/.NLService:@d481010}
binding=AppBindRecord{24ce493 io.appium.settings/.NLService:system}
conn=android.app.LoadedApk$ServiceDispatcher$InnerConnection@d481010 flags=0x5000101`;

mocks.adb.expects('getApiLevel').once().returns(26);
mocks.adb.expects('processExists').never();
mocks.adb.expects('getActivityService').once().returns(getActivityServiceOutput);
await adb.hasRunningSettingsAppForegroundService().should.eventually.false;
});
it('should rely on processExists for api level 25 and lower', async function () {
mocks.adb.expects('getApiLevel').once().returns(25);
mocks.adb.expects('processExists').once().returns(1000);
mocks.adb.expects('getActivityService').never();
await adb.hasRunningSettingsAppForegroundService().should.eventually.eql(1000);
});

});
}));