# Copyright (C) 2023 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """class to enable/disable GMS auto update.""" import logging import os import tempfile # from xml import etree has a problem, don't use it. from xml.etree import ElementTree from mobly.controllers import android_device from mobly.controllers.android_device_lib import adb _FINSKY_CONFIG_FILE = '/data/data/com.android.vending/shared_prefs/finsky.xml' _FINSKY_CONFIG_NAME = 'auto_update_enabled' _FINSKY_CONFIG_VALUE_DISABLE = 'false' _FINSKY_CONFIG_VALUE_ENABLE = 'true' _VENDING_CONFIG_FILE = '/data/data/com.android.vending/shared_prefs/com.android.vending_preferences.xml' _VENDING_CONFIG_NAME = 'auto-update-mode' _VENDING_CONFIG_VALUE_DISABLE = 'AUTO_UPDATE_NEVER' _VENDING_CONFIG_VALUE_ENABLE = 'AUTO_UPDATE_WIFI' _BLANK_CONFIG = '' _XML_BOOL_TYPE = 'boolean' _XML_STRING_TYPE = 'string' _ENABLE_GSERVICES_CMD_TEMPLATE = [ ( 'am broadcast ' '-a com.google.gservices.intent.action.GSERVICES_OVERRIDE ' '-e finsky.play_services_auto_update_enabled {}' ), ( 'am broadcast ' '-a com.google.gservices.intent.action.GSERVICES_OVERRIDE ' '-e finsky.setup_wizard_additional_account_vpa_enable {}' ), ] class GmsAutoUpdatesUtil: """class to enable/disable GMS auto updates.""" def __init__(self, ad: android_device.AndroidDevice): self._device: android_device.AndroidDevice = ad def enable_gms_auto_updates(self) -> None: self._config_gms_auto_updates(True) def disable_gms_auto_updates(self) -> None: self._config_gms_auto_updates(False) def _config_gms_auto_updates(self, enable_updates: bool) -> None: """Configures GMS auto updates.""" if not self._device.is_adb_root: self._device.log.info( f'failed to set the play store auto updates as {enable_updates}' 'you should enable/disable it manually on an unrooted device.') else: if enable_updates: self._configure_play_store_updates( _FINSKY_CONFIG_VALUE_ENABLE, _VENDING_CONFIG_VALUE_ENABLE ) else: self._configure_play_store_updates( _FINSKY_CONFIG_VALUE_DISABLE, _VENDING_CONFIG_VALUE_DISABLE ) self._configure_gservice_updates(enable_updates) def _configure_gservice_updates(self, enable_updates: bool) -> None: """Overwites Gservice to enable/disable updates.""" for cmd in _ENABLE_GSERVICES_CMD_TEMPLATE: self._device.adb.shell( cmd.format('true' if enable_updates else 'false') ) def _create_or_update_play_store_config( self, tmp_dir: str, value_type: str, name: str, value: str, device_path: str, ) -> str: """Creates or updates a Play Store configuration file. The function retrieves the Play Store configuration file from the device then update it. If the file does not exist, it creates a new one. Args: tmp_dir: The temporary directory to store the configuration file. value_type: The type of the configuration field. name: The name of the configuration field. value: The value of the configuration field. device_path: The path to the configuration file on the device. Returns: The path to the updated configuration file. """ path = os.path.join(tmp_dir, f'play_store_config_{name}.xml') try: self._device.adb.pull([device_path, path]) except adb.AdbError as e: self._device.log.warning('failed to pull %s: %s', device_path, e) config_doc = ElementTree.parse(path) if os.path.isfile(path) else None changing_element = None root = ( ElementTree.fromstring(_BLANK_CONFIG.encode()) if config_doc is None else config_doc.getroot() ) # find the element, xPath doesn't work as the name is a reserved word. for child in root: if child.attrib['name'] == name: changing_element = child break if changing_element is None: if value_type == _XML_BOOL_TYPE: changing_element = ElementTree.SubElement(root, 'boolean') else: changing_element = ElementTree.SubElement(root, 'string') logging.info('element for %s is %s, %s', name, changing_element.tag, changing_element.attrib) if value_type == _XML_BOOL_TYPE: changing_element.set('name', name) changing_element.set('value', value) else: changing_element.attrib['name'] = name changing_element.text = value tree = ElementTree.ElementTree(root) tree.write(path, xml_declaration=True, encoding='utf-8') return path def _configure_play_store_updates( self, finsky_config_value: str, vending_config_value: str ) -> None: """Configures the Play Store update related settings.""" with tempfile.TemporaryDirectory() as tmp_dir: finsky_config = self._create_or_update_play_store_config( tmp_dir, _XML_BOOL_TYPE, _FINSKY_CONFIG_NAME, finsky_config_value, _FINSKY_CONFIG_FILE, ) self._device.adb.push([finsky_config, _FINSKY_CONFIG_FILE]) try: os.remove(finsky_config) except OSError as e: logging.warning('failed to remove %s: %s', finsky_config, e) vending_config = self._create_or_update_play_store_config( tmp_dir, _XML_STRING_TYPE, _VENDING_CONFIG_NAME, vending_config_value, _VENDING_CONFIG_FILE, ) self._device.adb.push([vending_config, _VENDING_CONFIG_FILE]) try: os.remove(vending_config) except OSError as e: logging.warning('failed to remove %s: %s', vending_config, e)