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