1# Copyright (C) 2024 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""class to enable/disable GMS auto update.""" 16 17import logging 18import os 19import tempfile 20# from xml import etree has a problem, don't use it. 21from xml.etree import ElementTree 22from mobly.controllers import android_device 23from mobly.controllers.android_device_lib import adb 24 25 26_FINSKY_CONFIG_FILE = '/data/data/com.android.vending/shared_prefs/finsky.xml' 27_FINSKY_CONFIG_NAME = 'auto_update_enabled' 28_FINSKY_CONFIG_VALUE_DISABLE = 'false' 29_FINSKY_CONFIG_VALUE_ENABLE = 'true' 30_VENDING_CONFIG_FILE = '/data/data/com.android.vending/shared_prefs/com.android.vending_preferences.xml' 31_VENDING_CONFIG_NAME = 'auto-update-mode' 32_VENDING_CONFIG_VALUE_DISABLE = 'AUTO_UPDATE_NEVER' 33_VENDING_CONFIG_VALUE_ENABLE = 'AUTO_UPDATE_WIFI' 34_BLANK_CONFIG = '<?xml version="1.0" encoding="utf-8"?><map></map>' 35_XML_BOOL_TYPE = 'boolean' 36_XML_STRING_TYPE = 'string' 37_ENABLE_GSERVICES_CMD_TEMPLATE = [ 38 ( 39 'am broadcast ' 40 '-a com.google.gservices.intent.action.GSERVICES_OVERRIDE ' 41 '-e finsky.play_services_auto_update_enabled {}' 42 ), 43 ( 44 'am broadcast ' 45 '-a com.google.gservices.intent.action.GSERVICES_OVERRIDE ' 46 '-e finsky.setup_wizard_additional_account_vpa_enable {}' 47 ), 48] 49 50 51class GmsAutoUpdatesUtil: 52 """class to enable/disable GMS auto updates.""" 53 54 def __init__(self, ad: android_device.AndroidDevice): 55 self._device: android_device.AndroidDevice = ad 56 57 def enable_gms_auto_updates(self) -> None: 58 self._config_gms_auto_updates(True) 59 60 def disable_gms_auto_updates(self) -> None: 61 self._config_gms_auto_updates(False) 62 63 def _config_gms_auto_updates(self, enable_updates: bool) -> None: 64 """Configures GMS auto updates.""" 65 if not self._device.is_adb_root: 66 self._device.log.info( 67 f'failed to set the play store auto updates as {enable_updates}' 68 'you should enable/disable it manually on an unrooted device.') 69 else: 70 if enable_updates: 71 self._configure_play_store_updates( 72 _FINSKY_CONFIG_VALUE_ENABLE, _VENDING_CONFIG_VALUE_ENABLE 73 ) 74 else: 75 self._configure_play_store_updates( 76 _FINSKY_CONFIG_VALUE_DISABLE, _VENDING_CONFIG_VALUE_DISABLE 77 ) 78 self._configure_gservice_updates(enable_updates) 79 80 def _configure_gservice_updates(self, enable_updates: bool) -> None: 81 """Overwites Gservice to enable/disable updates.""" 82 for cmd in _ENABLE_GSERVICES_CMD_TEMPLATE: 83 self._device.adb.shell( 84 cmd.format('true' if enable_updates else 'false') 85 ) 86 87 def _create_or_update_play_store_config( 88 self, 89 tmp_dir: str, 90 value_type: str, 91 name: str, 92 value: str, 93 device_path: str, 94 ) -> str: 95 """Creates or updates a Play Store configuration file. 96 97 The function retrieves the Play Store configuration file from the device 98 then update it. If the file does not exist, it creates a new one. 99 100 Args: 101 tmp_dir: The temporary directory to store the configuration file. 102 value_type: The type of the configuration field. 103 name: The name of the configuration field. 104 value: The value of the configuration field. 105 device_path: The path to the configuration file on the device. 106 107 Returns: 108 The path to the updated configuration file. 109 """ 110 path = os.path.join(tmp_dir, f'play_store_config_{name}.xml') 111 try: 112 self._device.adb.pull([device_path, path]) 113 except adb.AdbError as e: 114 self._device.log.warning('failed to pull %s: %s', device_path, e) 115 116 config_doc = ElementTree.parse(path) if os.path.isfile(path) else None 117 118 changing_element = None 119 root = ( 120 ElementTree.fromstring(_BLANK_CONFIG.encode()) 121 if config_doc is None 122 else config_doc.getroot() 123 ) 124 125 # find the element, xPath doesn't work as the name is a reserved word. 126 for child in root: 127 if child.attrib['name'] == name: 128 changing_element = child 129 break 130 if changing_element is None: 131 if value_type == _XML_BOOL_TYPE: 132 changing_element = ElementTree.SubElement(root, 'boolean') 133 else: 134 changing_element = ElementTree.SubElement(root, 'string') 135 logging.info('element for %s is %s, %s', name, changing_element.tag, 136 changing_element.attrib) 137 if value_type == _XML_BOOL_TYPE: 138 changing_element.set('name', name) 139 changing_element.set('value', value) 140 else: 141 changing_element.attrib['name'] = name 142 changing_element.text = value 143 144 tree = ElementTree.ElementTree(root) 145 tree.write(path, xml_declaration=True, encoding='utf-8') 146 return path 147 148 def _configure_play_store_updates( 149 self, finsky_config_value: str, vending_config_value: str 150 ) -> None: 151 """Configures the Play Store update related settings.""" 152 with tempfile.TemporaryDirectory() as tmp_dir: 153 finsky_config = self._create_or_update_play_store_config( 154 tmp_dir, 155 _XML_BOOL_TYPE, 156 _FINSKY_CONFIG_NAME, 157 finsky_config_value, 158 _FINSKY_CONFIG_FILE, 159 ) 160 self._device.adb.push([finsky_config, _FINSKY_CONFIG_FILE]) 161 try: 162 os.remove(finsky_config) 163 except OSError as e: 164 logging.warning('failed to remove %s: %s', finsky_config, e) 165 166 vending_config = self._create_or_update_play_store_config( 167 tmp_dir, 168 _XML_STRING_TYPE, 169 _VENDING_CONFIG_NAME, 170 vending_config_value, 171 _VENDING_CONFIG_FILE, 172 ) 173 self._device.adb.push([vending_config, _VENDING_CONFIG_FILE]) 174 try: 175 os.remove(vending_config) 176 except OSError as e: 177 logging.warning('failed to remove %s: %s', vending_config, e) 178