diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index f765d5dfb58082ae7dc92c10e8b412d77eb92ee3..9ad0b4b364d3197a1399dc7334a9a468d5e6014a 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -70,10 +70,6 @@ - - - - @@ -82,7 +78,6 @@ - preKeysMarkedForRemoval = new HashSet<>(); - - private final LruCache trustCache = - new LruCache(NUM_TRUSTS_TO_CACHE) { - @Override - protected FingerprintStatus create(String fingerprint) { - return mXmppConnectionService.databaseBackend.getFingerprintStatus(account, fingerprint); - } - }; - - private static IdentityKeyPair generateIdentityKeyPair() { - Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl IdentityKeyPair..."); - ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); - return new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), - identityKeyPairKeys.getPrivateKey()); - } - - private static int generateRegistrationId() { - Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl registration ID..."); - return KeyHelper.generateRegistrationId(true); - } - - public SQLiteAxolotlStore(Account account, XmppConnectionService service) { - this.account = account; - this.mXmppConnectionService = service; - this.localRegistrationId = loadRegistrationId(); - this.currentPreKeyId = loadCurrentPreKeyId(); - } - - public int getCurrentPreKeyId() { - return currentPreKeyId; - } - - // -------------------------------------- - // IdentityKeyStore - // -------------------------------------- - - private IdentityKeyPair loadIdentityKeyPair() { - synchronized (mXmppConnectionService) { - IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account); - - if (ownKey != null) { - return ownKey; - } else { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve own IdentityKeyPair"); - ownKey = generateIdentityKeyPair(); - mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownKey); - } - return ownKey; - } - } - - private int loadRegistrationId() { - return loadRegistrationId(false); - } - - private int loadRegistrationId(boolean regenerate) { - String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); - int reg_id; - if (!regenerate && regIdString != null) { - reg_id = Integer.valueOf(regIdString); - } else { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl registration id for account " + account.getJid()); - reg_id = generateRegistrationId(); - boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id)); - if (success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new key to the database!"); - } - } - return reg_id; - } - - private int loadCurrentPreKeyId() { - String prekeyIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); - int prekey_id; - if (prekeyIdString != null) { - prekey_id = Integer.valueOf(prekeyIdString); - } else { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve current prekey id for account " + account.getJid()); - prekey_id = 0; - } - return prekey_id; - } - - public void regenerate() { - mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); - trustCache.evictAll(); - account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0)); - identityKeyPair = loadIdentityKeyPair(); - localRegistrationId = loadRegistrationId(true); - currentPreKeyId = 0; - mXmppConnectionService.updateAccountUi(); - } - - /** - * Get the local client's identity key pair. - * - * @return The local client's persistent identity key pair. - */ - @Override - public IdentityKeyPair getIdentityKeyPair() { - if (identityKeyPair == null) { - identityKeyPair = loadIdentityKeyPair(); - } - return identityKeyPair; - } - - /** - * Return the local client's registration ID. - *

- * Clients should maintain a registration ID, a random number - * between 1 and 16380 that's generated once at install time. - * - * @return the local client's registration ID. - */ - @Override - public int getLocalRegistrationId() { - return localRegistrationId; - } - - /** - * Save a remote client's identity key - *

- * Store a remote client's identity key as trusted. - * - * @param address The address of the remote client. - * @param identityKey The remote client's identity key. - * @return true on success - */ - @Override - public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { - if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, address.getName()).contains(identityKey)) { - String fingerprint = CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()); - FingerprintStatus status = getFingerprintStatus(fingerprint); - if (status == null) { - if (mXmppConnectionService.blindTrustBeforeVerification() && !account.getAxolotlService().hasVerifiedKeys(address.getName())) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": blindly trusted "+fingerprint+" of "+address.getName()); - status = FingerprintStatus.createActiveTrusted(); - } else { - status = FingerprintStatus.createActiveUndecided(); - } - } else { - status = status.toActive(); - } - mXmppConnectionService.databaseBackend.storeIdentityKey(account, address.getName(), identityKey, status); - trustCache.remove(fingerprint); - } - return true; - } - - /** - * Verify a remote client's identity key. - *

- * Determine whether a remote client's identity is trusted. Convention is - * that the TextSecure protocol is 'trust on first use.' This means that - * an identity key is considered 'trusted' if there is no entry for the recipient - * in the local store, or if it matches the saved key for a recipient in the local - * store. Only if it mismatches an entry in the local store is it considered - * 'untrusted.' - * - * @param identityKey The identity key to verify. - * @return true if trusted, false if untrusted. - */ - @Override - public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) { - return true; - } - - public FingerprintStatus getFingerprintStatus(String fingerprint) { - return (fingerprint == null)? null : trustCache.get(fingerprint); - } - - public void setFingerprintStatus(String fingerprint, FingerprintStatus status) { - mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, status); - trustCache.remove(fingerprint); - } - - public void setFingerprintCertificate(String fingerprint, X509Certificate x509Certificate) { - mXmppConnectionService.databaseBackend.setIdentityKeyCertificate(account, fingerprint, x509Certificate); - } - - public X509Certificate getFingerprintCertificate(String fingerprint) { - return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate(account, fingerprint); - } - - public Set getContactKeysWithTrust(String bareJid, FingerprintStatus status) { - return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, status); - } - - public long getContactNumTrustedKeys(String bareJid) { - return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid); - } - - // -------------------------------------- - // SessionStore - // -------------------------------------- - - /** - * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple, - * or a new SessionRecord if one does not currently exist. - *

- * It is important that implementations return a copy of the current durable information. The - * returned SessionRecord may be modified, but those changes should not have an effect on the - * durable session state (what is returned by subsequent calls to this method) without the - * store method being called here first. - * - * @param address The name and device ID of the remote client. - * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or - * a new SessionRecord if one does not currently exist. - */ - @Override - public SessionRecord loadSession(SignalProtocolAddress address) { - SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address); - return (session != null) ? session : new SessionRecord(); - } - - /** - * Returns all known devices with active sessions for a recipient - * - * @param name the name of the client. - * @return all known sub-devices with active sessions. - */ - @Override - public List getSubDeviceSessions(String name) { - return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, - new SignalProtocolAddress(name, 0)); - } - - - public List getKnownAddresses() { - return mXmppConnectionService.databaseBackend.getKnownSignalAddresses(account); - } - /** - * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. - * - * @param address the address of the remote client. - * @param record the current SessionRecord for the remote client. - */ - @Override - public void storeSession(SignalProtocolAddress address, SessionRecord record) { - mXmppConnectionService.databaseBackend.storeSession(account, address, record); - } - - /** - * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple. - * - * @param address the address of the remote client. - * @return true if a {@link SessionRecord} exists, false otherwise. - */ - @Override - public boolean containsSession(SignalProtocolAddress address) { - return mXmppConnectionService.databaseBackend.containsSession(account, address); - } - - /** - * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. - * - * @param address the address of the remote client. - */ - @Override - public void deleteSession(SignalProtocolAddress address) { - mXmppConnectionService.databaseBackend.deleteSession(account, address); - } - - /** - * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. - * - * @param name the name of the remote client. - */ - @Override - public void deleteAllSessions(String name) { - SignalProtocolAddress address = new SignalProtocolAddress(name, 0); - mXmppConnectionService.databaseBackend.deleteAllSessions(account, - address); - } - - // -------------------------------------- - // PreKeyStore - // -------------------------------------- - - /** - * Load a local PreKeyRecord. - * - * @param preKeyId the ID of the local PreKeyRecord. - * @return the corresponding PreKeyRecord. - * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. - */ - @Override - public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { - PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); - if (record == null) { - throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); - } - return record; - } - - /** - * Store a local PreKeyRecord. - * - * @param preKeyId the ID of the PreKeyRecord to store. - * @param record the PreKeyRecord. - */ - @Override - public void storePreKey(int preKeyId, PreKeyRecord record) { - mXmppConnectionService.databaseBackend.storePreKey(account, record); - currentPreKeyId = preKeyId; - boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId)); - if (success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new prekey id to the database!"); - } - } - - /** - * @param preKeyId A PreKeyRecord ID. - * @return true if the store has a record for the preKeyId, otherwise false. - */ - @Override - public boolean containsPreKey(int preKeyId) { - return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); - } - - /** - * Delete a PreKeyRecord from local storage. - * - * @param preKeyId The ID of the PreKeyRecord to remove. - */ - @Override - public void removePreKey(int preKeyId) { - Log.d(Config.LOGTAG,"mark prekey for removal "+preKeyId); - synchronized (preKeysMarkedForRemoval) { - preKeysMarkedForRemoval.add(preKeyId); - } - } - - - public boolean flushPreKeys() { - Log.d(Config.LOGTAG,"flushing pre keys"); - int count = 0; - synchronized (preKeysMarkedForRemoval) { - for(Integer preKeyId : preKeysMarkedForRemoval) { - count += mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); - } - preKeysMarkedForRemoval.clear(); - } - return count > 0; - } - - // -------------------------------------- - // SignedPreKeyStore - // -------------------------------------- - - /** - * Load a local SignedPreKeyRecord. - * - * @param signedPreKeyId the ID of the local SignedPreKeyRecord. - * @return the corresponding SignedPreKeyRecord. - * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. - */ - @Override - public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { - SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); - if (record == null) { - throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); - } - return record; - } - - /** - * Load all local SignedPreKeyRecords. - * - * @return All stored SignedPreKeyRecords. - */ - @Override - public List loadSignedPreKeys() { - return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); - } - - public int getSignedPreKeysCount() { - return mXmppConnectionService.databaseBackend.getSignedPreKeysCount(account); - } - - /** - * Store a local SignedPreKeyRecord. - * - * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. - * @param record the SignedPreKeyRecord. - */ - @Override - public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { - mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); - } - - /** - * @param signedPreKeyId A SignedPreKeyRecord ID. - * @return true if the store has a record for the signedPreKeyId, otherwise false. - */ - @Override - public boolean containsSignedPreKey(int signedPreKeyId) { - return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); - } - - /** - * Delete a SignedPreKeyRecord from local storage. - * - * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. - */ - @Override - public void removeSignedPreKey(int signedPreKeyId) { - mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); - } - - public void preVerifyFingerprint(Account account, String name, String fingerprint) { - mXmppConnectionService.databaseBackend.storePreVerification(account,name,fingerprint,FingerprintStatus.createInactiveVerified()); - } + public static final String PREKEY_TABLENAME = "prekeys"; + public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; + public static final String SESSION_TABLENAME = "sessions"; + public static final String IDENTITIES_TABLENAME = "identities"; + public static final String ACCOUNT = "account"; + public static final String DEVICE_ID = "device_id"; + public static final String ID = "id"; + public static final String KEY = "key"; + public static final String FINGERPRINT = "fingerprint"; + public static final String NAME = "name"; + public static final String TRUSTED = "trusted"; // no longer used + public static final String TRUST = "trust"; + public static final String ACTIVE = "active"; + public static final String LAST_ACTIVATION = "last_activation"; + public static final String OWN = "ownkey"; + public static final String CERTIFICATE = "certificate"; + + public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; + public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; + + private static final int NUM_TRUSTS_TO_CACHE = 100; + + private final Account account; + private final XmppConnectionService mXmppConnectionService; + + private IdentityKeyPair identityKeyPair; + private int localRegistrationId; + private int currentPreKeyId = 0; + + private final HashSet preKeysMarkedForRemoval = new HashSet<>(); + + private final LruCache trustCache = + new LruCache(NUM_TRUSTS_TO_CACHE) { + @Override + protected FingerprintStatus create(String fingerprint) { + return mXmppConnectionService.databaseBackend.getFingerprintStatus( + account, fingerprint); + } + }; + + private static IdentityKeyPair generateIdentityKeyPair() { + Log.i( + Config.LOGTAG, + AxolotlService.LOGPREFIX + " : " + "Generating axolotl IdentityKeyPair..."); + ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); + return new IdentityKeyPair( + new IdentityKey(identityKeyPairKeys.getPublicKey()), + identityKeyPairKeys.getPrivateKey()); + } + + private static int generateRegistrationId() { + Log.i( + Config.LOGTAG, + AxolotlService.LOGPREFIX + " : " + "Generating axolotl registration ID..."); + return KeyHelper.generateRegistrationId(true); + } + + public SQLiteAxolotlStore(Account account, XmppConnectionService service) { + this.account = account; + this.mXmppConnectionService = service; + this.localRegistrationId = loadRegistrationId(); + this.currentPreKeyId = loadCurrentPreKeyId(); + } + + public int getCurrentPreKeyId() { + return currentPreKeyId; + } + + // -------------------------------------- + // IdentityKeyStore + // -------------------------------------- + + private IdentityKeyPair loadIdentityKeyPair() { + synchronized (mXmppConnectionService) { + IdentityKeyPair ownKey = + mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account); + + if (ownKey != null) { + return ownKey; + } else { + Log.i( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Could not retrieve own IdentityKeyPair"); + ownKey = generateIdentityKeyPair(); + mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownKey); + } + return ownKey; + } + } + + private int loadRegistrationId() { + return loadRegistrationId(false); + } + + private int loadRegistrationId(boolean regenerate) { + String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); + int reg_id; + if (!regenerate && regIdString != null) { + reg_id = Integer.valueOf(regIdString); + } else { + Log.i( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Could not retrieve axolotl registration id for account " + + account.getJid()); + reg_id = generateRegistrationId(); + boolean success = + this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id)); + if (success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Failed to write new key to the database!"); + } + } + return reg_id; + } + + private int loadCurrentPreKeyId() { + String prekeyIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); + int prekey_id; + if (prekeyIdString != null) { + prekey_id = Integer.valueOf(prekeyIdString); + } else { + Log.w( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Could not retrieve current prekey id for account " + + account.getJid()); + prekey_id = 0; + } + return prekey_id; + } + + public void regenerate() { + mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); + trustCache.evictAll(); + account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0)); + identityKeyPair = loadIdentityKeyPair(); + localRegistrationId = loadRegistrationId(true); + currentPreKeyId = 0; + mXmppConnectionService.updateAccountUi(); + } + + /** + * Get the local client's identity key pair. + * + * @return The local client's persistent identity key pair. + */ + @Override + public IdentityKeyPair getIdentityKeyPair() { + if (identityKeyPair == null) { + identityKeyPair = loadIdentityKeyPair(); + } + return identityKeyPair; + } + + /** + * Return the local client's registration ID. + * + *

Clients should maintain a registration ID, a random number between 1 and 16380 that's + * generated once at install time. + * + * @return the local client's registration ID. + */ + @Override + public int getLocalRegistrationId() { + return localRegistrationId; + } + + /** + * Save a remote client's identity key + * + *

Store a remote client's identity key as trusted. + * + * @param address The address of the remote client. + * @param identityKey The remote client's identity key. + * @return true on success + */ + @Override + public boolean saveIdentity( + final SignalProtocolAddress address, final IdentityKey identityKey) { + if (!mXmppConnectionService + .databaseBackend + .loadIdentityKeys(account, address.getName()) + .contains(identityKey)) { + String fingerprint = CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()); + FingerprintStatus status = getFingerprintStatus(fingerprint); + if (status == null) { + if (mXmppConnectionService.getAppSettings().isBTBVEnabled() + && !account.getAxolotlService().hasVerifiedKeys(address.getName())) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": blindly trusted " + + fingerprint + + " of " + + address.getName()); + status = FingerprintStatus.createActiveTrusted(); + } else { + status = FingerprintStatus.createActiveUndecided(); + } + } else { + status = status.toActive(); + } + mXmppConnectionService.databaseBackend.storeIdentityKey( + account, address.getName(), identityKey, status); + trustCache.remove(fingerprint); + } + return true; + } + + /** + * Verify a remote client's identity key. + * + *

Determine whether a remote client's identity is trusted. Convention is that the TextSecure + * protocol is 'trust on first use.' This means that an identity key is considered 'trusted' if + * there is no entry for the recipient in the local store, or if it matches the saved key for a + * recipient in the local store. Only if it mismatches an entry in the local store is it + * considered 'untrusted.' + * + * @param identityKey The identity key to verify. + * @return true if trusted, false if untrusted. + */ + @Override + public boolean isTrustedIdentity( + SignalProtocolAddress address, IdentityKey identityKey, Direction direction) { + return true; + } + + public FingerprintStatus getFingerprintStatus(String fingerprint) { + return (fingerprint == null) ? null : trustCache.get(fingerprint); + } + + public void setFingerprintStatus(String fingerprint, FingerprintStatus status) { + mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, status); + trustCache.remove(fingerprint); + } + + public void setFingerprintCertificate(String fingerprint, X509Certificate x509Certificate) { + mXmppConnectionService.databaseBackend.setIdentityKeyCertificate( + account, fingerprint, x509Certificate); + } + + public X509Certificate getFingerprintCertificate(String fingerprint) { + return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate( + account, fingerprint); + } + + public Set getContactKeysWithTrust(String bareJid, FingerprintStatus status) { + return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, status); + } + + public long getContactNumTrustedKeys(String bareJid) { + return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid); + } + + // -------------------------------------- + // SessionStore + // -------------------------------------- + + /** + * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId + * tuple, or a new SessionRecord if one does not currently exist. + * + *

It is important that implementations return a copy of the current durable information. The + * returned SessionRecord may be modified, but those changes should not have an effect on the + * durable session state (what is returned by subsequent calls to this method) without the store + * method being called here first. + * + * @param address The name and device ID of the remote client. + * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or a + * new SessionRecord if one does not currently exist. + */ + @Override + public SessionRecord loadSession(SignalProtocolAddress address) { + SessionRecord session = + mXmppConnectionService.databaseBackend.loadSession(this.account, address); + return (session != null) ? session : new SessionRecord(); + } + + /** + * Returns all known devices with active sessions for a recipient + * + * @param name the name of the client. + * @return all known sub-devices with active sessions. + */ + @Override + public List getSubDeviceSessions(String name) { + return mXmppConnectionService.databaseBackend.getSubDeviceSessions( + account, new SignalProtocolAddress(name, 0)); + } + + public List getKnownAddresses() { + return mXmppConnectionService.databaseBackend.getKnownSignalAddresses(account); + } + + /** + * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @param record the current SessionRecord for the remote client. + */ + @Override + public void storeSession(SignalProtocolAddress address, SessionRecord record) { + mXmppConnectionService.databaseBackend.storeSession(account, address, record); + } + + /** + * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId + * tuple. + * + * @param address the address of the remote client. + * @return true if a {@link SessionRecord} exists, false otherwise. + */ + @Override + public boolean containsSession(SignalProtocolAddress address) { + return mXmppConnectionService.databaseBackend.containsSession(account, address); + } + + /** + * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + */ + @Override + public void deleteSession(SignalProtocolAddress address) { + mXmppConnectionService.databaseBackend.deleteSession(account, address); + } + + /** + * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. + * + * @param name the name of the remote client. + */ + @Override + public void deleteAllSessions(String name) { + SignalProtocolAddress address = new SignalProtocolAddress(name, 0); + mXmppConnectionService.databaseBackend.deleteAllSessions(account, address); + } + + // -------------------------------------- + // PreKeyStore + // -------------------------------------- + + /** + * Load a local PreKeyRecord. + * + * @param preKeyId the ID of the local PreKeyRecord. + * @return the corresponding PreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. + */ + @Override + public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { + PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); + if (record == null) { + throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); + } + return record; + } + + /** + * Store a local PreKeyRecord. + * + * @param preKeyId the ID of the PreKeyRecord to store. + * @param record the PreKeyRecord. + */ + @Override + public void storePreKey(int preKeyId, PreKeyRecord record) { + mXmppConnectionService.databaseBackend.storePreKey(account, record); + currentPreKeyId = preKeyId; + boolean success = + this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId)); + if (success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Failed to write new prekey id to the database!"); + } + } + + /** + * @param preKeyId A PreKeyRecord ID. + * @return true if the store has a record for the preKeyId, otherwise false. + */ + @Override + public boolean containsPreKey(int preKeyId) { + return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); + } + + /** + * Delete a PreKeyRecord from local storage. + * + * @param preKeyId The ID of the PreKeyRecord to remove. + */ + @Override + public void removePreKey(int preKeyId) { + Log.d(Config.LOGTAG, "mark prekey for removal " + preKeyId); + synchronized (preKeysMarkedForRemoval) { + preKeysMarkedForRemoval.add(preKeyId); + } + } + + public boolean flushPreKeys() { + Log.d(Config.LOGTAG, "flushing pre keys"); + int count = 0; + synchronized (preKeysMarkedForRemoval) { + for (Integer preKeyId : preKeysMarkedForRemoval) { + count += mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); + } + preKeysMarkedForRemoval.clear(); + } + return count > 0; + } + + // -------------------------------------- + // SignedPreKeyStore + // -------------------------------------- + + /** + * Load a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the local SignedPreKeyRecord. + * @return the corresponding SignedPreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. + */ + @Override + public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { + SignedPreKeyRecord record = + mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); + if (record == null) { + throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); + } + return record; + } + + /** + * Load all local SignedPreKeyRecords. + * + * @return All stored SignedPreKeyRecords. + */ + @Override + public List loadSignedPreKeys() { + return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); + } + + public int getSignedPreKeysCount() { + return mXmppConnectionService.databaseBackend.getSignedPreKeysCount(account); + } + + /** + * Store a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. + * @param record the SignedPreKeyRecord. + */ + @Override + public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { + mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); + } + + /** + * @param signedPreKeyId A SignedPreKeyRecord ID. + * @return true if the store has a record for the signedPreKeyId, otherwise false. + */ + @Override + public boolean containsSignedPreKey(int signedPreKeyId) { + return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); + } + + /** + * Delete a SignedPreKeyRecord from local storage. + * + * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. + */ + @Override + public void removeSignedPreKey(int signedPreKeyId) { + mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); + } + + public void preVerifyFingerprint(Account account, String name, String fingerprint) { + mXmppConnectionService.databaseBackend.storePreVerification( + account, name, fingerprint, FingerprintStatus.createInactiveVerified()); + } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ce482490e10f26ed49e3ed29aea70d472cb4ef99..32b1ae4645475c275d3e39abf1156635168cb78e 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -6341,10 +6341,6 @@ public class XmppConnectionService extends Service { return verifiedSomething; } - public boolean blindTrustBeforeVerification() { - return getBooleanPreference(AppSettings.BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv); - } - public ShortcutService getShortcutService() { return mShortcutService; } diff --git a/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java index 739598a69009561a7f6a6bec4076fc6530d31c32..2a92bebf594fee436d42cedeb515ee250abcfe25 100644 --- a/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java @@ -1,12 +1,13 @@ package eu.siacs.conversations.ui.fragment.settings; import android.Manifest; +import android.content.Intent; import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.widget.Toast; - import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -22,16 +23,13 @@ import androidx.work.OneTimeWorkRequest; import androidx.work.OutOfQuotaPolicy; import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; - import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.base.Strings; import com.google.common.primitives.Longs; - +import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.worker.ExportBackupWorker; - import java.util.concurrent.TimeUnit; public class BackupSettingsFragment extends XmppPreferenceFragment { @@ -56,20 +54,33 @@ public class BackupSettingsFragment extends XmppPreferenceFragment { } }); + private final ActivityResultLauncher pickBackupLocationLauncher = + registerForActivityResult( + new ActivityResultContracts.OpenDocumentTree(), + uri -> { + if (uri == null) { + Log.d(Config.LOGTAG, "no backup location selected"); + return; + } + submitBackupLocationPreference(uri); + }); + @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { setPreferencesFromResource(R.xml.preferences_backup, rootKey); final var createOneOffBackup = findPreference(CREATE_ONE_OFF_BACKUP); final ListPreference recurringBackup = findPreference(RECURRING_BACKUP); - final var backupDirectory = findPreference("backup_directory"); - if (createOneOffBackup == null || recurringBackup == null || backupDirectory == null) { + final var backupLocation = findPreference(AppSettings.BACKUP_LOCATION); + if (createOneOffBackup == null || recurringBackup == null || backupLocation == null) { throw new IllegalStateException( "The preference resource file is missing some preferences"); } - backupDirectory.setSummary( + final var appSettings = new AppSettings(requireContext()); + backupLocation.setSummary( getString( R.string.pref_create_backup_summary, - FileBackend.getBackupDirectory(requireContext()).getAbsolutePath())); + appSettings.getBackupLocationAsPath())); + backupLocation.setOnPreferenceClickListener(this::onBackupLocationPreferenceClicked); createOneOffBackup.setOnPreferenceClickListener(this::onBackupPreferenceClicked); setValues( recurringBackup, @@ -77,6 +88,26 @@ public class BackupSettingsFragment extends XmppPreferenceFragment { value -> timeframeValueToName(requireContext(), value)); } + private boolean onBackupLocationPreferenceClicked(final Preference preference) { + this.pickBackupLocationLauncher.launch(null); + return false; + } + + private void submitBackupLocationPreference(final Uri uri) { + final var contentResolver = requireContext().getContentResolver(); + contentResolver.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + final var appSettings = new AppSettings(requireContext()); + appSettings.setBackupLocation(uri); + final var preference = findPreference(AppSettings.BACKUP_LOCATION); + if (preference == null) { + return; + } + preference.setSummary( + getString(R.string.pref_create_backup_summary, AppSettings.asPath(uri))); + } + @Override protected void onSharedPreferenceChanged(@NonNull String key) { super.onSharedPreferenceChanged(key); diff --git a/src/main/java/eu/siacs/conversations/utils/BackupFile.java b/src/main/java/eu/siacs/conversations/utils/BackupFile.java index 01ecf35b83d4d0d6973a54da84c3374d2a89289c..7644f6ccbf2f64f08140159e53ae9f1edd1ad34c 100644 --- a/src/main/java/eu/siacs/conversations/utils/BackupFile.java +++ b/src/main/java/eu/siacs/conversations/utils/BackupFile.java @@ -1,8 +1,12 @@ package eu.siacs.conversations.utils; import android.content.Context; +import android.content.UriPermission; import android.net.Uri; +import android.os.Build; +import android.provider.DocumentsContract; import android.util.Log; +import androidx.documentfile.provider.DocumentFile; import com.google.common.collect.Collections2; import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableList; @@ -15,6 +19,7 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.QuickConversationsService; +import eu.siacs.conversations.worker.ExportBackupWorker; import eu.siacs.conversations.xmpp.Jid; import java.io.DataInputStream; import java.io.File; @@ -81,11 +86,51 @@ public class BackupFile implements Comparable { final var backupFiles = new ImmutableList.Builder(); final var apps = ImmutableSet.of("Conversations", "Quicksy", context.getString(R.string.app_name)); + + final var uriPermissions = context.getContentResolver().getPersistedUriPermissions(); + + for (final UriPermission uriPermission : uriPermissions) { + final var uri = uriPermission.getUri(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + && DocumentsContract.isTreeUri(uri)) { + Log.d(Config.LOGTAG, "looking for backups in " + uri); + final var tree = DocumentFile.fromTreeUri(context, uriPermission.getUri()); + final var files = tree == null ? new DocumentFile[0] : tree.listFiles(); + for (final DocumentFile documentFile : files) { + final var name = documentFile.getName(); + if (documentFile.isFile() + && (ExportBackupWorker.MIME_TYPE.equals(documentFile.getType()) + || (name != null && name.endsWith(".ceb")))) { + try { + final BackupFile backupFile = + BackupFile.read(context, documentFile.getUri()); + if (accounts.contains(backupFile.getHeader().getJid())) { + Log.d( + Config.LOGTAG, + "skipping backup for " + backupFile.getHeader().getJid()); + } else { + backupFiles.add(backupFile); + } + } catch (final IOException + | IllegalArgumentException + | BackupFileHeader.OutdatedBackupFileVersion e) { + Log.d(Config.LOGTAG, "unable to read backup file ", e); + } + } + } + } + } + final List directories = new ArrayList<>(); for (final String app : apps) { directories.add(FileBackend.getLegacyBackupDirectory(app)); } - directories.add(FileBackend.getBackupDirectory(context)); + if (uriPermissions.isEmpty()) { + Log.d( + Config.LOGTAG, + "including default directory since no uri permissions have been granted"); + directories.add(FileBackend.getBackupDirectory(context)); + } for (final File directory : directories) { if (!directory.exists() || !directory.isDirectory()) { Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath()); @@ -134,7 +179,7 @@ public class BackupFile implements Comparable { public int compareTo(final BackupFile o) { return ComparisonChain.start() .compare(header.getJid(), o.header.getJid()) - .compare(header.getTimestamp(), o.header.getTimestamp()) + .compare(o.header.getTimestamp(), header.getTimestamp()) .result(); } } diff --git a/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java b/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java index c5178e0adfa91c251b8afa028711964fd1ba5891..2ec4fc45e1b60038933bdbdc6100f076e260067a 100644 --- a/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java +++ b/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java @@ -15,14 +15,15 @@ import android.os.SystemClock; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; +import androidx.documentfile.provider.DocumentFile; import androidx.work.ForegroundInfo; import androidx.work.WorkManager; import androidx.work.Worker; import androidx.work.WorkerParameters; -import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.gson.stream.JsonWriter; +import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore; @@ -38,6 +39,7 @@ import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; @@ -92,7 +94,7 @@ public class ExportBackupWorker extends Worker { @Override public Result doWork() { setForegroundAsync(getForegroundInfo()); - final List files; + final List files; try { files = export(); } catch (final IOException @@ -132,7 +134,7 @@ public class ExportBackupWorker extends Worker { } } - private List export() + private List export() throws IOException, InvalidKeySpecException, InvalidAlgorithmParameterException, @@ -141,17 +143,19 @@ public class ExportBackupWorker extends Worker { NoSuchAlgorithmException, NoSuchProviderException { final Context context = getApplicationContext(); + final var appSettings = new AppSettings(context); + final var backupLocation = appSettings.getBackupLocation(); final var database = DatabaseBackend.getInstance(context); final var accounts = database.getAccounts(); int count = 0; final int max = accounts.size(); - final ImmutableList.Builder files = new ImmutableList.Builder<>(); + final ImmutableList.Builder locations = new ImmutableList.Builder<>(); Log.d(Config.LOGTAG, "starting backup for " + max + " accounts"); for (final Account account : accounts) { if (isStopped()) { Log.d(Config.LOGTAG, "ExportBackupWorker has stopped. Returning what we have"); - return files.build(); + return locations.build(); } final String password = account.getPassword(); if (Strings.nullToEmpty(password).trim().isEmpty()) { @@ -164,34 +168,24 @@ public class ExportBackupWorker extends Worker { count++; continue; } - final String filename = - String.format( - "%s.%s.ceb", - account.getJid().asBareJid().toString(), - DATE_FORMAT.format(new Date())); - final File file = new File(FileBackend.getBackupDirectory(context), filename); + final Uri uri; try { - export(database, account, password, file, max, count); + uri = export(database, account, password, backupLocation, max, count); } catch (final WorkStoppedException e) { - if (file.delete()) { - Log.d( - Config.LOGTAG, - "deleted in progress backup file " + file.getAbsolutePath()); - } Log.d(Config.LOGTAG, "ExportBackupWorker has stopped. Returning what we have"); - return files.build(); + return locations.build(); } - files.add(file); + locations.add(uri); count++; } - return files.build(); + return locations.build(); } - private void export( + private Uri export( final DatabaseBackend database, final Account account, final String password, - final File file, + final Uri backupLocation, final int max, final int count) throws IOException, @@ -230,12 +224,36 @@ public class ExportBackupWorker extends Worker { cancelPendingIntent) .build()); final Progress progress = new Progress(notification, max, count); - final File directory = file.getParentFile(); - if (directory != null && directory.mkdirs()) { - Log.d(Config.LOGTAG, "created backup directory " + directory.getAbsolutePath()); + final String filename = + String.format( + "%s.%s.ceb", + account.getJid().asBareJid().toString(), DATE_FORMAT.format(new Date())); + final OutputStream outputStream; + final Uri location; + if ("file".equalsIgnoreCase(backupLocation.getScheme())) { + final File file = new File(backupLocation.getPath(), filename); + final File directory = file.getParentFile(); + if (directory != null && directory.mkdirs()) { + Log.d(Config.LOGTAG, "created backup directory " + directory.getAbsolutePath()); + } + outputStream = new FileOutputStream(file); + location = Uri.fromFile(file); + } else { + final var tree = DocumentFile.fromTreeUri(context, backupLocation); + if (tree == null) { + throw new IOException( + String.format( + "DocumentFile.fromTreeUri returned null for %s", backupLocation)); + } + final var file = tree.createFile(MIME_TYPE, filename); + if (file == null) { + throw new IOException( + String.format("Could not create %s in %s", filename, backupLocation)); + } + location = file.getUri(); + outputStream = context.getContentResolver().openOutputStream(location); } - final FileOutputStream fileOutputStream = new FileOutputStream(file); - final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); + final DataOutputStream dataOutputStream = new DataOutputStream(outputStream); backupFileHeader.write(dataOutputStream); dataOutputStream.flush(); @@ -247,7 +265,7 @@ public class ExportBackupWorker extends Worker { SecretKeySpec keySpec = new SecretKeySpec(key, KEY_TYPE); IvParameterSpec ivSpec = new IvParameterSpec(IV); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); - CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher); + CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher); final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream); final JsonWriter jsonWriter = @@ -257,21 +275,24 @@ public class ExportBackupWorker extends Worker { final String uuid = account.getUuid(); accountExport(db, uuid, jsonWriter); simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, jsonWriter); - messageExport(db, uuid, jsonWriter, progress); + messageExport(db, uuid, location, jsonWriter, progress); for (final String table : Arrays.asList( SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) { - throwIfWorkStopped(); + throwIfWorkStopped(location); simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, jsonWriter); } jsonWriter.endArray(); jsonWriter.flush(); jsonWriter.close(); - mediaScannerScanFile(file); - Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile()); + if ("file".equalsIgnoreCase(location.getScheme())) { + mediaScannerScanFile(new File(location.getPath())); + } + Log.d(Config.LOGTAG, "written backup to " + location); + return location; } private NotificationCompat.Builder getNotification() { @@ -287,8 +308,20 @@ public class ExportBackupWorker extends Worker { return notification; } - private void throwIfWorkStopped() throws WorkStoppedException { + private void throwIfWorkStopped(final Uri location) throws WorkStoppedException { if (isStopped()) { + if ("file".equalsIgnoreCase(location.getScheme())) { + final var file = new File(location.getPath()); + if (file.delete()) { + Log.d(Config.LOGTAG, "deleted " + file.getAbsolutePath()); + } + } else { + final var documentFile = + DocumentFile.fromSingleUri(getApplicationContext(), location); + if (documentFile != null && documentFile.delete()) { + Log.d(Config.LOGTAG, "deleted " + location); + } + } throw new WorkStoppedException(); } } @@ -371,6 +404,7 @@ public class ExportBackupWorker extends Worker { private void messageExport( final SQLiteDatabase db, final String uuid, + final Uri location, final JsonWriter writer, final Progress progress) throws IOException, WorkStoppedException { @@ -388,7 +422,7 @@ public class ExportBackupWorker extends Worker { int i = 0; int p = Integer.MIN_VALUE; while (cursor != null && cursor.moveToNext()) { - throwIfWorkStopped(); + throwIfWorkStopped(location); writer.beginObject(); writer.name("table"); writer.value(Message.TABLENAME); @@ -425,16 +459,18 @@ public class ExportBackupWorker extends Worker { .getEncoded(); } - private void notifySuccess(final List files) { + private void notifySuccess(final List locations) { final var context = getApplicationContext(); - final String path = FileBackend.getBackupDirectory(context).getAbsolutePath(); - - final var openFolderIntent = getOpenFolderIntent(path); - + final var appSettings = new AppSettings(context); + final String path = appSettings.getBackupLocationAsPath(); final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); final ArrayList uris = new ArrayList<>(); - for (final File file : files) { - uris.add(FileBackend.getUriForFile(context, file)); + for (final Uri uri : locations) { + if ("file".equalsIgnoreCase(uri.getScheme())) { + uris.add(FileBackend.getUriForFile(context, new File(uri.getPath()))); + } else { + uris.add(uri); + } } intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); @@ -444,8 +480,8 @@ public class ExportBackupWorker extends Worker { final var shareFilesIntent = PendingIntent.getActivity(context, 190, chooser, PENDING_INTENT_FLAGS); - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, "backup"); - mBuilder.setContentTitle(context.getString(R.string.notification_backup_created_title)) + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "backup"); + builder.setContentTitle(context.getString(R.string.notification_backup_created_title)) .setContentText( context.getString(R.string.notification_backup_created_subtitle, path)) .setStyle( @@ -453,60 +489,17 @@ public class ExportBackupWorker extends Worker { .bigText( context.getString( R.string.notification_backup_created_subtitle, - FileBackend.getBackupDirectory(context) - .getAbsolutePath()))) + path))) .setAutoCancel(true) .setSmallIcon(R.drawable.ic_archive_24dp); - if (openFolderIntent.isPresent()) { - mBuilder.setContentIntent(openFolderIntent.get()); - } else { - Log.w(Config.LOGTAG, "no app can display folders"); - } - - mBuilder.addAction( + builder.addAction( R.drawable.ic_share_24dp, context.getString(R.string.share_backup_files), shareFilesIntent); + builder.setLocalOnly(true); final var notificationManager = context.getSystemService(NotificationManager.class); - notificationManager.notify(BACKUP_CREATED_NOTIFICATION_ID, mBuilder.build()); - } - - private Optional getOpenFolderIntent(final String path) { - final var context = getApplicationContext(); - for (final Intent intent : getPossibleFileOpenIntents(context, path)) { - if (intent.resolveActivityInfo(context.getPackageManager(), 0) != null) { - return Optional.of( - PendingIntent.getActivity(context, 189, intent, PENDING_INTENT_FLAGS)); - } - } - return Optional.absent(); - } - - private static List getPossibleFileOpenIntents( - final Context context, final String path) { - - // http://www.openintents.org/action/android-intent-action-view/file-directory - // do not use 'vnd.android.document/directory' since this will trigger system file manager - final Intent openIntent = new Intent(Intent.ACTION_VIEW); - openIntent.addCategory(Intent.CATEGORY_DEFAULT); - if (Compatibility.runsAndTargetsTwentyFour(context)) { - openIntent.setType("resource/folder"); - } else { - openIntent.setDataAndType(Uri.parse("file://" + path), "resource/folder"); - } - openIntent.putExtra("org.openintents.extra.ABSOLUTE_PATH", path); - - final Intent amazeIntent = new Intent(Intent.ACTION_VIEW); - amazeIntent.setDataAndType(Uri.parse("com.amaze.filemanager:" + path), "resource/folder"); - - // will open a file manager at root and user can navigate themselves - final Intent systemFallBack = new Intent(Intent.ACTION_VIEW); - systemFallBack.addCategory(Intent.CATEGORY_DEFAULT); - systemFallBack.setData( - Uri.parse("content://com.android.externalstorage.documents/root/primary")); - - return Arrays.asList(openIntent, amazeIntent, systemFallBack); + notificationManager.notify(BACKUP_CREATED_NOTIFICATION_ID, builder.build()); } private static class Progress { diff --git a/src/main/res/drawable/ic_folder_open_24dp.xml b/src/main/res/drawable/ic_folder_open_24dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..9881c7748400cbf8037c92bfd11c0fea7454421c --- /dev/null +++ b/src/main/res/drawable/ic_folder_open_24dp.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index a7df4b226bd6e4a304b2a2a3d49b77034692c088..c8cfeb49493bbee6995daab50f06e1bfcbdc398a 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -331,7 +331,7 @@ Foreground service Prevents the operating system from killing your connection Create backup - Backup files will be stored in %s + Backups will be stored in %s Creating backup files Your backup has been created The backup files have been stored in %s @@ -1107,4 +1107,5 @@ Enable customized notification settings (importance, sound, vibration) settings for this conversation? Would you like to delete your avatar? Some clients might continue to display a cached copy of your avatar. Show to contacts only + Backup location diff --git a/src/main/res/xml/preferences_backup.xml b/src/main/res/xml/preferences_backup.xml index b0362c7856b02349c266f04c63359e97e9634a51..f026103ceed70f37a2b150d0ec533912a9850e39 100644 --- a/src/main/res/xml/preferences_backup.xml +++ b/src/main/res/xml/preferences_backup.xml @@ -14,7 +14,9 @@ android:title="@string/pref_create_backup" /> + android:icon="@drawable/ic_folder_open_24dp" + android:key="backup_location" + android:summary="@string/pref_create_backup_summary" + android:title="@string/pref_backup_location" /> \ No newline at end of file