Skip to content

Commit

Permalink
Get the 'running_apps' property for Android TV devices (#126)
Browse files Browse the repository at this point in the history
* Get the running apps as part of 'AndroidTV.update()'

* Get coverage to 100%

* Set default value for 'get_running_apps' to True

* Clean up parameters and returns

* Update parameter order in docstring

* Update descriptions for the Android TV 'get_properties' commands

* Fix STATE_PLEX_STANDBY

* Use different commands to get the running apps for Android TV vs. Fire TV (#128)

* Use different commands to get the running apps for Android TV vs. Fire TV

* Add missing ' && '

* Move methods from AndroidTV and FireTV classes to BaseTV
  • Loading branch information
JeffLIrion committed Dec 6, 2019
1 parent 79f1862 commit e8b09aa
Show file tree
Hide file tree
Showing 8 changed files with 391 additions and 277 deletions.
102 changes: 67 additions & 35 deletions androidtv/androidtv.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,34 +39,25 @@ def __init__(self, host, adbkey='', adb_server_ip='', adb_server_port=5037, stat

# ======================================================================= #
# #
# ADB methods #
# Home Assistant Update #
# #
# ======================================================================= #
def start_intent(self, uri):
"""Start an intent on the device.
def update(self, get_running_apps=True):
"""Get the info needed for a Home Assistant update.
Parameters
----------
uri : str
The intent that will be sent is ``am start -a android.intent.action.VIEW -d <uri>``
"""
self._adb.shell("am start -a android.intent.action.VIEW -d {}".format(uri))

# ======================================================================= #
# #
# Home Assistant Update #
# #
# ======================================================================= #
def update(self):
"""Get the info needed for a Home Assistant update.
get_running_apps : bool
Whether or not to get the :attr:`~androidtv.basetv.BaseTV.running_apps` property
Returns
-------
state : str
The state of the device
current_app : str
The current running app
running_apps : list
A list of the running apps if ``get_running_apps`` is True, otherwise the list ``[current_app]``
device : str
The current playback device
is_volume_muted : bool
Expand All @@ -76,7 +67,7 @@ def update(self):
"""
# Get the properties needed for the update
screen_on, awake, audio_state, wake_lock_size, current_app, media_session_state, device, is_volume_muted, volume = self.get_properties(lazy=True)
screen_on, awake, audio_state, wake_lock_size, current_app, media_session_state, device, is_volume_muted, volume, running_apps = self.get_properties(get_running_apps=get_running_apps, lazy=True)

# Get the volume (between 0 and 1)
volume_level = self._volume_level(volume)
Expand All @@ -94,10 +85,14 @@ def update(self):
state = constants.STATE_IDLE

else:
# Get the running apps
if not running_apps and current_app:
running_apps = [current_app]

# Determine the state using custom rules
state = self._custom_state_detection(current_app=current_app, media_session_state=media_session_state, wake_lock_size=wake_lock_size, audio_state=audio_state)
if state:
return state, current_app, device, is_volume_muted, volume_level
return state, current_app, running_apps, device, is_volume_muted, volume_level

# ATV Launcher
if current_app in [constants.APP_ATV_LAUNCHER, None]:
Expand Down Expand Up @@ -179,14 +174,14 @@ def update(self):
else:
state = constants.STATE_STANDBY

return state, current_app, device, is_volume_muted, volume_level
return state, current_app, running_apps, device, is_volume_muted, volume_level

# ======================================================================= #
# #
# properties #
# #
# ======================================================================= #
def get_properties(self, lazy=False):
def get_properties(self, get_running_apps=True, lazy=False):
"""Get the properties needed for Home Assistant updates.
This will send one of the following ADB commands:
Expand All @@ -196,6 +191,8 @@ def get_properties(self, lazy=False):
Parameters
----------
get_running_apps : bool
Whether or not to get the :attr:`~androidtv.basetv.BaseTV.running_apps` property
lazy : bool
Whether or not to continue retrieving properties if the device is off or the screensaver is running
Expand All @@ -219,53 +216,61 @@ def get_properties(self, lazy=False):
Whether or not the volume is muted, or ``None`` if it was not determined
volume : int, None
The absolute volume level, or ``None`` if it was not determined
running_apps : list, None
A list of the running apps, or ``None`` if it was not determined
"""
if lazy:
output = self._adb.shell(constants.CMD_ANDROIDTV_PROPERTIES_LAZY)
if get_running_apps:
output = self._adb.shell(constants.CMD_ANDROIDTV_PROPERTIES_LAZY_RUNNING_APPS)
else:
output = self._adb.shell(constants.CMD_ANDROIDTV_PROPERTIES_LAZY_NO_RUNNING_APPS)
else:
output = self._adb.shell(constants.CMD_ANDROIDTV_PROPERTIES_NOT_LAZY)
if get_running_apps:
output = self._adb.shell(constants.CMD_ANDROIDTV_PROPERTIES_NOT_LAZY_RUNNING_APPS)
else:
output = self._adb.shell(constants.CMD_ANDROIDTV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS)
_LOGGER.debug("Android TV %s `get_properties` response: %s", self.host, output)

# ADB command was unsuccessful
if output is None:
return None, None, None, None, None, None, None, None, None
return None, None, None, None, None, None, None, None, None, None

# `screen_on` property
if not output:
return False, False, None, -1, None, None, None, None, None
return False, False, None, -1, None, None, None, None, None, None
screen_on = output[0] == '1'

# `awake` property
if len(output) < 2:
return screen_on, False, None, -1, None, None, None, None, None
return screen_on, False, None, -1, None, None, None, None, None, None
awake = output[1] == '1'

# `audio_state` property
if len(output) < 3:
return screen_on, awake, None, -1, None, None, None, None, None
return screen_on, awake, None, -1, None, None, None, None, None, None
audio_state = self._audio_state(output[2])

lines = output.strip().splitlines()

# `wake_lock_size` property
if len(lines[0]) < 4:
return screen_on, awake, audio_state, -1, None, None, None, None, None
return screen_on, awake, audio_state, -1, None, None, None, None, None, None
wake_lock_size = self._wake_lock_size(lines[0])

# `current_app` property
if len(lines) < 2:
return screen_on, awake, audio_state, wake_lock_size, None, None, None, None, None
return screen_on, awake, audio_state, wake_lock_size, None, None, None, None, None, None
current_app = self._current_app(lines[1])

# `media_session_state` property
if len(lines) < 3:
return screen_on, awake, audio_state, wake_lock_size, current_app, None, None, None, None
return screen_on, awake, audio_state, wake_lock_size, current_app, None, None, None, None, None
media_session_state = self._media_session_state(lines[2], current_app)

# "STREAM_MUSIC" block
if len(lines) < 4:
return screen_on, awake, audio_state, wake_lock_size, current_app, media_session_state, None, None, None
return screen_on, awake, audio_state, wake_lock_size, current_app, media_session_state, None, None, None, None

# reconstruct the output of `constants.CMD_STREAM_MUSIC`
stream_music_raw = "\n".join(lines[3:])
Expand All @@ -282,24 +287,31 @@ def get_properties(self, lazy=False):
# `is_volume_muted` property
is_volume_muted = self._is_volume_muted(stream_music)

return screen_on, awake, audio_state, wake_lock_size, current_app, media_session_state, device, is_volume_muted, volume
# `running_apps` property
if not get_running_apps or len(lines) < 16:
return screen_on, awake, audio_state, wake_lock_size, current_app, media_session_state, device, is_volume_muted, volume, None
running_apps = self._running_apps(lines[15:])

return screen_on, awake, audio_state, wake_lock_size, current_app, media_session_state, device, is_volume_muted, volume, running_apps

def get_properties_dict(self, lazy=True):
def get_properties_dict(self, get_running_apps=True, lazy=True):
"""Get the properties needed for Home Assistant updates and return them as a dictionary.
Parameters
----------
get_running_apps : bool
Whether or not to get the :attr:`~androidtv.basetv.BaseTV.running_apps` property
lazy : bool
Whether or not to continue retrieving properties if the device is off or the screensaver is running
Returns
-------
dict
A dictionary with keys ``'screen_on'``, ``'awake'``, ``'wake_lock_size'``, ``'current_app'``,
``'media_session_state'``, ``'audio_state'``, ``'device'``, ``'is_volume_muted'``, and ``'volume'``
``'media_session_state'``, ``'audio_state'``, ``'device'``, ``'is_volume_muted'``, ``'volume'``, and ``'running_apps'``
"""
screen_on, awake, audio_state, wake_lock_size, current_app, media_session_state, device, is_volume_muted, volume = self.get_properties(lazy=lazy)
screen_on, awake, audio_state, wake_lock_size, current_app, media_session_state, device, is_volume_muted, volume, running_apps = self.get_properties(get_running_apps=get_running_apps, lazy=lazy)

return {'screen_on': screen_on,
'awake': awake,
Expand All @@ -309,7 +321,27 @@ def get_properties_dict(self, lazy=True):
'media_session_state': media_session_state,
'device': device,
'is_volume_muted': is_volume_muted,
'volume': volume}
'volume': volume,
'running_apps': running_apps}

# ======================================================================= #
# #
# Properties #
# #
# ======================================================================= #
@property
def running_apps(self):
"""Return a list of running user applications.
Returns
-------
list
A list of the running apps
"""
running_apps_response = self._adb.shell(constants.CMD_ANDROIDTV_RUNNING_APPS)

return self._running_apps(running_apps_response)

# ======================================================================= #
# #
Expand Down
116 changes: 91 additions & 25 deletions androidtv/basetv.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,17 +181,6 @@ def adb_close(self):
"""
self._adb.close()

def _key(self, key):
"""Send a key event to device.
Parameters
----------
key : str, int
The Key constant
"""
self._adb.shell('input keyevent {0}'.format(key))

# ======================================================================= #
# #
# Home Assistant device info #
Expand Down Expand Up @@ -445,20 +434,6 @@ def media_session_state(self):

return media_session_state

@property
def running_apps(self):
"""Return a list of running user applications.
Returns
-------
list
A list of the running apps
"""
running_apps_response = self._adb.shell(constants.CMD_RUNNING_APPS)

return self._running_apps(running_apps_response)

@property
def screen_on(self):
"""Check if the screen is on.
Expand Down Expand Up @@ -790,11 +765,102 @@ def _wake_lock_size(wake_lock_size_response):

return None

# ======================================================================= #
# #
# App methods #
# #
# ======================================================================= #
def _send_intent(self, pkg, intent, count=1):
"""Send an intent to the device.
Parameters
----------
pkg : str
The command that will be sent is ``monkey -p <pkg> -c <intent> <count>; echo $?``
intent : str
The command that will be sent is ``monkey -p <pkg> -c <intent> <count>; echo $?``
count : int, str
The command that will be sent is ``monkey -p <pkg> -c <intent> <count>; echo $?``
Returns
-------
dict
A dictionary with keys ``'output'`` and ``'retcode'``, if they could be determined; otherwise, an empty dictionary
"""
cmd = 'monkey -p {} -c {} {}; echo $?'.format(pkg, intent, count)

# adb shell outputs in weird format, so we cut it into lines,
# separate the retcode and return info to the user
res = self._adb.shell(cmd)
if res is None:
return {}

res = res.strip().split("\r\n")
retcode = res[-1]
output = "\n".join(res[:-1])

return {"output": output, "retcode": retcode}

def launch_app(self, app):
"""Launch an app.
Parameters
----------
app : str
The ID of the app that will be launched
Returns
-------
dict
A dictionary with keys ``'output'`` and ``'retcode'``, if they could be determined; otherwise, an empty dictionary
"""
return self._send_intent(app, constants.INTENT_LAUNCH)

def stop_app(self, app):
"""Stop an app.
Parameters
----------
app : str
The ID of the app that will be stopped
Returns
-------
str, None
The output of the ``am force-stop`` ADB shell command, or ``None`` if the device is unavailable
"""
return self._adb.shell("am force-stop {0}".format(app))

def start_intent(self, uri):
"""Start an intent on the device.
Parameters
----------
uri : str
The intent that will be sent is ``am start -a android.intent.action.VIEW -d <uri>``
"""
self._adb.shell("am start -a android.intent.action.VIEW -d {}".format(uri))

# ======================================================================= #
# #
# "key" methods: basic commands #
# #
# ======================================================================= #
def _key(self, key):
"""Send a key event to device.
Parameters
----------
key : str, int
The Key constant
"""
self._adb.shell('input keyevent {0}'.format(key))

def power(self):
"""Send power action."""
self._key(constants.KEY_POWER)
Expand Down
Loading

0 comments on commit e8b09aa

Please sign in to comment.