Change alarm implementation, start working on alarms on Android

This commit is contained in:
ivk 2019-06-14 18:01:48 +02:00
parent aa59b17bf7
commit 389810b609
50 changed files with 1845 additions and 847 deletions

View file

@ -5,7 +5,6 @@ import android.support.annotation.NonNull;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.codehaus.jackson.map.ObjectMapper;
import org.json.JSONException;
@ -18,17 +17,9 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
@ -75,7 +66,7 @@ public class CompatibilityTest {
}
@Test
public void rsa() throws NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, JSONException, BadPaddingException, NoSuchProviderException, InvalidKeyException, InvalidKeySpecException {
public void rsa() throws CryptoError {
for (EncryptionTestData testData : CompatibilityTest.testData.getRsaEncryptionTests()) {
Crypto crypto = new Crypto(null, stubRandom(testData.seed));
@ -183,4 +174,4 @@ public class CompatibilityTest {
};
}
}
}

View file

@ -1,326 +1,326 @@
package de.tutao.tutanota;
import org.codehaus.jackson.annotate.JsonIgnore;
import java.util.LinkedList;
import java.util.List;
import org.codehaus.jackson.annotate.JsonIgnore;
public class TestData {
List<EncryptionTestData> rsaEncryptionTests = new LinkedList();
List<SignatureTestData> rsaSignatureTests = new LinkedList();
List<AesTestData> aes256Tests = new LinkedList();
List<AesTestData> aes128MacTests = new LinkedList();
List<AesTestData> aes128Tests = new LinkedList();
List<EncodingTestData> encodingTests = new LinkedList();
List<BcryptTestData> bcrypt128Tests = new LinkedList();
List<BcryptTestData> bcrypt256Tests = new LinkedList();
List<EncryptionTestData> rsaEncryptionTests = new LinkedList<>();
List<SignatureTestData> rsaSignatureTests = new LinkedList<>();
List<AesTestData> aes256Tests = new LinkedList<>();
List<AesTestData> aes128MacTests = new LinkedList<>();
List<AesTestData> aes128Tests = new LinkedList<>();
List<EncodingTestData> encodingTests = new LinkedList<>();
List<BcryptTestData> bcrypt128Tests = new LinkedList<>();
List<BcryptTestData> bcrypt256Tests = new LinkedList<>();
public TestData addRsaEncryptionTest(EncryptionTestData test) {
this.rsaEncryptionTests.add(test);
return this;
}
public TestData addRsaEncryptionTest(EncryptionTestData test) {
this.rsaEncryptionTests.add(test);
return this;
}
public TestData addRsaSignatureTest(SignatureTestData test) {
this.rsaSignatureTests.add(test);
return this;
}
public TestData addRsaSignatureTest(SignatureTestData test) {
this.rsaSignatureTests.add(test);
return this;
}
public TestData addAes256Test(AesTestData test) {
this.aes256Tests.add(test);
return this;
}
public TestData addAes256Test(AesTestData test) {
this.aes256Tests.add(test);
return this;
}
public TestData addAes128Test(AesTestData test) {
this.aes128Tests.add(test);
return this;
}
public TestData addAes128Test(AesTestData test) {
this.aes128Tests.add(test);
return this;
}
public TestData addAes128MacTest(AesTestData test) {
this.aes128MacTests.add(test);
return this;
}
public TestData addAes128MacTest(AesTestData test) {
this.aes128MacTests.add(test);
return this;
}
public TestData addEncodingTest(EncodingTestData test) {
this.encodingTests.add(test);
return this;
}
public TestData addEncodingTest(EncodingTestData test) {
this.encodingTests.add(test);
return this;
}
public TestData addBcrypt128Test(BcryptTestData test) {
this.bcrypt128Tests.add(test);
return this;
}
public TestData addBcrypt128Test(BcryptTestData test) {
this.bcrypt128Tests.add(test);
return this;
}
public TestData addBcrypt256Test(BcryptTestData test) {
this.bcrypt256Tests.add(test);
return this;
}
public TestData addBcrypt256Test(BcryptTestData test) {
this.bcrypt256Tests.add(test);
return this;
}
public List<EncryptionTestData> getRsaEncryptionTests() {
return rsaEncryptionTests;
}
public List<EncryptionTestData> getRsaEncryptionTests() {
return rsaEncryptionTests;
}
public List<SignatureTestData> getRsaSignatureTests() {
return rsaSignatureTests;
}
public List<SignatureTestData> getRsaSignatureTests() {
return rsaSignatureTests;
}
public List<AesTestData> getAes256Tests() {
return aes256Tests;
}
public List<AesTestData> getAes256Tests() {
return aes256Tests;
}
public List<AesTestData> getAes128Tests() {
return aes128Tests;
}
public List<AesTestData> getAes128Tests() {
return aes128Tests;
}
public List<AesTestData> getAes128MacTests() {
return aes128MacTests;
}
public List<AesTestData> getAes128MacTests() {
return aes128MacTests;
}
public List<EncodingTestData> getEncodingTests() {
return encodingTests;
}
public List<EncodingTestData> getEncodingTests() {
return encodingTests;
}
public List<BcryptTestData> getBcrypt128Tests() {
return bcrypt128Tests;
}
public List<BcryptTestData> getBcrypt128Tests() {
return bcrypt128Tests;
}
public List<BcryptTestData> getBcrypt256Tests() {
return bcrypt256Tests;
}
public List<BcryptTestData> getBcrypt256Tests() {
return bcrypt256Tests;
}
}
class AesTestData {
private String plainTextBase64;
private String ivBase64;
private String cipherTextBase64;
private String hexKey;
private String plainTextBase64;
private String ivBase64;
private String cipherTextBase64;
private String hexKey;
/**
* Empty constructor needed for creating from json.
*/
public AesTestData() {
/**
* Empty constructor needed for creating from json.
*/
public AesTestData() {
}
}
@JsonIgnore
public AesTestData(String plainTextBase64, String ivBase64,String cipherTextBase64, String hexKey) {
this.plainTextBase64 = plainTextBase64;
this.ivBase64 = ivBase64;
this.cipherTextBase64 = cipherTextBase64;
this.hexKey = hexKey;
}
@JsonIgnore
public AesTestData(String plainTextBase64, String ivBase64, String cipherTextBase64, String hexKey) {
this.plainTextBase64 = plainTextBase64;
this.ivBase64 = ivBase64;
this.cipherTextBase64 = cipherTextBase64;
this.hexKey = hexKey;
}
public String getPlainTextBase64() {
return plainTextBase64;
}
public String getPlainTextBase64() {
return plainTextBase64;
}
public void setPlainTextBase64(String plainTextBase64) {
this.plainTextBase64 = plainTextBase64;
}
public void setPlainTextBase64(String plainTextBase64) {
this.plainTextBase64 = plainTextBase64;
}
public String getIvBase64() {
return ivBase64;
}
public String getIvBase64() {
return ivBase64;
}
public void setIvBase64(String ivBase64) {
this.ivBase64 = ivBase64;
}
public void setIvBase64(String ivBase64) {
this.ivBase64 = ivBase64;
}
public String getCipherTextBase64() {
return cipherTextBase64;
}
public String getCipherTextBase64() {
return cipherTextBase64;
}
public void setCipherTextBase64(String cipherTextBase64) {
this.cipherTextBase64 = cipherTextBase64;
}
public void setCipherTextBase64(String cipherTextBase64) {
this.cipherTextBase64 = cipherTextBase64;
}
public String getHexKey() {
return hexKey;
}
public String getHexKey() {
return hexKey;
}
public void setHexKey(String hexKey) {
this.hexKey = hexKey;
}
public void setHexKey(String hexKey) {
this.hexKey = hexKey;
}
}
class EncodingTestData {
public String string;
public String encodedString;
public String string;
public String encodedString;
/**
* Empty constructor needed for creating from json.
*/
public EncodingTestData() {
/**
* Empty constructor needed for creating from json.
*/
public EncodingTestData() {
}
}
@JsonIgnore
public EncodingTestData(String string, String encodedString) {
this.string = string;
this.encodedString = encodedString;
}
@JsonIgnore
public EncodingTestData(String string, String encodedString) {
this.string = string;
this.encodedString = encodedString;
}
public String getString() {
return string;
}
public String getString() {
return string;
}
public void setString(String string) {
this.string = string;
}
public void setString(String string) {
this.string = string;
}
public String getEncodedString() {
return encodedString;
}
public String getEncodedString() {
return encodedString;
}
public void setEncodedString(String encodedString) {
this.encodedString = encodedString;
}
public void setEncodedString(String encodedString) {
this.encodedString = encodedString;
}
}
class EncryptionTestData {
String publicKey;
String privateKey;
String input;
String seed;
String result;
String publicKey;
String privateKey;
String input;
String seed;
String result;
public String getPublicKey() {
return publicKey;
}
public String getPublicKey() {
return publicKey;
}
public String getPrivateKey() {
return privateKey;
}
public String getPrivateKey() {
return privateKey;
}
public String getInput() {
return input;
}
public String getInput() {
return input;
}
public String getSeed() {
return seed;
}
public String getSeed() {
return seed;
}
public String getResult() {
return result;
}
public String getResult() {
return result;
}
public EncryptionTestData setPublicKey(String publicKey) {
this.publicKey = publicKey;
return this;
}
public EncryptionTestData setPublicKey(String publicKey) {
this.publicKey = publicKey;
return this;
}
public EncryptionTestData setPrivateKey(String privateKey) {
this.privateKey = privateKey;
return this;
}
public EncryptionTestData setPrivateKey(String privateKey) {
this.privateKey = privateKey;
return this;
}
public EncryptionTestData setInput(String input) {
this.input = input;
return this;
}
public EncryptionTestData setInput(String input) {
this.input = input;
return this;
}
public EncryptionTestData setSeed(String seed) {
this.seed = seed;
return this;
}
public EncryptionTestData setSeed(String seed) {
this.seed = seed;
return this;
}
public EncryptionTestData setResult(String result) {
this.result = result;
return this;
}
public EncryptionTestData setResult(String result) {
this.result = result;
return this;
}
}
class SignatureTestData {
String publicKey;
String privateKey;
String input;
String seed;
String result;
String publicKey;
String privateKey;
String input;
String seed;
String result;
public String getPublicKey() {
return publicKey;
}
public String getPublicKey() {
return publicKey;
}
public String getPrivateKey() {
return privateKey;
}
public String getPrivateKey() {
return privateKey;
}
public String getInput() {
return input;
}
public String getInput() {
return input;
}
public String getSeed() {
return seed;
}
public String getSeed() {
return seed;
}
public String getResult() {
return result;
}
public String getResult() {
return result;
}
public SignatureTestData setPublicKey(String publicKey) {
this.publicKey = publicKey;
return this;
}
public SignatureTestData setPublicKey(String publicKey) {
this.publicKey = publicKey;
return this;
}
public SignatureTestData setPrivateKey(String privateKey) {
this.privateKey = privateKey;
return this;
}
public SignatureTestData setPrivateKey(String privateKey) {
this.privateKey = privateKey;
return this;
}
public SignatureTestData setInput(String input) {
this.input = input;
return this;
}
public SignatureTestData setInput(String input) {
this.input = input;
return this;
}
public SignatureTestData setSeed(String seed) {
this.seed = seed;
return this;
}
public SignatureTestData setSeed(String seed) {
this.seed = seed;
return this;
}
public SignatureTestData setResult(String result) {
this.result = result;
return this;
}
public SignatureTestData setResult(String result) {
this.result = result;
return this;
}
}
class BcryptTestData{
String password;
String keyHex;
String saltHex;
class BcryptTestData {
String password;
String keyHex;
String saltHex;
public BcryptTestData() {
}
public BcryptTestData() {
}
public BcryptTestData(String password, String keyHex, String saltHex) {
this.password = password;
this.keyHex = keyHex;
this.saltHex = saltHex;
}
public BcryptTestData(String password, String keyHex, String saltHex) {
this.password = password;
this.keyHex = keyHex;
this.saltHex = saltHex;
}
public String getPassword() {
return password;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public void setPassword(String password) {
this.password = password;
}
public String getKeyHex() {
return keyHex;
}
public String getKeyHex() {
return keyHex;
}
public void setKeyHex(String keyHex) {
this.keyHex = keyHex;
}
public void setKeyHex(String keyHex) {
this.keyHex = keyHex;
}
public String getSaltHex() {
return saltHex;
}
public String getSaltHex() {
return saltHex;
}
public void setSaltHex(String saltHex) {
this.saltHex = saltHex;
}
}
public void setSaltHex(String saltHex) {
this.saltHex = saltHex;
}
}

View file

@ -14,6 +14,8 @@ import android.os.Build;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import java.util.Date;
import static de.tutao.tutanota.Utils.atLeastOreo;
import static de.tutao.tutanota.push.PushNotificationService.VIBRATION_PATTERN;
@ -21,6 +23,19 @@ public class AlarmBroadcastReceiver extends BroadcastReceiver {
private static final String ALARM_NOTIFICATION_CHANNEL_ID = "alarms";
private static final String TAG = "AlarmBroadcastReceiver";
private static final String SUMMARY_EXTRA = "summary";
public static final String EVENT_DATE_EXTRA = "eventDate";
public static Intent makeAlarmIntent(int occurrence, String identifier, String summary, Date eventDate) {
String occurrenceIdentifier = identifier + "#" + occurrence;
Intent intent = new Intent("de.tutao.tutanota.ALARM", Uri.fromParts("alarm", occurrenceIdentifier, ""));
// TODO: maybe encrypt them?
intent.putExtra(SUMMARY_EXTRA, summary);
intent.putExtra(EVENT_DATE_EXTRA, eventDate.getTime());
return intent;
}
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Received broadcast");
@ -34,7 +49,8 @@ public class AlarmBroadcastReceiver extends BroadcastReceiver {
.setSmallIcon(R.drawable.ic_status)
.setContentTitle("Reminder")
.setColor(context.getResources().getColor(R.color.colorPrimary))
.setContentText("Tutanota calendar notification" + intent.getData())
.setContentText(intent.getStringExtra(SUMMARY_EXTRA))
.setWhen(intent.getLongExtra(EVENT_DATE_EXTRA, System.currentTimeMillis()))
.setDefaults(NotificationCompat.DEFAULT_SOUND)
.build());
}

View file

@ -0,0 +1,61 @@
package de.tutao.tutanota;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
public final class AlarmInfo {
public final String trigger;
public final String identifier;
public static AlarmInfo fromJson(JSONObject jsonObject) throws JSONException {
String trigger = jsonObject.getString("trigger");
String identifier = jsonObject.getString("identifier");
return new AlarmInfo(trigger, identifier);
}
public AlarmInfo(String trigger, String identifier) {
this.trigger = trigger;
this.identifier = Objects.requireNonNull(identifier);
}
public String getTriggerEnc() {
return trigger;
}
public String getTrigger(Crypto crypto, byte[] sessionKey) throws CryptoError {
return new String(crypto.aesDecrypt(sessionKey, this.trigger), StandardCharsets.UTF_8);
}
public String getIdentifier() {
return identifier;
}
public JSONObject toJSON() {
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("trigger", trigger);
jsonObject.put("identifier", identifier);
return jsonObject;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AlarmInfo alarmInfo = (AlarmInfo) o;
return identifier.equals(alarmInfo.identifier);
}
@Override
public int hashCode() {
return Objects.hash(identifier);
}
}

View file

@ -0,0 +1,27 @@
package de.tutao.tutanota;
public enum AlarmInterval {
FIVE_MINUTES("5M"),
TEN_MINUTES("10M"),
THIRTY_MINUTES("30M"),
ONE_HOUR("1H"),
ONE_DAY("1D"),
TWO_DAYS("2D"),
THREE_DAYS("3D"),
ONE_WEEK("1W");
private String value;
AlarmInterval(String value) {
this.value = value;
}
static AlarmInterval byValue(String value) {
for (AlarmInterval alarmInterval : AlarmInterval.values()) {
if (alarmInterval.value.equals(value)) {
return alarmInterval;
}
}
throw new IllegalArgumentException("No AlarmInterval for value" + value);
}
}

View file

@ -0,0 +1,124 @@
package de.tutao.tutanota;
import android.support.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class AlarmNotification {
private OperationType operation;
private String summary;
private String eventStartEnc;
private AlarmInfo alarmInfo;
private RepeatRule repeatRule;
private List<NotificationSessionKey> notificationSessionKeys;
public AlarmNotification(OperationType operation, String summaryEnc, String eventStartEnc, AlarmInfo alarmInfo, RepeatRule repeatRule, List<NotificationSessionKey> notificationSessionKeys) {
this.operation = operation;
this.summary = summaryEnc;
this.eventStartEnc = eventStartEnc;
this.alarmInfo = alarmInfo;
this.repeatRule = repeatRule;
this.notificationSessionKeys = notificationSessionKeys;
}
public static AlarmNotification fromJson(JSONObject jsonObject) throws JSONException {
OperationType operationType = OperationType.values()[jsonObject.getInt("operation")];
String summaryEnc = jsonObject.getString("summary");
String eventStartEnc = jsonObject.getString("eventStart");
RepeatRule repeatRule;
if (jsonObject.isNull("repeatRule")) {
repeatRule = null;
} else {
repeatRule = RepeatRule.fromJson(jsonObject.getJSONObject("repeatRule"));
}
AlarmInfo alarmInfo = AlarmInfo.fromJson(jsonObject.getJSONObject("alarmInfo"));
JSONArray deviceSessionKeysJson = jsonObject.getJSONArray("deviceSessionKeys");
List<NotificationSessionKey> notificationSessionKeys = new ArrayList<>();
for (int i = 0; i < deviceSessionKeysJson.length(); i++) {
notificationSessionKeys.add(NotificationSessionKey.fromJson(deviceSessionKeysJson.getJSONObject(i)));
}
return new AlarmNotification(operationType, summaryEnc, eventStartEnc, alarmInfo, repeatRule, notificationSessionKeys);
}
public JSONObject toJSON() {
try {
JSONObject jsonObject = new JSONObject();
if (this.repeatRule != null) {
jsonObject.put("repeatRule", this.repeatRule.toJson());
}
return jsonObject;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public OperationType getOperation() {
return operation;
}
public String getEventStartEnc() {
return eventStartEnc;
}
@Nullable
public Date getEventStart(Crypto crypto, byte[] sessionKey) throws CryptoError {
return EncryptionUtils.decryptDate(this.eventStartEnc, crypto, sessionKey);
}
public String getSummary(Crypto crypto, byte[] sessionKey) throws CryptoError {
return EncryptionUtils.decryptString(this.summary, crypto, sessionKey);
}
public RepeatRule getRepeatRule() {
return repeatRule;
}
public AlarmInfo getAlarmInfo() {
return alarmInfo;
}
public List<NotificationSessionKey> getNotificationSessionKeys() {
return notificationSessionKeys;
}
public static class NotificationSessionKey {
private final IdTuple pushIdentifier;
private final String pushIdentifierSessionEncSessionKey;
public static NotificationSessionKey fromJson(JSONObject jsonObject) throws JSONException {
JSONArray id = jsonObject.getJSONArray("pushIdentifier");
return new NotificationSessionKey(
new IdTuple(id.getString(0), id.getString(1)),
jsonObject.getString("pushIdentifierSessionEncSessionKey")
);
}
public NotificationSessionKey(IdTuple pushIdentifier, String pushIdentifierSessionEncSessionKey) {
this.pushIdentifier = pushIdentifier;
this.pushIdentifierSessionEncSessionKey = pushIdentifierSessionEncSessionKey;
}
public JSONObject toJson() throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("pushIdentifier", pushIdentifier);
jsonObject.put("pushIdentifierSessionEncSessionKey", pushIdentifierSessionEncSessionKey);
return jsonObject;
}
public IdTuple getPushIdentifier() {
return pushIdentifier;
}
public String getPushIdentifierSessionEncSessionKey() {
return pushIdentifierSessionEncSessionKey;
}
}
}

View file

@ -0,0 +1,270 @@
package de.tutao.tutanota;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.preference.PreferenceManager;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.KeyStoreException;
import java.security.UnrecoverableEntryException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.TimeZone;
import de.tutao.tutanota.push.SseInfo;
import de.tutao.tutanota.push.SseStorage;
public class AlarmNotificationsManager {
private static final String TAG = "AlarmNotificationsMngr";
private static final String RECURRING_ALARMS_PREF_NAME = "RECURRING_ALARMS";
private final Context context;
private final AndroidKeyStoreFacade keyStoreFacade;
private final SseStorage sseStorage;
private final Crypto crypto;
public AlarmNotificationsManager(Context context, SseStorage sseStorage) {
this.context = context;
this.sseStorage = sseStorage;
this.keyStoreFacade = new AndroidKeyStoreFacade(context);
crypto = new Crypto(context);
}
public void reScheduleAlarms() {
List<AlarmNotification> alarmInfos = this.readSavedAlarmNotifications();
for (AlarmNotification alarmNotification : alarmInfos) {
byte[] sessionKey = this.resolveSessionKey(alarmNotification);
if (sessionKey != null) {
this.schedule(alarmNotification, sessionKey);
}
}
}
public byte[] resolveSessionKey(AlarmNotification notification) {
for (AlarmNotification.NotificationSessionKey notificationSessionKey : notification.getNotificationSessionKeys()) {
SseInfo sseInfo = this.sseStorage.getSseInfo();
String deviceEncPushIdentifierSessionKey = sseInfo.getPushIdentifierToSessionKey().get(notificationSessionKey.getPushIdentifier().getElementId());
if (deviceEncPushIdentifierSessionKey != null) {
try {
byte[] pushIdentifierSessionKey = this.keyStoreFacade.decryptKey(deviceEncPushIdentifierSessionKey);
byte[] encNotificationSessionKeyKey = Utils.base64ToBytes(notificationSessionKey.getPushIdentifierSessionEncSessionKey());
return this.crypto.decryptKey(pushIdentifierSessionKey, encNotificationSessionKeyKey);
} catch (UnrecoverableEntryException | KeyStoreException | CryptoError e) {
Log.d(TAG, "could not decrypt session key", e);
return null;
}
}
}
return null;
}
public void scheduleNewAlarms(List<AlarmNotification> alarmNotifications) {
byte[] sessionKey = this.resolveSessionKey(alarmNotifications.get(0));
if (sessionKey == null) {
return;
}
List<AlarmNotification> savedInfos = this.readSavedAlarmNotifications();
for (AlarmNotification alarmNotification : alarmNotifications) {
if (alarmNotification.getOperation() == OperationType.CREATE) {
this.schedule(alarmNotification, sessionKey);
if (alarmNotification.getRepeatRule() != null) {
savedInfos.add(alarmNotification);
}
} else {
this.cancelScheduledAlarm(alarmNotification);
savedInfos.remove(alarmNotification);
}
}
this.writeAlarmInfos(savedInfos);
}
private List<AlarmNotification> readSavedAlarmNotifications() {
String jsonString = PreferenceManager.getDefaultSharedPreferences(context)
.getString(RECURRING_ALARMS_PREF_NAME, "[]");
ArrayList<AlarmNotification> alarmInfos = new ArrayList<>();
try {
JSONArray jsonArray = new JSONArray(jsonString);
for (int i = 0; i < jsonArray.length(); i++) {
alarmInfos.add(AlarmNotification.fromJson(jsonArray.getJSONObject(i)));
}
} catch (JSONException e) {
alarmInfos = new ArrayList<>();
}
return alarmInfos;
}
private void writeAlarmInfos(List<AlarmNotification> alarmNotifications) {
List<JSONObject> jsonObjectList = new ArrayList<>(alarmNotifications.size());
for (AlarmNotification alarmNotification : alarmNotifications) {
jsonObjectList.add(alarmNotification.toJSON());
}
String jsonString = JSONObject.wrap(jsonObjectList).toString();
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putString(RECURRING_ALARMS_PREF_NAME, jsonString)
.apply();
}
private void schedule(AlarmNotification alarmInfAlarmNotification, byte[] sessionKey) {
String trigger;
AlarmInterval alarmInterval;
String summary;
Date eventStart;
String identifier = alarmInfAlarmNotification.getAlarmInfo().identifier;
try {
trigger = alarmInfAlarmNotification.getAlarmInfo().getTrigger(crypto, sessionKey);
alarmInterval = AlarmInterval.byValue(trigger);
summary = alarmInfAlarmNotification.getSummary(crypto, sessionKey);
eventStart = alarmInfAlarmNotification.getEventStart(crypto, sessionKey);
} catch (CryptoError cryptoError) {
Log.w(TAG, "Error when decrypting alarmNotificaiton", cryptoError);
return;
}
if (alarmInfAlarmNotification.getRepeatRule() == null) {
// TODO: check for all-day event
Date alarmTime = calculateAlarmTime(eventStart, null, alarmInterval);
scheduleAlarmOccurrenceWithSystem(alarmTime, 0, identifier, summary, eventStart);
} else {
this.iterateAlarmOccurrences(alarmInfAlarmNotification, (time, occurrence) -> {
// TODO: fix times here
this.scheduleAlarmOccurrenceWithSystem(time, occurrence, identifier, summary, time);
});
}
}
private void iterateAlarmOccurrences(AlarmNotification alarmNotification, AlarmIterationCallback callback) {
Objects.requireNonNull(alarmNotification.getRepeatRule());
// TODO: Timezone should be local for all-day events?
Calendar calendar = Calendar.getInstance(alarmNotification.getRepeatRule().timeZone);
long now = System.currentTimeMillis();
int occurrences = 0;
int futureOccurrences = 0;
while (futureOccurrences < 10
&& (alarmNotification.getRepeatRule().endType != EndType.COUNT
|| occurrences < alarmNotification.getRepeatRule().endValue)) {
// TODO: increment time
//calendar.setTime(alarmNotification.getAlarmInfo().trigger); // reset time to the initial
incrementByRepeatPeriod(calendar, alarmNotification.getRepeatRule().frequency,
alarmNotification.getRepeatRule().interval * occurrences);
// TODO: All-day events
if (alarmNotification.getRepeatRule().endType == EndType.UNTIL
&& calendar.getTimeInMillis() > alarmNotification.getRepeatRule().endValue) {
break;
}
if (calendar.getTimeInMillis() >= now) {
callback.call(calendar.getTime(), occurrences);
futureOccurrences++;
}
occurrences++;
}
}
private void scheduleAlarmOccurrenceWithSystem(Date alarmTime, int occurrence, String identifier, String summary, Date eventDate) {
Log.d(TAG, "Scheduled notificaiton at" + alarmTime);
AlarmManager alarmManager = getAlarmManager();
// TODO: check how to identify occurrence uniquely
PendingIntent pendingIntent = makeAlarmPendingIntent(occurrence, identifier, summary, eventDate);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, alarmTime.getTime(), pendingIntent);
} else {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, alarmTime.getTime(), pendingIntent);
}
}
private AlarmManager getAlarmManager() {
return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
}
private void cancelScheduledAlarm(AlarmNotification alarmInfo) {
if (alarmInfo.getRepeatRule() == null) {
PendingIntent pendingIntent = makeAlarmPendingIntent(0, alarmInfo.getAlarmInfo().identifier, "", new Date());
getAlarmManager().cancel(pendingIntent);
} else {
this.iterateAlarmOccurrences(alarmInfo, (time, occurrence) -> {
getAlarmManager().cancel(makeAlarmPendingIntent(0, alarmInfo.getAlarmInfo().identifier, "", new Date()));
});
}
}
private PendingIntent makeAlarmPendingIntent(int occurrence, String identifier, String summary, Date eventDate) {
Intent intent = AlarmBroadcastReceiver.makeAlarmIntent(occurrence, identifier, summary, eventDate);
return PendingIntent.getBroadcast(context, 1, intent, 0);
}
private void incrementByRepeatPeriod(Calendar calendar, RepeatPeriod period,
int interval) {
int field;
switch (period) {
case DAILY:
field = Calendar.DAY_OF_MONTH;
break;
case WEEKLY:
field = Calendar.WEEK_OF_YEAR;
break;
case MONTHLY:
field = Calendar.MONTH;
break;
case ANNUALLY:
field = Calendar.YEAR;
break;
default:
throw new AssertionError("Unknown repeatPeriod: " + period);
}
calendar.add(field, interval);
}
private Date calculateAlarmTime(Date eventStart, TimeZone timeZone,
AlarmInterval alarmInterval) {
Calendar calendar;
if (timeZone != null) {
calendar = Calendar.getInstance(timeZone);
} else {
calendar = Calendar.getInstance();
}
calendar.setTime(eventStart);
switch (alarmInterval) {
case FIVE_MINUTES:
calendar.add(Calendar.MINUTE, -5);
break;
case TEN_MINUTES:
calendar.add(Calendar.MINUTE, -10);
break;
case THIRTY_MINUTES:
calendar.add(Calendar.MINUTE, -30);
break;
case ONE_HOUR:
calendar.add(Calendar.HOUR, -1);
break;
case ONE_DAY:
calendar.add(Calendar.DAY_OF_MONTH, -1);
break;
case TWO_DAYS:
calendar.add(Calendar.DAY_OF_MONTH, -2);
break;
case THREE_DAYS:
calendar.add(Calendar.DAY_OF_MONTH, -3);
break;
case ONE_WEEK:
calendar.add(Calendar.WEEK_OF_MONTH, -1);
break;
}
return calendar.getTime();
}
private interface AlarmIterationCallback {
void call(Date time, int occurrence);
}
}

View file

@ -0,0 +1,88 @@
package de.tutao.tutanota;
import android.content.Context;
import android.security.KeyPairGeneratorSpec;
import android.util.Log;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableEntryException;
import java.security.cert.CertificateException;
import java.util.Calendar;
import javax.security.auth.x500.X500Principal;
public class AndroidKeyStoreFacade {
public static final String TAG = "AndroidKeyStoreFacade";
private static final String AndroidKeyStore = "AndroidKeyStore";
private static final String KEY_ALIAS = "TutanotaAppDeviceKey";
private final Crypto crypto;
private KeyStore keyStore;
private Context context;
public AndroidKeyStoreFacade(Context context) {
this.context = context;
this.crypto = new Crypto(context);
try {
keyStore = KeyStore.getInstance(AndroidKeyStore);
keyStore.load(null);
// Generate the RSA key pairs
if (!keyStore.containsAlias(KEY_ALIAS)) {
// Generate a key pair for encryption
Calendar start = Calendar.getInstance();
Calendar end = Calendar.getInstance();
end.add(Calendar.YEAR, 50);
KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context)
.setAlias(KEY_ALIAS)
.setSubject(new X500Principal("CN=" + KEY_ALIAS))
.setSerialNumber(BigInteger.TEN)
.setStartDate(start.getTime())
.setEndDate(end.getTime())
.build();
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", AndroidKeyStore);
kpg.initialize(spec);
kpg.generateKeyPair();
}
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException | KeyStoreException | IOException | CertificateException e) {
Log.w(TAG, "Keystore could not be initialized", e);
}
}
public String encryptKey(byte[] sessionKey) throws UnrecoverableEntryException, KeyStoreException, CryptoError {
if (keyStore == null) {
throw new KeyStoreException("Keystore was not initialized");
}
KeyStore.PrivateKeyEntry privateKeyEntry;
try {
privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(KEY_ALIAS, null);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
return this.crypto.rsaEncrypt(privateKeyEntry.getCertificate().getPublicKey(), sessionKey, new byte[0]);
}
public byte[] decryptKey(String encSessionKey) throws UnrecoverableEntryException, KeyStoreException, CryptoError {
if (keyStore == null) {
throw new KeyStoreException("Keystore was not initialized");
}
KeyStore.PrivateKeyEntry privateKeyEntry;
try {
privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(KEY_ALIAS, null);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
byte[] encSessionKeyRaw = Utils.base64ToBytes(encSessionKey);
return this.crypto.rsaDecrypt(privateKeyEntry.getPrivateKey(), encSessionKeyRaw);
}
}

View file

@ -1,12 +1,8 @@
package de.tutao.tutanota;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.support.annotation.VisibleForTesting;
import org.apache.commons.io.IOUtils;
@ -53,12 +49,14 @@ public final class Crypto {
public static final String TEMP_DIR_ENCRYPTED = "temp/encrypted";
public static final String TEMP_DIR_DECRYPTED = "temp/decrypted";
private static final String PROVIDER = "BC";
public static final byte[] FIXED_IV = new byte[16];
private final static int RSA_KEY_LENGTH_IN_BITS = 2048;
private static final String RSA_ALGORITHM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
private final static int RSA_PUBLIC_EXPONENT = 65537;
public static final String AES_MODE_PADDING = "AES/CBC/PKCS5Padding";
private static final String AES_MODE_PADDING = "AES/CBC/PKCS5Padding";
private static final String AES_MODE_NO_PADDING = "AES/CBC/NoPadding";
public static final int AES_KEY_LENGTH = 128;
public static final int AES_KEY_LENGTH_BYTES = AES_KEY_LENGTH / 8;
@ -72,6 +70,9 @@ public final class Crypto {
static {
// see: http://android-developers.blogspot.de/2013/08/some-securerandom-thoughts.html
PRNGFixes.apply();
for (int i = 0; i < FIXED_IV.length; i++) {
FIXED_IV[i] = (byte) 0x88;
}
}
public static final String HMAC_256 = "HmacSHA256";
@ -147,23 +148,33 @@ public final class Crypto {
String rsaEncrypt(JSONObject publicKeyJson, byte[] data, byte[] random) throws CryptoError {
try {
PublicKey publicKey = jsonToPublicKey(publicKeyJson);
this.randomizer.setSeed(random);
byte[] encrypted = rsaEncrypt(data, publicKey, this.randomizer);
return Utils.bytesToBase64(encrypted);
} catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
// These types of errors are normal crypto errors and will be handled by the web part.
throw new CryptoError(e);
} catch (JSONException | NoSuchAlgorithmException |
NoSuchProviderException | NoSuchPaddingException e) {
return this.rsaEncrypt(publicKey, data, random);
} catch (JSONException e) {
// These types of errors are unexpected and fatal.
throw new RuntimeException(e);
}
}
private byte[] rsaEncrypt(byte[] data, PublicKey publicKey, SecureRandom randomizer) throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, publicKey, randomizer);
return cipher.doFinal(data);
/**
* Encrypts an aes key with RSA to a byte array.
*/
String rsaEncrypt(PublicKey publicKey, byte[] data, byte[] random) throws CryptoError {
this.randomizer.setSeed(random);
byte[] encrypted = rsaEncrypt(data, publicKey, this.randomizer);
return Utils.bytesToBase64(encrypted);
}
private byte[] rsaEncrypt(byte[] data, PublicKey publicKey, SecureRandom randomizer) throws CryptoError {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey, randomizer);
return cipher.doFinal(data);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException(e);
} catch (BadPaddingException | IllegalBlockSizeException | InvalidKeyException e) {
throw new CryptoError(e);
}
}
/**
@ -171,28 +182,36 @@ public final class Crypto {
*/
String rsaDecrypt(JSONObject jsonPrivateKey, byte[] encryptedKey) throws CryptoError {
try {
byte[] decrypted = rsaDecrypt(jsonPrivateKey, encryptedKey, this.randomizer);
PrivateKey privateKey = jsonToPrivateKey(jsonPrivateKey);
byte[] decrypted = rsaDecrypt(privateKey, encryptedKey);
return Utils.bytesToBase64(decrypted);
} catch (InvalidKeySpecException | BadPaddingException | InvalidKeyException
| IllegalBlockSizeException e) {
} catch (InvalidKeySpecException e) {
// These types of errors can happen and that's okay, they should be handled gracefully.
throw new CryptoError(e);
} catch (JSONException | NoSuchAlgorithmException | NoSuchProviderException |
NoSuchPaddingException e) {
// These errors are not expected, fatal for the whole application and should be
} catch (JSONException | NoSuchAlgorithmException e) {
// These errors are not expected, fatal for the whole application and should be
// reported.
throw new RuntimeException("rsaDecrypt error", e);
}
}
private byte[] rsaDecrypt(JSONObject jsonPrivateKey, byte[] encryptedKey, SecureRandom randomizer) throws JSONException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
Cipher cipher;
PrivateKey privateKey = jsonToPrivateKey(jsonPrivateKey);
cipher = Cipher.getInstance(RSA_ALGORITHM, PROVIDER);
cipher.init(Cipher.DECRYPT_MODE, privateKey, randomizer);
return cipher.doFinal(encryptedKey);
public byte[] rsaDecrypt(PrivateKey privateKey, byte[] encryptedKey) throws CryptoError {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey, this.randomizer);
return cipher.doFinal(encryptedKey);
} catch (BadPaddingException | InvalidKeyException
| IllegalBlockSizeException e) {
throw new CryptoError(e);
} catch (NoSuchAlgorithmException |
NoSuchPaddingException e) {
// These errors are not expected, fatal for the whole application and should be
// reported.
throw new RuntimeException("rsaDecrypt error", e);
}
}
/**
* Converts the given byte array to a key.
*
@ -201,7 +220,7 @@ public final class Crypto {
*/
public static SecretKeySpec bytesToKey(byte[] key) {
if (key.length != AES_KEY_LENGTH_BYTES) {
throw new RuntimeException("invalid key length");
throw new RuntimeException("invalid key length: " + key.length);
}
return new SecretKeySpec(key, "AES");
}
@ -262,6 +281,35 @@ public final class Crypto {
return Uri.fromFile(outputFile).toString();
}
public byte[] aesDecrypt(final byte[] key, String base64EncData) throws CryptoError {
byte[] encData = Utils.base64ToBytes(base64EncData);
return this.aesDecrypt(key, encData);
}
public byte[] decryptKey(final byte[] encryptionKey, final byte[] encryptedKeyWithoutIV) throws CryptoError {
try {
Cipher cipher = Cipher.getInstance(AES_MODE_NO_PADDING);
IvParameterSpec params = new IvParameterSpec(FIXED_IV);
cipher.init(Cipher.DECRYPT_MODE, bytesToKey(encryptionKey), params);
return cipher.doFinal(encryptedKeyWithoutIV);
} catch (BadPaddingException | IllegalBlockSizeException | InvalidKeyException e) {
throw new CryptoError(e);
} catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException(e);
}
}
public byte[] aesDecrypt(final byte[] key, byte[] encData) throws CryptoError {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
this.aesDecrypt(key, new ByteArrayInputStream(encData), out, encData.length);
} catch (IOException e) {
throw new CryptoError(e);
}
return out.toByteArray();
}
public void aesDecrypt(final byte[] key, InputStream in, OutputStream out, long inputSize) throws IOException, CryptoError {
InputStream decrypted = null;
try {
@ -372,4 +420,4 @@ public final class Crypto {
return json;
}
}
}
}

View file

@ -0,0 +1,16 @@
package de.tutao.tutanota;
import java.nio.charset.StandardCharsets;
import java.util.Date;
public class EncryptionUtils {
public static Date decryptDate(String encryptedData, Crypto crypto, byte[] sessionKey) throws CryptoError {
byte[] decBytes = crypto.aesDecrypt(sessionKey, encryptedData);
return new Date(Long.valueOf(new String(decBytes, StandardCharsets.UTF_8)));
}
public static String decryptString(String encryptedData, Crypto crypto, byte[] sessionKey) throws CryptoError {
byte[] decBytes = crypto.aesDecrypt(sessionKey, encryptedData);
return new String(decBytes, StandardCharsets.UTF_8);
}
}

View file

@ -0,0 +1,21 @@
package de.tutao.tutanota;
public final class IdTuple {
private final String listId;
private final String elementId;
public IdTuple(String listId, String elementId) {
this.listId = listId;
this.elementId = elementId;
}
public String getElementId() {
return elementId;
}
public String getListId() {
return listId;
}
}

View file

@ -3,7 +3,6 @@ package de.tutao.tutanota;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.ActivityNotFoundException;
@ -30,11 +29,8 @@ import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.View;
import android.webkit.CookieManager;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
@ -49,13 +45,12 @@ import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Calendar;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.tutao.tutanota.push.PushNotificationService;
import de.tutao.tutanota.push.SseStorage;
@ -516,4 +511,4 @@ class ActivityResult {
this.resultCode = resultCode;
this.data = data;
}
}
}

View file

@ -22,7 +22,6 @@ import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@ -37,7 +36,8 @@ public final class Native {
private static final String JS_NAME = "nativeApp";
private final static String TAG = "Native";
static int requestId = 0;
private static int requestId = 0;
private final AndroidKeyStoreFacade keyStoreFacade;
private Crypto crypto;
private FileUtil files;
private Contact contact;
@ -53,6 +53,7 @@ public final class Native {
contact = new Contact(activity);
files = new FileUtil(activity);
sseStorage = new SseStorage(activity);
keyStoreFacade = new AndroidKeyStoreFacade(activity);
}
public void setup() {
@ -219,8 +220,10 @@ public final class Native {
promise.resolve(sseStorage.getPushIdentifier());
break;
case "storePushIdentifierLocally":
String pushIdentifierSessionKeyB64 = args.getString(4);
String deviceEncSessionKey = this.keyStoreFacade.encryptKey(Utils.base64ToBytes(pushIdentifierSessionKeyB64));
sseStorage.storePushIdentifier(args.getString(0), args.getString(1),
args.getString(2));
args.getString(2), args.getString(3), deviceEncSessionKey);
promise.resolve(true);
break;
case "closePushNotifications":

View file

@ -0,0 +1,5 @@
package de.tutao.tutanota;
public enum OperationType {
CREATE, UPDATE, DELETE
}

View file

@ -0,0 +1,52 @@
package de.tutao.tutanota;
import android.support.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.TimeZone;
final class RepeatRule {
final RepeatPeriod frequency;
final int interval;
final TimeZone timeZone;
// TODO: serialize them
@Nullable
EndType endType;
long endValue;
static RepeatRule fromJson(JSONObject jsonObject) throws JSONException {
RepeatPeriod repeatPeriod = RepeatPeriod.values()[jsonObject.getInt("frequency")];
int interval = jsonObject.getInt("interval");
TimeZone timeZone = TimeZone.getTimeZone(jsonObject.getString("timeZone"));
return new RepeatRule(repeatPeriod, interval, timeZone);
}
RepeatRule(RepeatPeriod frequency, int interval, TimeZone timeZone) {
this.frequency = frequency;
this.interval = interval;
this.timeZone = timeZone;
}
JSONObject toJson() {
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("frequency", this.frequency.ordinal());
jsonObject.put("interval", this.interval);
jsonObject.put("timeZone", this.timeZone.getID());
return jsonObject;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
}
enum RepeatPeriod {
DAILY, WEEKLY, MONTHLY, ANNUALLY;
}
enum EndType {
NEVER, UNTIL, COUNT
}

View file

@ -0,0 +1,97 @@
package de.tutao.tutanota.push;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import de.tutao.tutanota.AlarmNotification;
final class PushMessage {
private static final String TITLE_KEY = "title";
private static final String ADDRESS_KEY = "address";
private static final String COUNTER_KEY = "counter";
private static final String USER_ID_KEY = "userId";
private static final String NOTIFICATIONS_KEY = "notificationInfos";
private static final String CONFIRMATION_ID_KEY = "confirmationId";
private static final String ALARM_INFOS_KEY = "alarmInfos";
private static final String ALARM_NOTIFICATIONS_KEY = "alarmNotifications";
private final String title;
private final String confirmationId;
private final List<NotificationInfo> notificationInfos;
private final List<AlarmNotification> alarmInfos;
public static PushMessage fromJson(String json) throws JSONException {
JSONObject jsonObject = new JSONObject(json);
String title = jsonObject.getString(TITLE_KEY);
String confirmationId = jsonObject.getString(CONFIRMATION_ID_KEY);
JSONArray recipientInfosJsonArray = jsonObject.getJSONArray(NOTIFICATIONS_KEY);
List<NotificationInfo> notificationInfos = new ArrayList<>(recipientInfosJsonArray.length());
for (int i = 0; i < recipientInfosJsonArray.length(); i++) {
JSONObject itemObject = recipientInfosJsonArray.getJSONObject(i);
String address = itemObject.getString(ADDRESS_KEY);
int counter = itemObject.getInt(COUNTER_KEY);
String userId = itemObject.getString(USER_ID_KEY);
notificationInfos.add(new NotificationInfo(address, counter, userId));
}
JSONArray alarmInfosJsonArray = jsonObject.getJSONArray(ALARM_NOTIFICATIONS_KEY);
List<AlarmNotification> alarmNotifications = new ArrayList<>();
for (int i = 0; i < alarmInfosJsonArray.length(); i++) {
JSONObject itemObject = alarmInfosJsonArray.getJSONObject(i);
alarmNotifications.add(AlarmNotification.fromJson(itemObject));
}
return new PushMessage(title, confirmationId, notificationInfos, alarmNotifications);
}
private PushMessage(String title, String confirmationId,
List<NotificationInfo> notificationInfos,
List<AlarmNotification> alarmInfos) {
this.title = title;
this.confirmationId = confirmationId;
this.notificationInfos = notificationInfos;
this.alarmInfos = alarmInfos;
}
public String getTitle() {
return title;
}
public List<NotificationInfo> getNotificationInfos() {
return notificationInfos;
}
public String getConfirmationId() {
return confirmationId;
}
public List<AlarmNotification> getAlarmInfos() {
return alarmInfos;
}
final static class NotificationInfo {
private final String address;
private final int counter;
private String userId;
NotificationInfo(String address, int counter, String userId) {
this.address = address;
this.counter = counter;
this.userId = userId;
}
public String getAddress() {
return address;
}
public int getCounter() {
return counter;
}
public String getUserId() {
return userId;
}
}
}

View file

@ -1,7 +1,6 @@
package de.tutao.tutanota.push;
import android.annotation.TargetApi;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@ -45,7 +44,6 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Random;
@ -56,6 +54,8 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import de.tutao.tutanota.AlarmNotification;
import de.tutao.tutanota.AlarmNotificationsManager;
import de.tutao.tutanota.Crypto;
import de.tutao.tutanota.MainActivity;
import de.tutao.tutanota.R;
@ -268,6 +268,8 @@ public final class PushNotificationService extends JobService {
Log.d(TAG, "onStartJob");
restartConnectionIfNeeded(null);
jobParameters = params;
// TODO: handle rescheduling
// new AlarmNotificationsManager(this).reScheduleAlarms();
return true;
}
@ -288,7 +290,7 @@ public final class PushNotificationService extends JobService {
NotificationManager notificationManager = getNotificationManager();
try {
URL = new URL(connectedSseInfo.getSseOrigin() + "/sse?_body=" + requestJson(connectedSseInfo));
URL url = new URL(connectedSseInfo.getSseOrigin() + "/sse?_body=" + requestJson(connectedSseInfo));
HttpURLConnection httpsURLConnection = (HttpURLConnection) url.openConnection();
this.httpsURLConnectionRef.set(httpsURLConnection);
httpsURLConnection.setRequestProperty("Content-Type", "application/json");
@ -316,7 +318,8 @@ public final class PushNotificationService extends JobService {
continue;
}
event = event.substring(6);
if (event.matches("^[0-9]{1,}$"))
Log.d(TAG, "Event: " + event);
if (event.matches("^[0-9]+$"))
continue;
if (event.startsWith("heartbeatTimeout:")) {
@ -336,66 +339,17 @@ public final class PushNotificationService extends JobService {
}
List<PushMessage.NotificationInfo> notificationInfos = pushMessage.getNotificationInfos();
for (int i = 0; i < notificationInfos.size(); i++) {
PushMessage.NotificationInfo notificationInfo = notificationInfos.get(i);
LocalNotificationInfo counterPerAlias =
aliasNotification.get(notificationInfo.getAddress());
if (counterPerAlias == null) {
counterPerAlias = new LocalNotificationInfo(
pushMessage.getTitle(),
notificationInfo.getCounter(), notificationInfo);
} else {
counterPerAlias = counterPerAlias.incremented(notificationInfo.getCounter());
}
aliasNotification.put(notificationInfo.getAddress(), counterPerAlias);
handleNotificationInfos(notificationManager, pushMessage, notificationInfos);
handleAlarmNotifications(pushMessage.getAlarmInfos());
Log.d(TAG, "Event: " + event);
int notificationId = notificationId(notificationInfo.getAddress());
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(this, EMAIL_NOTIFICATION_CHANNEL_ID)
.setLights(getResources().getColor(R.color.colorPrimary), 1000, 1000);
ArrayList<String> addresses = new ArrayList<>();
addresses.add(notificationInfo.getAddress());
notificationBuilder.setContentTitle(pushMessage.getTitle())
.setColor(getResources().getColor(R.color.colorPrimary))
.setContentText(notificationContent(notificationInfo.getAddress()))
.setNumber(counterPerAlias.counter)
.setSmallIcon(R.drawable.ic_status)
.setDeleteIntent(this.intentForDelete(addresses))
.setContentIntent(intentOpenMailbox(notificationInfo, false))
.setGroup(NOTIFICATION_EMAIL_GROUP)
.setAutoCancel(true)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY);
notificationManager.notify(notificationId, notificationBuilder.build());
sendSummaryNotification(notificationManager, pushMessage.getTitle(),
notificationInfo);
}
for (PushMessage.AlarmInfo alarmInfo : pushMessage.getAlarmInfos()) {
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent("de.tutao.tutanota.ALARM", Uri.fromParts("alarm", alarmInfo.getIdentifier(), ""));
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 0);
if (alarmInfo.getOperation() == OperationType.CREATE) {
Log.d(TAG, "Scheduling alarm at: " + alarmInfo.getTrigger() + " " + intent.getData());
alarmManager.setExact(AlarmManager.RTC_WAKEUP, alarmInfo.getTrigger().getTime(), pendingIntent);
} else if (alarmInfo.getOperation() == OperationType.DELETE) {
Log.d(TAG, "Cancel alarm" + alarmInfo.getTrigger() + " " + intent.getData());
alarmManager.cancel(pendingIntent);
}
}
Log.d(TAG, "Scheduling confirmation for " + connectedSseInfo.getPushIdentifier());
confirmationThreadPool.execute(
() -> sendConfirmation(connectedSseInfo.getPushIdentifier(), pushMessage));
Log.d(TAG, "Executing jobFinished after receiving notifications");
finishJobIfNeeded();
}
} catch (Exception ignored) {
} catch (Exception exception) {
HttpURLConnection httpURLConnection = httpsURLConnectionRef.get();
try {
// we get not authorized for the stored identifier and user ids, so remove them
@ -411,12 +365,11 @@ public final class PushNotificationService extends JobService {
int delay = (random.nextInt(timeoutInSeconds) + delayBoundary) / 2;
if (this.hasNetworkConnection()) {
Log.e(TAG, "error opening sse, rescheduling after " + delay, ignored);
Log.e(TAG, "error opening sse, rescheduling after " + delay, exception);
reschedule(delay);
} else {
Log.e(TAG, "network is not connected, do not reschedule ", ignored);
Log.e(TAG, "network is not connected, do not reschedule ", exception);
}
} finally {
if (reader != null) {
try {
@ -428,6 +381,53 @@ public final class PushNotificationService extends JobService {
}
}
private void handleAlarmNotifications(List<AlarmNotification> alarmNotifications) {
new AlarmNotificationsManager(this, this.sseStorage)
.scheduleNewAlarms(alarmNotifications);
}
private void handleNotificationInfos(NotificationManager notificationManager, PushMessage pushMessage, List<PushMessage.NotificationInfo> notificationInfos) {
for (int i = 0; i < notificationInfos.size(); i++) {
PushMessage.NotificationInfo notificationInfo = notificationInfos.get(i);
LocalNotificationInfo counterPerAlias =
aliasNotification.get(notificationInfo.getAddress());
if (counterPerAlias == null) {
counterPerAlias = new LocalNotificationInfo(
pushMessage.getTitle(),
notificationInfo.getCounter(), notificationInfo);
} else {
counterPerAlias = counterPerAlias.incremented(notificationInfo.getCounter());
}
aliasNotification.put(notificationInfo.getAddress(), counterPerAlias);
int notificationId = notificationId(notificationInfo.getAddress());
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(this, EMAIL_NOTIFICATION_CHANNEL_ID)
.setLights(getResources().getColor(R.color.colorPrimary), 1000, 1000);
ArrayList<String> addresses = new ArrayList<>();
addresses.add(notificationInfo.getAddress());
notificationBuilder.setContentTitle(pushMessage.getTitle())
.setColor(getResources().getColor(R.color.colorPrimary))
.setContentText(notificationContent(notificationInfo.getAddress()))
.setNumber(counterPerAlias.counter)
.setSmallIcon(R.drawable.ic_status)
.setDeleteIntent(this.intentForDelete(addresses))
.setContentIntent(intentOpenMailbox(notificationInfo, false))
.setGroup(NOTIFICATION_EMAIL_GROUP)
.setAutoCancel(true)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY);
notificationManager.notify(notificationId, notificationBuilder.build());
sendSummaryNotification(notificationManager, pushMessage.getTitle(),
notificationInfo);
}
}
private void scheduleJobFinish() {
if (jobParameters != null) {
new Thread(() -> {
@ -612,120 +612,6 @@ final class LooperThread extends Thread {
}
}
final class PushMessage {
private static final String TITLE_KEY = "title";
private static final String ADDRESS_KEY = "address";
private static final String COUNTER_KEY = "counter";
private static final String USER_ID_KEY = "userId";
private static final String NOTIFICATIONS_KEY = "notificationInfos";
private static final String CONFIRMATION_ID_KEY = "confirmationId";
private static final String ALARM_INFOS_KEY = "alarmInfos";
private final String title;
private final String confirmationId;
private final List<NotificationInfo> notificationInfos;
private final List<AlarmInfo> alarmInfos;
public static PushMessage fromJson(String json) throws JSONException {
JSONObject jsonObject = new JSONObject(json);
String title = jsonObject.getString(TITLE_KEY);
String confirmationId = jsonObject.getString(CONFIRMATION_ID_KEY);
JSONArray recipientInfosJsonArray = jsonObject.getJSONArray(NOTIFICATIONS_KEY);
List<NotificationInfo> notificationInfos = new ArrayList<>(recipientInfosJsonArray.length());
for (int i = 0; i < recipientInfosJsonArray.length(); i++) {
JSONObject itemObject = recipientInfosJsonArray.getJSONObject(i);
String address = itemObject.getString(ADDRESS_KEY);
int counter = itemObject.getInt(COUNTER_KEY);
String userId = itemObject.getString(USER_ID_KEY);
notificationInfos.add(new NotificationInfo(address, counter, userId));
}
JSONArray alarmInfosJsonArray = jsonObject.getJSONArray(ALARM_INFOS_KEY);
List<AlarmInfo> alarmInfos = new ArrayList<>();
for (int i = 0; i < alarmInfosJsonArray.length(); i++) {
JSONObject itemObject = alarmInfosJsonArray.getJSONObject(i);
String trigger = itemObject.getString("trigger");
String identifier = itemObject.getString("identifier");
String operation = itemObject.getString("operation");
alarmInfos.add(new AlarmInfo(trigger, identifier, operation));
}
return new PushMessage(title, confirmationId, notificationInfos, alarmInfos);
}
private PushMessage(String title, String confirmationId,
List<NotificationInfo> notificationInfos,
List<AlarmInfo> alarmInfos) {
this.title = title;
this.confirmationId = confirmationId;
this.notificationInfos = notificationInfos;
this.alarmInfos = alarmInfos;
}
public String getTitle() {
return title;
}
public List<NotificationInfo> getNotificationInfos() {
return notificationInfos;
}
public String getConfirmationId() {
return confirmationId;
}
public List<AlarmInfo> getAlarmInfos() {
return alarmInfos;
}
final static class NotificationInfo {
private final String address;
private final int counter;
private String userId;
NotificationInfo(String address, int counter, String userId) {
this.address = address;
this.counter = counter;
this.userId = userId;
}
public String getAddress() {
return address;
}
public int getCounter() {
return counter;
}
public String getUserId() {
return userId;
}
}
final static class AlarmInfo {
private final Date trigger;
private final String identifier;
private final OperationType operation;
AlarmInfo(String trigger, String identifier, String operation) {
this.trigger = new Date(Long.valueOf(trigger));
this.identifier = identifier;
this.operation = OperationType.values()[Integer.valueOf(operation)];
}
public Date getTrigger() {
return trigger;
}
public String getIdentifier() {
return identifier;
}
public OperationType getOperation() {
return operation;
}
}
}
final class LocalNotificationInfo {
final String message;
final int counter;
@ -742,6 +628,3 @@ final class LocalNotificationInfo {
}
}
enum OperationType {
CREATE, UPDATE, DELETE
}

View file

@ -0,0 +1,100 @@
package de.tutao.tutanota.push;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public final class SseInfo {
private static final String PUSH_IDENTIFIER_JSON_KEY = "pushIdentifier";
private static final String USER_IDS_JSON_KEY = "userIds";
private static final String SSE_ORIGIN_JSON_KEY = "sseOrigin";
public static final String PUSH_IDENTIFIER_TO_SESSION_KEY = "pushIdentifierToSessionKey";
final private String pushIdentifier;
final private List<String> userIds;
final private String sseOrigin;
final private Map<String, String> pushIdentifierToSessionKey;
@Nullable
public static SseInfo fromJson(@NonNull String json) {
try {
JSONObject jsonObject = new JSONObject(json);
String identifier = jsonObject.getString(PUSH_IDENTIFIER_JSON_KEY);
JSONArray userIdsArray = jsonObject.getJSONArray(USER_IDS_JSON_KEY);
List<String> userIds = new ArrayList<>(userIdsArray.length());
for (int i = 0; i < userIdsArray.length(); i++) {
userIds.add(userIdsArray.getString(i));
}
String sseOrigin = jsonObject.getString(SSE_ORIGIN_JSON_KEY);
Map<String, String> pushIdentifierToSessionKey = new HashMap<>();
JSONObject sessionKeysJson = jsonObject.getJSONObject(PUSH_IDENTIFIER_TO_SESSION_KEY);
Iterator<String> keys = sessionKeysJson.keys();
while (keys.hasNext()) {
String key = keys.next();
pushIdentifierToSessionKey.put(key, sessionKeysJson.getString(key));
}
return new SseInfo(identifier, userIds, sseOrigin, pushIdentifierToSessionKey);
} catch (JSONException e) {
Log.w("SseInfo", "could read sse info", e);
return null;
}
}
SseInfo(String pushIdentifier, List<String> userIds, String sseOrigin, Map<String, String> pushIdentifierToSessionKey) {
this.pushIdentifier = pushIdentifier;
this.userIds = userIds;
this.sseOrigin = sseOrigin;
this.pushIdentifierToSessionKey = pushIdentifierToSessionKey;
}
public String getPushIdentifier() {
return pushIdentifier;
}
public List<String> getUserIds() {
return userIds;
}
public String getSseOrigin() {
return sseOrigin;
}
public Map<String, String> getPushIdentifierToSessionKey() {
return pushIdentifierToSessionKey;
}
public String toJSON() {
HashMap<String, Object> sseInfoMap = new HashMap<>();
sseInfoMap.put(PUSH_IDENTIFIER_JSON_KEY, this.pushIdentifier);
sseInfoMap.put(USER_IDS_JSON_KEY, this.userIds);
sseInfoMap.put(SSE_ORIGIN_JSON_KEY, this.sseOrigin);
sseInfoMap.put(PUSH_IDENTIFIER_TO_SESSION_KEY, this.pushIdentifierToSessionKey);
JSONObject jsonObject = new JSONObject(sseInfoMap);
return jsonObject.toString();
}
@Override
public boolean equals(Object other) {
if (other instanceof SseInfo) {
return ((SseInfo) other).toJSON().equals(this.toJSON());
} else {
return false;
}
}
@Override
public String toString() {
return toJSON();
}
}

View file

@ -3,13 +3,7 @@ package de.tutao.tutanota.push;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
@ -22,7 +16,6 @@ public final class SseStorage {
private static final String SSE_INFO_PREF = "sseInfo";
private static final String TAG = SseStorage.class.getSimpleName();
private Context context;
public SseStorage(Context context) {
@ -47,17 +40,20 @@ public final class SseStorage {
return SseInfo.fromJson(pushIdentifierPref);
}
public void storePushIdentifier(String identifier, String userId, String sseOrigin) {
public void storePushIdentifier(String identifier, String userId, String sseOrigin, String pushIdentifierElementId, String encSessionKey) {
final SseInfo sseInfo = getSseInfo();
SseInfo newInfo;
if (sseInfo == null) {
newInfo = new SseInfo(identifier, Collections.singletonList(userId), sseOrigin);
newInfo = new SseInfo(identifier, Collections.singletonList(userId), sseOrigin, new HashMap<>());
newInfo.getPushIdentifierToSessionKey().put(pushIdentifierElementId, encSessionKey);
} else {
List<String> userList = new ArrayList<>(sseInfo.getUserIds());
if (!userList.contains(userId)) {
userList.add(userId);
}
newInfo = new SseInfo(identifier, userList, sseOrigin);
sseInfo.getPushIdentifierToSessionKey().put(pushIdentifierElementId, encSessionKey);
newInfo = new SseInfo(identifier, userList, sseOrigin, sseInfo.getPushIdentifierToSessionKey());
}
getPrefs().edit().putString(SSE_INFO_PREF, newInfo.toJSON()).apply();
}
@ -71,71 +67,3 @@ public final class SseStorage {
}
}
final class SseInfo {
private static final String PUSH_IDENTIFIER_JSON_KEY = "pushIdentifier";
private static final String USER_IDS_JSON_KEY = "userIds";
private static final String SSE_ORIGIN_JSON_KEY = "sseOrigin";
final private String pushIdentifier;
final private List<String> userIds;
final private String sseOrigin;
@Nullable
public static SseInfo fromJson(@NonNull String json) {
try {
JSONObject jsonObject = new JSONObject(json);
String identifier = jsonObject.getString(PUSH_IDENTIFIER_JSON_KEY);
JSONArray userIdsArray = jsonObject.getJSONArray(USER_IDS_JSON_KEY);
List<String> userIds = new ArrayList<>(userIdsArray.length());
for (int i = 0; i < userIdsArray.length(); i++) {
userIds.add(userIdsArray.getString(i));
}
String sseOrigin = jsonObject.getString(SSE_ORIGIN_JSON_KEY);
return new SseInfo(identifier, userIds, sseOrigin);
} catch (JSONException e) {
Log.w("SseInfo", "could read sse info", e);
return null;
}
}
SseInfo(String pushIdentifier, List<String> userIds, String sseOrigin) {
this.pushIdentifier = pushIdentifier;
this.userIds = userIds;
this.sseOrigin = sseOrigin;
}
public String getPushIdentifier() {
return pushIdentifier;
}
public List<String> getUserIds() {
return userIds;
}
public String getSseOrigin() {
return sseOrigin;
}
public String toJSON() {
HashMap<String, Object> sseInfoMap = new HashMap<>();
sseInfoMap.put(PUSH_IDENTIFIER_JSON_KEY, this.pushIdentifier);
sseInfoMap.put(USER_IDS_JSON_KEY, this.userIds);
sseInfoMap.put(SSE_ORIGIN_JSON_KEY, this.sseOrigin);
JSONObject jsonObject = new JSONObject(sseInfoMap);
return jsonObject.toString();
}
@Override
public boolean equals(Object other) {
if (other instanceof SseInfo) {
return ((SseInfo) other).toJSON().equals(this.toJSON());
} else {
return false;
}
}
@Override
public String toString() {
return toJSON();
}
}

View file

@ -168,6 +168,7 @@ type User = {
userEncClientKey: Uint8Array;
verifier: Uint8Array;
alarmInfoList: ?UserAlarmInfoListType;
auth: ?UserAuthentication;
authenticatedDevices: AuthenticatedDevice[];
externalAuthInfo: ?UserExternalAuthInfo;
@ -1602,16 +1603,65 @@ type NotificationMailTemplate = {
type AlarmInfo = {
_type: TypeRef<AlarmInfo>;
_id: Id;
identifier: NumberString;
operation: NumberString;
trigger: Date;
identifier: string;
trigger: string;
}
type UserAlarmInfo = {
_type: TypeRef<UserAlarmInfo>;
_errors: Object;
_format: NumberString;
_id: IdTuple;
_ownerEncSessionKey: ?Uint8Array;
_ownerGroup: ?Id;
_permissions: Id;
alarmInfo: AlarmInfo;
}
type UserAlarmInfoListType = {
_type: TypeRef<UserAlarmInfoListType>;
_id: Id;
alarms: Id;
}
type NotificationSessionKey = {
_type: TypeRef<NotificationSessionKey>;
_id: Id;
pushIdentifierSessionEncSessionKey: Uint8Array;
pushIdentifier: IdTuple;
}
type RepeatRule = {
_type: TypeRef<RepeatRule>;
_id: Id;
endType: NumberString;
endValue: ?NumberString;
frequency: NumberString;
interval: NumberString;
timeZone: string;
}
type AlarmNotification = {
_type: TypeRef<AlarmNotification>;
_id: Id;
eventStart: Date;
operation: NumberString;
summary: string;
alarmInfo: AlarmInfo;
deviceSessionKeys: NotificationSessionKey[];
repeatRule: ?RepeatRule;
}
type AlarmServicePost = {
_type: TypeRef<AlarmServicePost>;
_errors: Object;
_format: NumberString;
alarmInfos: AlarmInfo[];
group: Id;
alarmNotifications: AlarmNotification[];
}

View file

@ -999,15 +999,8 @@ type UnencryptedStatisticLogRef = {
items: Id;
}
type EncDateWrapper = {
_type: TypeRef<EncDateWrapper>;
_id: Id;
value: Date;
}
type RepeatRule = {
_type: TypeRef<RepeatRule>;
type CalendarRepeatRule = {
_type: TypeRef<CalendarRepeatRule>;
_id: Id;
endType: NumberString;
endValue: ?NumberString;
@ -1015,15 +1008,6 @@ type RepeatRule = {
interval: NumberString;
timeZone: string;
exceptionDates: EncDateWrapper[];
}
type CalendarAlarmInfo = {
_type: TypeRef<CalendarAlarmInfo>;
_id: Id;
identifier: string;
trigger: string;
}
type CalendarEvent = {
@ -1040,8 +1024,8 @@ type CalendarEvent = {
startTime: Date;
summary: string;
alarmInfo: ?CalendarAlarmInfo;
repeatRule: ?RepeatRule;
repeatRule: ?CalendarRepeatRule;
alarmInfos: IdTuple[];
}
type CalendarGroupRoot = {
@ -1052,7 +1036,6 @@ type CalendarGroupRoot = {
_ownerEncSessionKey: ?Uint8Array;
_ownerGroup: ?Id;
_permissions: Id;
color: string;
name: string;
longEvents: Id;
@ -1064,7 +1047,6 @@ type CalendarGroupData = {
_id: Id;
adminEncGroupKey: Uint8Array;
calendarEncCalendarGroupRootSessionKey: Uint8Array;
encColor: Uint8Array;
encName: Uint8Array;
ownerEncGroupInfoSessionKey: Uint8Array;
userEncGroupKey: ?Uint8Array;

View file

@ -126,6 +126,8 @@ type WorkerRequestType = 'setup'
| 'extendMailIndex'
| 'resetSession'
| 'downloadFileContentNative'
| 'createCalendarEvent'
| 'resolveSessionKey'
type MainRequestType = 'execNative'
| 'entityEvent'
| 'error'

View file

@ -0,0 +1,54 @@
// @flow
import {DAY_IN_MILLIS} from "./DateUtils"
import {stringToCustomId} from "../EntityFunctions"
export const DAYS_SHIFTED_MS = 15 * DAY_IN_MILLIS
export function getAllDayDateLocal(utcDate: Date, timezone?: string): Date {
return new Date(utcDate.getUTCFullYear(), utcDate.getUTCMonth(), utcDate.getUTCDate(), 0, 0, 0, 0)
}
export function isAllDayEvent(event: CalendarEvent): boolean {
const {startTime, endTime} = event
return startTime.getUTCHours() === 0 && startTime.getUTCMinutes() === 0 && startTime.getUTCSeconds() === 0
&& endTime.getUTCHours() === 0 && endTime.getUTCMinutes() === 0 && endTime.getUTCSeconds() === 0
}
export function generateEventElementId(timestamp: number): string {
const randomDay = Math.floor((Math.random() * DAYS_SHIFTED_MS)) * 2
return createEventElementId(timestamp, randomDay - DAYS_SHIFTED_MS)
}
function createEventElementId(timestamp: number, shiftDays: number): string {
return stringToCustomId(String(timestamp + shiftDays))
}
export function geEventElementMaxId(timestamp: number): string {
return createEventElementId(timestamp, DAYS_SHIFTED_MS)
}
export function getEventElementMinId(timestamp: number): string {
return createEventElementId(timestamp, -DAYS_SHIFTED_MS)
}
export function getEventEnd(event: CalendarEvent): Date {
if (isAllDayEvent(event)) {
return getAllDayDateLocal(event.endTime)
} else {
return event.endTime
}
}
export function getEventStart(event: CalendarEvent): Date {
if (isAllDayEvent(event)) {
return getAllDayDateLocal(event.startTime)
} else {
return event.startTime
}
}
export function isLongEvent(event: CalendarEvent): boolean {
return getEventEnd(event).getTime() - getEventStart(event).getTime() > DAYS_SHIFTED_MS
}

View file

@ -2,9 +2,25 @@
import {create, TypeRef} from "../../common/EntityFunctions"
export const AlarmInfoTypeRef:TypeRef<AlarmInfo> = new TypeRef("sys", "AlarmInfo")
export const _TypeModel:TypeModel= {"name":"AlarmInfo","since":47,"type":"AGGREGATED_TYPE","id":1525,"rootId":"A3N5cwAF9Q","versioned":false,"encrypted":false,"values":{"_id":{"name":"_id","id":1526,"since":47,"type":"CustomId","cardinality":"One","final":true,"encrypted":false},"identifier":{"name":"identifier","id":1529,"since":47,"type":"Number","cardinality":"One","final":false,"encrypted":false},"operation":{"name":"operation","id":1527,"since":47,"type":"Number","cardinality":"One","final":false,"encrypted":false},"trigger":{"name":"trigger","id":1528,"since":47,"type":"Date","cardinality":"One","final":false,"encrypted":false}},"associations":{},"app":"sys","version":"47"}
export function createAlarmInfo():AlarmInfo {
return create(_TypeModel, AlarmInfoTypeRef)
export const AlarmInfoTypeRef: TypeRef<AlarmInfo> = new TypeRef("sys", "AlarmInfo")
export const _TypeModel: TypeModel = {
"name": "AlarmInfo",
"since": 47,
"type": "AGGREGATED_TYPE",
"id": 1525,
"rootId": "A3N5cwAF9Q",
"versioned": false,
"encrypted": false,
"values": {
"_id": {"name": "_id", "id": 1526, "since": 47, "type": "CustomId", "cardinality": "One", "final": true, "encrypted": false},
"identifier": {"name": "identifier", "id": 1528, "since": 47, "type": "String", "cardinality": "One", "final": true, "encrypted": true},
"trigger": {"name": "trigger", "id": 1527, "since": 47, "type": "String", "cardinality": "One", "final": true, "encrypted": true}
},
"associations": {},
"app": "sys",
"version": "47"
}
export function createAlarmInfo(): AlarmInfo {
return create(_TypeModel, AlarmInfoTypeRef)
}

View file

@ -0,0 +1,47 @@
// @flow
import {create, TypeRef} from "../../common/EntityFunctions"
export const AlarmNotificationTypeRef: TypeRef<AlarmNotification> = new TypeRef("sys", "AlarmNotification")
export const _TypeModel: TypeModel = {
"name": "AlarmNotification",
"since": 47,
"type": "AGGREGATED_TYPE",
"id": 1552,
"rootId": "A3N5cwAGEA",
"versioned": false,
"encrypted": false,
"values": {
"_id": {"name": "_id", "id": 1553, "since": 47, "type": "CustomId", "cardinality": "One", "final": true, "encrypted": false},
"eventStart": {"name": "eventStart", "id": 1556, "since": 47, "type": "Date", "cardinality": "One", "final": true, "encrypted": true},
"operation": {"name": "operation", "id": 1554, "since": 47, "type": "Number", "cardinality": "One", "final": true, "encrypted": false},
"summary": {"name": "summary", "id": 1555, "since": 47, "type": "String", "cardinality": "One", "final": true, "encrypted": true}
},
"associations": {
"alarmInfo": {
"name": "alarmInfo",
"id": 1557,
"since": 47,
"type": "AGGREGATION",
"cardinality": "One",
"refType": "AlarmInfo",
"final": true
},
"deviceSessionKeys": {
"name": "deviceSessionKeys",
"id": 1559,
"since": 47,
"type": "AGGREGATION",
"cardinality": "Any",
"refType": "NotificationSessionKey",
"final": true
},
"repeatRule": {"name": "repeatRule", "id": 1558, "since": 47, "type": "AGGREGATION", "cardinality": "ZeroOrOne", "refType": "RepeatRule", "final": true}
},
"app": "sys",
"version": "47"
}
export function createAlarmNotification(): AlarmNotification {
return create(_TypeModel, AlarmNotificationTypeRef)
}

View file

@ -2,9 +2,31 @@
import {create, TypeRef} from "../../common/EntityFunctions"
export const AlarmServicePostTypeRef:TypeRef<AlarmServicePost> = new TypeRef("sys", "AlarmServicePost")
export const _TypeModel:TypeModel= {"name":"AlarmServicePost","since":47,"type":"DATA_TRANSFER_TYPE","id":1531,"rootId":"A3N5cwAF-w","versioned":false,"encrypted":false,"values":{"_format":{"name":"_format","id":1532,"since":47,"type":"Number","cardinality":"One","final":false,"encrypted":false}},"associations":{"alarmInfos":{"name":"alarmInfos","id":1533,"since":47,"type":"AGGREGATION","cardinality":"Any","refType":"AlarmInfo","final":false},"group":{"name":"group","id":1534,"since":47,"type":"ELEMENT_ASSOCIATION","cardinality":"One","refType":"Group","final":false,"external":false}},"app":"sys","version":"47"}
export function createAlarmServicePost():AlarmServicePost {
return create(_TypeModel, AlarmServicePostTypeRef)
export const AlarmServicePostTypeRef: TypeRef<AlarmServicePost> = new TypeRef("sys", "AlarmServicePost")
export const _TypeModel: TypeModel = {
"name": "AlarmServicePost",
"since": 47,
"type": "DATA_TRANSFER_TYPE",
"id": 1562,
"rootId": "A3N5cwAGGg",
"versioned": false,
"encrypted": true,
"values": {"_format": {"name": "_format", "id": 1563, "since": 47, "type": "Number", "cardinality": "One", "final": false, "encrypted": false}},
"associations": {
"alarmNotifications": {
"name": "alarmNotifications",
"id": 1564,
"since": 47,
"type": "AGGREGATION",
"cardinality": "Any",
"refType": "AlarmNotification",
"final": false
}
},
"app": "sys",
"version": "47"
}
export function createAlarmServicePost(): AlarmServicePost {
return create(_TypeModel, AlarmServicePostTypeRef)
}

View file

@ -0,0 +1,44 @@
// @flow
import {create, TypeRef} from "../../common/EntityFunctions"
export const NotificationSessionKeyTypeRef: TypeRef<NotificationSessionKey> = new TypeRef("sys", "NotificationSessionKey")
export const _TypeModel: TypeModel = {
"name": "NotificationSessionKey",
"since": 47,
"type": "AGGREGATED_TYPE",
"id": 1541,
"rootId": "A3N5cwAGBQ",
"versioned": false,
"encrypted": false,
"values": {
"_id": {"name": "_id", "id": 1542, "since": 47, "type": "CustomId", "cardinality": "One", "final": true, "encrypted": false},
"pushIdentifierSessionEncSessionKey": {
"name": "pushIdentifierSessionEncSessionKey",
"id": 1544,
"since": 47,
"type": "Bytes",
"cardinality": "One",
"final": false,
"encrypted": false
}
},
"associations": {
"pushIdentifier": {
"name": "pushIdentifier",
"id": 1543,
"since": 47,
"type": "LIST_ELEMENT_ASSOCIATION",
"cardinality": "One",
"refType": "PushIdentifier",
"final": false,
"external": false
}
},
"app": "sys",
"version": "47"
}
export function createNotificationSessionKey(): NotificationSessionKey {
return create(_TypeModel, NotificationSessionKeyTypeRef)
}

View file

@ -0,0 +1,10 @@
// @flow
import {create, TypeRef} from "../../common/EntityFunctions"
export const RepeatRuleTypeRef:TypeRef<RepeatRule> = new TypeRef("sys", "RepeatRule")
export const _TypeModel:TypeModel= {"name":"RepeatRule","since":47,"type":"AGGREGATED_TYPE","id":1545,"rootId":"A3N5cwAGCQ","versioned":false,"encrypted":false,"values":{"_id":{"name":"_id","id":1546,"since":47,"type":"CustomId","cardinality":"One","final":true,"encrypted":false},"endType":{"name":"endType","id":1548,"since":47,"type":"Number","cardinality":"One","final":false,"encrypted":true},"endValue":{"name":"endValue","id":1549,"since":47,"type":"Number","cardinality":"ZeroOrOne","final":false,"encrypted":true},"frequency":{"name":"frequency","id":1547,"since":47,"type":"Number","cardinality":"One","final":false,"encrypted":true},"interval":{"name":"interval","id":1550,"since":47,"type":"Number","cardinality":"One","final":false,"encrypted":true},"timeZone":{"name":"timeZone","id":1551,"since":47,"type":"String","cardinality":"One","final":false,"encrypted":true}},"associations":{},"app":"sys","version":"47"}
export function createRepeatRule():RepeatRule {
return create(_TypeModel, RepeatRuleTypeRef)
}

View file

@ -24,6 +24,15 @@ export const _TypeModel: TypeModel = {
"userEncClientKey": {"name": "userEncClientKey", "id": 89, "since": 1, "type": "Bytes", "cardinality": "One", "final": true, "encrypted": false},
"verifier": {"name": "verifier", "id": 91, "since": 1, "type": "Bytes", "cardinality": "One", "final": true, "encrypted": false}
}, "associations": {
"alarmInfoList": {
"name": "alarmInfoList",
"id": 1540,
"since": 47,
"type": "AGGREGATION",
"cardinality": "ZeroOrOne",
"refType": "UserAlarmInfoListType",
"final": false
},
"auth": {"name": "auth", "id": 1210, "since": 23, "type": "AGGREGATION", "cardinality": "ZeroOrOne", "refType": "UserAuthentication", "final": true},
"authenticatedDevices": {
"name": "authenticatedDevices",

View file

@ -0,0 +1,10 @@
// @flow
import {create, TypeRef} from "../../common/EntityFunctions"
export const UserAlarmInfoTypeRef:TypeRef<UserAlarmInfo> = new TypeRef("sys", "UserAlarmInfo")
export const _TypeModel:TypeModel= {"name":"UserAlarmInfo","since":47,"type":"LIST_ELEMENT_TYPE","id":1529,"rootId":"A3N5cwAF-Q","versioned":false,"encrypted":true,"values":{"_format":{"name":"_format","id":1533,"since":47,"type":"Number","cardinality":"One","final":false,"encrypted":false},"_id":{"name":"_id","id":1531,"since":47,"type":"GeneratedId","cardinality":"One","final":true,"encrypted":false},"_ownerEncSessionKey":{"name":"_ownerEncSessionKey","id":1535,"since":47,"type":"Bytes","cardinality":"ZeroOrOne","final":true,"encrypted":false},"_ownerGroup":{"name":"_ownerGroup","id":1534,"since":47,"type":"GeneratedId","cardinality":"ZeroOrOne","final":true,"encrypted":false},"_permissions":{"name":"_permissions","id":1532,"since":47,"type":"GeneratedId","cardinality":"One","final":true,"encrypted":false}},"associations":{"alarmInfo":{"name":"alarmInfo","id":1536,"since":47,"type":"AGGREGATION","cardinality":"One","refType":"AlarmInfo","final":false}},"app":"sys","version":"47"}
export function createUserAlarmInfo():UserAlarmInfo {
return create(_TypeModel, UserAlarmInfoTypeRef)
}

View file

@ -0,0 +1,10 @@
// @flow
import {create, TypeRef} from "../../common/EntityFunctions"
export const UserAlarmInfoListTypeTypeRef:TypeRef<UserAlarmInfoListType> = new TypeRef("sys", "UserAlarmInfoListType")
export const _TypeModel:TypeModel= {"name":"UserAlarmInfoListType","since":47,"type":"AGGREGATED_TYPE","id":1537,"rootId":"A3N5cwAGAQ","versioned":false,"encrypted":false,"values":{"_id":{"name":"_id","id":1538,"since":47,"type":"CustomId","cardinality":"One","final":true,"encrypted":false}},"associations":{"alarms":{"name":"alarms","id":1539,"since":47,"type":"LIST_ASSOCIATION","cardinality":"One","refType":"UserAlarmInfo","final":true,"external":false}},"app":"sys","version":"47"}
export function createUserAlarmInfoListType():UserAlarmInfoListType {
return create(_TypeModel, UserAlarmInfoListTypeTypeRef)
}

View file

@ -1,10 +0,0 @@
// @flow
import {create, TypeRef} from "../../common/EntityFunctions"
export const CalendarAlarmInfoTypeRef:TypeRef<CalendarAlarmInfo> = new TypeRef("tutanota", "CalendarAlarmInfo")
export const _TypeModel:TypeModel= {"name":"CalendarAlarmInfo","since":33,"type":"AGGREGATED_TYPE","id":937,"rootId":"CHR1dGFub3RhAAOp","versioned":false,"encrypted":false,"values":{"_id":{"name":"_id","id":938,"since":33,"type":"CustomId","cardinality":"One","final":true,"encrypted":false},"identifier":{"name":"identifier","id":940,"since":33,"type":"String","cardinality":"One","final":false,"encrypted":true},"trigger":{"name":"trigger","id":939,"since":33,"type":"String","cardinality":"One","final":false,"encrypted":true}},"associations":{},"app":"tutanota","version":"33"}
export function createCalendarAlarmInfo():CalendarAlarmInfo {
return create(_TypeModel, CalendarAlarmInfoTypeRef)
}

View file

@ -7,41 +7,50 @@ export const _TypeModel: TypeModel = {
"name": "CalendarEvent",
"since": 33,
"type": "LIST_ELEMENT_TYPE",
"id": 941,
"rootId": "CHR1dGFub3RhAAOt",
"id": 933,
"rootId": "CHR1dGFub3RhAAOl",
"versioned": false,
"encrypted": true,
"values": {
"_format": {"name": "_format", "id": 945, "since": 33, "type": "Number", "cardinality": "One", "final": false, "encrypted": false},
"_id": {"name": "_id", "id": 943, "since": 33, "type": "CustomId", "cardinality": "One", "final": true, "encrypted": false},
"_format": {"name": "_format", "id": 937, "since": 33, "type": "Number", "cardinality": "One", "final": false, "encrypted": false},
"_id": {"name": "_id", "id": 935, "since": 33, "type": "CustomId", "cardinality": "One", "final": true, "encrypted": false},
"_ownerEncSessionKey": {
"name": "_ownerEncSessionKey",
"id": 947,
"id": 939,
"since": 33,
"type": "Bytes",
"cardinality": "ZeroOrOne",
"final": true,
"encrypted": false
},
"_ownerGroup": {"name": "_ownerGroup", "id": 946, "since": 33, "type": "GeneratedId", "cardinality": "ZeroOrOne", "final": true, "encrypted": false},
"_permissions": {"name": "_permissions", "id": 944, "since": 33, "type": "GeneratedId", "cardinality": "One", "final": true, "encrypted": false},
"description": {"name": "description", "id": 949, "since": 33, "type": "String", "cardinality": "One", "final": false, "encrypted": true},
"endTime": {"name": "endTime", "id": 951, "since": 33, "type": "Date", "cardinality": "One", "final": false, "encrypted": true},
"location": {"name": "location", "id": 952, "since": 33, "type": "String", "cardinality": "One", "final": false, "encrypted": true},
"startTime": {"name": "startTime", "id": 950, "since": 33, "type": "Date", "cardinality": "One", "final": false, "encrypted": true},
"summary": {"name": "summary", "id": 948, "since": 33, "type": "String", "cardinality": "One", "final": false, "encrypted": true}
"_ownerGroup": {"name": "_ownerGroup", "id": 938, "since": 33, "type": "GeneratedId", "cardinality": "ZeroOrOne", "final": true, "encrypted": false},
"_permissions": {"name": "_permissions", "id": 936, "since": 33, "type": "GeneratedId", "cardinality": "One", "final": true, "encrypted": false},
"description": {"name": "description", "id": 941, "since": 33, "type": "String", "cardinality": "One", "final": false, "encrypted": true},
"endTime": {"name": "endTime", "id": 943, "since": 33, "type": "Date", "cardinality": "One", "final": false, "encrypted": true},
"location": {"name": "location", "id": 944, "since": 33, "type": "String", "cardinality": "One", "final": false, "encrypted": true},
"startTime": {"name": "startTime", "id": 942, "since": 33, "type": "Date", "cardinality": "One", "final": false, "encrypted": true},
"summary": {"name": "summary", "id": 940, "since": 33, "type": "String", "cardinality": "One", "final": false, "encrypted": true}
},
"associations": {
"alarmInfo": {
"name": "alarmInfo",
"id": 954,
"repeatRule": {
"name": "repeatRule",
"id": 945,
"since": 33,
"type": "AGGREGATION",
"cardinality": "ZeroOrOne",
"refType": "CalendarAlarmInfo",
"refType": "CalendarRepeatRule",
"final": false
},
"repeatRule": {"name": "repeatRule", "id": 953, "since": 33, "type": "AGGREGATION", "cardinality": "ZeroOrOne", "refType": "RepeatRule", "final": false}
"alarmInfos": {
"name": "alarmInfos",
"id": 946,
"since": 33,
"type": "LIST_ELEMENT_ASSOCIATION",
"cardinality": "Any",
"refType": "UserAlarmInfo",
"final": false,
"external": true
}
},
"app": "tutanota",
"version": "33"

View file

@ -7,34 +7,33 @@ export const _TypeModel: TypeModel = {
"name": "CalendarGroupData",
"since": 33,
"type": "AGGREGATED_TYPE",
"id": 966,
"rootId": "CHR1dGFub3RhAAPG",
"id": 957,
"rootId": "CHR1dGFub3RhAAO9",
"versioned": false,
"encrypted": false,
"values": {
"_id": {"name": "_id", "id": 967, "since": 33, "type": "CustomId", "cardinality": "One", "final": true, "encrypted": false},
"adminEncGroupKey": {"name": "adminEncGroupKey", "id": 969, "since": 33, "type": "Bytes", "cardinality": "One", "final": false, "encrypted": false},
"_id": {"name": "_id", "id": 958, "since": 33, "type": "CustomId", "cardinality": "One", "final": true, "encrypted": false},
"adminEncGroupKey": {"name": "adminEncGroupKey", "id": 960, "since": 33, "type": "Bytes", "cardinality": "One", "final": false, "encrypted": false},
"calendarEncCalendarGroupRootSessionKey": {
"name": "calendarEncCalendarGroupRootSessionKey",
"id": 968,
"id": 959,
"since": 33,
"type": "Bytes",
"cardinality": "One",
"final": false,
"encrypted": false
},
"encColor": {"name": "encColor", "id": 973, "since": 33, "type": "Bytes", "cardinality": "One", "final": false, "encrypted": false},
"encName": {"name": "encName", "id": 972, "since": 33, "type": "Bytes", "cardinality": "One", "final": false, "encrypted": false},
"encName": {"name": "encName", "id": 963, "since": 33, "type": "Bytes", "cardinality": "One", "final": false, "encrypted": false},
"ownerEncGroupInfoSessionKey": {
"name": "ownerEncGroupInfoSessionKey",
"id": 970,
"id": 961,
"since": 33,
"type": "Bytes",
"cardinality": "One",
"final": false,
"encrypted": false
},
"userEncGroupKey": {"name": "userEncGroupKey", "id": 971, "since": 33, "type": "Bytes", "cardinality": "ZeroOrOne", "final": false, "encrypted": false}
"userEncGroupKey": {"name": "userEncGroupKey", "id": 962, "since": 33, "type": "Bytes", "cardinality": "ZeroOrOne", "final": false, "encrypted": false}
},
"associations": {},
"app": "tutanota",

View file

@ -7,31 +7,30 @@ export const _TypeModel: TypeModel = {
"name": "CalendarGroupRoot",
"since": 33,
"type": "ELEMENT_TYPE",
"id": 955,
"rootId": "CHR1dGFub3RhAAO7",
"id": 947,
"rootId": "CHR1dGFub3RhAAOz",
"versioned": false,
"encrypted": true,
"values": {
"_format": {"name": "_format", "id": 959, "since": 33, "type": "Number", "cardinality": "One", "final": false, "encrypted": false},
"_id": {"name": "_id", "id": 957, "since": 33, "type": "GeneratedId", "cardinality": "One", "final": true, "encrypted": false},
"_format": {"name": "_format", "id": 951, "since": 33, "type": "Number", "cardinality": "One", "final": false, "encrypted": false},
"_id": {"name": "_id", "id": 949, "since": 33, "type": "GeneratedId", "cardinality": "One", "final": true, "encrypted": false},
"_ownerEncSessionKey": {
"name": "_ownerEncSessionKey",
"id": 961,
"id": 953,
"since": 33,
"type": "Bytes",
"cardinality": "ZeroOrOne",
"final": true,
"encrypted": false
},
"_ownerGroup": {"name": "_ownerGroup", "id": 960, "since": 33, "type": "GeneratedId", "cardinality": "ZeroOrOne", "final": true, "encrypted": false},
"_permissions": {"name": "_permissions", "id": 958, "since": 33, "type": "GeneratedId", "cardinality": "One", "final": true, "encrypted": false},
"color": {"name": "color", "id": 963, "since": 33, "type": "String", "cardinality": "One", "final": false, "encrypted": true},
"name": {"name": "name", "id": 962, "since": 33, "type": "String", "cardinality": "One", "final": false, "encrypted": true}
"_ownerGroup": {"name": "_ownerGroup", "id": 952, "since": 33, "type": "GeneratedId", "cardinality": "ZeroOrOne", "final": true, "encrypted": false},
"_permissions": {"name": "_permissions", "id": 950, "since": 33, "type": "GeneratedId", "cardinality": "One", "final": true, "encrypted": false},
"name": {"name": "name", "id": 954, "since": 33, "type": "String", "cardinality": "One", "final": false, "encrypted": true}
},
"associations": {
"longEvents": {
"name": "longEvents",
"id": 965,
"id": 956,
"since": 33,
"type": "LIST_ASSOCIATION",
"cardinality": "One",
@ -41,7 +40,7 @@ export const _TypeModel: TypeModel = {
},
"shortEvents": {
"name": "shortEvents",
"id": 964,
"id": 955,
"since": 33,
"type": "LIST_ASSOCIATION",
"cardinality": "One",

View file

@ -0,0 +1,10 @@
// @flow
import {create, TypeRef} from "../../common/EntityFunctions"
export const CalendarRepeatRuleTypeRef:TypeRef<CalendarRepeatRule> = new TypeRef("tutanota", "CalendarRepeatRule")
export const _TypeModel:TypeModel= {"name":"CalendarRepeatRule","since":33,"type":"AGGREGATED_TYPE","id":926,"rootId":"CHR1dGFub3RhAAOe","versioned":false,"encrypted":false,"values":{"_id":{"name":"_id","id":927,"since":33,"type":"CustomId","cardinality":"One","final":true,"encrypted":false},"endType":{"name":"endType","id":929,"since":33,"type":"Number","cardinality":"One","final":false,"encrypted":true},"endValue":{"name":"endValue","id":930,"since":33,"type":"Number","cardinality":"ZeroOrOne","final":false,"encrypted":true},"frequency":{"name":"frequency","id":928,"since":33,"type":"Number","cardinality":"One","final":false,"encrypted":true},"interval":{"name":"interval","id":931,"since":33,"type":"Number","cardinality":"One","final":false,"encrypted":true},"timeZone":{"name":"timeZone","id":932,"since":33,"type":"String","cardinality":"One","final":false,"encrypted":true}},"associations":{},"app":"tutanota","version":"33"}
export function createCalendarRepeatRule():CalendarRepeatRule {
return create(_TypeModel, CalendarRepeatRuleTypeRef)
}

View file

@ -1,25 +0,0 @@
// @flow
import {create, TypeRef} from "../../common/EntityFunctions"
export const EncDateWrapperTypeRef: TypeRef<EncDateWrapper> = new TypeRef("tutanota", "EncDateWrapper")
export const _TypeModel: TypeModel = {
"name": "EncDateWrapper",
"since": 33,
"type": "AGGREGATED_TYPE",
"id": 926,
"rootId": "CHR1dGFub3RhAAOe",
"versioned": false,
"encrypted": false,
"values": {
"_id": {"name": "_id", "id": 927, "since": 33, "type": "CustomId", "cardinality": "One", "final": true, "encrypted": false},
"value": {"name": "value", "id": 928, "since": 33, "type": "Date", "cardinality": "One", "final": false, "encrypted": true}
},
"associations": {},
"app": "tutanota",
"version": "33"
}
export function createEncDateWrapper(): EncDateWrapper {
return create(_TypeModel, EncDateWrapperTypeRef)
}

View file

@ -1,39 +0,0 @@
// @flow
import {create, TypeRef} from "../../common/EntityFunctions"
export const RepeatRuleTypeRef: TypeRef<RepeatRule> = new TypeRef("tutanota", "RepeatRule")
export const _TypeModel: TypeModel = {
"name": "RepeatRule",
"since": 33,
"type": "AGGREGATED_TYPE",
"id": 929,
"rootId": "CHR1dGFub3RhAAOh",
"versioned": false,
"encrypted": false,
"values": {
"_id": {"name": "_id", "id": 930, "since": 33, "type": "CustomId", "cardinality": "One", "final": true, "encrypted": false},
"endType": {"name": "endType", "id": 932, "since": 33, "type": "Number", "cardinality": "One", "final": false, "encrypted": true},
"endValue": {"name": "endValue", "id": 933, "since": 33, "type": "Number", "cardinality": "ZeroOrOne", "final": false, "encrypted": true},
"frequency": {"name": "frequency", "id": 931, "since": 33, "type": "Number", "cardinality": "One", "final": false, "encrypted": true},
"interval": {"name": "interval", "id": 934, "since": 33, "type": "Number", "cardinality": "One", "final": false, "encrypted": true},
"timeZone": {"name": "timeZone", "id": 935, "since": 33, "type": "String", "cardinality": "One", "final": false, "encrypted": true}
},
"associations": {
"exceptionDates": {
"name": "exceptionDates",
"id": 936,
"since": 33,
"type": "AGGREGATION",
"cardinality": "Any",
"refType": "EncDateWrapper",
"final": false
}
},
"app": "tutanota",
"version": "33"
}
export function createRepeatRule(): RepeatRule {
return create(_TypeModel, RepeatRuleTypeRef)
}

View file

@ -142,7 +142,7 @@ export const _TypeModel: TypeModel = {
"associations": {
"calendarGroupData": {
"name": "calendarGroupData",
"id": 974,
"id": 964,
"since": 33,
"type": "AGGREGATION",
"cardinality": "ZeroOrOne",

View file

@ -407,6 +407,10 @@ export class WorkerClient {
return this._postRequest(new Request('cancelCreateSession', []))
}
resolveSessionKey(typeModel: TypeModel, instance: Object): Promise<?string> {
return this._postRequest(new Request('resolveSessionKey', arguments))
}
entityRequest<T>(typeRef: TypeRef<T>, method: HttpMethodEnum, listId: ?Id, id: ?Id, entity: ?T, queryParameter: ?Params): Promise<any> {
return this._postRequest(new Request('entityRequest', Array.from(arguments)))
}
@ -480,6 +484,10 @@ export class WorkerClient {
resetSession() {
return this._queue.postMessage(new Request("resetSession", []))
}
createCalendarEvent(groupRoot: CalendarGroupRoot, event: CalendarEvent, alarmInfo: ?AlarmInfo, oldEvent: ?CalendarEvent, oldUserAlarmInfo: ?UserAlarmInfo) {
return this._queue.postMessage(new Request("createCalendarEvent", [groupRoot, event, alarmInfo, oldEvent, oldUserAlarmInfo]))
}
}
export const worker = new WorkerClient()

View file

@ -17,6 +17,7 @@ import {keyToBase64} from "./crypto/CryptoUtils"
import {aes256RandomKey} from "./crypto/Aes"
import type {BrowserData} from "../../misc/ClientConstants"
import type {InfoMessage} from "../common/CommonTypes"
import {resolveSessionKey} from "./crypto/CryptoFacade"
assertWorkerOrNode()
@ -274,7 +275,14 @@ export class WorkerImpl {
resetSecondFactors: (message: Request) => {
return locator.login.resetSecondFactors.apply(locator.login, message.args)
},
resetSession: () => locator.login.reset()
resetSession: () => locator.login.reset(),
createCalendarEvent: (message: Request) => {
return locator.calendar.createCalendarEvent.apply(locator.calendar, message.args)
},
resolveSessionKey: (message: Request) => {
return resolveSessionKey.apply(null, message.args).then(sk => sk ? keyToBase64(sk) : null)
}
})
Promise.onPossiblyUnhandledRejection(e => this.sendError(e));

View file

@ -17,6 +17,7 @@ import {EventBusClient} from "./EventBusClient"
import {assertWorkerOrNode, isAdminClient} from "../Env"
import {CloseEventBusOption, Const} from "../common/TutanotaConstants"
import type {BrowserData} from "../../misc/ClientConstants"
import {CalendarFacade} from "./facades/CalendarFacade"
assertWorkerOrNode()
type WorkerLocatorType = {
@ -29,6 +30,7 @@ type WorkerLocatorType = {
customer: CustomerFacade;
file: FileFacade;
mail: MailFacade;
calendar: CalendarFacade;
mailAddress: MailAddressFacade;
counters: CounterFacade;
eventBusClient: EventBusClient;
@ -60,6 +62,7 @@ export function initLocator(worker: WorkerImpl, browserData: BrowserData) {
locator.customer = new CustomerFacade(worker, locator.login, locator.groupManagement, locator.userManagement, locator.counters)
locator.file = new FileFacade(locator.login)
locator.mail = new MailFacade(locator.login, locator.file)
locator.calendar = new CalendarFacade(locator.login)
locator.mailAddress = new MailAddressFacade(locator.login)
locator.eventBusClient = new EventBusClient(worker, locator.indexer, locator.cache, locator.mail, locator.login)
locator.login.init(locator.indexer, locator.eventBusClient)

View file

@ -0,0 +1,121 @@
//@flow
import {erase, setup} from "../EntityWorker"
import {assertWorkerOrNode} from "../../Env"
import {createUserAlarmInfo} from "../../entities/sys/UserAlarmInfo"
import type {LoginFacade} from "./LoginFacade"
import {neverNull} from "../../common/utils/Utils"
import {findAllAndRemove, removeAll} from "../../common/utils/ArrayUtils"
import {elementIdPart, HttpMethod, isSameId, listIdPart} from "../../common/EntityFunctions"
import {generateEventElementId, getEventStart, isLongEvent} from "../../common/utils/CommonCalendarUtils"
import {loadAll, serviceRequestVoid} from "../../worker/EntityWorker"
import {PushIdentifierTypeRef} from "../../entities/sys/PushIdentifier"
import {encryptKey, resolveSessionKey} from "../crypto/CryptoFacade"
import {createAlarmServicePost} from "../../entities/sys/AlarmServicePost"
import {SysService} from "../../entities/sys/Services"
import {aes128RandomKey} from "../crypto/Aes"
import {createAlarmNotification} from "../../entities/sys/AlarmNotification"
import {OperationType} from "../../common/TutanotaConstants"
import {createNotificationSessionKey} from "../../entities/sys/NotificationSessionKey"
import {_TypeModel as PushIdentifierTypeModel} from "../../entities/sys/PushIdentifier"
import {bitArrayToUint8Array} from "../crypto/CryptoUtils"
assertWorkerOrNode()
export class CalendarFacade {
_loginFacade: LoginFacade;
constructor(loginFacade: LoginFacade) {
this._loginFacade = loginFacade
}
createCalendarEvent(groupRoot: CalendarGroupRoot, event: CalendarEvent, alarmInfo: ?AlarmInfo, oldEvent: ?CalendarEvent, oldUserAlarmInfo: ?UserAlarmInfo): Promise<void> {
const user = this._loginFacade.getLoggedInUser()
const userAlarmInfoListId = neverNull(user.alarmInfoList).alarms
let p = Promise.resolve()
const alarmNotifications: Array<AlarmNotification> = []
// delete old calendar event
if (oldEvent) {
p = erase(oldEvent).then(() => {
// delete old alarm
if (oldUserAlarmInfo) {
const alarmNotification = Object.assign(createAlarmNotification(), {
alarmInfo: oldUserAlarmInfo.alarmInfo,
repeatRule: null,
deviceSessionKeys: [],
operation: OperationType.DELETE
})
alarmNotifications.push(alarmNotification)
return erase(oldUserAlarmInfo)
}
})
}
return p
.then(() => {
if (alarmInfo) {
const newAlarm = createUserAlarmInfo()
newAlarm._ownerGroup = user.userGroup.group
newAlarm.alarmInfo = alarmInfo
const alarmNotification = Object.assign(createAlarmNotification(), {
alarmInfo,
repeatRule: event.repeatRule,
deviceSessionKeys: [],
operation: OperationType.CREATE,
summary: event.summary,
eventStart: event.startTime
})
alarmNotifications.push(alarmNotification)
return setup(userAlarmInfoListId, newAlarm)
}
})
.then(newUserAlarmElementId => {
findAllAndRemove(event.alarmInfos, (userAlarmInfoId) => isSameId(userAlarmInfoListId, listIdPart(userAlarmInfoId)))
if (newUserAlarmElementId) {
event.alarmInfos.push([userAlarmInfoListId, newUserAlarmElementId])
}
const listId = event.repeatRule || isLongEvent(event) ? groupRoot.longEvents : groupRoot.shortEvents
event._id = [listId, generateEventElementId(event.startTime.getTime())]
return setup(listId, event)
})
.then(() => this._sendAlarmNotifications(alarmNotifications))
}
_sendAlarmNotifications(alarmNotifications: Array<AlarmNotification>): Promise<void> {
return loadAll(PushIdentifierTypeRef, neverNull(this._loginFacade.getLoggedInUser().pushIdentifierList).list)
.then((pushIdentifierList) => {
const notificationSessionKey = aes128RandomKey()
return Promise
.map(pushIdentifierList, identifier => {
return resolveSessionKey(PushIdentifierTypeModel, identifier).then(pushIdentifierSk => {
if (pushIdentifierSk) {
const pushIdentifierSessionEncSessionKey = encryptKey(pushIdentifierSk, notificationSessionKey)
return {identifierId: identifier._id, pushIdentifierSessionEncSessionKey}
} else {
return null
}
})
})
.then(maybeEncSessionKeys => {
const encSessionKeys = maybeEncSessionKeys.filter(Boolean)
for (let notification of alarmNotifications) {
notification.deviceSessionKeys = encSessionKeys.map(esk => {
return Object.assign(createNotificationSessionKey(), {
pushIdentifier: esk.identifierId,
pushIdentifierSessionEncSessionKey: esk.pushIdentifierSessionEncSessionKey
})
})
}
const requestEntity = createAlarmServicePost()
requestEntity.alarmNotifications = alarmNotifications
return serviceRequestVoid(SysService.AlarmService, HttpMethod.POST, requestEntity, null, notificationSessionKey)
})
})
}
}

View file

@ -6,12 +6,13 @@ import {theme} from "../gui/theme"
import {px, size} from "../gui/size"
import {formatDateWithWeekday, formatTime} from "../misc/Formatter"
import {getFromMap} from "../api/common/utils/MapUtils"
import {eventEndsAfterDay, eventStartsBefore, expandEvent, getEventText, isAllDayEvent, layOutEvents} from "./CalendarUtils"
import {DAY_IN_MILLIS} from "../api/common/utils/DateUtils"
import {defaultCalendarColor} from "../api/common/TutanotaConstants"
import {CalendarEventBubble} from "./CalendarEventBubble"
import {styles} from "../gui/styles"
import {ContinuingCalendarEventBubble} from "./ContinuingCalendarEventBubble"
import {isAllDayEvent} from "../api/common/utils/CommonCalendarUtils"
import {eventEndsAfterDay, eventStartsBefore, expandEvent, getEventText, layOutEvents} from "./CalendarUtils"
export type CalendarDayViewAttrs = {
selectedDate: Stream<Date>,

View file

@ -12,31 +12,22 @@ import type {DropDownSelectorAttrs} from "../gui/base/DropDownSelectorN"
import {DropDownSelectorN} from "../gui/base/DropDownSelectorN"
import {Icons} from "../gui/base/icons/Icons"
import {createCalendarEvent} from "../api/entities/tutanota/CalendarEvent"
import {erase, serviceRequestVoid, setup} from "../api/main/Entity"
import {
createRepeatRuleWithValues,
generateEventElementId,
getAllDayDateUTC,
getEventEnd,
getEventStart,
isAllDayEvent,
isLongEvent,
parseTimeTo,
timeString
} from "./CalendarUtils"
import {erase, load} from "../api/main/Entity"
import {downcast, neverNull} from "../api/common/utils/Utils"
import {ButtonN, ButtonType} from "../gui/base/ButtonN"
import type {EndTypeEnum, OperationTypeEnum, RepeatPeriodEnum} from "../api/common/TutanotaConstants"
import {EndType, OperationType, RepeatPeriod} from "../api/common/TutanotaConstants"
import type {EndTypeEnum, RepeatPeriodEnum} from "../api/common/TutanotaConstants"
import {EndType, RepeatPeriod} from "../api/common/TutanotaConstants"
import {numberRange} from "../api/common/utils/ArrayUtils"
import {incrementByRepeatPeriod} from "./CalendarModel"
import {DateTime} from "luxon"
import {TutanotaService} from "../api/entities/tutanota/Services"
import {SysService} from "../api/entities/sys/Services"
import {createAlarmServicePost} from "../api/entities/sys/AlarmServicePost"
import {createAlarmInfo} from "../api/entities/sys/AlarmInfo"
import {HttpMethod} from "../api/common/EntityFunctions"
import {createCalendarAlarmInfo} from "../api/entities/tutanota/CalendarAlarmInfo"
import {elementIdPart, isSameId, listIdPart} from "../api/common/EntityFunctions"
import {logins} from "../api/main/LoginController"
import {UserAlarmInfoTypeRef} from "../api/entities/sys/UserAlarmInfo"
import {createRepeatRuleWithValues, getAllDayDateUTC, parseTimeTo, timeString} from "./CalendarUtils"
import {generateEventElementId, getEventEnd, getEventStart, isAllDayEvent} from "../api/common/utils/CommonCalendarUtils"
import {worker} from "../api/main/WorkerClient"
// allDay event consists of full UTC days. It always starts at 00:00:00.00 of its start day in UTC and ends at
// 0 of the next day in UTC. Full day event time is relative to the local timezone. So startTime and endTime of
@ -45,7 +36,7 @@ import {createCalendarAlarmInfo} from "../api/entities/tutanota/CalendarAlarmInf
// {startTime: new Date(Date.UTC(2019, 04, 2, 0, 0, 0, 0)), {endTime: new Date(Date.UTC(2019, 04, 3, 0, 0, 0, 0))}}
// We check the condition with time == 0 and take a UTC date (which is [2-3) so full day on the 2nd of May). We
// interpret it as full day in Europe/Berlin, not in the UTC.
export function showCalendarEventDialog(date: Date, calendars: Map<Id, CalendarInfo>, event?: CalendarEvent) {
export function showCalendarEventDialog(date: Date, calendars: Map<Id, CalendarInfo>, existingEvent?: CalendarEvent) {
const summary = stream("")
const calendarArray = Array.from(calendars.values())
const selectedCalendar = stream(calendarArray[0])
@ -65,22 +56,25 @@ export function showCalendarEventDialog(date: Date, calendars: Map<Id, CalendarI
const endCountPickerAttrs = createEndCountPicker()
const alarmPickerAttrs = createAlarmrPicker()
if (event) {
summary(event.summary)
const calendarForGroup = calendars.get(neverNull(event._ownerGroup))
let loadedUserAlarmInfo: ?UserAlarmInfo = null
const user = logins.getUserController().user
if (existingEvent) {
summary(existingEvent.summary)
const calendarForGroup = calendars.get(neverNull(existingEvent._ownerGroup))
if (calendarForGroup) {
selectedCalendar(calendarForGroup)
}
startTime(timeString(getEventStart(event)))
allDay(event && isAllDayEvent(event))
startTime(timeString(getEventStart(existingEvent)))
allDay(existingEvent && isAllDayEvent(existingEvent))
if (allDay()) {
endDatePicker.setDate(incrementDate(getEventEnd(event), -1))
endDatePicker.setDate(incrementDate(getEventEnd(existingEvent), -1))
} else {
endDatePicker.setDate(getStartOfDay(getEventEnd(event)))
endDatePicker.setDate(getStartOfDay(getEventEnd(existingEvent)))
}
endTime(timeString(getEventEnd(event)))
if (event.repeatRule) {
const existingRule = event.repeatRule
endTime(timeString(getEventEnd(existingEvent)))
if (existingEvent.repeatRule) {
const existingRule = existingEvent.repeatRule
repeatPickerAttrs.selectedValue(downcast(existingRule.frequency))
repeatIntervalPickerAttrs.selectedValue(Number(existingRule.interval))
endTypePickerAttrs.selectedValue(downcast(existingRule.endType))
@ -89,12 +83,20 @@ export function showCalendarEventDialog(date: Date, calendars: Map<Id, CalendarI
} else {
repeatPickerAttrs.selectedValue(null)
}
locationValue(event.location)
notesValue(event.description)
locationValue(existingEvent.location)
notesValue(existingEvent.description)
if (event.alarmInfo) {
alarmPickerAttrs.selectedValue(downcast(event.alarmInfo.trigger))
for (let alarmInfoId of existingEvent.alarmInfos) {
if (isSameId(listIdPart(alarmInfoId), neverNull(user.alarmInfoList).alarms)) {
load(UserAlarmInfoTypeRef, alarmInfoId).then((userAlarmInfo) => {
loadedUserAlarmInfo = userAlarmInfo
alarmPickerAttrs.selectedValue(downcast(userAlarmInfo.alarmInfo.trigger))
m.redraw()
})
break
}
}
} else {
const endTimeDate = new Date(date)
endTimeDate.setHours(endTimeDate.getHours() + 1)
@ -179,17 +181,17 @@ export function showCalendarEventDialog(date: Date, calendars: Map<Id, CalendarI
value: notesValue,
type: Type.Area
}),
event ? m(".mr-negative-s.float-right.flex-end-on-child", m(ButtonN, {
existingEvent ? m(".mr-negative-s.float-right.flex-end-on-child", m(ButtonN, {
label: "delete_action",
type: ButtonType.Primary,
click: () => {
erase(event)
erase(existingEvent)
dialog.close()
}
})) : null,
],
okAction: () => {
const calendarEvent = createCalendarEvent()
const newEvent = createCalendarEvent()
let startDate = neverNull(startDatePicker.date())
const parsedStartTime = parseTimeTo(startTime())
const parsedEndTime = parseTimeTo(endTime())
@ -212,20 +214,20 @@ export function showCalendarEventDialog(date: Date, calendars: Map<Id, CalendarI
endDate.setMinutes(parsedEndTime.minutes)
}
calendarEvent.startTime = startDate
calendarEvent.description = notesValue()
calendarEvent.summary = summary()
calendarEvent.location = locationValue()
calendarEvent.endTime = endDate
newEvent.startTime = startDate
newEvent.description = notesValue()
newEvent.summary = summary()
newEvent.location = locationValue()
newEvent.endTime = endDate
const groupRoot = selectedCalendar().groupRoot
calendarEvent._ownerGroup = selectedCalendar().groupRoot._id
newEvent._ownerGroup = selectedCalendar().groupRoot._id
const repeatFrequency = repeatPickerAttrs.selectedValue()
if (repeatFrequency == null) {
calendarEvent.repeatRule = null
newEvent.repeatRule = null
} else {
const interval = repeatIntervalPickerAttrs.selectedValue() || 1
const repeatRule = createRepeatRuleWithValues(repeatFrequency, interval)
calendarEvent.repeatRule = repeatRule
newEvent.repeatRule = repeatRule
const stopType = neverNull(endTypePickerAttrs.selectedValue())
repeatRule.endType = stopType
@ -238,7 +240,7 @@ export function showCalendarEventDialog(date: Date, calendars: Map<Id, CalendarI
}
} else if (stopType === EndType.UntilDate) {
const repeatEndDate = getStartOfNextDay(neverNull(repeatEndDatePicker.date()))
if (repeatEndDate.getTime() < getEventStart(calendarEvent)) {
if (repeatEndDate.getTime() < getEventStart(newEvent)) {
Dialog.error("startAfterEnd_label")
return
} else {
@ -250,54 +252,23 @@ export function showCalendarEventDialog(date: Date, calendars: Map<Id, CalendarI
}
}
}
const alarmInterval = alarmPickerAttrs.selectedValue()
const alarmValue = alarmPickerAttrs.selectedValue()
const newAlarm = alarmValue
&& createCalendarAlarm(generateEventElementId(Date.now()), alarmValue)
worker.createCalendarEvent(groupRoot, newEvent, newAlarm, existingEvent, loadedUserAlarmInfo)
const alarmInfos = []
if (alarmInterval) {
calendarEvent.alarmInfo = createCalendarAlarm(generateEventElementId(Date.now()), alarmInterval)
alarmInfos.push(toAlarm(calendarEvent.alarmInfo, calendarEvent, OperationType.CREATE))
}
let p = Promise.resolve()
if (event) {
p = erase(event)
if (event.alarmInfo) {
alarmInfos.push(toAlarm(event.alarmInfo, event, OperationType.DELETE))
}
}
const listId = calendarEvent.repeatRule || isLongEvent(calendarEvent) ? groupRoot.longEvents : groupRoot.shortEvents
calendarEvent._id = [listId, generateEventElementId(calendarEvent.startTime.getTime())]
p.then(() => setup(listId, calendarEvent))
.then(() => {
if (alarmInfos.length > 0) {
const requestEntity = createAlarmServicePost()
requestEntity.alarmInfos = alarmInfos
requestEntity.group = selectedCalendar().groupRoot._id
serviceRequestVoid(SysService.AlarmService, HttpMethod.POST, requestEntity)
}
})
dialog.close()
}
})
}
function createCalendarAlarm(identifier: string, trigger: string): CalendarAlarmInfo {
const calendarAlarmInfo = createCalendarAlarmInfo()
function createCalendarAlarm(identifier: string, trigger: string): AlarmInfo {
const calendarAlarmInfo = createAlarmInfo()
calendarAlarmInfo.identifier = identifier
calendarAlarmInfo.trigger = trigger
return calendarAlarmInfo
}
function toAlarm(calendarAlarmInfo: CalendarAlarmInfo, calendarEvent: CalendarEvent, operation: OperationTypeEnum): AlarmInfo {
const alarmInfo = createAlarmInfo()
alarmInfo.identifier = calendarAlarmInfo.identifier
alarmInfo.operation = operation
alarmInfo.trigger = decrementByAlarmInterval(getEventStart(calendarEvent), downcast(calendarAlarmInfo.trigger))
return alarmInfo
}
const repeatValues = [
{name: "Do not repeat", value: null},
{name: "Repeat daily", value: RepeatPeriod.DAILY},
@ -417,4 +388,4 @@ function decrementByAlarmInterval(date: Date, interval: AlarmIntervalEnum): Date
diff = {}
}
return DateTime.fromJSDate(date).minus(diff).toJSDate()
}
}

View file

@ -1,12 +1,13 @@
//@flow
import type {CalendarMonthTimeRange} from "./CalendarUtils"
import {getAllDayDateLocal, getAllDayDateUTC, getEventEnd, getEventStart, isAllDayEvent, isLongEvent} from "./CalendarUtils"
import {getStartOfDay, incrementDate} from "../api/common/utils/DateUtils"
import {getFromMap} from "../api/common/utils/MapUtils"
import {clone, downcast} from "../api/common/utils/Utils"
import type {RepeatPeriodEnum} from "../api/common/TutanotaConstants"
import {EndType, RepeatPeriod} from "../api/common/TutanotaConstants"
import {DateTime} from "luxon"
import {getAllDayDateLocal, getEventEnd, getEventStart, isAllDayEvent, isLongEvent} from "../api/common/utils/CommonCalendarUtils"
import {getAllDayDateUTC} from "./CalendarUtils"
export function addDaysForEvent(events: Map<number, Array<CalendarEvent>>, event: CalendarEvent, month: CalendarMonthTimeRange) {
const calculationDate = getStartOfDay(getEventStart(event))

View file

@ -6,13 +6,14 @@ import {px, size} from "../gui/size"
import {defaultCalendarColor} from "../api/common/TutanotaConstants"
import {CalendarEventBubble} from "./CalendarEventBubble"
import type {CalendarDay} from "./CalendarUtils"
import {eventEndsAfterDay, eventStartsBefore, getCalendarMonth, getEventEnd, getEventStart, isAllDayEvent, layOutEvents} from "./CalendarUtils"
import {getDayShifted, getStartOfDay} from "../api/common/utils/DateUtils"
import {lastThrow} from "../api/common/utils/ArrayUtils"
import {theme} from "../gui/theme"
import {ContinuingCalendarEventBubble} from "./ContinuingCalendarEventBubble"
import {styles} from "../gui/styles"
import {formatMonthWithYear} from "../misc/Formatter"
import {eventEndsAfterDay, eventStartsBefore, getCalendarMonth, layOutEvents} from "./CalendarUtils"
import {getEventEnd, getEventStart, isAllDayEvent} from "../api/common/utils/CommonCalendarUtils"
type CalendarMonthAttrs = {
selectedDate: Stream<Date>,

View file

@ -2,13 +2,13 @@
import {stringToCustomId} from "../api/common/EntityFunctions"
import {DAY_IN_MILLIS, getStartOfDay, getStartOfNextDay, incrementDate} from "../api/common/utils/DateUtils"
import {pad} from "../api/common/utils/StringUtils"
import {createRepeatRule} from "../api/entities/tutanota/RepeatRule"
import type {RepeatPeriodEnum} from "../api/common/TutanotaConstants"
import {DateTime} from "luxon"
import {formatWeekdayShort} from "../misc/Formatter"
import {clone} from "../api/common/utils/Utils"
import {createCalendarRepeatRule} from "../api/entities/tutanota/CalendarRepeatRule"
import {getAllDayDateLocal, getEventEnd, getEventStart, isAllDayEvent} from "../api/common/utils/CommonCalendarUtils"
const DAYS_SHIFTED_MS = 15 * DAY_IN_MILLIS
export type CalendarMonthTimeRange = {
start: Date,
@ -16,23 +16,6 @@ export type CalendarMonthTimeRange = {
}
export function generateEventElementId(timestamp: number): string {
const randomDay = Math.floor((Math.random() * DAYS_SHIFTED_MS)) * 2
return createEventElementId(timestamp, randomDay - DAYS_SHIFTED_MS)
}
function createEventElementId(timestamp: number, shiftDays: number): string {
return stringToCustomId(String(timestamp + shiftDays))
}
export function geEventElementMaxId(timestamp: number): string {
return createEventElementId(timestamp, DAYS_SHIFTED_MS)
}
export function getEventElementMinId(timestamp: number): string {
return createEventElementId(timestamp, -DAYS_SHIFTED_MS)
}
export function eventStartsBefore(currentDate: Date, event: CalendarEvent): boolean {
return getEventStart(event).getTime() < currentDate.getTime()
}
@ -59,40 +42,11 @@ export function timeString(date: Date): string {
return hours + ":" + minutes
}
export function getEventEnd(event: CalendarEvent): Date {
if (isAllDayEvent(event)) {
return getAllDayDateLocal(event.endTime)
} else {
return event.endTime
}
}
export function getEventStart(event: CalendarEvent): Date {
if (isAllDayEvent(event)) {
return getAllDayDateLocal(event.startTime)
} else {
return event.startTime
}
}
export function getAllDayDateUTC(localDate: Date): Date {
return new Date(Date.UTC(localDate.getFullYear(), localDate.getMonth(), localDate.getDate(), 0, 0, 0, 0))
}
export function getAllDayDateLocal(utcDate: Date, timezone?: string): Date {
return new Date(utcDate.getUTCFullYear(), utcDate.getUTCMonth(), utcDate.getUTCDate(), 0, 0, 0, 0)
}
export function isAllDayEvent(event: CalendarEvent): boolean {
const {startTime, endTime} = event
return startTime.getUTCHours() === 0 && startTime.getUTCMinutes() === 0 && startTime.getUTCSeconds() === 0
&& endTime.getUTCHours() === 0 && endTime.getUTCMinutes() === 0 && endTime.getUTCSeconds() === 0
}
export function isLongEvent(event: CalendarEvent): boolean {
return getEventEnd(event).getTime() - getEventStart(event).getTime() > DAYS_SHIFTED_MS
}
export function getMonth(date: Date): CalendarMonthTimeRange {
const start = new Date(date)
@ -104,8 +58,8 @@ export function getMonth(date: Date): CalendarMonthTimeRange {
}
export function createRepeatRuleWithValues(frequency: RepeatPeriodEnum, interval: number): RepeatRule {
const rule = createRepeatRule()
export function createRepeatRuleWithValues(frequency: RepeatPeriodEnum, interval: number): CalendarRepeatRule {
const rule = createCalendarRepeatRule()
rule.timeZone = DateTime.local().zoneName
rule.frequency = frequency
rule.interval = String(interval)

View file

@ -20,7 +20,6 @@ import {defaultCalendarColor, OperationType} from "../api/common/TutanotaConstan
import {locator} from "../api/main/MainLocator"
import {neverNull} from "../api/common/utils/Utils"
import type {CalendarMonthTimeRange} from "./CalendarUtils"
import {geEventElementMaxId, getEventElementMinId, getEventStart, getMonth} from "./CalendarUtils"
import {showCalendarEventDialog} from "./CalendarEventDialog"
import {worker} from "../api/main/WorkerClient"
import {ButtonColors, ButtonN, ButtonType} from "../gui/base/ButtonN"
@ -30,6 +29,8 @@ import {formatDateWithWeekday, formatMonthWithYear} from "../misc/Formatter"
import {NavButtonN} from "../gui/base/NavButtonN"
import {CalendarMonthView} from "./CalendarMonthView"
import {CalendarDayView} from "./CalendarDayView"
import {geEventElementMaxId, getEventElementMinId, getEventStart} from "../api/common/utils/CommonCalendarUtils"
import {getMonth} from "./CalendarUtils"
export type CalendarInfo = {
groupRoot: CalendarGroupRoot,

View file

@ -1,6 +1,6 @@
//@flow
import {loadAll, setup, update} from "../api/main/Entity"
import {createPushIdentifier, PushIdentifierTypeRef} from "../api/entities/sys/PushIdentifier"
import {loadAll, load, setup, update} from "../api/main/Entity"
import {_TypeModel as PushIdentifierModel, createPushIdentifier, PushIdentifierTypeRef} from "../api/entities/sys/PushIdentifier"
import {neverNull} from "../api/common/utils/Utils"
import type {PushServiceTypeEnum} from "../api/common/TutanotaConstants"
import {PushServiceType} from "../api/common/TutanotaConstants"
@ -11,6 +11,7 @@ import {Request} from "../api/common/WorkerProtocol"
import {logins} from "../api/main/LoginController"
import {worker} from "../api/main/WorkerClient"
import {client} from "../misc/ClientDetector.js"
import {elementIdPart, getElementId, listIdPart} from "../api/common/EntityFunctions"
class PushServiceApp {
_pushNotification: ?Object;
@ -30,7 +31,7 @@ class PushServiceApp {
return this._loadPushIdentifier(identifier).then(pushIdentifier => {
if (!pushIdentifier) { // push identifier is not associated with current user
return this._createPushIdentiferInstance(identifier, PushServiceType.SSE)
.then(pushIdentifier => this._storePushIdentifierLocally(pushIdentifier.identifier))
.then(pushIdentifier => this._storePushIdentifierLocally(pushIdentifier))
} else {
return Promise.resolve()
}
@ -40,7 +41,7 @@ class PushServiceApp {
.then(identifier => this._createPushIdentiferInstance(identifier, PushServiceType.SSE))
.then(pushIdentifier => {
this._currentIdentifier = pushIdentifier.identifier
return this._storePushIdentifierLocally(pushIdentifier.identifier)
return this._storePushIdentifierLocally(pushIdentifier)
})
}
}).then(this._initPushNotifications)
@ -54,10 +55,10 @@ class PushServiceApp {
pushIdentifier.language = lang.code
update(pushIdentifier)
}
return this._storePushIdentifierLocally(pushIdentifier.identifier)
return this._storePushIdentifierLocally(pushIdentifier)
} else {
return this._createPushIdentiferInstance(identifier, PushServiceType.IOS)
.then(pushIdentifier => this._storePushIdentifierLocally(pushIdentifier.identifier))
.then(pushIdentifier => this._storePushIdentifierLocally(pushIdentifier))
}
})
} else {
@ -70,9 +71,14 @@ class PushServiceApp {
}
_storePushIdentifierLocally(identifier: string): Promise<void> {
_storePushIdentifierLocally(pushIdentifier: PushIdentifier): Promise<void> {
const userId = logins.getUserController().user._id
return nativeApp.invokeNative(new Request("storePushIdentifierLocally", [identifier, userId, getHttpOrigin()]))
return worker.resolveSessionKey(PushIdentifierModel, pushIdentifier).then(skB64 => {
return nativeApp.invokeNative(new Request("storePushIdentifierLocally", [
pushIdentifier.identifier, userId, getHttpOrigin(), getElementId(pushIdentifier), skB64
]))
})
}
@ -94,9 +100,8 @@ class PushServiceApp {
pushIdentifier.identifier = identifier
pushIdentifier.language = lang.code
return setup(neverNull(list).list, pushIdentifier).then(id => {
pushIdentifier._id = [neverNull(list).list, id]
return pushIdentifier
})
return [neverNull(list).list, id]
}).then(id => load(PushIdentifierTypeRef, id))
}