From be0502e28a15916bd4bcb079d569aa7b7d5803fe Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 9 Jan 2024 11:33:12 -0800 Subject: [PATCH] feat: add isSettingsAppServiceRunningInForeground to check the settings' service existence better (#715) * feat: add forceStartSettingsApp in requireRunningSettingsApp * use foreground app existing check instead of process check * fix lint * test: add test, add description * chore: add , * change the behavior depending on api level * fix reviews * calls shell directly * add comment * fix indentation... --- lib/tools/settings-client-commands.js | 35 +++++++- test/unit/adb-commands-specs.js | 115 ++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 3 deletions(-) diff --git a/lib/tools/settings-client-commands.js b/lib/tools/settings-client-commands.js index 93890580..85cd9106 100644 --- a/lib/tools/settings-client-commands.js +++ b/lib/tools/settings-client-commands.js @@ -39,8 +39,33 @@ const GPS_CACHE_REFRESHED_LOGS = [ const GPS_COORDINATES_PATTERN = /data="(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)"/; +const FOREGROUND_APP_KEYWORD = 'isForeground=true'; + 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} + * @throws {Error} If the method gets an error in the adb shell execution. + * @returns {Promise} Return true if the device has running settings app foreground service. + */ +commands.isSettingsAppServiceRunningInForeground = async function isSettingsAppServiceRunningInForeground () { + 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+. + + // 'dumpsys activity services ' had slightly better performance + // than 'dumpsys activity services' and parsing the foreground apps. + const output = await this.shell(['dumpsys', 'activity', 'services', SETTINGS_HELPER_ID]); + return output.includes(FOREGROUND_APP_KEYWORD); +}; + /** * @typedef {Object} SettingsAppStartupOptions * @property {number} [timeout=5000] The maximum number of milliseconds @@ -51,7 +76,11 @@ 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={}] @@ -59,7 +88,7 @@ const commands = {}; * @returns {Promise} self instance for chaining */ commands.requireRunningSettingsApp = async function requireRunningSettingsApp (opts = {}) { - if (await this.processExists(SETTINGS_HELPER_ID)) { + if (await this.isSettingsAppServiceRunningInForeground()) { return this; } @@ -85,7 +114,7 @@ commands.requireRunningSettingsApp = async function requireRunningSettingsApp (o waitForLaunch: false, }); try { - await waitForCondition(async () => await this.processExists(SETTINGS_HELPER_ID), { + await waitForCondition(async () => await this.isSettingsAppServiceRunningInForeground(), { waitMs: timeout, intervalMs: 300, }); diff --git a/test/unit/adb-commands-specs.js b/test/unit/adb-commands-specs.js index 1563a0b5..8767acbb 100644 --- a/test/unit/adb-commands-specs.js +++ b/test/unit/adb-commands-specs.js @@ -1244,4 +1244,119 @@ describe('adb commands', withMocks({adb, logcat, teen_process, net}, function (m await adb.setDefaultHiddenApiPolicy(); }); }); + + describe('isSettingsAppServiceRunningInForeground', function () { + 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('shell').once().withArgs(['dumpsys', 'activity', 'services', 'io.appium.settings']).returns(getActivityServiceOutput); + await adb.isSettingsAppServiceRunningInForeground().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('shell').once().withArgs(['dumpsys', 'activity', 'services', 'io.appium.settings']).returns(getActivityServiceOutput); + await adb.isSettingsAppServiceRunningInForeground().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('shell').never().withArgs(['dumpsys', 'activity', 'services', 'io.appium.settings']); + await adb.isSettingsAppServiceRunningInForeground().should.eventually.eql(1000); + }); + + }); }));