parent
7f31aff144
commit
c16c95d7a7
@ -1,27 +0,0 @@ |
||||
/* Copyright 2017 Andrew Dawson |
||||
* |
||||
* This file is a part of Tusky. |
||||
* |
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the |
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
||||
* Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky; |
||||
|
||||
/** Android Studio complains about built-in assertions so this is an alternative. */ |
||||
class Assert { |
||||
private static boolean ENABLED = BuildConfig.DEBUG; |
||||
|
||||
static void expect(boolean expression) { |
||||
if (ENABLED && !expression) { |
||||
throw new AssertionError(); |
||||
} |
||||
} |
||||
} |
@ -1,40 +0,0 @@ |
||||
/* Copyright 2017 Andrew Dawson |
||||
* |
||||
* This file is a part of Tusky. |
||||
* |
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the |
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
||||
* Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky; |
||||
|
||||
class CountUpDownLatch { |
||||
private int count; |
||||
|
||||
CountUpDownLatch() { |
||||
this.count = 0; |
||||
} |
||||
|
||||
synchronized void countDown() { |
||||
count--; |
||||
notifyAll(); |
||||
} |
||||
|
||||
synchronized void countUp() { |
||||
count++; |
||||
notifyAll(); |
||||
} |
||||
|
||||
synchronized void await() throws InterruptedException { |
||||
while (count != 0) { |
||||
wait(); |
||||
} |
||||
} |
||||
} |
@ -1,50 +0,0 @@ |
||||
/* Copyright 2017 Andrew Dawson |
||||
* |
||||
* This file is a part of Tusky. |
||||
* |
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the |
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
||||
* Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky; |
||||
|
||||
class DateUtils { |
||||
/* This is a rough duplicate of android.text.format.DateUtils.getRelativeTimeSpanString, |
||||
* but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough. */ |
||||
static String getRelativeTimeSpanString(long then, long now) { |
||||
final long MINUTE = 60; |
||||
final long HOUR = 60 * MINUTE; |
||||
final long DAY = 24 * HOUR; |
||||
final long YEAR = 365 * DAY; |
||||
long span = (now - then) / 1000; |
||||
String prefix = ""; |
||||
if (span < 0) { |
||||
prefix = "in "; |
||||
span = -span; |
||||
} |
||||
String unit; |
||||
if (span < MINUTE) { |
||||
unit = "s"; |
||||
} else if (span < HOUR) { |
||||
span /= MINUTE; |
||||
unit = "m"; |
||||
} else if (span < DAY) { |
||||
span /= HOUR; |
||||
unit = "h"; |
||||
} else if (span < YEAR) { |
||||
span /= DAY; |
||||
unit = "d"; |
||||
} else { |
||||
span /= YEAR; |
||||
unit = "y"; |
||||
} |
||||
return prefix + span + unit; |
||||
} |
||||
} |
@ -1,54 +0,0 @@ |
||||
/* Copyright 2017 Andrew Dawson |
||||
* |
||||
* This file is a part of Tusky. |
||||
* |
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the |
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
||||
* Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky; |
||||
|
||||
import android.os.Build; |
||||
import android.text.Html; |
||||
import android.text.Spanned; |
||||
|
||||
public class HtmlUtils { |
||||
private static CharSequence trimTrailingWhitespace(CharSequence s) { |
||||
int i = s.length(); |
||||
do { |
||||
i--; |
||||
} while (i >= 0 && Character.isWhitespace(s.charAt(i))); |
||||
return s.subSequence(0, i + 1); |
||||
} |
||||
|
||||
@SuppressWarnings("deprecation") |
||||
public static Spanned fromHtml(String html) { |
||||
Spanned result; |
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { |
||||
result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); |
||||
} else { |
||||
result = Html.fromHtml(html); |
||||
} |
||||
/* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which |
||||
* all status contents do, so it should be trimmed. */ |
||||
return (Spanned) trimTrailingWhitespace(result); |
||||
} |
||||
|
||||
@SuppressWarnings("deprecation") |
||||
public static String toHtml(Spanned text) { |
||||
String result; |
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { |
||||
result = Html.toHtml(text, Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE); |
||||
} else { |
||||
result = Html.toHtml(text); |
||||
} |
||||
return result; |
||||
} |
||||
} |
@ -1,44 +0,0 @@ |
||||
/* Copyright 2017 Andrew Dawson |
||||
* |
||||
* This file is a part of Tusky. |
||||
* |
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the |
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
||||
* Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky; |
||||
|
||||
import android.support.annotation.Nullable; |
||||
|
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.OutputStream; |
||||
|
||||
class IOUtils { |
||||
static void closeQuietly(@Nullable InputStream stream) { |
||||
try { |
||||
if (stream != null) { |
||||
stream.close(); |
||||
} |
||||
} catch (IOException e) { |
||||
// intentionally unhandled
|
||||
} |
||||
} |
||||
|
||||
static void closeQuietly(@Nullable OutputStream stream) { |
||||
try { |
||||
if (stream != null) { |
||||
stream.close(); |
||||
} |
||||
} catch (IOException e) { |
||||
// intentionally unhandled
|
||||
} |
||||
} |
||||
} |
@ -1,51 +0,0 @@ |
||||
/* Copyright 2017 Andrew Dawson |
||||
* |
||||
* This file is a part of Tusky. |
||||
* |
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the |
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
||||
* Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky; |
||||
|
||||
/**A wrapper for android.util.Log that allows for disabling logging, such as for release builds.*/ |
||||
public class Log { |
||||
private static final boolean LOGGING_ENABLED = BuildConfig.DEBUG; |
||||
|
||||
public static void i(String tag, String string) { |
||||
if (LOGGING_ENABLED) { |
||||
android.util.Log.i(tag, string); |
||||
} |
||||
} |
||||
|
||||
public static void e(String tag, String string) { |
||||
if (LOGGING_ENABLED) { |
||||
android.util.Log.e(tag, string); |
||||
} |
||||
} |
||||
|
||||
public static void d(String tag, String string) { |
||||
if (LOGGING_ENABLED) { |
||||
android.util.Log.d(tag, string); |
||||
} |
||||
} |
||||
|
||||
public static void v(String tag, String string) { |
||||
if (LOGGING_ENABLED) { |
||||
android.util.Log.v(tag, string); |
||||
} |
||||
} |
||||
|
||||
public static void w(String tag, String string) { |
||||
if (LOGGING_ENABLED) { |
||||
android.util.Log.w(tag, string); |
||||
} |
||||
} |
||||
} |
@ -1,242 +0,0 @@ |
||||
/* Copyright 2017 Andrew Dawson |
||||
* |
||||
* This file is part of Tusky. |
||||
* |
||||
* Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU |
||||
* Lesser General Public License as published by the Free Software Foundation, either version 3 of |
||||
* the License, or (at your option) any later version. |
||||
* |
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser |
||||
* General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU Lesser General Public License along with Tusky. If |
||||
* not, see <http://www.gnu.org/licenses/>. */
|
||||
|
||||
package com.keylesspalace.tusky; |
||||
|
||||
import android.os.Build; |
||||
import android.support.annotation.NonNull; |
||||
|
||||
import java.io.IOException; |
||||
import java.net.InetAddress; |
||||
import java.net.Socket; |
||||
import java.security.KeyManagementException; |
||||
import java.security.KeyStore; |
||||
import java.security.KeyStoreException; |
||||
import java.security.NoSuchAlgorithmException; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
|
||||
import javax.net.ssl.SSLContext; |
||||
import javax.net.ssl.SSLSocket; |
||||
import javax.net.ssl.SSLSocketFactory; |
||||
import javax.net.ssl.TrustManager; |
||||
import javax.net.ssl.TrustManagerFactory; |
||||
import javax.net.ssl.X509TrustManager; |
||||
|
||||
import okhttp3.ConnectionSpec; |
||||
import okhttp3.Interceptor; |
||||
import okhttp3.OkHttpClient; |
||||
import okhttp3.Request; |
||||
import okhttp3.Response; |
||||
|
||||
public class OkHttpUtils { |
||||
static final String TAG = "OkHttpUtils"; // logging tag
|
||||
|
||||
/** |
||||
* Makes a Builder with the maximum range of TLS versions and cipher suites enabled. |
||||
* |
||||
* It first tries the "approved" list of cipher suites given in OkHttp (the default in |
||||
* ConnectionSpec.MODERN_TLS) and if that doesn't work falls back to the set of ALL enabled, |
||||
* then falls back to plain http. |
||||
* |
||||
* API level 24 has a regression in elliptic curves where it only supports secp256r1, so this |
||||
* first tries a fallback without elliptic curves at all, and then tries them after. |
||||
* |
||||
* TLS 1.1 and 1.2 have to be manually enabled on API levels 16-20. |
||||
*/ |
||||
@NonNull |
||||
public static OkHttpClient.Builder getCompatibleClientBuilder() { |
||||
ConnectionSpec fallback = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) |
||||
.allEnabledCipherSuites() |
||||
.supportsTlsExtensions(true) |
||||
.build(); |
||||
|
||||
List<ConnectionSpec> specList = new ArrayList<>(); |
||||
specList.add(ConnectionSpec.MODERN_TLS); |
||||
addNougatFixConnectionSpec(specList); |
||||
specList.add(fallback); |
||||
specList.add(ConnectionSpec.CLEARTEXT); |
||||
|
||||
OkHttpClient.Builder builder = new OkHttpClient.Builder() |
||||
.addInterceptor(getUserAgentInterceptor()) |
||||
.connectionSpecs(specList); |
||||
|
||||
return enableHigherTlsOnPreLollipop(builder); |
||||
} |
||||
|
||||
@NonNull |
||||
public static OkHttpClient getCompatibleClient() { |
||||
return getCompatibleClientBuilder().build(); |
||||
} |
||||
|
||||
/** |
||||
* Add a custom User-Agent that contains Tusky & Android Version to all requests |
||||
* Example: |
||||
* User-Agent: Tusky/1.1.2 Android/5.0.2 |
||||
*/ |
||||
@NonNull |
||||
private static Interceptor getUserAgentInterceptor() { |
||||
return new Interceptor() { |
||||
@Override |
||||
public Response intercept(Chain chain) throws IOException { |
||||
Request originalRequest = chain.request(); |
||||
Request requestWithUserAgent = originalRequest.newBuilder() |
||||
.header("User-Agent", "Tusky/"+BuildConfig.VERSION_NAME+" Android/"+Build.VERSION.RELEASE) |
||||
.build(); |
||||
return chain.proceed(requestWithUserAgent); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Android version Nougat has a regression where elliptic curve cipher suites are supported, but |
||||
* only the curve secp256r1 is allowed. So, first it's best to just disable all elliptic |
||||
* ciphers, try the connection, and fall back to the all cipher suites enabled list after. |
||||
*/ |
||||
private static void addNougatFixConnectionSpec(List<ConnectionSpec> specList) { |
||||
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.N) { |
||||
return; |
||||
} |
||||
SSLSocketFactory socketFactory; |
||||
try { |
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( |
||||
TrustManagerFactory.getDefaultAlgorithm()); |
||||
trustManagerFactory.init((KeyStore) null); |
||||
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); |
||||
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { |
||||
throw new IllegalStateException("Unexpected default trust managers:" |
||||
+ Arrays.toString(trustManagers)); |
||||
} |
||||
|
||||
X509TrustManager trustManager = (X509TrustManager) trustManagers[0]; |
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS"); |
||||
sslContext.init(null, new TrustManager[] { trustManager }, null); |
||||
socketFactory = sslContext.getSocketFactory(); |
||||
} catch (NoSuchAlgorithmException|KeyStoreException|KeyManagementException e) { |
||||
Log.e(TAG, "Failed obtaining the SSL socket factory."); |
||||
return; |
||||
} |
||||
String[] cipherSuites = socketFactory.getDefaultCipherSuites(); |
||||
ArrayList<String> allowedList = new ArrayList<>(); |
||||
for (String suite : cipherSuites) { |
||||
if (!suite.contains("ECDH")) { |
||||
allowedList.add(suite); |
||||
} |
||||
} |
||||
ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) |
||||
.cipherSuites(allowedList.toArray(new String[0])) |
||||
.supportsTlsExtensions(true) |
||||
.build(); |
||||
specList.add(spec); |
||||
} |
||||
|
||||
private static OkHttpClient.Builder enableHigherTlsOnPreLollipop(OkHttpClient.Builder builder) { |
||||
if (Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT < 22) { |
||||
try { |
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( |
||||
TrustManagerFactory.getDefaultAlgorithm()); |
||||
trustManagerFactory.init((KeyStore) null); |
||||
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); |
||||
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { |
||||
throw new IllegalStateException("Unexpected default trust managers:" |
||||
+ Arrays.toString(trustManagers)); |
||||
} |
||||
|
||||
X509TrustManager trustManager = (X509TrustManager) trustManagers[0]; |
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS"); |
||||
sslContext.init(null, new TrustManager[] { trustManager }, null); |
||||
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); |
||||
|
||||
builder.sslSocketFactory(new SSLSocketFactoryCompat(sslSocketFactory), |
||||
trustManager); |
||||
} catch (NoSuchAlgorithmException|KeyStoreException|KeyManagementException e) { |
||||
Log.e(TAG, "Failed enabling TLS 1.1 & 1.2. " + e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
return builder; |
||||
} |
||||
|
||||
private static class SSLSocketFactoryCompat extends SSLSocketFactory { |
||||
private static final String[] DESIRED_TLS_VERSIONS = { "TLSv1", "TLSv1.1", "TLSv1.2", |
||||
"TLSv1.3" }; |
||||
|
||||
final SSLSocketFactory delegate; |
||||
|
||||
SSLSocketFactoryCompat(SSLSocketFactory base) { |
||||
this.delegate = base; |
||||
} |
||||
|
||||
@Override |
||||
public String[] getDefaultCipherSuites() { |
||||
return delegate.getDefaultCipherSuites(); |
||||
} |
||||
|
||||
@Override |
||||
public String[] getSupportedCipherSuites() { |
||||
return delegate.getSupportedCipherSuites(); |
||||
} |
||||
|
||||
@Override |
||||
public Socket createSocket(Socket s, String host, int port, boolean autoClose) |
||||
throws IOException { |
||||
return patch(delegate.createSocket(s, host, port, autoClose)); |
||||
} |
||||
|
||||
@Override |
||||
public Socket createSocket(String host, int port) throws IOException { |
||||
return patch(delegate.createSocket(host, port)); |
||||
} |
||||
|
||||
@Override |
||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) |
||||
throws IOException { |
||||
return patch(delegate.createSocket(host, port, localHost, localPort)); |
||||
} |
||||
|
||||
@Override |
||||
public Socket createSocket(InetAddress host, int port) throws IOException { |
||||
return patch(delegate.createSocket(host, port)); |
||||
} |
||||
|
||||
@Override |
||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, |
||||
int localPort) throws IOException { |
||||
return patch(delegate.createSocket(address, port, localAddress, localPort)); |
||||
} |
||||
|
||||
@NonNull |
||||
private static String[] getMatches(String[] wanted, String[] have) { |
||||
List<String> a = new ArrayList<>(Arrays.asList(wanted)); |
||||
List<String> b = Arrays.asList(have); |
||||
a.retainAll(b); |
||||
return a.toArray(new String[0]); |
||||
} |
||||
|
||||
private Socket patch(Socket socket) { |
||||
if (socket instanceof SSLSocket) { |
||||
SSLSocket sslSocket = (SSLSocket) socket; |
||||
String[] protocols = getMatches(DESIRED_TLS_VERSIONS, |
||||
sslSocket.getSupportedProtocols()); |
||||
sslSocket.setEnabledProtocols(protocols); |
||||
} |
||||
return socket; |
||||
} |
||||
} |
||||
} |
@ -1,129 +0,0 @@ |
||||
/* Copyright 2017 Andrew Dawson |
||||
* |
||||
* This file is a part of Tusky. |
||||
* |
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the |
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
||||
* Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky; |
||||
|
||||
import android.text.Spannable; |
||||
import android.text.Spanned; |
||||
import android.text.style.ForegroundColorSpan; |
||||
|
||||
class SpanUtils { |
||||
private static class FindCharsResult { |
||||
int charIndex; |
||||
int stringIndex; |
||||
|
||||
FindCharsResult() { |
||||
charIndex = -1; |
||||
stringIndex = -1; |
||||
} |
||||
} |
||||
|
||||
private static FindCharsResult findChars(String string, int fromIndex, char[] chars) { |
||||
FindCharsResult result = new FindCharsResult(); |
||||
final int length = string.length(); |
||||
for (int i = fromIndex; i < length; i++) { |
||||
char c = string.charAt(i); |
||||
for (int j = 0; j < chars.length; j++) { |
||||
if (chars[j] == c) { |
||||
result.charIndex = j; |
||||
result.stringIndex = i; |
||||
return result; |
||||
} |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
private static FindCharsResult findStart(String string, int fromIndex, char[] chars) { |
||||
final int length = string.length(); |
||||
while (fromIndex < length) { |
||||
FindCharsResult found = findChars(string, fromIndex, chars); |
||||
int i = found.stringIndex; |
||||
if (i < 0) { |
||||
break; |
||||
} else if (i == 0 || i >= 1 && Character.isWhitespace(string.codePointBefore(i))) { |
||||
return found; |
||||
} else { |
||||
fromIndex = i + 1; |
||||
} |
||||
} |
||||
return new FindCharsResult(); |
||||
} |
||||
|
||||
private static int findEndOfHashtag(String string, int fromIndex) { |
||||
final int length = string.length(); |
||||
for (int i = fromIndex + 1; i < length;) { |
||||
int codepoint = string.codePointAt(i); |
||||
if (Character.isWhitespace(codepoint)) { |
||||
return i; |
||||
} else if (codepoint == '#') { |
||||
return -1; |
||||
} |
||||
i += Character.charCount(codepoint); |
||||
} |
||||
return length; |
||||
} |
||||
|
||||
private static int findEndOfMention(String string, int fromIndex) { |
||||
int atCount = 0; |
||||
final int length = string.length(); |
||||
for (int i = fromIndex + 1; i < length;) { |
||||
int codepoint = string.codePointAt(i); |
||||
if (Character.isWhitespace(codepoint)) { |
||||
return i; |
||||
} else if (codepoint == '@') { |
||||
atCount += 1; |
||||
if (atCount >= 2) { |
||||
return -1; |
||||
} |
||||
} |
||||
i += Character.charCount(codepoint); |
||||
} |
||||
return length; |
||||
} |
||||
|
||||
static void highlightSpans(Spannable text, int colour) { |
||||
// Strip all existing colour spans.
|
||||
int n = text.length(); |
||||
ForegroundColorSpan[] oldSpans = text.getSpans(0, n, ForegroundColorSpan.class); |
||||
for (int i = oldSpans.length - 1; i >= 0; i--) { |
||||
text.removeSpan(oldSpans[i]); |
||||
} |
||||
// Colour the mentions and hashtags.
|
||||
String string = text.toString(); |
||||
int start; |
||||
int end = 0; |
||||
while (end < n) { |
||||
char[] chars = { '#', '@' }; |
||||
FindCharsResult found = findStart(string, end, chars); |
||||
start = found.stringIndex; |
||||
if (start < 0) { |
||||
break; |
||||
} |
||||
if (found.charIndex == 0) { |
||||
end = findEndOfHashtag(string, start); |
||||
} else if (found.charIndex == 1) { |
||||
end = findEndOfMention(string, start); |
||||
} else { |
||||
break; |
||||
} |
||||
if (end < 0) { |
||||
break; |
||||
} |
||||
text.setSpan(new ForegroundColorSpan(colour), start, end, |
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE); |
||||
} |
||||
} |
||||
} |
@ -1,68 +0,0 @@ |
||||
/* Copyright 2017 Andrew Dawson |
||||
* |
||||
* This file is a part of Tusky. |
||||
* |
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the |
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
||||
* Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky; |
||||
|
||||
import android.content.Context; |
||||
import android.graphics.Color; |
||||
import android.graphics.PorterDuff; |
||||
import android.graphics.drawable.Drawable; |
||||
import android.support.annotation.AttrRes; |
||||
import android.support.annotation.ColorInt; |
||||
import android.support.annotation.DrawableRes; |
||||
import android.support.v4.content.ContextCompat; |
||||
import android.util.TypedValue; |
||||
import android.widget.ImageView; |
||||
|
||||
class ThemeUtils { |
||||
static Drawable getDrawable(Context context, @AttrRes int attribute, |
||||
@DrawableRes int fallbackDrawable) { |
||||
TypedValue value = new TypedValue(); |
||||
@DrawableRes int resourceId; |
||||
if (context.getTheme().resolveAttribute(attribute, value, true)) { |
||||
resourceId = value.resourceId; |
||||
} else { |
||||
resourceId = fallbackDrawable; |
||||
} |
||||
return ContextCompat.getDrawable(context, resourceId); |
||||
} |
||||
|
||||
static @DrawableRes int getDrawableId(Context context, @AttrRes int attribute, |
||||
@DrawableRes int fallbackDrawableId) { |
||||
TypedValue value = new TypedValue(); |
||||
if (context.getTheme().resolveAttribute(attribute, value, true)) { |
||||
return value.resourceId; |
||||
} else { |
||||
return fallbackDrawableId; |
||||
} |
||||
} |
||||
|
||||
static @ColorInt int getColor(Context context, @AttrRes int attribute) { |
||||
TypedValue value = new TypedValue(); |
||||
if (context.getTheme().resolveAttribute(attribute, value, true)) { |
||||
return value.data; |
||||
} else { |
||||
return Color.BLACK; |
||||
} |
||||
} |
||||
|
||||
static void setImageViewTint(ImageView view, @AttrRes int attribute) { |
||||
view.setColorFilter(getColor(view.getContext(), attribute), PorterDuff.Mode.SRC_IN); |
||||
} |
||||
|
||||
static void setDrawableTint(Context context, Drawable drawable, @AttrRes int attribute) { |
||||
drawable.setColorFilter(getColor(context, attribute), PorterDuff.Mode.SRC_IN); |
||||
} |
||||
} |
Loading…
Reference in new issue