Reorganizes the whole codebase.
This commit is contained in:
parent
bdca1d1c94
commit
aa2394748c
70 changed files with 1012 additions and 138 deletions
29
app/src/main/java/com/keylesspalace/tusky/util/Assert.java
Normal file
29
app/src/main/java/com/keylesspalace/tusky/util/Assert.java
Normal file
|
@ -0,0 +1,29 @@
|
|||
/* 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.util;
|
||||
|
||||
import com.keylesspalace.tusky.BuildConfig;
|
||||
|
||||
/** Android Studio complains about built-in assertions so this is an alternative. */
|
||||
public class Assert {
|
||||
private static boolean ENABLED = BuildConfig.DEBUG;
|
||||
|
||||
public static void expect(boolean expression) {
|
||||
if (ENABLED && !expression) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/* 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.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
|
||||
public class ConversationLineItemDecoration extends RecyclerView.ItemDecoration {
|
||||
private final Context mContext;
|
||||
private final Drawable mDivider;
|
||||
|
||||
public ConversationLineItemDecoration(Context context, Drawable divider) {
|
||||
mContext = context;
|
||||
mDivider = divider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
|
||||
int dividerLeft = parent.getPaddingLeft() + mContext.getResources().getDimensionPixelSize(R.dimen.status_left_line_margin);
|
||||
int dividerRight = dividerLeft + mDivider.getIntrinsicWidth();
|
||||
|
||||
int childCount = parent.getChildCount();
|
||||
int avatarMargin = mContext.getResources().getDimensionPixelSize(R.dimen.account_avatar_margin);
|
||||
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
View child = parent.getChildAt(i);
|
||||
|
||||
int dividerTop = child.getTop() + (i == 0 ? avatarMargin : 0);
|
||||
int dividerBottom = (i == childCount - 1 ? child.getTop() + avatarMargin : child.getBottom());
|
||||
|
||||
mDivider.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom);
|
||||
mDivider.draw(c);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/* 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.util;
|
||||
|
||||
public class CountUpDownLatch {
|
||||
private int count;
|
||||
|
||||
public CountUpDownLatch() {
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
public synchronized void countDown() {
|
||||
count--;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
public synchronized void countUp() {
|
||||
count++;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
public synchronized void await() throws InterruptedException {
|
||||
while (count != 0) {
|
||||
wait();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.customtabs.CustomTabsIntent;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.text.style.URLSpan;
|
||||
import android.view.View;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
|
||||
class CustomTabURLSpan extends URLSpan {
|
||||
CustomTabURLSpan(String url) {
|
||||
super(url);
|
||||
}
|
||||
|
||||
private CustomTabURLSpan(Parcel src) {
|
||||
super(src);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<CustomTabURLSpan> CREATOR = new Parcelable.Creator<CustomTabURLSpan>() {
|
||||
|
||||
@Override
|
||||
public CustomTabURLSpan createFromParcel(Parcel source) {
|
||||
return new CustomTabURLSpan(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomTabURLSpan[] newArray(int size) {
|
||||
return new CustomTabURLSpan[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
Uri uri = Uri.parse(getURL());
|
||||
Context context = widget.getContext();
|
||||
boolean lightTheme = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("lightTheme", false);
|
||||
int toolbarColor = ContextCompat.getColor(context, lightTheme ? R.color.custom_tab_toolbar_light : R.color.custom_tab_toolbar_dark);
|
||||
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
|
||||
builder.setToolbarColor(toolbarColor);
|
||||
CustomTabsIntent customTabsIntent = builder.build();
|
||||
try {
|
||||
String packageName = CustomTabsHelper.getPackageNameToUse(context);
|
||||
|
||||
//If we cant find a package name, it means theres no browser that supports
|
||||
//Chrome Custom Tabs installed. So, we fallback to the webview
|
||||
if (packageName == null) {
|
||||
super.onClick(widget);
|
||||
} else {
|
||||
customTabsIntent.intent.setPackage(packageName);
|
||||
customTabsIntent.launchUrl(context, uri);
|
||||
}
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w("URLSpan", "Activity was not found for intent, " + customTabsIntent.toString());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package com.keylesspalace.tusky.util;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* stolen from https://github.com/GoogleChrome/custom-tabs-client/blob/master/shared/src/main/java/org/chromium/customtabsclient/shared/CustomTabsHelper.java
|
||||
*/
|
||||
|
||||
public class CustomTabsHelper {
|
||||
private static final String TAG = "CustomTabsHelper";
|
||||
static final String STABLE_PACKAGE = "com.android.chrome";
|
||||
static final String BETA_PACKAGE = "com.chrome.beta";
|
||||
static final String DEV_PACKAGE = "com.chrome.dev";
|
||||
static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
|
||||
private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE =
|
||||
"android.support.customtabs.extra.KEEP_ALIVE";
|
||||
private static final String ACTION_CUSTOM_TABS_CONNECTION =
|
||||
"android.support.customtabs.action.CustomTabsService";
|
||||
|
||||
private static String sPackageNameToUse;
|
||||
|
||||
private CustomTabsHelper() {}
|
||||
|
||||
/**
|
||||
* Goes through all apps that handle VIEW intents and have a warmup service. Picks
|
||||
* the one chosen by the user if there is one, otherwise makes a best effort to return a
|
||||
* valid package name.
|
||||
*
|
||||
* This is <strong>not</strong> threadsafe.
|
||||
*
|
||||
* @param context {@link Context} to use for accessing {@link PackageManager}.
|
||||
* @return The package name recommended to use for connecting to custom tabs related components.
|
||||
*/
|
||||
public static String getPackageNameToUse(Context context) {
|
||||
if (sPackageNameToUse != null) return sPackageNameToUse;
|
||||
|
||||
PackageManager pm = context.getPackageManager();
|
||||
// Get default VIEW intent handler.
|
||||
Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
|
||||
ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0);
|
||||
String defaultViewHandlerPackageName = null;
|
||||
if (defaultViewHandlerInfo != null) {
|
||||
defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName;
|
||||
}
|
||||
|
||||
// Get all apps that can handle VIEW intents.
|
||||
List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, 0);
|
||||
List<String> packagesSupportingCustomTabs = new ArrayList<>();
|
||||
for (ResolveInfo info : resolvedActivityList) {
|
||||
Intent serviceIntent = new Intent();
|
||||
serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION);
|
||||
serviceIntent.setPackage(info.activityInfo.packageName);
|
||||
if (pm.resolveService(serviceIntent, 0) != null) {
|
||||
packagesSupportingCustomTabs.add(info.activityInfo.packageName);
|
||||
}
|
||||
}
|
||||
|
||||
// Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents
|
||||
// and service calls.
|
||||
if (packagesSupportingCustomTabs.isEmpty()) {
|
||||
sPackageNameToUse = null;
|
||||
} else if (packagesSupportingCustomTabs.size() == 1) {
|
||||
sPackageNameToUse = packagesSupportingCustomTabs.get(0);
|
||||
} else if (!TextUtils.isEmpty(defaultViewHandlerPackageName)
|
||||
&& !hasSpecializedHandlerIntents(context, activityIntent)
|
||||
&& packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) {
|
||||
sPackageNameToUse = defaultViewHandlerPackageName;
|
||||
} else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
|
||||
sPackageNameToUse = STABLE_PACKAGE;
|
||||
} else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
|
||||
sPackageNameToUse = BETA_PACKAGE;
|
||||
} else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
|
||||
sPackageNameToUse = DEV_PACKAGE;
|
||||
} else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
|
||||
sPackageNameToUse = LOCAL_PACKAGE;
|
||||
}
|
||||
return sPackageNameToUse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check whether there is a specialized handler for a given intent.
|
||||
* @param intent The intent to check with.
|
||||
* @return Whether there is a specialized handler for the given intent.
|
||||
*/
|
||||
private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) {
|
||||
try {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
List<ResolveInfo> handlers = pm.queryIntentActivities(
|
||||
intent,
|
||||
PackageManager.GET_RESOLVED_FILTER);
|
||||
if (handlers == null || handlers.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
for (ResolveInfo resolveInfo : handlers) {
|
||||
IntentFilter filter = resolveInfo.filter;
|
||||
if (filter == null) continue;
|
||||
if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue;
|
||||
if (resolveInfo.activityInfo == null) continue;
|
||||
return true;
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, "Runtime exception while getting specialized handlers");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return All possible chrome package names that provide custom tabs feature.
|
||||
*/
|
||||
public static String[] getPackages() {
|
||||
return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/* 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.util;
|
||||
|
||||
public 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. */
|
||||
public 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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
/* 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.util;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Matrix;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.media.ExifInterface;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||
private int sizeLimit;
|
||||
private ContentResolver contentResolver;
|
||||
private Listener listener;
|
||||
private List<byte[]> resultList;
|
||||
|
||||
public DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, Listener listener) {
|
||||
this.sizeLimit = sizeLimit;
|
||||
this.contentResolver = contentResolver;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Bitmap reorientBitmap(Bitmap bitmap, int orientation) {
|
||||
Matrix matrix = new Matrix();
|
||||
switch (orientation) {
|
||||
default:
|
||||
case ExifInterface.ORIENTATION_NORMAL: {
|
||||
return bitmap;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: {
|
||||
matrix.setScale(-1, 1);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_ROTATE_180: {
|
||||
matrix.setRotate(180);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_FLIP_VERTICAL: {
|
||||
matrix.setRotate(180);
|
||||
matrix.postScale(-1, 1);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_TRANSPOSE: {
|
||||
matrix.setRotate(90);
|
||||
matrix.postScale(-1, 1);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_ROTATE_90: {
|
||||
matrix.setRotate(90);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_TRANSVERSE: {
|
||||
matrix.setRotate(-90);
|
||||
matrix.postScale(-1, 1);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_ROTATE_270: {
|
||||
matrix.setRotate(-90);
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
|
||||
bitmap.getHeight(), matrix, true);
|
||||
if (!bitmap.sameAs(result)) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
return result;
|
||||
} catch (OutOfMemoryError e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getOrientation(Uri uri, ContentResolver contentResolver) {
|
||||
InputStream inputStream;
|
||||
try {
|
||||
inputStream = contentResolver.openInputStream(uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
if (inputStream == null) {
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
ExifInterface exifInterface;
|
||||
try {
|
||||
exifInterface = new ExifInterface(inputStream);
|
||||
} catch (IOException e) {
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.ORIENTATION_NORMAL);
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
return orientation;
|
||||
}
|
||||
|
||||
private static int calculateInSampleSize(int width, int height, int requiredScale) {
|
||||
int inSampleSize = 1;
|
||||
if (height > requiredScale || width > requiredScale) {
|
||||
final int halfHeight = height / 2;
|
||||
final int halfWidth = width / 2;
|
||||
/* Calculate the largest inSampleSize value that is a power of 2 and keeps both height
|
||||
* and width larger than the requested height and width. */
|
||||
while (halfHeight / inSampleSize >= requiredScale
|
||||
&& halfWidth / inSampleSize >= requiredScale) {
|
||||
inSampleSize *= 2;
|
||||
}
|
||||
}
|
||||
return inSampleSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Uri... uris) {
|
||||
resultList = new ArrayList<>();
|
||||
for (Uri uri : uris) {
|
||||
InputStream inputStream;
|
||||
try {
|
||||
inputStream = contentResolver.openInputStream(uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
// Initially, just get the image dimensions.
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeStream(inputStream, null, options);
|
||||
int beforeWidth = options.outWidth;
|
||||
int beforeHeight = options.outHeight;
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
// Get EXIF data, for orientation info.
|
||||
int orientation = getOrientation(uri, contentResolver);
|
||||
// Then use that information to determine how much to compress.
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
/* Unfortunately, there isn't a determined worst case compression ratio for image
|
||||
* formats. So, the only way to tell if they're too big is to compress them and
|
||||
* test, and keep trying at smaller sizes. The initial estimate should be good for
|
||||
* many cases, so it should only iterate once, but the loop is used to be absolutely
|
||||
* sure it gets downsized to below the limit. */
|
||||
int iterations = 0;
|
||||
int scaledImageSize = 1024;
|
||||
do {
|
||||
stream.reset();
|
||||
try {
|
||||
inputStream = contentResolver.openInputStream(uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
options.inSampleSize = calculateInSampleSize(beforeWidth, beforeHeight,
|
||||
scaledImageSize);
|
||||
options.inJustDecodeBounds = false;
|
||||
Bitmap scaledBitmap;
|
||||
try {
|
||||
scaledBitmap = BitmapFactory.decodeStream(inputStream, null, options);
|
||||
} catch (OutOfMemoryError error) {
|
||||
return false;
|
||||
} finally {
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
}
|
||||
if (scaledBitmap == null) {
|
||||
return false;
|
||||
}
|
||||
Bitmap reorientedBitmap = reorientBitmap(scaledBitmap, orientation);
|
||||
if (reorientedBitmap == null) {
|
||||
scaledBitmap.recycle();
|
||||
return false;
|
||||
}
|
||||
Bitmap.CompressFormat format;
|
||||
/* It's not likely the user will give transparent images over the upload limit, but
|
||||
* if they do, make sure the transparency is retained. */
|
||||
if (!reorientedBitmap.hasAlpha()) {
|
||||
format = Bitmap.CompressFormat.JPEG;
|
||||
} else {
|
||||
format = Bitmap.CompressFormat.PNG;
|
||||
}
|
||||
reorientedBitmap.compress(format, 75, stream);
|
||||
reorientedBitmap.recycle();
|
||||
scaledImageSize /= 2;
|
||||
iterations++;
|
||||
} while (stream.size() > sizeLimit);
|
||||
Assert.expect(iterations < 3);
|
||||
resultList.add(stream.toByteArray());
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean successful) {
|
||||
if (successful) {
|
||||
listener.onSuccess(resultList);
|
||||
} else {
|
||||
listener.onFailure();
|
||||
}
|
||||
super.onPostExecute(successful);
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onSuccess(List<byte[]> contentList);
|
||||
void onFailure();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/* 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.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v13.view.inputmethod.EditorInfoCompat;
|
||||
import android.support.v13.view.inputmethod.InputConnectionCompat;
|
||||
import android.support.v7.widget.AppCompatEditText;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
public class EditTextTyped extends AppCompatEditText {
|
||||
InputConnectionCompat.OnCommitContentListener onCommitContentListener;
|
||||
String[] mimeTypes;
|
||||
|
||||
public EditTextTyped(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public EditTextTyped(Context context, AttributeSet attributeSet) {
|
||||
super(context, attributeSet);
|
||||
}
|
||||
|
||||
public void setMimeTypes(String[] types,
|
||||
InputConnectionCompat.OnCommitContentListener listener) {
|
||||
mimeTypes = types;
|
||||
onCommitContentListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
|
||||
InputConnection connection = super.onCreateInputConnection(editorInfo);
|
||||
if (onCommitContentListener != null) {
|
||||
Assert.expect(mimeTypes != null);
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes);
|
||||
return InputConnectionCompat.createWrapper(connection, editorInfo,
|
||||
onCommitContentListener);
|
||||
} else {
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/* 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.util;
|
||||
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
||||
public abstract class EndlessOnScrollListener extends RecyclerView.OnScrollListener {
|
||||
private static final int VISIBLE_THRESHOLD = 15;
|
||||
private int currentPage;
|
||||
private int previousTotalItemCount;
|
||||
private boolean loading;
|
||||
private int startingPageIndex;
|
||||
private LinearLayoutManager layoutManager;
|
||||
|
||||
public EndlessOnScrollListener(LinearLayoutManager layoutManager) {
|
||||
this.layoutManager = layoutManager;
|
||||
currentPage = 0;
|
||||
previousTotalItemCount = 0;
|
||||
loading = true;
|
||||
startingPageIndex = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(RecyclerView view, int dx, int dy) {
|
||||
int totalItemCount = layoutManager.getItemCount();
|
||||
int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();
|
||||
if (totalItemCount < previousTotalItemCount) {
|
||||
currentPage = startingPageIndex;
|
||||
previousTotalItemCount = totalItemCount;
|
||||
if (totalItemCount == 0) {
|
||||
loading = true;
|
||||
}
|
||||
}
|
||||
if (loading && totalItemCount > previousTotalItemCount) {
|
||||
loading = false;
|
||||
previousTotalItemCount = totalItemCount;
|
||||
}
|
||||
if (!loading && lastVisibleItemPosition + VISIBLE_THRESHOLD > totalItemCount) {
|
||||
currentPage++;
|
||||
onLoadMore(currentPage, totalItemCount, view);
|
||||
loading = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
currentPage = startingPageIndex;
|
||||
previousTotalItemCount = 0;
|
||||
loading = true;
|
||||
}
|
||||
|
||||
public abstract void onLoadMore(int page, int totalItemsCount, RecyclerView view);
|
||||
}
|
107
app/src/main/java/com/keylesspalace/tusky/util/FlowLayout.java
Normal file
107
app/src/main/java/com/keylesspalace/tusky/util/FlowLayout.java
Normal file
|
@ -0,0 +1,107 @@
|
|||
/* 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.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
|
||||
public class FlowLayout extends ViewGroup {
|
||||
private int paddingHorizontal; // internal padding between child views
|
||||
private int paddingVertical; //
|
||||
private int totalHeight;
|
||||
|
||||
public FlowLayout(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(
|
||||
attrs, R.styleable.FlowLayout, 0, 0);
|
||||
try {
|
||||
paddingHorizontal = a.getDimensionPixelSize(
|
||||
R.styleable.FlowLayout_paddingHorizontal, 0);
|
||||
paddingVertical = a.getDimensionPixelSize(R.styleable.FlowLayout_paddingVertical, 0);
|
||||
} finally {
|
||||
a.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
Assert.expect(MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED);
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
|
||||
int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
|
||||
int count = getChildCount();
|
||||
int x = getPaddingLeft();
|
||||
int y = getPaddingTop();
|
||||
int childHeightMeasureSpec;
|
||||
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
|
||||
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
|
||||
} else {
|
||||
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
|
||||
}
|
||||
totalHeight = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
View child = getChildAt(i);
|
||||
if (child.getVisibility() != GONE) {
|
||||
child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
|
||||
childHeightMeasureSpec);
|
||||
int childwidth = child.getMeasuredWidth();
|
||||
totalHeight = Math.max(totalHeight, child.getMeasuredHeight() + paddingVertical);
|
||||
if (x + childwidth > width) {
|
||||
x = getPaddingLeft();
|
||||
y += totalHeight;
|
||||
}
|
||||
x += childwidth + paddingHorizontal;
|
||||
}
|
||||
}
|
||||
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
|
||||
height = y + totalHeight;
|
||||
} else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
|
||||
if (y + totalHeight < height) {
|
||||
height = y + totalHeight;
|
||||
}
|
||||
}
|
||||
height += 5; // Fudge to avoid clipping bottom of last row.
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
final int width = r - l;
|
||||
int x = getPaddingLeft();
|
||||
int y = getPaddingTop();
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
View child = getChildAt(i);
|
||||
if (child.getVisibility() != GONE) {
|
||||
int childWidth = child.getMeasuredWidth();
|
||||
int childHeight = child.getMeasuredHeight();
|
||||
if (x + childWidth > width) {
|
||||
x = getPaddingLeft();
|
||||
y += totalHeight;
|
||||
}
|
||||
child.layout(x, y, x + childWidth, y + childHeight);
|
||||
x += childWidth + paddingHorizontal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/* 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.util;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
44
app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java
Normal file
44
app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java
Normal file
|
@ -0,0 +1,44 @@
|
|||
/* 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.util;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class IOUtils {
|
||||
public static void closeQuietly(@Nullable InputStream stream) {
|
||||
try {
|
||||
if (stream != null) {
|
||||
stream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// intentionally unhandled
|
||||
}
|
||||
}
|
||||
|
||||
public static void closeQuietly(@Nullable OutputStream stream) {
|
||||
try {
|
||||
if (stream != null) {
|
||||
stream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// intentionally unhandled
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/* 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.util;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||
|
||||
public class LinkHelper {
|
||||
public static void setClickableText(TextView view, Spanned content,
|
||||
@Nullable Status.Mention[] mentions, boolean useCustomTabs,
|
||||
final LinkListener listener) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(content);
|
||||
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class);
|
||||
for (URLSpan span : urlSpans) {
|
||||
int start = builder.getSpanStart(span);
|
||||
int end = builder.getSpanEnd(span);
|
||||
int flags = builder.getSpanFlags(span);
|
||||
CharSequence text = builder.subSequence(start, end);
|
||||
if (text.charAt(0) == '#') {
|
||||
final String tag = text.subSequence(1, text.length()).toString();
|
||||
ClickableSpan newSpan = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
listener.onViewTag(tag);
|
||||
}
|
||||
};
|
||||
builder.removeSpan(span);
|
||||
builder.setSpan(newSpan, start, end, flags);
|
||||
} else if (text.charAt(0) == '@' && mentions != null) {
|
||||
final String accountUsername = text.subSequence(1, text.length()).toString();
|
||||
String id = null;
|
||||
for (Status.Mention mention : mentions) {
|
||||
if (mention.localUsername.equals(accountUsername)) {
|
||||
id = mention.id;
|
||||
}
|
||||
}
|
||||
if (id != null) {
|
||||
final String accountId = id;
|
||||
ClickableSpan newSpan = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
listener.onViewAccount(accountId);
|
||||
}
|
||||
};
|
||||
builder.removeSpan(span);
|
||||
builder.setSpan(newSpan, start, end, flags);
|
||||
}
|
||||
} else if (useCustomTabs) {
|
||||
ClickableSpan newSpan = new CustomTabURLSpan(span.getURL());
|
||||
builder.removeSpan(span);
|
||||
builder.setSpan(newSpan, start, end, flags);
|
||||
}
|
||||
}
|
||||
view.setText(builder);
|
||||
view.setLinksClickable(true);
|
||||
view.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
}
|
53
app/src/main/java/com/keylesspalace/tusky/util/Log.java
Normal file
53
app/src/main/java/com/keylesspalace/tusky/util/Log.java
Normal file
|
@ -0,0 +1,53 @@
|
|||
/* 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.util;
|
||||
|
||||
import com.keylesspalace.tusky.BuildConfig;
|
||||
|
||||
/**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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/* 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.util;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public class NotificationClearBroadcastReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
SharedPreferences notificationPreferences = context.getSharedPreferences("Notifications", Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = notificationPreferences.edit();
|
||||
editor.putString("current", "[]");
|
||||
editor.apply();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
/* 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.util;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.TaskStackBuilder;
|
||||
|
||||
import com.keylesspalace.tusky.MainActivity;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.squareup.picasso.Picasso;
|
||||
import com.squareup.picasso.Target;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
public class NotificationMaker {
|
||||
public static void make(final Context context, final int notifyId, Notification body) {
|
||||
final SharedPreferences preferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final SharedPreferences notificationPreferences = context.getSharedPreferences(
|
||||
"Notifications", Context.MODE_PRIVATE);
|
||||
|
||||
if (!filterNotification(preferences, body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String rawCurrentNotifications = notificationPreferences.getString("current", "[]");
|
||||
JSONArray currentNotifications;
|
||||
|
||||
try {
|
||||
currentNotifications = new JSONArray(rawCurrentNotifications);
|
||||
} catch (JSONException e) {
|
||||
currentNotifications = new JSONArray();
|
||||
}
|
||||
|
||||
boolean alreadyContains = false;
|
||||
|
||||
for(int i = 0; i < currentNotifications.length(); i++) {
|
||||
try {
|
||||
if (currentNotifications.getString(i).equals(body.account.getDisplayName())) {
|
||||
alreadyContains = true;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyContains) {
|
||||
currentNotifications.put(body.account.getDisplayName());
|
||||
}
|
||||
|
||||
notificationPreferences.edit()
|
||||
.putString("current", currentNotifications.toString())
|
||||
.commit();
|
||||
|
||||
Intent resultIntent = new Intent(context, MainActivity.class);
|
||||
resultIntent.putExtra("tab_position", 1);
|
||||
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
|
||||
stackBuilder.addParentStack(MainActivity.class);
|
||||
stackBuilder.addNextIntent(resultIntent);
|
||||
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class);
|
||||
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, 0, deleteIntent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
|
||||
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setContentIntent(resultPendingIntent)
|
||||
.setDeleteIntent(deletePendingIntent)
|
||||
.setDefaults(0); // So it doesn't ring twice, notify only in Target callback
|
||||
|
||||
if (currentNotifications.length() == 1) {
|
||||
builder.setContentTitle(titleForType(context, body))
|
||||
.setContentText(truncateWithEllipses(bodyForType(body), 40));
|
||||
|
||||
Target mTarget = new Target() {
|
||||
@Override
|
||||
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
|
||||
builder.setLargeIcon(bitmap);
|
||||
|
||||
setupPreferences(preferences, builder);
|
||||
|
||||
((NotificationManager) (context.getSystemService(Context.NOTIFICATION_SERVICE)))
|
||||
.notify(notifyId, builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBitmapFailed(Drawable errorDrawable) {}
|
||||
|
||||
@Override
|
||||
public void onPrepareLoad(Drawable placeHolderDrawable) {}
|
||||
};
|
||||
|
||||
Picasso.with(context)
|
||||
.load(body.account.avatar)
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
.transform(new RoundedTransformation(7, 0))
|
||||
.into(mTarget);
|
||||
} else {
|
||||
setupPreferences(preferences, builder);
|
||||
try {
|
||||
builder.setContentTitle(String.format(context.getString(R.string.notification_title_summary), currentNotifications.length()))
|
||||
.setContentText(truncateWithEllipses(joinNames(context, currentNotifications), 40));
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
builder.setVisibility(android.app.Notification.VISIBILITY_PRIVATE);
|
||||
builder.setCategory(android.app.Notification.CATEGORY_SOCIAL);
|
||||
}
|
||||
|
||||
((NotificationManager) (context.getSystemService(Context.NOTIFICATION_SERVICE)))
|
||||
.notify(notifyId, builder.build());
|
||||
}
|
||||
|
||||
private static boolean filterNotification(SharedPreferences preferences,
|
||||
Notification notification) {
|
||||
switch (notification.type) {
|
||||
default:
|
||||
case MENTION: {
|
||||
return preferences.getBoolean("notificationFilterMentions", true);
|
||||
}
|
||||
case FOLLOW: {
|
||||
return preferences.getBoolean("notificationFilterFollows", true);
|
||||
}
|
||||
case REBLOG: {
|
||||
return preferences.getBoolean("notificationFilterReblogs", true);
|
||||
}
|
||||
case FAVOURITE: {
|
||||
return preferences.getBoolean("notificationFilterFavourites", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String truncateWithEllipses(String string, int limit) {
|
||||
if (string.length() < limit) {
|
||||
return string;
|
||||
} else {
|
||||
return string.substring(0, limit - 3) + "...";
|
||||
}
|
||||
}
|
||||
|
||||
private static void setupPreferences(SharedPreferences preferences,
|
||||
NotificationCompat.Builder builder) {
|
||||
if (preferences.getBoolean("notificationAlertSound", true)) {
|
||||
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
|
||||
}
|
||||
|
||||
if (preferences.getBoolean("notificationAlertVibrate", false)) {
|
||||
builder.setVibrate(new long[] { 500, 500 });
|
||||
}
|
||||
|
||||
if (preferences.getBoolean("notificationAlertLight", false)) {
|
||||
builder.setLights(0xFF00FF8F, 300, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String joinNames(Context context, JSONArray array) throws JSONException {
|
||||
if (array.length() > 3) {
|
||||
return String.format(context.getString(R.string.notification_summary_large), array.get(0), array.get(1), array.get(2), array.length() - 3);
|
||||
} else if (array.length() == 3) {
|
||||
return String.format(context.getString(R.string.notification_summary_medium), array.get(0), array.get(1), array.get(2));
|
||||
} else if (array.length() == 2) {
|
||||
return String.format(context.getString(R.string.notification_summary_small), array.get(0), array.get(1));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String titleForType(Context context, Notification notification) {
|
||||
switch (notification.type) {
|
||||
case MENTION:
|
||||
return String.format(context.getString(R.string.notification_mention_format), notification.account.getDisplayName());
|
||||
case FOLLOW:
|
||||
return String.format(context.getString(R.string.notification_follow_format), notification.account.getDisplayName());
|
||||
case FAVOURITE:
|
||||
return String.format(context.getString(R.string.notification_favourite_format), notification.account.getDisplayName());
|
||||
case REBLOG:
|
||||
return String.format(context.getString(R.string.notification_reblog_format), notification.account.getDisplayName());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String bodyForType(Notification notification) {
|
||||
switch (notification.type) {
|
||||
case FOLLOW:
|
||||
return notification.account.username;
|
||||
case MENTION:
|
||||
case FAVOURITE:
|
||||
case REBLOG:
|
||||
return notification.status.content.toString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
244
app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java
Normal file
244
app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java
Normal file
|
@ -0,0 +1,244 @@
|
|||
/* 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.util;
|
||||
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.keylesspalace.tusky.BuildConfig;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/* 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.util;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapShader;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
|
||||
import com.squareup.picasso.Transformation;
|
||||
|
||||
public class RoundedTransformation implements Transformation {
|
||||
|
||||
private final int radius;
|
||||
private final int margin;
|
||||
|
||||
public RoundedTransformation(final int radius, final int margin) {
|
||||
this.radius = radius;
|
||||
this.margin = margin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bitmap transform(Bitmap source) {
|
||||
final Paint paint = new Paint();
|
||||
|
||||
paint.setAntiAlias(true);
|
||||
paint.setShader(new BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
|
||||
|
||||
Bitmap output = Bitmap.createBitmap(source.getWidth(), source.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(output);
|
||||
|
||||
canvas.drawRoundRect(new RectF(margin, margin, source.getWidth() - margin, source.getHeight() - margin), radius, radius, paint);
|
||||
|
||||
if (source != output) {
|
||||
source.recycle();
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return "rounded";
|
||||
}
|
||||
}
|
129
app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.java
Normal file
129
app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.java
Normal file
|
@ -0,0 +1,129 @@
|
|||
/* 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.util;
|
||||
|
||||
import android.text.Spannable;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
public 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/* 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.util;
|
||||
|
||||
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;
|
||||
|
||||
public class ThemeUtils {
|
||||
public 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);
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setImageViewTint(ImageView view, @AttrRes int attribute) {
|
||||
view.setColorFilter(getColor(view.getContext(), attribute), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
public static void setDrawableTint(Context context, Drawable drawable, @AttrRes int attribute) {
|
||||
drawable.setColorFilter(getColor(context, attribute), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue