/* * Copyright 2018 Diego Rossi (@_HellPie) * * 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 com.keylesspalace.tusky.util; import android.text.InputFilter; import android.text.SpannableStringBuilder; import android.text.Spanned; /** * A customized version of {@link android.text.InputFilter.LengthFilter} which allows smarter * constraints and adds better visuals such as: * * * Some of these features are configurable through at instancing time. */ public class SmartLengthInputFilter implements InputFilter { /** * Defines how many characters to extend beyond the limit to cut at the end of the word on the * boundary of it rather than cutting at the word preceding that one. */ private static final int RUNWAY = 10; /** * Default for maximum status length on Mastodon and default collapsing length on Pleroma. */ public static final int LENGTH_DEFAULT = 500; /** * Stores a reusable singleton instance of a {@link SmartLengthInputFilter} already configured * to the default maximum length of {@value #LENGTH_DEFAULT}. */ public static final SmartLengthInputFilter INSTANCE = new SmartLengthInputFilter(LENGTH_DEFAULT); private final int max; private final boolean allowRunway; private final boolean skipIfBadRatio; /** * Creates a new {@link SmartLengthInputFilter} instance with a predefined maximum length and * all the smart constraint features this class supports. * * @param max The maximum length before trimming. May change based on other constraints. */ public SmartLengthInputFilter(int max) { this(max, true, true); } /** * Fully configures a new {@link SmartLengthInputFilter} to fine tune the state of the supported * smart constraints this class supports. * * @param max The maximum length before trimming. * @param allowRunway Whether to extend {@param max} by an extra 10 characters * and trim precisely at the end of the closest word. * @param skipIfBadRatio Whether to skip trimming entirely if the trimmed content * will be less than 25% of the shown content. */ public SmartLengthInputFilter(int max, boolean allowRunway, boolean skipIfBadRatio) { this.max = max; this.allowRunway = allowRunway; this.skipIfBadRatio = skipIfBadRatio; } /** * Calculates if it's worth trimming the message at a specific limit or if the content that will * be hidden will not be enough to justify the operation. * * @param message The message to trim. * @param limit The maximum length after trimming. * @return Whether the message should be trimmed or not. */ public static boolean hasBadRatio(Spanned message, int limit) { return (double) limit / message.length() > 0.75; } /** {@inheritDoc} */ @Override public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { // Code originally imported from InputFilter.LengthFilter but heavily customized. // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/InputFilter.java#175 int sourceLength = source.length(); int keep = max - (dest.length() - (dend - dstart)); if (keep <= 0) return ""; if (keep >= end - start) return null; // keep original keep += start; // Enable skipping trimming if the ratio is not good enough if (skipIfBadRatio && (double)keep / sourceLength > 0.75) return null; // Enable trimming at the end of the closest word if possible if (allowRunway && Character.isLetterOrDigit(source.charAt(keep))) { int boundary; // Android N+ offer a clone of the ICU APIs in Java for better internationalization and // unicode support. Using the ICU version of BreakIterator grants better support for // those without having to add the ICU4J library at a minimum Api trade-off. if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { android.icu.text.BreakIterator iterator = android.icu.text.BreakIterator.getWordInstance(); iterator.setText(source.toString()); boundary = iterator.following(keep); if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep); } else { java.text.BreakIterator iterator = java.text.BreakIterator.getWordInstance(); iterator.setText(source.toString()); boundary = iterator.following(keep); if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep); } keep = boundary; } else { // If no runway is allowed simply remove whitespaces if present while (Character.isWhitespace(source.charAt(keep - 1))) { --keep; if (keep == start) return ""; } } if (Character.isHighSurrogate(source.charAt(keep - 1))) { --keep; if (keep == start) return ""; } if (source instanceof Spanned) { return new SpannableStringBuilder(source, start, keep).append("…"); } else { return source.subSequence(start, keep) + "…"; } } }