1/*
2 * Copyright 2004-2013 Haiku, Inc. All rights reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		Mike Berg, mike@berg-net.us
7 *		Adrien Destugues, pulkomandy@pulkomandy.ath.cx
8 *		Julun, host.haiku@gmx.de
9 *		Hamish Morrison, hamish@lavabit.com
10 *		Philippe Saint-Pierre, stpere@gmail.com
11 *		John Scipione, jscipione@gmail.com
12 *		Oliver Tappe, zooey@hirschkaefer.de
13 */
14
15
16#include <unicode/uversion.h>
17#include "ZoneView.h"
18
19#include <stdlib.h>
20#include <syscalls.h>
21
22#include <map>
23#include <new>
24#include <vector>
25
26#include <AutoDeleter.h>
27#include <Button.h>
28#include <Catalog.h>
29#include <Collator.h>
30#include <ControlLook.h>
31#include <Country.h>
32#include <Directory.h>
33#include <Entry.h>
34#include <File.h>
35#include <FindDirectory.h>
36#include <ListItem.h>
37#include <Locale.h>
38#include <MutableLocaleRoster.h>
39#include <OutlineListView.h>
40#include <Path.h>
41#include <RadioButton.h>
42#include <ScrollView.h>
43#include <StorageDefs.h>
44#include <String.h>
45#include <StringView.h>
46#include <TimeZone.h>
47#include <View.h>
48#include <Window.h>
49
50#include <unicode/datefmt.h>
51#include <unicode/utmscale.h>
52#include <ICUWrapper.h>
53
54#include "TimeMessages.h"
55#include "TimeZoneListItem.h"
56#include "TimeZoneListView.h"
57#include "TZDisplay.h"
58
59
60#undef B_TRANSLATION_CONTEXT
61#define B_TRANSLATION_CONTEXT "Time"
62
63
64using BPrivate::MutableLocaleRoster;
65using BPrivate::ObjectDeleter;
66
67
68struct TimeZoneItemLess {
69	bool operator()(const BString& first, const BString& second) const
70	{
71		// sort anything starting with '<' behind anything else
72		if (first.ByteAt(0) == '<') {
73			if (second.ByteAt(0) != '<')
74				return false;
75		} else if (second.ByteAt(0) == '<')
76			return true;
77		return fCollator.Compare(first.String(), second.String()) < 0;
78	}
79private:
80	BCollator fCollator;
81};
82
83
84
85TimeZoneView::TimeZoneView(const char* name)
86	:
87	BGroupView(name, B_HORIZONTAL, B_USE_DEFAULT_SPACING),
88	fGmtTime(NULL),
89	fUseGmtTime(false),
90	fCurrentZoneItem(NULL),
91	fOldZoneItem(NULL),
92	fInitialized(false)
93{
94	_ReadRTCSettings();
95	_InitView();
96}
97
98
99bool
100TimeZoneView::CheckCanRevert()
101{
102	// check GMT vs Local setting
103	bool enable = fUseGmtTime != fOldUseGmtTime;
104
105	return enable || fCurrentZoneItem != fOldZoneItem;
106}
107
108
109TimeZoneView::~TimeZoneView()
110{
111	_WriteRTCSettings();
112}
113
114
115void
116TimeZoneView::AttachedToWindow()
117{
118	BView::AttachedToWindow();
119	AdoptParentColors();
120
121	if (!fInitialized) {
122		fInitialized = true;
123
124		fSetZone->SetTarget(this);
125		fZoneList->SetTarget(this);
126	}
127}
128
129
130void
131TimeZoneView::DoLayout()
132{
133	BView::DoLayout();
134	if (fCurrentZoneItem != NULL) {
135		fZoneList->Select(fZoneList->IndexOf(fCurrentZoneItem));
136		fCurrent->SetText(fCurrentZoneItem->Text());
137		fZoneList->ScrollToSelection();
138	}
139}
140
141
142void
143TimeZoneView::MessageReceived(BMessage* message)
144{
145	switch (message->what) {
146		case B_OBSERVER_NOTICE_CHANGE:
147		{
148			int32 change;
149			message->FindInt32(B_OBSERVE_WHAT_CHANGE, &change);
150			switch(change) {
151				case H_TM_CHANGED:
152					_UpdateDateTime(message);
153					break;
154
155				default:
156					BView::MessageReceived(message);
157					break;
158			}
159			break;
160		}
161
162		case H_CITY_CHANGED:
163			_UpdatePreview();
164			break;
165
166		case H_SET_TIME_ZONE:
167		{
168			_SetSystemTimeZone();
169			break;
170		}
171
172		case kMsgRevert:
173			_Revert();
174			break;
175
176		case kRTCUpdate:
177			fUseGmtTime = fGmtTime->Value() == B_CONTROL_ON;
178			_UpdateGmtSettings();
179			_UpdateCurrent();
180			_UpdatePreview();
181			break;
182
183		default:
184			BGroupView::MessageReceived(message);
185			break;
186	}
187}
188
189
190void
191TimeZoneView::_UpdateDateTime(BMessage* message)
192{
193	// only need to update once every minute
194	int32 minute;
195	if (message->FindInt32("minute", &minute) == B_OK) {
196		if (fLastUpdateMinute != minute) {
197			_UpdateCurrent();
198			_UpdatePreview();
199
200			fLastUpdateMinute = minute;
201		}
202	}
203}
204
205
206void
207TimeZoneView::_InitView()
208{
209	fZoneList = new TimeZoneListView();
210	fZoneList->SetSelectionMessage(new BMessage(H_CITY_CHANGED));
211	fZoneList->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE));
212	_BuildZoneMenu();
213	BScrollView* scrollList = new BScrollView("scrollList", fZoneList,
214		B_FRAME_EVENTS | B_WILL_DRAW, false, true);
215	scrollList->SetExplicitMinSize(
216		BSize(200 * be_plain_font->Size() / 12.0f, 0));
217
218	fCurrent = new TTZDisplay("currentTime", B_TRANSLATE("Current time:"));
219	fPreview = new TTZDisplay("previewTime", B_TRANSLATE("Preview time:"));
220
221	fSetZone = new BButton("setTimeZone", B_TRANSLATE("Set time zone"),
222		new BMessage(H_SET_TIME_ZONE));
223	fSetZone->SetEnabled(false);
224	fSetZone->SetExplicitAlignment(
225		BAlignment(B_ALIGN_RIGHT, B_ALIGN_BOTTOM));
226
227	fLocalTime = new BRadioButton("localTime",
228		B_TRANSLATE("Local time (Windows compatible)"),
229			new BMessage(kRTCUpdate));
230	fGmtTime = new BRadioButton("greenwichMeanTime",
231		B_TRANSLATE("GMT (UNIX compatible)"), new BMessage(kRTCUpdate));
232
233	if (fUseGmtTime)
234		fGmtTime->SetValue(B_CONTROL_ON);
235	else
236		fLocalTime->SetValue(B_CONTROL_ON);
237	_ShowOrHidePreview();
238	fOldUseGmtTime = fUseGmtTime;
239
240	BLayoutBuilder::Group<>(this)
241		.Add(scrollList)
242		.AddGroup(B_VERTICAL, 0)
243			.Add(new BStringView("clockSetTo",
244				B_TRANSLATE("Hardware clock set to:")))
245			.AddGroup(B_VERTICAL, 0)
246				.Add(fLocalTime)
247				.Add(fGmtTime)
248				.SetInsets(B_USE_WINDOW_SPACING, 0, 0, 0)
249			.End()
250			.AddGlue()
251			.AddGroup(B_VERTICAL, B_USE_DEFAULT_SPACING)
252				.Add(fCurrent)
253				.Add(fPreview)
254			.End()
255			.Add(fSetZone)
256		.End()
257		.SetInsets(B_USE_WINDOW_SPACING, B_USE_WINDOW_SPACING,
258			B_USE_WINDOW_SPACING, B_USE_DEFAULT_SPACING);
259}
260
261
262void
263TimeZoneView::_BuildZoneMenu()
264{
265	BTimeZone defaultTimeZone;
266	BLocaleRoster::Default()->GetDefaultTimeZone(&defaultTimeZone);
267
268	BLanguage language;
269	BLocale::Default()->GetLanguage(&language);
270
271	// Group timezones by regions, but filter out unwanted (duplicate) regions
272	// and add an additional region with generic GMT-offset timezones at the end
273	typedef std::map<BString, TimeZoneListItem*, TimeZoneItemLess> ZoneItemMap;
274	ZoneItemMap zoneItemMap;
275	const char* kOtherRegion = B_TRANSLATE_MARK("<Other>");
276	const char* kSupportedRegions[] = {
277		B_TRANSLATE_MARK("Africa"),		B_TRANSLATE_MARK("America"),
278		B_TRANSLATE_MARK("Antarctica"),	B_TRANSLATE_MARK("Arctic"),
279		B_TRANSLATE_MARK("Asia"),		B_TRANSLATE_MARK("Atlantic"),
280		B_TRANSLATE_MARK("Australia"),	B_TRANSLATE_MARK("Europe"),
281		B_TRANSLATE_MARK("Indian"),		B_TRANSLATE_MARK("Pacific"),
282		kOtherRegion,
283		NULL
284	};
285
286	// Since the zone-map contains translated country-names (we get those from
287	// ICU), we need to use translated region names in the zone-map, too:
288	typedef std::map<BString, BString> TranslatedRegionMap;
289	TranslatedRegionMap regionMap;
290	for (const char** region = kSupportedRegions; *region != NULL; ++region) {
291		BString translatedRegion = B_TRANSLATE_NOCOLLECT(*region);
292		regionMap[*region] = translatedRegion;
293
294		TimeZoneListItem* regionItem
295			= new TimeZoneListItem(translatedRegion, NULL, NULL);
296		regionItem->SetOutlineLevel(0);
297		zoneItemMap[translatedRegion] = regionItem;
298	}
299
300	// Get all time zones
301	BMessage zoneList;
302	BLocaleRoster::Default()->GetAvailableTimeZonesWithRegionInfo(&zoneList);
303
304	typedef std::map<BString, std::vector<const char*> > ZonesByCountyMap;
305	ZonesByCountyMap zonesByCountryMap;
306	const char* zoneID;
307	BString timeZoneCode;
308	for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK
309			&& zoneList.FindString("region", tz, &timeZoneCode) == B_OK; tz++) {
310		// From the global ("001") timezones, we only accept the generic GMT
311		// timezones, as all the other world-zones are duplicates of others.
312		if (timeZoneCode == "001" && strncmp(zoneID, "Etc/GMT", 7) != 0)
313			continue;
314		zonesByCountryMap[timeZoneCode].push_back(zoneID);
315	}
316
317	ZonesByCountyMap::const_iterator countryIter = zonesByCountryMap.begin();
318	for (; countryIter != zonesByCountryMap.end(); ++countryIter) {
319		const char* countryCode = countryIter->first.String();
320		if (countryCode == NULL)
321			continue;
322
323		size_t zoneCountInCountry = countryIter->second.size();
324		for (size_t tz = 0; tz < zoneCountInCountry; tz++) {
325			BString zoneID(countryIter->second[tz]);
326			BTimeZone* timeZone
327				= new(std::nothrow) BTimeZone(zoneID, &language);
328			if (timeZone == NULL)
329				continue;
330
331			int32 slashPos = zoneID.FindFirst('/');
332			BString region(zoneID, slashPos);
333			if (region == "Etc")
334				region = kOtherRegion;
335
336			// just accept timezones from our supported regions, others are
337			// aliases and would just make the list even longer
338			TranslatedRegionMap::iterator regionIter = regionMap.find(region);
339			if (regionIter == regionMap.end())
340				continue;
341
342			BString fullCountryID = regionIter->second;
343			BCountry* country = new(std::nothrow) BCountry(countryCode);
344			if (country == NULL)
345				continue;
346
347			BString countryName;
348			country->GetName(countryName);
349			bool hasUsedCountry = false;
350			bool countryIsRegion = countryName == regionIter->second
351				|| region == kOtherRegion;
352			if (!countryIsRegion)
353				fullCountryID << "/" << countryName;
354
355			BString timeZoneName;
356			BString fullZoneID = fullCountryID;
357			if (zoneCountInCountry > 1) {
358				// we can't use the country name as timezone name, since there
359				// are more than one timezones in this country - fetch the
360				// localized name of the timezone and use that
361				timeZoneName = timeZone->Name();
362				int32 openParenthesisPos = timeZoneName.FindFirst('(');
363				if (openParenthesisPos >= 0) {
364					timeZoneName.Remove(0, openParenthesisPos + 1);
365					int32 closeParenthesisPos = timeZoneName.FindLast(')');
366					if (closeParenthesisPos >= 0)
367						timeZoneName.Truncate(closeParenthesisPos);
368				}
369				fullZoneID << "/" << timeZoneName;
370			} else {
371				timeZoneName = countryName;
372				fullZoneID << "/" << zoneID;
373			}
374
375			// skip duplicates
376			ZoneItemMap::iterator zoneIter = zoneItemMap.find(fullZoneID);
377			if (zoneIter != zoneItemMap.end()) {
378				delete timeZone;
379				continue;
380			}
381
382			TimeZoneListItem* countryItem = NULL;
383			TimeZoneListItem* zoneItem = NULL;
384			if (zoneCountInCountry > 1) {
385				ZoneItemMap::iterator countryIter
386					= zoneItemMap.find(fullCountryID);
387				if (countryIter == zoneItemMap.end()) {
388					countryItem = new TimeZoneListItem(countryName.String(),
389						country, NULL);
390					countryItem->SetOutlineLevel(1);
391					zoneItemMap[fullCountryID] = countryItem;
392					hasUsedCountry = true;
393				} else
394					countryItem = countryIter->second;
395
396				zoneItem = new TimeZoneListItem(timeZoneName.String(),
397					NULL, timeZone);
398				zoneItem->SetOutlineLevel(countryIsRegion ? 1 : 2);
399			} else {
400				zoneItem = new TimeZoneListItem(timeZoneName.String(),
401					country, timeZone);
402				zoneItem->SetOutlineLevel(1);
403				hasUsedCountry = true;
404			}
405			zoneItemMap[fullZoneID] = zoneItem;
406
407			if (timeZone->ID() == defaultTimeZone.ID()) {
408				fCurrentZoneItem = zoneItem;
409				if (countryItem != NULL)
410					countryItem->SetExpanded(true);
411
412				ZoneItemMap::iterator regionItemIter
413					= zoneItemMap.find(regionIter->second);
414				if (regionItemIter != zoneItemMap.end())
415					regionItemIter->second->SetExpanded(true);
416			}
417
418			if (!hasUsedCountry)
419				delete country;
420		}
421	}
422
423	fOldZoneItem = fCurrentZoneItem;
424
425	ZoneItemMap::iterator zoneIter;
426	bool lastWasCountryItem = false;
427	TimeZoneListItem* currentItem = NULL;
428	for (zoneIter = zoneItemMap.begin(); zoneIter != zoneItemMap.end();
429			++zoneIter) {
430		if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) {
431			// Some countries (e.g. Spain and Chile) have their timezones
432			// spread across different regions. As a result, there might still
433			// be country items with only one timezone below them. We manually
434			// filter those country items here.
435			ZoneItemMap::iterator next = zoneIter;
436			++next;
437			if (next != zoneItemMap.end()
438				&& next->second->OutlineLevel() != 2) {
439				fZoneList->RemoveItem(currentItem);
440				zoneIter->second->SetText(currentItem->Text());
441				zoneIter->second->SetCountry(currentItem->HasCountry()
442					? new(std::nothrow) BCountry(currentItem->Country())
443					: NULL);
444				if (currentItem->HasTimeZone()) {
445					zoneIter->second->SetTimeZone(new(std::nothrow)
446						BTimeZone(currentItem->TimeZone()));
447				}
448				zoneIter->second->SetOutlineLevel(1);
449				delete currentItem;
450			}
451		}
452
453		fZoneList->AddItem(zoneIter->second);
454		if (zoneIter->second->OutlineLevel() == 1) {
455			lastWasCountryItem = true;
456			currentItem = zoneIter->second;
457		} else
458			lastWasCountryItem = false;
459	}
460}
461
462
463void
464TimeZoneView::_Revert()
465{
466	fCurrentZoneItem = fOldZoneItem;
467
468	if (fCurrentZoneItem != NULL) {
469		int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem);
470		fZoneList->Select(currentZoneIndex);
471	} else
472		fZoneList->DeselectAll();
473	fZoneList->ScrollToSelection();
474
475	fUseGmtTime = fOldUseGmtTime;
476	if (fUseGmtTime)
477		fGmtTime->SetValue(B_CONTROL_ON);
478	else
479		fLocalTime->SetValue(B_CONTROL_ON);
480	_ShowOrHidePreview();
481
482	_UpdateGmtSettings();
483	_SetSystemTimeZone();
484	_UpdatePreview();
485	_UpdateCurrent();
486}
487
488
489void
490TimeZoneView::_UpdatePreview()
491{
492	int32 selection = fZoneList->CurrentSelection();
493	TimeZoneListItem* item
494		= selection < 0
495			? NULL
496			: static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection));
497
498	if (item == NULL || !item->HasTimeZone()) {
499		fPreview->SetText("");
500		fPreview->SetTime("");
501		return;
502	}
503
504	BString timeString = _FormatTime(item->TimeZone());
505	fPreview->SetText(item->Text());
506	fPreview->SetTime(timeString.String());
507
508	fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0));
509}
510
511
512void
513TimeZoneView::_UpdateCurrent()
514{
515	if (fCurrentZoneItem == NULL)
516		return;
517
518	BString timeString = _FormatTime(fCurrentZoneItem->TimeZone());
519	fCurrent->SetText(fCurrentZoneItem->Text());
520	fCurrent->SetTime(timeString.String());
521}
522
523
524void
525TimeZoneView::_SetSystemTimeZone()
526{
527	/*	Set system timezone for all different API levels. How to do this?
528	 *	1) tell locale-roster about new default timezone
529	 *	2) tell kernel about new timezone offset
530	 */
531
532	int32 selection = fZoneList->CurrentSelection();
533	if (selection < 0)
534		return;
535
536	TimeZoneListItem* item
537		= static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection));
538	if (item == NULL || !item->HasTimeZone())
539		return;
540
541	fCurrentZoneItem = item;
542	const BTimeZone& timeZone = item->TimeZone();
543
544	MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone);
545
546	_kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(),
547		timeZone.ID().Length());
548
549	fSetZone->SetEnabled(false);
550	fLastUpdateMinute = -1;
551		// just to trigger updating immediately
552}
553
554
555BString
556TimeZoneView::_FormatTime(const BTimeZone& timeZone)
557{
558	BString result;
559
560	time_t now = time(NULL);
561	bool rtcIsGMT;
562	_kern_get_real_time_clock_is_gmt(&rtcIsGMT);
563	if (!rtcIsGMT) {
564		int32 currentOffset
565			= fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone()
566				? fCurrentZoneItem->OffsetFromGMT()
567				: 0;
568		now -= timeZone.OffsetFromGMT() - currentOffset;
569	}
570	fTimeFormat.Format(result, now, B_SHORT_TIME_FORMAT, &timeZone);
571
572	return result;
573}
574
575
576void
577TimeZoneView::_ReadRTCSettings()
578{
579	BPath path;
580	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
581		return;
582
583	path.Append("RTC_time_settings");
584
585	BEntry entry(path.Path());
586	if (entry.Exists()) {
587		BFile file(&entry, B_READ_ONLY);
588		if (file.InitCheck() == B_OK) {
589			char buffer[6];
590			file.Read(buffer, 6);
591			if (strncmp(buffer, "gmt", 3) == 0)
592				fUseGmtTime = true;
593		}
594	}
595}
596
597
598void
599TimeZoneView::_WriteRTCSettings()
600{
601	BPath path;
602	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, true) != B_OK)
603		return;
604
605	path.Append("RTC_time_settings");
606
607	BFile file(path.Path(), B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY);
608	if (file.InitCheck() == B_OK) {
609		if (fUseGmtTime)
610			file.Write("gmt", 3);
611		else
612			file.Write("local", 5);
613	}
614}
615
616
617void
618TimeZoneView::_UpdateGmtSettings()
619{
620	_WriteRTCSettings();
621
622	_ShowOrHidePreview();
623
624	_kern_set_real_time_clock_is_gmt(fUseGmtTime);
625}
626
627
628void
629TimeZoneView::_ShowOrHidePreview()
630{
631	if (fUseGmtTime) {
632		// Hardware clock uses GMT time, changing timezone will adjust the
633		// offset and we need to display a preview
634		fCurrent->Show();
635		fPreview->Show();
636	} else {
637		// Hardware clock uses local time, changing timezone will adjust the
638		// clock and there is no offset to manage, thus, no preview.
639		fCurrent->Hide();
640		fPreview->Hide();
641	}
642}
643