OSDN Git Service

Updating README.md
[rebornos/cnchi-gnome-osdn.git] / Cnchi / timezone.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # timezone.py
5 #
6 # Copyright © 2013-2018 Antergos
7 #
8 # This file is part of Cnchi.
9 #
10 # Cnchi is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # Cnchi is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 # GNU General Public License for more details.
19 #
20 # The following additional terms are in effect as per Section 7 of the license:
21 #
22 # The preservation of all legal notices and author attributions in
23 # the material or in the Appropriate Legal Notices displayed
24 # by works containing it is required.
25 #
26 # You should have received a copy of the GNU General Public License
27 # along with Cnchi; If not, see <http://www.gnu.org/licenses/>.
28
29 """ Timezone screen """
30
31 import hashlib
32 import http.client
33 import logging
34 import multiprocessing
35 import os
36 import queue
37 import time
38 import urllib.request
39 import urllib.error
40
41 import misc.tz as tz
42 import misc.extra as misc
43 import widgets.timezonemap as timezonemap
44 from pages.gtkbasebox import GtkBaseBox
45
46 import geoip
47
48 # When testing, no _() is available
49 try:
50     _("")
51 except NameError as err:
52     def _(message):
53         return message
54
55
56 class Timezone(GtkBaseBox):
57     """ Timezone screen """
58
59     def __init__(self, params, prev_page="location", next_page="keymap"):
60         super().__init__(self, params, "timezone", prev_page, next_page)
61
62         self.map_window = self.gui.get_object('timezone_map_window')
63
64         self.combobox_zone = self.gui.get_object('comboboxtext_zone')
65         self.combobox_region = self.gui.get_object('comboboxtext_region')
66
67         # Show regions in three columns
68         self.combobox_region.set_wrap_width(3)
69
70         self.tzdb = tz.Database()
71         self.timezone = None
72
73         # This is for populate_cities
74         self.old_zone = None
75
76         # Autotimezone process will store detected coords in this queue
77         self.auto_timezone_coords = multiprocessing.Queue()
78
79         # Process to try to determine timezone.
80         self.autodetected_coords = None
81         self.start_auto_timezone_process()
82
83         # Setup window
84         self.tzmap = timezonemap.TimezoneMap()
85         self.tzmap.connect('location-changed', self.on_location_changed)
86
87         # Strip .UTF-8 from locale, icu doesn't parse it
88         self.locale = os.environ['LANG'].rsplit('.', 1)[0]
89         self.map_window.add(self.tzmap)
90         self.tzmap.show()
91
92     def translate_ui(self):
93         """ Translates all ui elements """
94         label = self.gui.get_object('label_zone')
95         txt = _("Zone:")
96         label.set_markup(txt)
97
98         label = self.gui.get_object('label_region')
99         txt = _("Region:")
100         label.set_markup(txt)
101
102         label = self.gui.get_object('label_ntp')
103         txt = _("Use Network Time Protocol (NTP) for clock synchronization")
104         label.set_markup(txt)
105
106         self.header.set_subtitle(_("Select Your Timezone"))
107
108     def on_location_changed(self, _tzmap, tz_location):
109         """ User changed its location """
110         # loc = self.tzdb.get_loc(self.timezone)
111         if not tz_location:
112             self.timezone = None
113             self.forward_button.set_sensitive(False)
114         else:
115             self.timezone = tz_location.get_property('zone')
116             logging.info("Location changed to : %s", self.timezone)
117             self.update_comboboxes(self.timezone)
118             self.forward_button.set_sensitive(True)
119
120     def update_comboboxes(self, timezone):
121         """ Location has changed, update comboboxes """
122         zone, region = timezone.split('/', 1)
123         self.select_combobox_item(self.combobox_zone, zone)
124         self.populate_cities(zone)
125         self.select_combobox_item(self.combobox_region, region)
126
127     @staticmethod
128     def select_combobox_item(combobox, item):
129         """ Make combobox select an item """
130         tree_model = combobox.get_model()
131         tree_iter = tree_model.get_iter_first()
132
133         while tree_iter is not None:
134             value = tree_model.get_value(tree_iter, 0)
135             if value == item:
136                 combobox.set_active_iter(tree_iter)
137                 tree_iter = None
138             else:
139                 tree_iter = tree_model.iter_next(tree_iter)
140
141     def set_timezone(self, timezone):
142         """ Set timezone in tzmap """
143         if timezone:
144             self.timezone = timezone
145             res = self.tzmap.set_timezone(timezone)
146             # res will be False if the timezone is unrecognised
147             self.forward_button.set_sensitive(res)
148
149     def on_zone_combobox_changed(self, _widget):
150         """ Zone changed """
151         new_zone = self.combobox_zone.get_active_text()
152         if new_zone is not None:
153             self.populate_cities(new_zone)
154
155     def on_region_combobox_changed(self, _widget):
156         """ Region changed """
157         new_zone = self.combobox_zone.get_active_text()
158         new_region = self.combobox_region.get_active_text()
159         if new_zone is not None and new_region is not None:
160             new_timezone = "{0}/{1}".format(new_zone, new_region)
161             # Only set timezone if it has changed :p
162             if self.timezone != new_timezone:
163                 self.set_timezone(new_timezone)
164
165     def populate_zones(self):
166         """ Get all zones and fill our model """
167         zones = []
168         for loc in self.tzdb.locations:
169             zone = loc.zone.split('/', 1)[0]
170             if zone not in zones:
171                 zones.append(zone)
172         zones.sort()
173         tree_model = self.combobox_zone.get_model()
174         tree_model.clear()
175         for zone in zones:
176             tree_model.append([zone, zone])
177
178     def populate_cities(self, selected_zone):
179         """ Get all cities and populate our model """
180         if self.old_zone != selected_zone:
181             regions = []
182             for loc in self.tzdb.locations:
183                 zone, region = loc.zone.split('/', 1)
184                 if zone == selected_zone:
185                     regions.append(region)
186             regions.sort()
187             tree_model = self.combobox_region.get_model()
188             tree_model.clear()
189             for region in regions:
190                 tree_model.append([region, region])
191             self.old_zone = selected_zone
192
193     def prepare(self, direction):
194         """ Prepare screen before showing it """
195         self.translate_ui()
196         self.populate_zones()
197         self.timezone = None
198         self.forward_button.set_sensitive(False)
199
200         if self.autodetected_coords is None:
201             try:
202                 self.autodetected_coords = self.auto_timezone_coords.get(
203                     False, timeout=20)
204             except queue.Empty:
205                 logging.warning("Can't autodetect timezone coordinates")
206
207         if self.autodetected_coords:
208             coords = self.autodetected_coords
209             try:
210                 latitude = float(coords[0])
211                 longitude = float(coords[1])
212                 timezone = self.tzmap.get_timezone_at_coords(
213                     latitude, longitude)
214                 self.set_timezone(timezone)
215                 self.forward_button.set_sensitive(True)
216             except ValueError as value_error:
217                 self.autodetected_coords = None
218                 logging.warning(
219                     "Can't autodetect timezone coordinates: %s", value_error)
220
221         self.show_all()
222
223     def start_auto_timezone_process(self):
224         """ Starts timezone thread """
225         proc = AutoTimezoneProcess(self.auto_timezone_coords, self.settings)
226         proc.daemon = True
227         proc.name = "timezone"
228         proc.start()
229
230     @staticmethod
231     def log_location(loc):
232         """ Log selected location """
233         logging.debug("timezone human zone: %s", loc.human_zone)
234         logging.debug("timezone country: %s", loc.country)
235         logging.debug("timezone zone: %s", loc.zone)
236         logging.debug("timezone human country: %s", loc.human_country)
237
238         if loc.comment:
239             logging.debug("timezone comment: %s", loc.comment)
240
241         if loc.latitude:
242             logging.debug("timezone latitude: %s", loc.latitude)
243
244         if loc.longitude:
245             logging.debug("timezone longitude: %s", loc.longitude)
246
247     def store_values(self):
248         """ The user clicks 'next' """
249         loc = self.tzdb.get_loc(self.timezone)
250
251         if loc:
252             self.settings.set("timezone_zone", loc.zone)
253             self.settings.set("timezone_human_zone", loc.human_zone)
254             self.settings.set("timezone_country", loc.country)
255             self.settings.set("timezone_human_country", loc.human_country)
256
257             if loc.comment:
258                 self.settings.set("timezone_comment", loc.comment)
259             else:
260                 self.settings.set("timezone_comment", "")
261
262             if loc.latitude:
263                 self.settings.set("timezone_latitude", loc.latitude)
264             else:
265                 self.settings.set("timezone_latitude", "")
266
267             if loc.longitude:
268                 self.settings.set("timezone_longitude", loc.longitude)
269             else:
270                 self.settings.set("timezone_longitude", "")
271
272             # Logs timezone info
273             self.log_location(loc)
274
275         # This way process.py will know that all info has been entered
276         self.settings.set("timezone_done", True)
277
278         if self.settings.get('use_timesyncd'):
279             logging.debug(
280                 "Cnchi will setup network time using systemd-timesyncd")
281         else:
282             logging.debug("Cnchi won't setup network time")
283
284         return True
285
286     def on_switch_ntp_activate(self, ntp_switch, _data):
287         """ activated/deactivated ntp switch """
288         self.settings.set('use_timesyncd', ntp_switch.get_active())
289
290
291 class AutoTimezoneProcess(multiprocessing.Process):
292     """ Thread that asks our server for user's location """
293
294     def __init__(self, coords_queue, settings):
295         super(AutoTimezoneProcess, self).__init__()
296         self.coords_queue = coords_queue
297         self.settings = settings
298
299     def run(self):
300         """ main thread method """
301         # Do not start looking for our timezone until we've reached the
302         # language screen (welcome.py sets timezone_start to true when
303         # next is clicked)
304         while not self.settings.get('timezone_start'):
305             time.sleep(2)
306
307         coords = self.use_geoip()
308         if not coords:
309             msg = "Could not detect your timezone using GeoIP database. Let's use another method."
310             logging.warning(msg)
311             coords = self.use_geo_antergos()
312
313         if coords:
314             logging.debug(
315                 _("Timezone (latitude %s, longitude %s) detected."),
316                 coords[0],
317                 coords[1])
318             self.coords_queue.put(coords)
319         else:
320             logging.warning("Could not detect your timezone!")
321
322     @staticmethod
323     def use_geoip():
324         """ Determine our location using GeoIP database """
325         logging.debug("Getting your location using GeoIP database")
326         location = geoip.GeoIP().get_location()
327         if location:
328             return [location.latitude, location.longitude]
329         return None
330
331     @staticmethod
332     def maybe_wait_for_network():
333         """ Waits until there is an Internet connection available """
334         if not misc.has_connection():
335             logging.warning(
336                 "Can't get network status. Cnchi will try again in a moment")
337             while not misc.has_connection():
338                 time.sleep(4)  # Wait 4 seconds and try again
339         logging.debug("A working network connection has been detected.")
340
341
342     def use_geo_antergos(self):
343         """ Determine our location using geo.antergos.com """
344         # Calculate logo hash
345         logo = "data/images/antergos/antergos-logo-mini2.png"
346         logo_path = os.path.join(self.settings.get("cnchi"), logo)
347         with open(logo_path, "rb") as logo_file:
348             logo_bytes = logo_file.read()
349         logo_hasher = hashlib.sha1()
350         logo_hasher.update(logo_bytes)
351         logo_digest = logo_hasher.digest()
352
353         # Wait until there is an Internet connection available
354         self.maybe_wait_for_network()
355
356         # OK, now get our timezone
357         logging.debug("We have connection. Let's get our timezone")
358
359         try:
360             url = urllib.request.Request(
361                 url="http://geo.antergos.com",
362                 data=logo_digest,
363                 headers={"User-Agent": "RebornOS Installer", "Connection": "close"})
364             with urllib.request.urlopen(url) as conn:
365                 coords = conn.read().decode('utf-8').strip()
366             if coords == "0 0":
367                 # Sometimes server returns 0 0, we treat it as an error
368                 coords = None
369             else:
370                 coords = coords.split()
371         except (OSError, urllib.error.HTTPError, http.client.HTTPException) as err:
372             template = "Error getting timezone coordinates. " \
373                 "An exception of type {0} occured. Arguments:\n{1!r}"
374             message = template.format(type(err).__name__, err.args)
375             logging.error(message)
376             coords = None
377         return coords