Clean code

This commit is contained in:
Ganard 2020-02-03 16:14:36 +01:00
parent 88755a79b4
commit f454078c6b
32 changed files with 430 additions and 489 deletions

View File

@ -1,5 +1,6 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="160" />
<AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings>

View File

@ -1,6 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@ -70,7 +70,6 @@ import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.database.query.whereType
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback

View File

@ -320,7 +320,6 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
// al devices =
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $models")
if (!models.isNullOrEmpty()) {
val workingCopy = models.toMutableMap()

View File

@ -36,7 +36,6 @@ import im.vector.matrix.android.internal.session.room.timeline.PaginationDirecti
import io.realm.Realm
import io.realm.Sort
import io.realm.kotlin.createObject
import kotlinx.coroutines.coroutineScope
import timber.log.Timber
internal fun ChunkEntity.deleteOnCascade() {
@ -110,7 +109,6 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
eventEntity: EventEntity,
direction: PaginationDirection,
roomMemberContentsByUser: HashMap<String, RoomMemberContent?>) {
val eventId = eventEntity.eventId
if (timelineEvents.find(eventId) != null) {
return
@ -134,8 +132,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
this.senderName = roomMemberContent?.displayName
if (roomMemberContent?.displayName != null) {
val isHistoricalUnique = roomMemberContentsByUser.values.find {
roomMemberContent != it &&
it?.displayName == roomMemberContent.displayName
it != roomMemberContent && it?.displayName == roomMemberContent.displayName
} == null
isUniqueDisplayName = if (isLastForward) {
val isLiveUnique = RoomMemberSummaryEntity

View File

@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.database.model
import io.realm.RealmObject
import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
internal open class CurrentStateEventEntity(var eventId: String = "",
var root: EventEntity? = null,

View File

@ -21,9 +21,7 @@ import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.di.MoshiProvider
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
internal open class EventEntity(@PrimaryKey var eventId: String = "",

View File

@ -20,7 +20,6 @@ import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
internal open class TimelineEventEntity(var localId: Long = 0,
@Index var eventId: String = "",

View File

@ -29,7 +29,8 @@ internal fun CurrentStateEventEntity.Companion.where(realm: Realm, roomId: Strin
.equalTo(CurrentStateEventEntityFields.TYPE, type)
}
internal fun CurrentStateEventEntity.Companion.whereStateKey(realm: Realm, roomId: String, type: String, stateKey: String): RealmQuery<CurrentStateEventEntity> {
internal fun CurrentStateEventEntity.Companion.whereStateKey(realm: Realm, roomId: String, type: String, stateKey: String)
: RealmQuery<CurrentStateEventEntity> {
return where(realm = realm, roomId = roomId, type = type)
.equalTo(CurrentStateEventEntityFields.STATE_KEY, stateKey)
}
@ -49,5 +50,3 @@ private fun create(realm: Realm, roomId: String, stateKey: String, type: String)
this.stateKey = stateKey
}
}

View File

@ -35,7 +35,8 @@ internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, e
internal fun TimelineEventEntity.Companion.whereRoomId(realm: Realm,
roomId: String): RealmQuery<TimelineEventEntity> {
return realm.where<TimelineEventEntity>().equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
return realm.where<TimelineEventEntity>()
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
}
internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List<TimelineEventEntity> {

View File

@ -26,7 +26,6 @@ import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.AuthModule
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers

View File

@ -21,6 +21,7 @@ import android.content.Context
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.Network
import android.os.Build
import javax.inject.Inject
internal interface NetworkCallbackStrategy {
@ -47,7 +48,7 @@ internal class FallbackNetworkCallbackStrategy @Inject constructor(private val c
}
}
@TargetApi(android.os.Build.VERSION_CODES.N)
@TargetApi(Build.VERSION_CODES.N)
internal class PreferredNetworkCallbackStrategy @Inject constructor(context: Context) : NetworkCallbackStrategy {
private var hasChangedCallback: (() -> Unit)? = null

View File

@ -42,10 +42,10 @@ interface NetworkConnectivityChecker {
}
@SessionScope
internal class DefaultNetworkConnectivityChecker @Inject constructor(private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val homeServerPinger: HomeServerPinger,
internal class DefaultNetworkConnectivityChecker @Inject constructor(private val homeServerPinger: HomeServerPinger,
private val backgroundDetectionObserver: BackgroundDetectionObserver,
private val networkCallbackStrategy: NetworkCallbackStrategy) : NetworkConnectivityChecker {
private val networkCallbackStrategy: NetworkCallbackStrategy)
: NetworkConnectivityChecker {
private val hasInternetAccess = AtomicBoolean(true)
private val listeners = Collections.synchronizedSet(LinkedHashSet<NetworkConnectivityChecker.Listener>())

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session
import android.content.Context
import android.os.Build
import com.zhuinden.monarchy.Monarchy
import dagger.Binds
import dagger.Lazy
@ -194,7 +195,7 @@ internal abstract class SessionModule {
fun providesNetworkCallbackStrategy(fallbackNetworkCallbackStrategy: Provider<FallbackNetworkCallbackStrategy>,
preferredNetworkCallbackStrategy: Provider<PreferredNetworkCallbackStrategy>
): NetworkCallbackStrategy {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
preferredNetworkCallbackStrategy.get()
} else {
fallbackNetworkCallbackStrategy.get()
@ -241,4 +242,3 @@ internal abstract class SessionModule {
@Binds
abstract fun bindHomeServerCapabilitiesService(homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService
}

View File

@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.homeserver
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.TaskExecutor
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject

View File

@ -24,7 +24,6 @@ import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
import im.vector.matrix.android.internal.database.model.EventEntity
@ -36,7 +35,6 @@ import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.getOrNull
import im.vector.matrix.android.internal.database.query.isEventRead
import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.database.query.whereType
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver

View File

@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.room.draft
import androidx.lifecycle.LiveData
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.send.DraftService
import im.vector.matrix.android.api.session.room.send.UserDraft

View File

@ -122,5 +122,4 @@ internal class DefaultReadService @AssistedInject constructor(
private fun ReadService.MarkAsReadParams.forceReadReceipt(): Boolean {
return this == ReadService.MarkAsReadParams.READ_RECEIPT || this == ReadService.MarkAsReadParams.BOTH
}
}

View File

@ -25,7 +25,6 @@ import im.vector.matrix.android.internal.database.query.isReadMarkerMoreRecent
import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.sync.ReadReceiptHandler

View File

@ -16,12 +16,10 @@
package im.vector.matrix.android.internal.session.room.send
import android.content.Context
import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.Operation
import androidx.work.WorkManager
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy

View File

@ -48,7 +48,6 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils
@ -57,7 +56,6 @@ import im.vector.matrix.android.internal.util.StringProvider
import kotlinx.coroutines.launch
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import timber.log.Timber
import javax.inject.Inject
/**

View File

@ -27,7 +27,6 @@ import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
import im.vector.matrix.android.api.util.CancelableBag
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
import im.vector.matrix.android.internal.database.mapper.asDomain
@ -665,7 +664,6 @@ internal class DefaultTimeline(
val params = GetContextOfEventTask.Params(roomId, eventId)
cancelableBag += contextOfEventTask.configureWith(params) {
callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
postSnapshot()
}

View File

@ -28,7 +28,6 @@ import im.vector.matrix.android.internal.database.helper.deleteOnCascade
import im.vector.matrix.android.internal.database.helper.merge
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
@ -168,13 +167,19 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) {
Timber.v("Reach end of $roomId")
roomId.isBlank()
if (direction == PaginationDirection.FORWARDS) {
val currentLiveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
if (currentChunk != currentLiveChunk) {
currentChunk.isLastForward = true
currentLiveChunk?.deleteOnCascade()
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES)
latestPreviewableEvent = TimelineEventEntity.latestEvent(
realm,
roomId,
includesSending = true,
filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES
)
}
}
} else {
@ -237,7 +242,12 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
}
if (shouldUpdateSummary) {
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES)
val latestPreviewableEvent = TimelineEventEntity.latestEvent(
realm,
roomId,
includesSending = true,
filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES
)
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
}
RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk)

View File

@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.sync.job
import android.app.Service
import android.content.Intent
import android.os.IBinder
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.failure.isTokenError
import im.vector.matrix.android.api.session.Session

View File

@ -158,4 +158,5 @@ Formatter\.formatFileSize===1
Formatter\.formatShortFileSize===1
### Use kotlin stdlib to test or compare strings
android\.text\.TextUtils
# DISABLED
# android\.text\.TextUtils

View File

@ -1,445 +0,0 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.riotx.core.platform;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.text.Layout;
import android.text.Layout.Alignment;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.text.style.ForegroundColorSpan;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatTextView;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* A {@link android.widget.TextView} that ellipsizes more intelligently.
* This class supports ellipsizing multiline text through setting {@code android:ellipsize}
* and {@code android:maxLines}.
* <p/>
* Note: {@link android.text.TextUtils.TruncateAt#MARQUEE} ellipsizing type is not supported.
* This as to be used to get rid of the StaticLayout issue with maxLines and ellipsize causing some performance issues.
*/
public class EllipsizingTextView extends AppCompatTextView {
public static final int ELLIPSIZE_ALPHA = 0x88;
private SpannableString ELLIPSIS = new SpannableString("\u2026");
private static final Pattern DEFAULT_END_PUNCTUATION
= Pattern.compile("[\\.!?,;:\u2026]*$", Pattern.DOTALL);
private final List<EllipsizeListener> mEllipsizeListeners = new ArrayList<>();
private EllipsizeStrategy mEllipsizeStrategy;
private boolean isEllipsized;
private boolean isStale;
private boolean programmaticChange;
private CharSequence mFullText;
private int mMaxLines;
private float mLineSpacingMult = 1.0f;
private float mLineAddVertPad = 0.0f;
/**
* The end punctuation which will be removed when appending {@link #ELLIPSIS}.
*/
private Pattern mEndPunctPattern;
public EllipsizingTextView(Context context) {
this(context, null);
}
public EllipsizingTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
public EllipsizingTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a = context.obtainStyledAttributes(attrs,
new int[]{android.R.attr.maxLines, android.R.attr.ellipsize}, defStyle, 0);
setMaxLines(a.getInt(0, Integer.MAX_VALUE));
a.recycle();
setEndPunctuationPattern(DEFAULT_END_PUNCTUATION);
final int currentTextColor = getCurrentTextColor();
final int ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor));
ELLIPSIS.setSpan(new ForegroundColorSpan(ellipsizeColor), 0, ELLIPSIS.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
public void setEndPunctuationPattern(Pattern pattern) {
mEndPunctPattern = pattern;
}
@SuppressWarnings("unused")
public void addEllipsizeListener(@NonNull EllipsizeListener listener) {
mEllipsizeListeners.add(listener);
}
@SuppressWarnings("unused")
public void removeEllipsizeListener(@NonNull EllipsizeListener listener) {
mEllipsizeListeners.remove(listener);
}
@SuppressWarnings("unused")
public boolean isEllipsized() {
return isEllipsized;
}
/**
* @return The maximum number of lines displayed in this {@link android.widget.TextView}.
*/
public int getMaxLines() {
return mMaxLines;
}
@Override
public void setMaxLines(int maxLines) {
super.setMaxLines(maxLines);
mMaxLines = maxLines;
isStale = true;
}
/**
* Determines if the last fully visible line is being ellipsized.
*
* @return {@code true} if the last fully visible line is being ellipsized;
* otherwise, returns {@code false}.
*/
public boolean ellipsizingLastFullyVisibleLine() {
return mMaxLines == Integer.MAX_VALUE;
}
@Override
public void setLineSpacing(float add, float mult) {
mLineAddVertPad = add;
mLineSpacingMult = mult;
super.setLineSpacing(add, mult);
}
@Override
public void setText(CharSequence text, BufferType type) {
if (!programmaticChange) {
mFullText = text instanceof Spanned ? (Spanned) text : text;
isStale = true;
}
super.setText(text, type);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (ellipsizingLastFullyVisibleLine()) {
isStale = true;
}
}
@Override
public void setPadding(int left, int top, int right, int bottom) {
super.setPadding(left, top, right, bottom);
if (ellipsizingLastFullyVisibleLine()) {
isStale = true;
}
}
@Override
protected void onDraw(@NonNull Canvas canvas) {
if (isStale) {
resetText();
}
super.onDraw(canvas);
}
/**
* Sets the ellipsized text if appropriate.
*/
private void resetText() {
int maxLines = getMaxLines();
CharSequence workingText = mFullText;
boolean ellipsized = false;
if (maxLines != -1) {
if (mEllipsizeStrategy == null) setEllipsize(null);
workingText = mEllipsizeStrategy.processText(mFullText);
ellipsized = !mEllipsizeStrategy.isInLayout(mFullText);
}
if (!workingText.equals(getText())) {
programmaticChange = true;
try {
setText(workingText);
} finally {
programmaticChange = false;
}
}
isStale = false;
if (ellipsized != isEllipsized) {
isEllipsized = ellipsized;
for (EllipsizeListener listener : mEllipsizeListeners) {
listener.ellipsizeStateChanged(ellipsized);
}
}
}
/**
* Causes words in the text that are longer than the view is wide to be ellipsized
* instead of broken in the middle. Use {@code null} to turn off ellipsizing.
* <p/>
* Note: Method does nothing for {@link android.text.TextUtils.TruncateAt#MARQUEE}
* ellipsizing type.
*
* @param where part of text to ellipsize
*/
@Override
public void setEllipsize(TruncateAt where) {
if (where == null) {
mEllipsizeStrategy = new EllipsizeNoneStrategy();
return;
}
switch (where) {
case END:
mEllipsizeStrategy = new EllipsizeEndStrategy();
break;
case START:
mEllipsizeStrategy = new EllipsizeStartStrategy();
break;
case MIDDLE:
mEllipsizeStrategy = new EllipsizeMiddleStrategy();
break;
case MARQUEE:
default:
mEllipsizeStrategy = new EllipsizeNoneStrategy();
break;
}
}
/**
* A listener that notifies when the ellipsize state has changed.
*/
public interface EllipsizeListener {
void ellipsizeStateChanged(boolean ellipsized);
}
/**
* A base class for an ellipsize strategy.
*/
private abstract class EllipsizeStrategy {
/**
* Returns ellipsized text if the text does not fit inside of the layout;
* otherwise, returns the full text.
*
* @param text text to process
* @return Ellipsized text if the text does not fit inside of the layout;
* otherwise, returns the full text.
*/
public CharSequence processText(CharSequence text) {
return !isInLayout(text) ? createEllipsizedText(text) : text;
}
/**
* Determines if the text fits inside of the layout.
*
* @param text text to fit
* @return {@code true} if the text fits inside of the layout;
* otherwise, returns {@code false}.
*/
public boolean isInLayout(CharSequence text) {
Layout layout = createWorkingLayout(text);
return layout.getLineCount() <= getLinesCount();
}
/**
* Creates a working layout with the given text.
*
* @param workingText text to create layout with
* @return {@link android.text.Layout} with the given text.
*/
protected Layout createWorkingLayout(CharSequence workingText) {
return new StaticLayout(workingText, getPaint(),
getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(),
Alignment.ALIGN_NORMAL, mLineSpacingMult,
mLineAddVertPad, false /* includepad */);
}
/**
* Get how many lines of text we are allowed to display.
*/
protected int getLinesCount() {
if (ellipsizingLastFullyVisibleLine()) {
int fullyVisibleLinesCount = getFullyVisibleLinesCount();
return fullyVisibleLinesCount == -1 ? 1 : fullyVisibleLinesCount;
} else {
return mMaxLines;
}
}
/**
* Get how many lines of text we can display so their full height is visible.
*/
protected int getFullyVisibleLinesCount() {
Layout layout = createWorkingLayout("");
int height = getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
int lineHeight = layout.getLineBottom(0);
return height / lineHeight;
}
/**
* Creates ellipsized text from the given text.
*
* @param fullText text to ellipsize
* @return Ellipsized text
*/
protected abstract CharSequence createEllipsizedText(CharSequence fullText);
}
/**
* An {@link EllipsizingTextView.EllipsizeStrategy} that
* does not ellipsize text.
*/
private class EllipsizeNoneStrategy extends EllipsizeStrategy {
@Override
protected CharSequence createEllipsizedText(CharSequence fullText) {
return fullText;
}
}
/**
* An {@link EllipsizingTextView.EllipsizeStrategy} that
* ellipsizes text at the end.
*/
private class EllipsizeEndStrategy extends EllipsizeStrategy {
@Override
protected CharSequence createEllipsizedText(CharSequence fullText) {
Layout layout = createWorkingLayout(fullText);
int cutOffIndex = layout.getLineEnd(mMaxLines - 1);
int textLength = fullText.length();
int cutOffLength = textLength - cutOffIndex;
if (cutOffLength < ELLIPSIS.length()) cutOffLength = ELLIPSIS.length();
CharSequence workingText = TextUtils.substring(fullText, 0, textLength - cutOffLength).trim();
while (!isInLayout(TextUtils.concat(stripEndPunctuation(workingText), ELLIPSIS))) {
int lastSpace = TextUtils.lastIndexOf(workingText, ' ');
if (lastSpace == -1) {
break;
}
workingText = TextUtils.substring(workingText, 0, lastSpace).trim();
}
workingText = TextUtils.concat(stripEndPunctuation(workingText), ELLIPSIS);
SpannableStringBuilder dest = new SpannableStringBuilder(workingText);
if (fullText instanceof Spanned) {
TextUtils.copySpansFrom((Spanned) fullText, 0, workingText.length(), null, dest, 0);
}
return dest;
}
/**
* Strips the end punctuation from a given text according to {@link #mEndPunctPattern}.
*
* @param workingText text to strip end punctuation from
* @return Text without end punctuation.
*/
public String stripEndPunctuation(CharSequence workingText) {
return mEndPunctPattern.matcher(workingText).replaceFirst("");
}
}
/**
* An {@link EllipsizingTextView.EllipsizeStrategy} that
* ellipsizes text at the start.
*/
private class EllipsizeStartStrategy extends EllipsizeStrategy {
@Override
protected CharSequence createEllipsizedText(CharSequence fullText) {
Layout layout = createWorkingLayout(fullText);
int cutOffIndex = layout.getLineEnd(mMaxLines - 1);
int textLength = fullText.length();
int cutOffLength = textLength - cutOffIndex;
if (cutOffLength < ELLIPSIS.length()) cutOffLength = ELLIPSIS.length();
CharSequence workingText = TextUtils.substring(fullText, cutOffLength, textLength).trim();
while (!isInLayout(TextUtils.concat(ELLIPSIS, workingText))) {
int firstSpace = TextUtils.indexOf(workingText, ' ');
if (firstSpace == -1) {
break;
}
workingText = TextUtils.substring(workingText, firstSpace, workingText.length()).trim();
}
workingText = TextUtils.concat(ELLIPSIS, workingText);
SpannableStringBuilder dest = new SpannableStringBuilder(workingText);
if (fullText instanceof Spanned) {
TextUtils.copySpansFrom((Spanned) fullText, textLength - workingText.length(),
textLength, null, dest, 0);
}
return dest;
}
}
/**
* An {@link EllipsizingTextView.EllipsizeStrategy} that
* ellipsizes text in the middle.
*/
private class EllipsizeMiddleStrategy extends EllipsizeStrategy {
@Override
protected CharSequence createEllipsizedText(CharSequence fullText) {
Layout layout = createWorkingLayout(fullText);
int cutOffIndex = layout.getLineEnd(mMaxLines - 1);
int textLength = fullText.length();
int cutOffLength = textLength - cutOffIndex;
if (cutOffLength < ELLIPSIS.length()) cutOffLength = ELLIPSIS.length();
cutOffLength += cutOffIndex % 2; // Make it even.
String firstPart = TextUtils.substring(
fullText, 0, textLength / 2 - cutOffLength / 2).trim();
String secondPart = TextUtils.substring(
fullText, textLength / 2 + cutOffLength / 2, textLength).trim();
while (!isInLayout(TextUtils.concat(firstPart, ELLIPSIS, secondPart))) {
int lastSpaceFirstPart = firstPart.lastIndexOf(' ');
int firstSpaceSecondPart = secondPart.indexOf(' ');
if (lastSpaceFirstPart == -1 || firstSpaceSecondPart == -1) break;
firstPart = firstPart.substring(0, lastSpaceFirstPart).trim();
secondPart = secondPart.substring(firstSpaceSecondPart, secondPart.length()).trim();
}
SpannableStringBuilder firstDest = new SpannableStringBuilder(firstPart);
SpannableStringBuilder secondDest = new SpannableStringBuilder(secondPart);
if (fullText instanceof Spanned) {
TextUtils.copySpansFrom((Spanned) fullText, 0, firstPart.length(),
null, firstDest, 0);
TextUtils.copySpansFrom((Spanned) fullText, textLength - secondPart.length(),
textLength, null, secondDest, 0);
}
return TextUtils.concat(firstDest, ELLIPSIS, secondDest);
}
}
}

View File

@ -0,0 +1,398 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.riotx.core.platform
import android.R
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.text.Layout
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.StaticLayout
import android.text.TextUtils.*
import android.text.style.ForegroundColorSpan
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import java.util.ArrayList
import java.util.regex.Pattern
/**
* A [android.widget.TextView] that ellipsizes more intelligently.
* This class supports ellipsizing multiline text through setting `android:ellipsize`
* and `android:maxLines`.
*
*
* Note: [TruncateAt.MARQUEE] ellipsizing type is not supported.
* This as to be used to get rid of the StaticLayout issue with maxLines and ellipsize causing some performance issues.
*/
class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = R.attr.textViewStyle)
: AppCompatTextView(context, attrs, defStyle) {
private val ELLIPSIS = SpannableString("\u2026")
private val ellipsizeListeners: MutableList<EllipsizeListener> = ArrayList()
private var ellipsizeStrategy: EllipsizeStrategy? = null
var isEllipsized = false
private set
private var isStale = false
private var programmaticChange = false
private var fullText: CharSequence? = null
private var maxLines = 0
private var lineSpacingMult = 1.0f
private var lineAddVertPad = 0.0f
/**
* The end punctuation which will be removed when appending [.ELLIPSIS].
*/
private var mEndPunctPattern: Pattern? = null
fun setEndPunctuationPattern(pattern: Pattern?) {
mEndPunctPattern = pattern
}
fun addEllipsizeListener(listener: EllipsizeListener) {
ellipsizeListeners.add(listener)
}
fun removeEllipsizeListener(listener: EllipsizeListener) {
ellipsizeListeners.remove(listener)
}
/**
* @return The maximum number of lines displayed in this [android.widget.TextView].
*/
override fun getMaxLines(): Int {
return maxLines
}
override fun setMaxLines(maxLines: Int) {
super.setMaxLines(maxLines)
this.maxLines = maxLines
isStale = true
}
/**
* Determines if the last fully visible line is being ellipsized.
*
* @return `true` if the last fully visible line is being ellipsized;
* otherwise, returns `false`.
*/
fun ellipsizingLastFullyVisibleLine(): Boolean {
return maxLines == Int.MAX_VALUE
}
override fun setLineSpacing(add: Float, mult: Float) {
lineAddVertPad = add
lineSpacingMult = mult
super.setLineSpacing(add, mult)
}
override fun setText(text: CharSequence, type: BufferType) {
if (!programmaticChange) {
fullText = if (text is Spanned) text else text
isStale = true
}
super.setText(text, type)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (ellipsizingLastFullyVisibleLine()) {
isStale = true
}
}
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
super.setPadding(left, top, right, bottom)
if (ellipsizingLastFullyVisibleLine()) {
isStale = true
}
}
override fun onDraw(canvas: Canvas) {
if (isStale) {
resetText()
}
super.onDraw(canvas)
}
/**
* Sets the ellipsized text if appropriate.
*/
private fun resetText() {
val maxLines = maxLines
var workingText = fullText
var ellipsized = false
if (maxLines != -1) {
if (ellipsizeStrategy == null) setEllipsize(null)
workingText = ellipsizeStrategy!!.processText(fullText)
ellipsized = !ellipsizeStrategy!!.isInLayout(fullText)
}
if (workingText != text) {
programmaticChange = true
text = try {
workingText
} finally {
programmaticChange = false
}
}
isStale = false
if (ellipsized != isEllipsized) {
isEllipsized = ellipsized
for (listener in ellipsizeListeners) {
listener.ellipsizeStateChanged(ellipsized)
}
}
}
/**
* Causes words in the text that are longer than the view is wide to be ellipsized
* instead of broken in the middle. Use `null` to turn off ellipsizing.
*
*
* Note: Method does nothing for [TruncateAt.MARQUEE]
* ellipsizing type.
*
* @param where part of text to ellipsize
*/
override fun setEllipsize(where: TruncateAt?) {
if (where == null) {
ellipsizeStrategy = EllipsizeNoneStrategy()
return
}
ellipsizeStrategy = when (where) {
TruncateAt.END -> EllipsizeEndStrategy()
TruncateAt.START -> EllipsizeStartStrategy()
TruncateAt.MIDDLE -> EllipsizeMiddleStrategy()
TruncateAt.MARQUEE -> EllipsizeNoneStrategy()
else -> EllipsizeNoneStrategy()
}
}
/**
* A listener that notifies when the ellipsize state has changed.
*/
interface EllipsizeListener {
fun ellipsizeStateChanged(ellipsized: Boolean)
}
/**
* A base class for an ellipsize strategy.
*/
private abstract inner class EllipsizeStrategy {
/**
* Returns ellipsized text if the text does not fit inside of the layout;
* otherwise, returns the full text.
*
* @param text text to process
* @return Ellipsized text if the text does not fit inside of the layout;
* otherwise, returns the full text.
*/
fun processText(text: CharSequence?): CharSequence? {
return if (!isInLayout(text)) createEllipsizedText(text) else text
}
/**
* Determines if the text fits inside of the layout.
*
* @param text text to fit
* @return `true` if the text fits inside of the layout;
* otherwise, returns `false`.
*/
fun isInLayout(text: CharSequence?): Boolean {
val layout = createWorkingLayout(text)
return layout.lineCount <= linesCount
}
/**
* Creates a working layout with the given text.
*
* @param workingText text to create layout with
* @return [android.text.Layout] with the given text.
*/
@Suppress("DEPRECATION")
protected fun createWorkingLayout(workingText: CharSequence?): Layout {
return StaticLayout(
workingText,
paint,
width - compoundPaddingLeft - compoundPaddingRight,
Layout.Alignment.ALIGN_NORMAL,
lineSpacingMult,
lineAddVertPad,
false
)
}
/**
* Get how many lines of text we are allowed to display.
*/
protected val linesCount: Int
get() = if (ellipsizingLastFullyVisibleLine()) {
val fullyVisibleLinesCount = fullyVisibleLinesCount
if (fullyVisibleLinesCount == -1) 1 else fullyVisibleLinesCount
} else {
maxLines
}
/**
* Get how many lines of text we can display so their full height is visible.
*/
protected val fullyVisibleLinesCount: Int
get() {
val layout = createWorkingLayout("")
val height = height - compoundPaddingTop - compoundPaddingBottom
val lineHeight = layout.getLineBottom(0)
return height / lineHeight
}
/**
* Creates ellipsized text from the given text.
*
* @param fullText text to ellipsize
* @return Ellipsized text
*/
protected abstract fun createEllipsizedText(fullText: CharSequence?): CharSequence?
}
/**
* An [EllipsizingTextView.EllipsizeStrategy] that
* does not ellipsize text.
*/
private inner class EllipsizeNoneStrategy : EllipsizeStrategy() {
override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
return fullText
}
}
/**
* An [EllipsizingTextView.EllipsizeStrategy] that
* ellipsizes text at the end.
*/
private inner class EllipsizeEndStrategy : EllipsizeStrategy() {
override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
val layout = createWorkingLayout(fullText)
val cutOffIndex = layout.getLineEnd(maxLines - 1)
val textLength = fullText!!.length
var cutOffLength = textLength - cutOffIndex
if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
var workingText: CharSequence = substring(fullText, 0, textLength - cutOffLength).trim()
while (!isInLayout(concat(stripEndPunctuation(workingText), ELLIPSIS))) {
val lastSpace = lastIndexOf(workingText, ' ')
if (lastSpace == -1) {
break
}
workingText = substring(workingText, 0, lastSpace).trim()
}
workingText = concat(stripEndPunctuation(workingText), ELLIPSIS)
val dest = SpannableStringBuilder(workingText)
if (fullText is Spanned) {
copySpansFrom(fullText as Spanned?, 0, workingText.length, null, dest, 0)
}
return dest
}
/**
* Strips the end punctuation from a given text according to [.mEndPunctPattern].
*
* @param workingText text to strip end punctuation from
* @return Text without end punctuation.
*/
fun stripEndPunctuation(workingText: CharSequence?): String {
return mEndPunctPattern!!.matcher(workingText).replaceFirst("")
}
}
/**
* An [EllipsizingTextView.EllipsizeStrategy] that
* ellipsizes text at the start.
*/
private inner class EllipsizeStartStrategy : EllipsizeStrategy() {
override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
val layout = createWorkingLayout(fullText)
val cutOffIndex = layout.getLineEnd(maxLines - 1)
val textLength = fullText!!.length
var cutOffLength = textLength - cutOffIndex
if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
var workingText: CharSequence = substring(fullText, cutOffLength, textLength).trim()
while (!isInLayout(concat(ELLIPSIS, workingText))) {
val firstSpace = indexOf(workingText, ' ')
if (firstSpace == -1) {
break
}
workingText = substring(workingText, firstSpace, workingText.length).trim()
}
workingText = concat(ELLIPSIS, workingText)
val dest = SpannableStringBuilder(workingText)
if (fullText is Spanned) {
copySpansFrom(fullText as Spanned?, textLength - workingText.length,
textLength, null, dest, 0)
}
return dest
}
}
/**
* An [EllipsizingTextView.EllipsizeStrategy] that
* ellipsizes text in the middle.
*/
private inner class EllipsizeMiddleStrategy : EllipsizeStrategy() {
override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
val layout = createWorkingLayout(fullText)
val cutOffIndex = layout.getLineEnd(maxLines - 1)
val textLength = fullText!!.length
var cutOffLength = textLength - cutOffIndex
if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
cutOffLength += cutOffIndex % 2 // Make it even.
var firstPart = substring(
fullText, 0, textLength / 2 - cutOffLength / 2).trim()
var secondPart = substring(
fullText, textLength / 2 + cutOffLength / 2, textLength).trim()
while (!isInLayout(concat(firstPart, ELLIPSIS, secondPart))) {
val lastSpaceFirstPart = firstPart.lastIndexOf(' ')
val firstSpaceSecondPart = secondPart.indexOf(' ')
if (lastSpaceFirstPart == -1 || firstSpaceSecondPart == -1) break
firstPart = firstPart.substring(0, lastSpaceFirstPart).trim()
secondPart = secondPart.substring(firstSpaceSecondPart, secondPart.length).trim()
}
val firstDest = SpannableStringBuilder(firstPart)
val secondDest = SpannableStringBuilder(secondPart)
if (fullText is Spanned) {
copySpansFrom(fullText as Spanned?, 0, firstPart.length,
null, firstDest, 0)
copySpansFrom(fullText as Spanned?, textLength - secondPart.length,
textLength, null, secondDest, 0)
}
return concat(firstDest, ELLIPSIS, secondDest)
}
}
companion object {
const val ELLIPSIZE_ALPHA = 0x88
private val DEFAULT_END_PUNCTUATION = Pattern.compile("[.!?,;:\u2026]*$", Pattern.DOTALL)
}
init {
val a = context.obtainStyledAttributes(attrs, intArrayOf(R.attr.maxLines, R.attr.ellipsize), defStyle, 0)
maxLines = a.getInt(0, Int.MAX_VALUE)
a.recycle()
setEndPunctuationPattern(DEFAULT_END_PUNCTUATION)
val currentTextColor = currentTextColor
val ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor))
ELLIPSIS.setSpan(ForegroundColorSpan(ellipsizeColor), 0, ELLIPSIS.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}

View File

@ -33,7 +33,6 @@ import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import butterknife.ButterKnife
import butterknife.Unbinder
import com.airbnb.mvrx.BaseMvRxFragment