Skip to content

Commit

Permalink
feat: add isSettingsAppServiceRunningInForeground to check the settin…
Browse files Browse the repository at this point in the history
…gs' 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...
  • Loading branch information
KazuCocoa authored Jan 9, 2024
1 parent 1cac1ae commit be0502e
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 3 deletions.
35 changes: 32 additions & 3 deletions lib/tools/settings-client-commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>} 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 <package>' 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
Expand All @@ -51,15 +76,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.isSettingsAppServiceRunningInForeground()) {
return this;
}

Expand All @@ -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,
});
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('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);
});

});
}));

0 comments on commit be0502e

Please sign in to comment.