summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/src/CalTimezoneService.jsm
blob: 7973435d7cbec75017f3f7ea68f53829d945646d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

var EXPORTED_SYMBOLS = ["CalTimezoneService"];

var { AppConstants } = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs");

var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
var { ICAL, unwrapSingle } = ChromeUtils.import("resource:///modules/calendar/Ical.jsm");

const { CalTimezone } = ChromeUtils.import("resource:///modules/CalTimezone.jsm");

const TIMEZONE_CHANGED_TOPIC = "default-timezone-changed";

// CalTimezoneService acts as an implementation of both ICAL.TimezoneService and
// the XPCOM calITimezoneService used for providing timezone objects to calendar
// code.
function CalTimezoneService() {
  this.wrappedJSObject = this;

  this._timezoneDatabase = Cc["@mozilla.org/calendar/timezone-database;1"].getService(
    Ci.calITimezoneDatabase
  );

  this.mZones = new Map();
  this.mZoneIds = [];

  ICAL.TimezoneService = this.wrappedJSObject;
}

var calTimezoneServiceClassID = Components.ID("{e736f2bd-7640-4715-ab35-887dc866c587}");
var calTimezoneServiceInterfaces = [Ci.calITimezoneService, Ci.calIStartupService];
CalTimezoneService.prototype = {
  mDefaultTimezone: null,
  mVersion: null,
  mZones: null,
  mZoneIds: null,

  classID: calTimezoneServiceClassID,
  QueryInterface: cal.generateQI(["calITimezoneService", "calIStartupService"]),
  classInfo: cal.generateCI({
    classID: calTimezoneServiceClassID,
    contractID: "@mozilla.org/calendar/timezone-service;1",
    classDescription: "Calendar Timezone Service",
    interfaces: calTimezoneServiceInterfaces,
    flags: Ci.nsIClassInfo.SINGLETON,
  }),

  // ical.js TimezoneService methods
  has(id) {
    return this.getTimezone(id) != null;
  },
  get(id) {
    return id ? unwrapSingle(ICAL.Timezone, this.getTimezone(id)) : null;
  },
  remove() {},
  register() {},

  // calIStartupService methods
  startup(aCompleteListener) {
    // Fetch list of supported canonical timezone IDs from the backing database
    this.mZoneIds = this._timezoneDatabase.getCanonicalTimezoneIds();

    // Fetch the version of the backing database
    this.mVersion = this._timezoneDatabase.version;
    cal.LOG("[CalTimezoneService] Timezones version " + this.version + " loaded");

    // Set up zones for special values
    const utc = new CalTimezone(ICAL.Timezone.utcTimezone);
    this.mZones.set("UTC", utc);

    const floating = new CalTimezone(ICAL.Timezone.localTimezone);
    this.mZones.set("floating", floating);

    // Initialize default timezone and, if unset, user timezone prefs
    this._initDefaultTimezone();

    // Watch for changes in system timezone or related user preferences
    Services.prefs.addObserver("calendar.timezone.useSystemTimezone", this);
    Services.prefs.addObserver("calendar.timezone.local", this);
    Services.obs.addObserver(this, TIMEZONE_CHANGED_TOPIC);

    // Notify the startup service that startup is complete
    if (aCompleteListener) {
      aCompleteListener.onResult(null, Cr.NS_OK);
    }
  },

  shutdown(aCompleteListener) {
    Services.obs.removeObserver(this, TIMEZONE_CHANGED_TOPIC);
    Services.prefs.removeObserver("calendar.timezone.local", this);
    Services.prefs.removeObserver("calendar.timezone.useSystemTimezone", this);
    aCompleteListener.onResult(null, Cr.NS_OK);
  },

  // calITimezoneService methods
  get UTC() {
    return this.mZones.get("UTC");
  },

  get floating() {
    return this.mZones.get("floating");
  },

  getTimezone(tzid) {
    if (!tzid) {
      cal.ERROR("Unknown timezone requested\n" + cal.STACK(10));
      return null;
    }

    if (tzid.startsWith("/mozilla.org/")) {
      // We know that our former tzids look like "/mozilla.org/<dtstamp>/continent/..."
      // The ending of the mozilla prefix is the index of that slash before the
      // continent. Therefore, we start looking for the prefix-ending slash
      // after position 13.
      tzid = tzid.substring(tzid.indexOf("/", 13) + 1);
    }

    // Per the IANA timezone database, "Z" is _not_ an alias for UTC, but our
    // previous list of zones included it and Ical.js at a minimum is expecting
    // it to be valid
    if (tzid === "Z") {
      return this.mZones.get("UTC");
    }

    // First check our cache of timezones
    let timezone = this.mZones.get(tzid);
    if (!timezone) {
      // The requested timezone is not in the cache; ask the backing database
      // for the timezone definition
      const tzdef = this._timezoneDatabase.getTimezoneDefinition(tzid);

      if (!tzdef) {
        cal.ERROR(`Could not find definition for ${tzid}`);
        return null;
      }

      timezone = new CalTimezone(
        ICAL.Timezone.fromData({
          tzid,
          component: tzdef,
        })
      );

      // Cache the resulting timezone
      this.mZones.set(tzid, timezone);
    }

    return timezone;
  },

  get timezoneIds() {
    return this.mZoneIds;
  },

  get version() {
    return this.mVersion;
  },

  _initDefaultTimezone() {
    // If the "use system timezone" preference is unset, we default to enabling
    // it if the user's system supports it
    let isSetSystemTimezonePref = Services.prefs.prefHasUserValue(
      "calendar.timezone.useSystemTimezone"
    );

    if (!isSetSystemTimezonePref) {
      let canUseSystemTimezone = AppConstants.MOZ_CAN_FOLLOW_SYSTEM_TIME;

      Services.prefs.setBoolPref("calendar.timezone.useSystemTimezone", canUseSystemTimezone);
    }

    this._updateDefaultTimezone();
  },

  _updateDefaultTimezone() {
    let prefUseSystemTimezone = Services.prefs.getBoolPref(
      "calendar.timezone.useSystemTimezone",
      true
    );
    let prefTzid = Services.prefs.getStringPref("calendar.timezone.local", null);

    let tzid;
    if (prefUseSystemTimezone || prefTzid === null || prefTzid === "floating") {
      // If we do not have a timezone preference set, we default to using the
      // system time; we may also do this if the user has set their preferences
      // accordingly
      tzid = Intl.DateTimeFormat().resolvedOptions().timeZone;
    } else {
      tzid = prefTzid;
    }

    // Update default timezone and preference if necessary
    if (!this.mDefaultTimezone || this.mDefaultTimezone.tzid != tzid) {
      this.mDefaultTimezone = this.getTimezone(tzid);
      cal.ASSERT(this.mDefaultTimezone, `Timezone not found: ${tzid}`);
      Services.obs.notifyObservers(null, "defaultTimezoneChanged");

      if (this.mDefaultTimezone.tzid != prefTzid) {
        Services.prefs.setStringPref("calendar.timezone.local", this.mDefaultTimezone.tzid);
      }
    }
  },

  get defaultTimezone() {
    // We expect this to be initialized when the service comes up and updated if
    // the underlying default changes
    return this.mDefaultTimezone;
  },

  observe(aSubject, aTopic, aData) {
    // Update the default timezone if the system timezone has changed; we
    // expect the update function to decide if actually making the change is
    // appropriate based on user prefs
    if (aTopic == TIMEZONE_CHANGED_TOPIC) {
      this._updateDefaultTimezone();
    } else if (
      aTopic == "nsPref:changed" &&
      (aData == "calendar.timezone.useSystemTimezone" || aData == "calendar.timezone.local")
    ) {
      // We may get a bogus second update from the timezone pref if its change
      // is a result of the system timezone changing, but it should settle, and
      // trying to guard against it is full of corner cases
      this._updateDefaultTimezone();
    }
  },
};