mirror of
https://github.com/tutao/tutanota.git
synced 2025-12-08 06:09:50 +00:00
Change alarm implementation, start working on alarms on Android
This commit is contained in:
parent
aa59b17bf7
commit
389810b609
50 changed files with 1845 additions and 847 deletions
|
|
@ -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 {
|
|||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
21
app-android/app/src/main/java/de/tutao/tutanota/IdTuple.java
Normal file
21
app-android/app/src/main/java/de/tutao/tutanota/IdTuple.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package de.tutao.tutanota;
|
||||
|
||||
public enum OperationType {
|
||||
CREATE, UPDATE, DELETE
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -126,6 +126,8 @@ type WorkerRequestType = 'setup'
|
|||
| 'extendMailIndex'
|
||||
| 'resetSession'
|
||||
| 'downloadFileContentNative'
|
||||
| 'createCalendarEvent'
|
||||
| 'resolveSessionKey'
|
||||
type MainRequestType = 'execNative'
|
||||
| 'entityEvent'
|
||||
| 'error'
|
||||
|
|
|
|||
54
src/api/common/utils/CommonCalendarUtils.js
Normal file
54
src/api/common/utils/CommonCalendarUtils.js
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
47
src/api/entities/sys/AlarmNotification.js
Normal file
47
src/api/entities/sys/AlarmNotification.js
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
44
src/api/entities/sys/NotificationSessionKey.js
Normal file
44
src/api/entities/sys/NotificationSessionKey.js
Normal 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)
|
||||
}
|
||||
10
src/api/entities/sys/RepeatRule.js
Normal file
10
src/api/entities/sys/RepeatRule.js
Normal 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)
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
10
src/api/entities/sys/UserAlarmInfo.js
Normal file
10
src/api/entities/sys/UserAlarmInfo.js
Normal 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)
|
||||
}
|
||||
10
src/api/entities/sys/UserAlarmInfoListType.js
Normal file
10
src/api/entities/sys/UserAlarmInfoListType.js
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
10
src/api/entities/tutanota/CalendarRepeatRule.js
Normal file
10
src/api/entities/tutanota/CalendarRepeatRule.js
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -142,7 +142,7 @@ export const _TypeModel: TypeModel = {
|
|||
"associations": {
|
||||
"calendarGroupData": {
|
||||
"name": "calendarGroupData",
|
||||
"id": 974,
|
||||
"id": 964,
|
||||
"since": 33,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "ZeroOrOne",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
121
src/api/worker/facades/CalendarFacade.js
Normal file
121
src/api/worker/facades/CalendarFacade.js
Normal 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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue