From d24cfef3d1d53d3d7f1dd0e7708ba3e6f43eb86d Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 11 Jul 2023 19:12:06 +0930 Subject: [PATCH 01/97] Remove unused sizeResId --- .../securesms/components/ProfilePictureView.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt index a827a7d260..b6f0e80b5b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt @@ -5,7 +5,6 @@ import android.util.AttributeSet import android.view.View import android.widget.ImageView import android.widget.RelativeLayout -import androidx.annotation.DimenRes import com.bumptech.glide.load.engine.DiskCacheStrategy import network.loki.messenger.R import network.loki.messenger.databinding.ViewProfilePictureBinding @@ -77,8 +76,8 @@ class ProfilePictureView @JvmOverloads constructor( val publicKey = publicKey ?: return val additionalPublicKey = additionalPublicKey if (additionalPublicKey != null) { - setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName, R.dimen.small_profile_picture_size) - setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName, R.dimen.small_profile_picture_size) + setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName) + setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName) binding.doubleModeImageViewContainer.visibility = View.VISIBLE } else { glide.clear(binding.doubleModeImageView1) @@ -86,14 +85,14 @@ class ProfilePictureView @JvmOverloads constructor( binding.doubleModeImageViewContainer.visibility = View.INVISIBLE } if (additionalPublicKey == null && !isLarge) { - setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName, R.dimen.medium_profile_picture_size) + setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName) binding.singleModeImageView.visibility = View.VISIBLE } else { glide.clear(binding.singleModeImageView) binding.singleModeImageView.visibility = View.INVISIBLE } if (additionalPublicKey == null && isLarge) { - setProfilePictureIfNeeded(binding.largeSingleModeImageView, publicKey, displayName, R.dimen.large_profile_picture_size) + setProfilePictureIfNeeded(binding.largeSingleModeImageView, publicKey, displayName) binding.largeSingleModeImageView.visibility = View.VISIBLE } else { glide.clear(binding.largeSingleModeImageView) @@ -101,7 +100,7 @@ class ProfilePictureView @JvmOverloads constructor( } } - private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) { + private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?) { if (publicKey.isNotEmpty()) { val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false) if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return From 8390be0489d85041da00d14f5b3647aed0516d7a Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 11 Jul 2023 19:44:58 +0930 Subject: [PATCH 02/97] Fix caching --- .../securesms/components/ProfilePictureView.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt index b6f0e80b5b..fcc8a97ca1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt @@ -30,7 +30,7 @@ class ProfilePictureView @JvmOverloads constructor( var additionalDisplayName: String? = null var isLarge = false - private val profilePicturesCache = mutableMapOf() + private val profilePicturesCache = mutableMapOf() private val unknownRecipientDrawable = ResourceContactPhoto(R.drawable.ic_profile_default) .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) private val unknownOpenGroupDrawable = ResourceContactPhoto(R.drawable.ic_notification) @@ -103,12 +103,13 @@ class ProfilePictureView @JvmOverloads constructor( private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?) { if (publicKey.isNotEmpty()) { val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false) - if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return + if (profilePicturesCache[imageView] == recipient) return val signalProfilePicture = recipient.contactPhoto val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject + glide.clear(imageView) + if (signalProfilePicture != null && avatar != "0" && avatar != "") { - glide.clear(imageView) glide.load(signalProfilePicture) .placeholder(unknownRecipientDrawable) .centerCrop() @@ -117,18 +118,15 @@ class ProfilePictureView @JvmOverloads constructor( .circleCrop() .into(imageView) } else if (recipient.isOpenGroupRecipient && recipient.groupAvatarId == null) { - glide.clear(imageView) imageView.setImageDrawable(unknownOpenGroupDrawable) } else { val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}") - - glide.clear(imageView) glide.load(placeholder) .placeholder(unknownRecipientDrawable) .centerCrop() .diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView) } - profilePicturesCache[publicKey] = recipient.profileAvatar + profilePicturesCache[imageView] = recipient } else { imageView.setImageDrawable(null) } From 916f705c50deabe69f9cb92771285fff7b53dd0f Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 31 Jul 2023 14:44:43 +0930 Subject: [PATCH 03/97] Prefix message with name in HomeActivity --- .../securesms/database/MmsSmsDatabase.java | 10 +++++++ .../securesms/database/ThreadDatabase.java | 10 ++++++- .../database/model/ThreadRecord.java | 4 ++- .../securesms/home/ConversationView.kt | 29 +++++++++++++------ 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 0db4dd00e5..3d175fd8bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -209,6 +209,16 @@ public class MmsSmsDatabase extends Database { } } + public long getLastMessageTimestamp(long threadId) { + String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; + String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; + + try (Cursor cursor = queryTables(PROJECTION, selection, order, "1")) { + cursor.moveToFirst(); + return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_SENT)); + } + } + public Cursor getUnread() { String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " ASC"; String selection = "(" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1) AND " + MmsSmsColumns.NOTIFIED + " = 0"; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 5044529981..c525f05e7d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -951,7 +951,9 @@ public class ThreadDatabase extends Database { readReceiptCount = 0; } - return new ThreadRecord(body, snippetUri, recipient, date, count, + MessageRecord lastMessage = count > 0 ? getLastMessage(threadId) : null; + + return new ThreadRecord(body, snippetUri, lastMessage, recipient, date, count, unreadCount, unreadMentionCount, threadId, deliveryReceiptCount, status, type, distributionType, archived, expiresIn, lastSeen, readReceiptCount, pinned); } @@ -976,4 +978,10 @@ public class ThreadDatabase extends Database { } } } + + private MessageRecord getLastMessage(long threadId) { + MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase(); + long messageTimestamp = mmsSmsDatabase.getLastMessageTimestamp(threadId); + return mmsSmsDatabase.getMessageForTimestamp(messageTimestamp); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java index f3e72a8747..0c023a8f29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -43,6 +43,7 @@ import network.loki.messenger.R; public class ThreadRecord extends DisplayRecord { private @Nullable final Uri snippetUri; + public @Nullable final MessageRecord lastMessage; private final long count; private final int unreadCount; private final int unreadMentionCount; @@ -54,13 +55,14 @@ public class ThreadRecord extends DisplayRecord { private final int initialRecipientHash; public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri, - @NonNull Recipient recipient, long date, long count, int unreadCount, + @Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount, int unreadMentionCount, long threadId, int deliveryReceiptCount, int status, long snippetType, int distributionType, boolean archived, long expiresIn, long lastSeen, int readReceiptCount, boolean pinned) { super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount); this.snippetUri = snippetUri; + this.lastMessage = lastMessage; this.count = count; this.unreadCount = unreadCount; this.unreadMentionCount = unreadMentionCount; diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index 31b281c6de..454bcef18d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -4,6 +4,8 @@ import android.content.Context import android.content.res.Resources import android.graphics.Typeface import android.graphics.drawable.ColorDrawable +import android.text.SpannableString +import android.text.TextUtils import android.util.AttributeSet import android.util.TypedValue import android.view.View @@ -89,7 +91,7 @@ class ConversationView : LinearLayout { || (configFactory.convoVolatile?.getConversationUnread(thread) == true) binding.unreadMentionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup) - val senderDisplayName = getUserDisplayName(thread.recipient) + val senderDisplayName = getTitle(thread.recipient) ?: thread.recipient.address.toString() binding.conversationViewDisplayNameTextView.text = senderDisplayName binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date) @@ -101,9 +103,7 @@ class ConversationView : LinearLayout { R.drawable.ic_notifications_mentions } binding.muteIndicatorImageView.setImageResource(drawableRes) - val rawSnippet = thread.getDisplayBody(context) - val snippet = highlightMentions(rawSnippet, thread.threadId, context) - binding.snippetTextView.text = snippet + binding.snippetTextView.text = highlightMentions(getSnippet(thread), thread.threadId, context) binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE if (isTyping) { @@ -131,11 +131,22 @@ class ConversationView : LinearLayout { binding.profilePictureView.recycle() } - private fun getUserDisplayName(recipient: Recipient): String? { - return if (recipient.isLocalNumber) { - context.getString(R.string.note_to_self) - } else { - recipient.toShortString() // Internally uses the Contact API + private fun getTitle(recipient: Recipient): String? = when { + recipient.isLocalNumber -> context.getString(R.string.note_to_self) + else -> recipient.toShortString() // Internally uses the Contact API + } + + private fun getSnippet(thread: ThreadRecord): CharSequence { + thread.apply { + val body = getDisplayBody(context) + + val snippetAuthor = lastMessage?.individualRecipient + + return if (lastMessage?.isOutgoing == true) { + TextUtils.concat(resources.getString(R.string.MessageRecord_you), ": ", body) + } else { + return snippetAuthor?.toShortString()?.let { TextUtils.concat(it, ": ", body) } ?: body + } } } // endregion From dd345cbf074bb54e7e86d1f4e2a82f446f3dd2d3 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 31 Jul 2023 19:40:20 +0930 Subject: [PATCH 04/97] Hide sender prefix for note to self --- .../securesms/home/ConversationView.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index 454bcef18d..5191e05d77 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -136,17 +136,19 @@ class ConversationView : LinearLayout { else -> recipient.toShortString() // Internally uses the Contact API } - private fun getSnippet(thread: ThreadRecord): CharSequence { - thread.apply { - val body = getDisplayBody(context) + private fun getSnippet(thread: ThreadRecord): CharSequence = thread.run { + val body = getDisplayBody(context) - val snippetAuthor = lastMessage?.individualRecipient - - return if (lastMessage?.isOutgoing == true) { + when { + recipient.isLocalNumber -> body // Note to self + lastMessage?.isOutgoing == true -> { TextUtils.concat(resources.getString(R.string.MessageRecord_you), ": ", body) - } else { - return snippetAuthor?.toShortString()?.let { TextUtils.concat(it, ": ", body) } ?: body } + else -> lastMessage + ?.individualRecipient + ?.toShortString() + ?.let { TextUtils.concat(it, ": ", body) } + ?: body } } // endregion From b487d5aa6407a1b91f48346ee4a7171401828814 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 31 Jul 2023 19:48:45 +0930 Subject: [PATCH 05/97] Hide sender prefix for control messages --- .../java/org/thoughtcrime/securesms/home/ConversationView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index 5191e05d77..b1bd55b20a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -140,7 +140,7 @@ class ConversationView : LinearLayout { val body = getDisplayBody(context) when { - recipient.isLocalNumber -> body // Note to self + recipient.isLocalNumber || lastMessage?.isControlMessage == true -> body // Note to self lastMessage?.isOutgoing == true -> { TextUtils.concat(resources.getString(R.string.MessageRecord_you), ": ", body) } From f70aa9155b04b2980da1ce726826f483ceef6c49 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 7 Aug 2023 10:26:20 +0930 Subject: [PATCH 06/97] Remove problematic getLastMessage() --- .../securesms/database/ThreadDatabase.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index c525f05e7d..f4ab65f56c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -951,7 +951,13 @@ public class ThreadDatabase extends Database { readReceiptCount = 0; } - MessageRecord lastMessage = count > 0 ? getLastMessage(threadId) : null; + MessageRecord lastMessage = null; + + if (count > 0) { + MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase(); + long messageTimestamp = mmsSmsDatabase.getLastMessageTimestamp(threadId); + lastMessage = mmsSmsDatabase.getMessageForTimestamp(messageTimestamp); + } return new ThreadRecord(body, snippetUri, lastMessage, recipient, date, count, unreadCount, unreadMentionCount, threadId, deliveryReceiptCount, status, type, @@ -978,10 +984,4 @@ public class ThreadDatabase extends Database { } } } - - private MessageRecord getLastMessage(long threadId) { - MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase(); - long messageTimestamp = mmsSmsDatabase.getLastMessageTimestamp(threadId); - return mmsSmsDatabase.getMessageForTimestamp(messageTimestamp); - } } From 50fd3292c8b9cb45d6b7f8da212adcf2d15da2f7 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 7 Aug 2023 10:46:38 +0930 Subject: [PATCH 07/97] Refactor snippet formatting --- .../securesms/home/ConversationView.kt | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index b1bd55b20a..47a37a092a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -103,7 +103,7 @@ class ConversationView : LinearLayout { R.drawable.ic_notifications_mentions } binding.muteIndicatorImageView.setImageResource(drawableRes) - binding.snippetTextView.text = highlightMentions(getSnippet(thread), thread.threadId, context) + binding.snippetTextView.text = highlightMentions(thread.getSnippet(), thread.threadId, context) binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE if (isTyping) { @@ -136,20 +136,16 @@ class ConversationView : LinearLayout { else -> recipient.toShortString() // Internally uses the Contact API } - private fun getSnippet(thread: ThreadRecord): CharSequence = thread.run { - val body = getDisplayBody(context) + private fun ThreadRecord.getSnippet(): CharSequence = + concatSnippet(getSnippetPrefix(), getDisplayBody(context)) - when { - recipient.isLocalNumber || lastMessage?.isControlMessage == true -> body // Note to self - lastMessage?.isOutgoing == true -> { - TextUtils.concat(resources.getString(R.string.MessageRecord_you), ": ", body) - } - else -> lastMessage - ?.individualRecipient - ?.toShortString() - ?.let { TextUtils.concat(it, ": ", body) } - ?: body - } + private fun concatSnippet(prefix: CharSequence?, body: CharSequence): CharSequence = + prefix?.let { TextUtils.concat(it, ": ", body) } ?: body + + private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when { + recipient.isLocalNumber || lastMessage?.isControlMessage == true -> null + lastMessage?.isOutgoing == true -> resources.getString(R.string.MessageRecord_you) + else -> lastMessage?.individualRecipient?.toShortString() } // endregion } From 1937fca681f0006615e7195100f2bbb83bd5fe9e Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 15 Aug 2023 00:10:37 +0930 Subject: [PATCH 08/97] Remove unused RecoveryPhraseRestoreActivity --- app/src/main/AndroidManifest.xml | 5 - .../RecoveryPhraseRestoreActivity.kt | 114 ------------------ .../activity_recovery_phrase_restore.xml | 78 ------------ .../activity_recovery_phrase_restore.xml | 78 ------------ 4 files changed, 275 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt delete mode 100644 app/src/main/res/layout-sw400dp/activity_recovery_phrase_restore.xml delete mode 100644 app/src/main/res/layout/activity_recovery_phrase_restore.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aa81fafc2b..317ab7cfc8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -104,11 +104,6 @@ android:name="org.thoughtcrime.securesms.onboarding.RegisterActivity" android:screenOrientation="portrait" android:theme="@style/Theme.Session.DayNight.FlatActionBar" /> - String = { fileName -> - MnemonicUtilities.loadFileContents(this, fileName) - } - val hexEncodedSeed = MnemonicCodec(loadFileContents).decode(mnemonic) - val seed = Hex.fromStringCondensed(hexEncodedSeed) - val keyPairGenerationResult = KeyPairUtilities.generate(seed) - val x25519KeyPair = keyPairGenerationResult.x25519KeyPair - KeyPairUtilities.store(this, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair) - configFactory.keyPairChanged() - val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey - val registrationID = KeyHelper.generateRegistrationId(false) - TextSecurePreferences.setLocalRegistrationId(this, registrationID) - TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey) - val intent = Intent(this, DisplayNameActivity::class.java) - push(intent) - } catch (e: Exception) { - val message = if (e is MnemonicCodec.DecodingError) e.description else MnemonicCodec.DecodingError.Generic.description - return Toast.makeText(this, message, Toast.LENGTH_SHORT).show() - } - } - - private fun openURL(url: String) { - try { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - startActivity(intent) - } catch (e: Exception) { - Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show() - } - } - // endregion -} \ No newline at end of file diff --git a/app/src/main/res/layout-sw400dp/activity_recovery_phrase_restore.xml b/app/src/main/res/layout-sw400dp/activity_recovery_phrase_restore.xml deleted file mode 100644 index 88d90f5aaf..0000000000 --- a/app/src/main/res/layout-sw400dp/activity_recovery_phrase_restore.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - -