2018-03-12 00:05:53 +01:00
|
|
|
/*#######################################################
|
2017-05-29 19:05:37 +02:00
|
|
|
*
|
2018-03-12 00:05:53 +01:00
|
|
|
* Maintained by Gregor Santner, 2016-
|
|
|
|
* https://gsantner.net/
|
|
|
|
*
|
2018-10-01 21:12:17 +02:00
|
|
|
* License of this file: Apache 2.0 (Commercial upon request)
|
|
|
|
* https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
* https://github.com/gsantner/opoc/#licensing
|
2018-03-12 00:05:53 +01:00
|
|
|
*
|
|
|
|
#########################################################*/
|
2017-09-09 17:09:04 +02:00
|
|
|
package net.gsantner.opoc.util;
|
2017-05-29 19:05:37 +02:00
|
|
|
|
|
|
|
import android.annotation.SuppressLint;
|
2018-07-25 01:59:25 +02:00
|
|
|
import android.app.Activity;
|
2019-09-07 21:54:08 +02:00
|
|
|
import android.app.ActivityManager;
|
2017-05-29 19:05:37 +02:00
|
|
|
import android.app.AlarmManager;
|
|
|
|
import android.app.PendingIntent;
|
|
|
|
import android.content.ActivityNotFoundException;
|
2019-01-11 02:38:30 +01:00
|
|
|
import android.content.ContentResolver;
|
2017-05-29 19:05:37 +02:00
|
|
|
import android.content.Context;
|
|
|
|
import android.content.Intent;
|
|
|
|
import android.content.pm.PackageInfo;
|
|
|
|
import android.content.pm.PackageManager;
|
|
|
|
import android.content.res.Configuration;
|
2017-08-24 13:34:32 +02:00
|
|
|
import android.content.res.Resources;
|
2017-09-09 17:09:04 +02:00
|
|
|
import android.graphics.Bitmap;
|
|
|
|
import android.graphics.BitmapFactory;
|
|
|
|
import android.graphics.Canvas;
|
2017-08-13 23:02:04 +02:00
|
|
|
import android.graphics.Color;
|
2017-09-09 17:09:04 +02:00
|
|
|
import android.graphics.Matrix;
|
2017-10-28 09:56:05 +02:00
|
|
|
import android.graphics.Paint;
|
|
|
|
import android.graphics.Rect;
|
2018-03-30 00:14:54 +02:00
|
|
|
import android.graphics.drawable.AdaptiveIconDrawable;
|
2017-09-09 17:09:04 +02:00
|
|
|
import android.graphics.drawable.BitmapDrawable;
|
2017-05-29 19:05:37 +02:00
|
|
|
import android.graphics.drawable.Drawable;
|
2017-09-09 17:09:04 +02:00
|
|
|
import android.graphics.drawable.VectorDrawable;
|
2018-04-08 17:52:04 +02:00
|
|
|
import android.media.MediaScannerConnection;
|
2017-05-29 19:05:37 +02:00
|
|
|
import android.net.ConnectivityManager;
|
|
|
|
import android.net.NetworkInfo;
|
|
|
|
import android.net.Uri;
|
2017-09-09 17:09:04 +02:00
|
|
|
import android.os.Build;
|
2019-07-26 03:19:28 +02:00
|
|
|
import android.os.Environment;
|
2019-01-11 02:38:30 +01:00
|
|
|
import android.os.SystemClock;
|
2019-11-20 00:34:10 +01:00
|
|
|
import android.os.VibrationEffect;
|
|
|
|
import android.os.Vibrator;
|
2017-10-29 14:47:00 +01:00
|
|
|
import android.support.annotation.ColorInt;
|
2017-05-29 19:05:37 +02:00
|
|
|
import android.support.annotation.ColorRes;
|
|
|
|
import android.support.annotation.DrawableRes;
|
2017-09-09 17:09:04 +02:00
|
|
|
import android.support.annotation.Nullable;
|
2017-05-29 19:05:37 +02:00
|
|
|
import android.support.annotation.RawRes;
|
|
|
|
import android.support.annotation.StringRes;
|
2017-09-09 17:09:04 +02:00
|
|
|
import android.support.graphics.drawable.VectorDrawableCompat;
|
2019-09-07 21:54:08 +02:00
|
|
|
import android.support.v4.app.ActivityManagerCompat;
|
2017-05-29 19:05:37 +02:00
|
|
|
import android.support.v4.content.ContextCompat;
|
2017-09-09 17:09:04 +02:00
|
|
|
import android.support.v4.graphics.drawable.DrawableCompat;
|
2019-11-20 00:34:10 +01:00
|
|
|
import android.support.v4.text.TextUtilsCompat;
|
2019-07-26 03:19:28 +02:00
|
|
|
import android.support.v4.util.Pair;
|
2019-11-20 00:34:10 +01:00
|
|
|
import android.support.v4.view.ViewCompat;
|
2017-07-29 04:44:28 +02:00
|
|
|
import android.text.Html;
|
2019-01-11 02:38:30 +01:00
|
|
|
import android.text.InputFilter;
|
2017-07-29 04:44:28 +02:00
|
|
|
import android.text.SpannableString;
|
|
|
|
import android.text.Spanned;
|
2017-05-29 19:05:37 +02:00
|
|
|
import android.text.TextUtils;
|
2017-07-29 04:44:28 +02:00
|
|
|
import android.text.method.LinkMovementMethod;
|
2017-05-29 19:05:37 +02:00
|
|
|
import android.util.DisplayMetrics;
|
2018-03-12 00:05:53 +01:00
|
|
|
import android.util.Log;
|
2017-10-29 14:47:00 +01:00
|
|
|
import android.view.Menu;
|
|
|
|
import android.view.MenuItem;
|
2019-01-11 02:38:30 +01:00
|
|
|
import android.view.MotionEvent;
|
2021-01-12 20:23:10 +01:00
|
|
|
import android.view.Surface;
|
2019-01-11 02:38:30 +01:00
|
|
|
import android.view.View;
|
2021-01-12 20:23:10 +01:00
|
|
|
import android.view.WindowManager;
|
2019-01-11 02:38:30 +01:00
|
|
|
import android.webkit.MimeTypeMap;
|
2017-09-09 17:09:04 +02:00
|
|
|
import android.widget.ImageView;
|
2017-07-29 04:44:28 +02:00
|
|
|
import android.widget.TextView;
|
2017-05-29 19:05:37 +02:00
|
|
|
|
2018-03-02 15:56:14 +01:00
|
|
|
import net.gsantner.opoc.format.markdown.SimpleMarkdownParser;
|
|
|
|
|
2017-05-29 19:05:37 +02:00
|
|
|
import java.io.BufferedReader;
|
2017-09-09 17:09:04 +02:00
|
|
|
import java.io.File;
|
|
|
|
import java.io.FileNotFoundException;
|
|
|
|
import java.io.FileOutputStream;
|
2017-05-29 19:05:37 +02:00
|
|
|
import java.io.IOException;
|
|
|
|
import java.io.InputStreamReader;
|
2017-10-29 14:47:00 +01:00
|
|
|
import java.lang.reflect.Method;
|
2019-01-11 02:38:30 +01:00
|
|
|
import java.text.SimpleDateFormat;
|
2019-07-26 03:19:28 +02:00
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.List;
|
2017-05-29 19:05:37 +02:00
|
|
|
import java.util.Locale;
|
|
|
|
|
2019-11-20 00:34:10 +01:00
|
|
|
import static android.content.Context.VIBRATOR_SERVICE;
|
2018-03-12 00:05:53 +01:00
|
|
|
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
|
2017-09-09 17:09:04 +02:00
|
|
|
import static android.graphics.Bitmap.CompressFormat;
|
|
|
|
|
2021-01-12 20:23:10 +01:00
|
|
|
@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "ObsoleteSdkInt", "deprecation", "SpellCheckingInspection", "TryFinallyCanBeTryWithResources", "UnusedAssignment", "UnusedReturnValue"})
|
2017-09-09 17:09:04 +02:00
|
|
|
public class ContextUtils {
|
2018-03-30 00:14:54 +02:00
|
|
|
//
|
|
|
|
// Members, Constructors
|
|
|
|
//
|
2017-08-09 17:23:19 +02:00
|
|
|
protected Context _context;
|
2017-05-29 19:05:37 +02:00
|
|
|
|
2017-09-09 17:09:04 +02:00
|
|
|
public ContextUtils(Context context) {
|
2017-08-09 17:23:19 +02:00
|
|
|
_context = context;
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
|
|
|
|
2017-08-29 14:44:43 +02:00
|
|
|
public Context context() {
|
|
|
|
return _context;
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
|
|
|
|
2019-07-26 03:19:28 +02:00
|
|
|
public void freeContextRef() {
|
|
|
|
_context = null;
|
|
|
|
}
|
2018-03-30 00:14:54 +02:00
|
|
|
|
|
|
|
//
|
|
|
|
// Class Methods
|
|
|
|
//
|
|
|
|
public enum ResType {
|
|
|
|
ID, BOOL, INTEGER, COLOR, STRING, ARRAY, DRAWABLE, PLURALS,
|
|
|
|
ANIM, ATTR, DIMEN, LAYOUT, MENU, RAW, STYLE, XML,
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find out the nuermical ressource id by given {@link ResType}
|
|
|
|
*
|
|
|
|
* @return A valid id if the id could be found, else 0
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public int getResId(final ResType resType, final String name) {
|
|
|
|
try {
|
|
|
|
return _context.getResources().getIdentifier(name, resType.name().toLowerCase(), _context.getPackageName());
|
|
|
|
} catch (Exception e) {
|
|
|
|
return 0;
|
|
|
|
}
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Get String by given string ressource id (nuermic)
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public String rstr(@StringRes final int strResId) {
|
|
|
|
try {
|
|
|
|
return _context.getString(strResId);
|
|
|
|
} catch (Exception e) {
|
|
|
|
return null;
|
|
|
|
}
|
2017-08-29 14:44:43 +02:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Get String by given string ressource identifier (textual)
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public String rstr(final String strResKey) {
|
2018-03-30 00:14:54 +02:00
|
|
|
try {
|
|
|
|
return rstr(getResId(ResType.STRING, strResKey));
|
|
|
|
} catch (Resources.NotFoundException e) {
|
|
|
|
return null;
|
|
|
|
}
|
2018-03-05 23:37:24 +01:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Get drawable from given ressource identifier
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public Drawable rdrawable(@DrawableRes final int resId) {
|
|
|
|
try {
|
|
|
|
return ContextCompat.getDrawable(_context, resId);
|
|
|
|
} catch (Exception e) {
|
|
|
|
return null;
|
|
|
|
}
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Get color by given color ressource id
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public int rcolor(@ColorRes final int resId) {
|
|
|
|
if (resId == 0) {
|
|
|
|
Log.e(getClass().getName(), "ContextUtils::rcolor: resId is 0!");
|
|
|
|
return Color.BLACK;
|
|
|
|
}
|
2017-08-09 17:23:19 +02:00
|
|
|
return ContextCompat.getColor(_context, resId);
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Checks if all given (textual) ressource ids are available
|
|
|
|
*
|
|
|
|
* @param resType A {@link ResType}
|
|
|
|
* @param resIdsTextual A (textual) identifier to be awaited at R.restype.resIdsTextual
|
|
|
|
* @return True if all given ids are available
|
|
|
|
*/
|
|
|
|
public boolean areRessourcesAvailable(final ResType resType, final String... resIdsTextual) {
|
|
|
|
for (String name : resIdsTextual) {
|
|
|
|
if (getResId(resType, name) == 0) {
|
2017-08-29 14:44:43 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Convert an int color to a hex string. Optionally including alpha value.
|
|
|
|
*
|
|
|
|
* @param intColor The color coded in int
|
|
|
|
* @param withAlpha Optional; Set first bool parameter to true to also include alpha value
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public static String colorToHexString(final int intColor, final boolean... withAlpha) {
|
2018-03-30 00:14:54 +02:00
|
|
|
boolean a = withAlpha != null && withAlpha.length >= 1 && withAlpha[0];
|
|
|
|
return String.format(a ? "#%08X" : "#%06X", (a ? 0xFFFFFFFF : 0xFFFFFF) & intColor);
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
|
|
|
|
2019-11-20 00:34:10 +01:00
|
|
|
public static String getAndroidVersion() {
|
2019-07-26 03:19:28 +02:00
|
|
|
return Build.VERSION.RELEASE + " (" + Build.VERSION.SDK_INT + ")";
|
|
|
|
}
|
|
|
|
|
2017-05-29 19:05:37 +02:00
|
|
|
public String getAppVersionName() {
|
2019-07-26 03:19:28 +02:00
|
|
|
PackageManager manager = _context.getPackageManager();
|
2017-05-29 19:05:37 +02:00
|
|
|
try {
|
2018-10-01 21:12:17 +02:00
|
|
|
PackageInfo info = manager.getPackageInfo(getPackageIdManifest(), 0);
|
2017-05-29 19:05:37 +02:00
|
|
|
return info.versionName;
|
|
|
|
} catch (PackageManager.NameNotFoundException e) {
|
2019-07-26 03:19:28 +02:00
|
|
|
try {
|
|
|
|
PackageInfo info = manager.getPackageInfo(getPackageIdReal(), 0);
|
|
|
|
return info.versionName;
|
|
|
|
} catch (PackageManager.NameNotFoundException ignored) {
|
|
|
|
}
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
2019-07-26 03:19:28 +02:00
|
|
|
return "?";
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
|
|
|
|
2018-05-13 12:08:27 +02:00
|
|
|
public String getAppInstallationSource() {
|
|
|
|
String src = null;
|
|
|
|
try {
|
2018-10-01 21:12:17 +02:00
|
|
|
src = _context.getPackageManager().getInstallerPackageName(getPackageIdManifest());
|
2018-05-13 12:08:27 +02:00
|
|
|
} catch (Exception ignored) {
|
|
|
|
}
|
2019-11-20 00:34:10 +01:00
|
|
|
if (src == null || src.trim().isEmpty()) {
|
2018-05-13 12:08:27 +02:00
|
|
|
return "Sideloaded";
|
|
|
|
} else if (src.toLowerCase().contains(".amazon.")) {
|
|
|
|
return "Amazon Appstore";
|
|
|
|
}
|
|
|
|
switch (src) {
|
|
|
|
case "com.android.vending":
|
|
|
|
case "com.google.android.feedback": {
|
2019-11-20 00:34:10 +01:00
|
|
|
return "Google Play";
|
2018-05-13 12:08:27 +02:00
|
|
|
}
|
|
|
|
case "org.fdroid.fdroid.privileged":
|
|
|
|
case "org.fdroid.fdroid": {
|
|
|
|
return "F-Droid";
|
|
|
|
}
|
|
|
|
case "com.github.yeriomin.yalpstore": {
|
|
|
|
return "Yalp Store";
|
|
|
|
}
|
|
|
|
case "cm.aptoide.pt": {
|
|
|
|
return "Aptoide";
|
|
|
|
}
|
|
|
|
case "com.android.packageinstaller": {
|
|
|
|
return "Package Installer";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return src;
|
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Send a {@link Intent#ACTION_VIEW} Intent with given paramter
|
|
|
|
* If the parameter is an string a browser will get triggered
|
|
|
|
*/
|
2021-01-12 20:23:10 +01:00
|
|
|
public ContextUtils openWebpageInExternalBrowser(final String url) {
|
2018-03-02 15:56:14 +01:00
|
|
|
try {
|
2019-11-20 00:34:10 +01:00
|
|
|
Uri uri = Uri.parse(url);
|
|
|
|
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
|
|
|
|
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
2018-03-02 15:56:14 +01:00
|
|
|
_context.startActivity(intent);
|
2019-11-20 00:34:10 +01:00
|
|
|
} catch (Exception e) {
|
2018-03-02 15:56:14 +01:00
|
|
|
e.printStackTrace();
|
|
|
|
}
|
2021-01-12 20:23:10 +01:00
|
|
|
return this;
|
2017-08-09 17:23:19 +02:00
|
|
|
}
|
|
|
|
|
2018-05-13 12:08:27 +02:00
|
|
|
/**
|
2018-10-01 21:12:17 +02:00
|
|
|
* Get the apps base packagename, which is equal with all build flavors and variants
|
2018-05-13 12:08:27 +02:00
|
|
|
*/
|
2018-10-01 21:12:17 +02:00
|
|
|
public String getPackageIdManifest() {
|
2018-05-13 12:08:27 +02:00
|
|
|
String pkg = rstr("manifest_package_id");
|
2019-11-20 00:34:10 +01:00
|
|
|
return !TextUtils.isEmpty(pkg) ? pkg : _context.getPackageName();
|
2018-05-13 12:08:27 +02:00
|
|
|
}
|
|
|
|
|
2018-10-01 21:12:17 +02:00
|
|
|
/**
|
|
|
|
* Get this apps package name, returns the flavor specific package name.
|
|
|
|
*/
|
|
|
|
public String getPackageIdReal() {
|
2019-01-11 02:38:30 +01:00
|
|
|
return _context.getPackageName();
|
2018-10-01 21:12:17 +02:00
|
|
|
}
|
|
|
|
|
2017-08-09 17:23:19 +02:00
|
|
|
/**
|
2018-03-05 23:37:24 +01:00
|
|
|
* Get field from ${applicationId}.BuildConfig
|
2017-08-24 13:34:32 +02:00
|
|
|
* May be helpful in libraries, where a access to
|
|
|
|
* BuildConfig would only get values of the library
|
2018-03-05 23:37:24 +01:00
|
|
|
* rather than the app ones. It awaits a string resource
|
|
|
|
* of the package set in manifest (root element).
|
|
|
|
* Falls back to applicationId of the app which may differ from manifest.
|
2017-08-09 17:23:19 +02:00
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public Object getBuildConfigValue(final String fieldName) {
|
2018-10-01 21:12:17 +02:00
|
|
|
String pkg = getPackageIdManifest() + ".BuildConfig";
|
2018-03-05 23:37:24 +01:00
|
|
|
try {
|
|
|
|
Class<?> c = Class.forName(pkg);
|
2017-08-24 13:34:32 +02:00
|
|
|
return c.getField(fieldName).get(null);
|
|
|
|
} catch (Exception e) {
|
2017-08-09 17:23:19 +02:00
|
|
|
e.printStackTrace();
|
2017-08-24 13:34:32 +02:00
|
|
|
return null;
|
2017-08-09 17:23:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Get a BuildConfig bool value
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public Boolean bcbool(final String fieldName, final Boolean defaultValue) {
|
2017-08-09 17:23:19 +02:00
|
|
|
Object field = getBuildConfigValue(fieldName);
|
2019-11-20 00:34:10 +01:00
|
|
|
if (field instanceof Boolean) {
|
2017-08-09 17:23:19 +02:00
|
|
|
return (Boolean) field;
|
|
|
|
}
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Get a BuildConfig string value
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public String bcstr(final String fieldName, final String defaultValue) {
|
2018-03-05 23:37:24 +01:00
|
|
|
Object field = getBuildConfigValue(fieldName);
|
2019-11-20 00:34:10 +01:00
|
|
|
if (field instanceof String) {
|
2018-03-05 23:37:24 +01:00
|
|
|
return (String) field;
|
|
|
|
}
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
|
2018-05-13 12:08:27 +02:00
|
|
|
/**
|
|
|
|
* Get a BuildConfig string value
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public Integer bcint(final String fieldName, final int defaultValue) {
|
2018-05-13 12:08:27 +02:00
|
|
|
Object field = getBuildConfigValue(fieldName);
|
2019-11-20 00:34:10 +01:00
|
|
|
if (field instanceof Integer) {
|
2018-05-13 12:08:27 +02:00
|
|
|
return (Integer) field;
|
|
|
|
}
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Check if this is a gplay build (requires BuildConfig field)
|
|
|
|
*/
|
2017-08-09 17:23:19 +02:00
|
|
|
public boolean isGooglePlayBuild() {
|
2018-03-05 23:37:24 +01:00
|
|
|
return bcbool("IS_GPLAY_BUILD", true);
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Check if this is a foss build (requires BuildConfig field)
|
|
|
|
*/
|
2017-08-09 17:23:19 +02:00
|
|
|
public boolean isFossBuild() {
|
2018-03-05 23:37:24 +01:00
|
|
|
return bcbool("IS_FOSS_BUILD", false);
|
2017-08-09 17:23:19 +02:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Request a bitcoin donation with given details.
|
|
|
|
* All parameters are awaited as string resource ids
|
|
|
|
*/
|
|
|
|
public void showDonateBitcoinRequest(@StringRes final int srBitcoinId, @StringRes final int srBitcoinAmount, @StringRes final int srBitcoinMessage, @StringRes final int srAlternativeDonateUrl) {
|
2017-08-09 17:23:19 +02:00
|
|
|
if (!isGooglePlayBuild()) {
|
2017-05-29 19:05:37 +02:00
|
|
|
String btcUri = String.format("bitcoin:%s?amount=%s&label=%s&message=%s",
|
2018-03-30 00:14:54 +02:00
|
|
|
rstr(srBitcoinId), rstr(srBitcoinAmount),
|
|
|
|
rstr(srBitcoinMessage), rstr(srBitcoinMessage));
|
2017-05-29 19:05:37 +02:00
|
|
|
Intent intent = new Intent(Intent.ACTION_VIEW);
|
|
|
|
intent.setData(Uri.parse(btcUri));
|
2018-03-12 00:05:53 +01:00
|
|
|
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
2017-05-29 19:05:37 +02:00
|
|
|
try {
|
2017-08-09 17:23:19 +02:00
|
|
|
_context.startActivity(intent);
|
2017-05-29 19:05:37 +02:00
|
|
|
} catch (ActivityNotFoundException e) {
|
2018-03-30 00:14:54 +02:00
|
|
|
openWebpageInExternalBrowser(rstr(srAlternativeDonateUrl));
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public String readTextfileFromRawRes(@RawRes int rawResId, String linePrefix, String linePostfix) {
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
BufferedReader br = null;
|
|
|
|
String line;
|
|
|
|
|
|
|
|
linePrefix = linePrefix == null ? "" : linePrefix;
|
|
|
|
linePostfix = linePostfix == null ? "" : linePostfix;
|
|
|
|
|
|
|
|
try {
|
2017-08-09 17:23:19 +02:00
|
|
|
br = new BufferedReader(new InputStreamReader(_context.getResources().openRawResource(rawResId)));
|
2017-05-29 19:05:37 +02:00
|
|
|
while ((line = br.readLine()) != null) {
|
|
|
|
sb.append(linePrefix);
|
|
|
|
sb.append(line);
|
|
|
|
sb.append(linePostfix);
|
|
|
|
sb.append("\n");
|
|
|
|
}
|
|
|
|
} catch (Exception ignored) {
|
|
|
|
} finally {
|
|
|
|
if (br != null) {
|
|
|
|
try {
|
|
|
|
br.close();
|
|
|
|
} catch (IOException ignored) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return sb.toString();
|
|
|
|
}
|
|
|
|
|
2018-03-12 00:05:53 +01:00
|
|
|
/**
|
|
|
|
* Get internet connection state - the permission ACCESS_NETWORK_STATE is required
|
|
|
|
*
|
|
|
|
* @return True if internet connection available
|
|
|
|
*/
|
2017-05-29 19:05:37 +02:00
|
|
|
public boolean isConnectedToInternet() {
|
2018-03-12 00:05:53 +01:00
|
|
|
try {
|
|
|
|
ConnectivityManager con = (ConnectivityManager) _context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
|
|
@SuppressLint("MissingPermission") NetworkInfo activeNetInfo =
|
|
|
|
con == null ? null : con.getActiveNetworkInfo();
|
|
|
|
return activeNetInfo != null && activeNetInfo.isConnectedOrConnecting();
|
|
|
|
} catch (Exception ignored) {
|
|
|
|
throw new RuntimeException("Error: Developer forgot to declare a permission");
|
|
|
|
}
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Check if app with given {@code packageName} is installed
|
|
|
|
*/
|
2018-03-12 00:05:53 +01:00
|
|
|
public boolean isAppInstalled(String packageName) {
|
|
|
|
try {
|
2019-11-20 00:34:10 +01:00
|
|
|
PackageManager pm = _context.getApplicationContext().getPackageManager();
|
2018-03-12 00:05:53 +01:00
|
|
|
pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES);
|
|
|
|
return true;
|
|
|
|
} catch (PackageManager.NameNotFoundException e) {
|
|
|
|
return false;
|
|
|
|
}
|
2017-09-09 17:09:04 +02:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Restart the current app. Supply the class to start on startup
|
|
|
|
*/
|
2018-03-12 00:05:53 +01:00
|
|
|
public void restartApp(Class classToStart) {
|
2019-11-20 00:34:10 +01:00
|
|
|
Intent intent = new Intent(_context, classToStart);
|
|
|
|
PendingIntent pendi = PendingIntent.getActivity(_context, 555, intent, PendingIntent.FLAG_CANCEL_CURRENT);
|
2017-08-09 17:23:19 +02:00
|
|
|
AlarmManager mgr = (AlarmManager) _context.getSystemService(Context.ALARM_SERVICE);
|
2018-07-25 01:59:25 +02:00
|
|
|
if (_context instanceof Activity) {
|
|
|
|
((Activity) _context).finish();
|
|
|
|
}
|
2018-03-12 00:05:53 +01:00
|
|
|
if (mgr != null) {
|
2019-11-20 00:34:10 +01:00
|
|
|
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendi);
|
2018-03-12 00:05:53 +01:00
|
|
|
} else {
|
2019-11-20 00:34:10 +01:00
|
|
|
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
|
|
|
_context.startActivity(intent);
|
2018-03-12 00:05:53 +01:00
|
|
|
}
|
|
|
|
Runtime.getRuntime().exit(0);
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Load a markdown file from a {@link RawRes}, prepend each line with {@code prepend} text
|
|
|
|
* and convert markdown to html using {@link SimpleMarkdownParser}
|
|
|
|
*/
|
2017-05-29 19:05:37 +02:00
|
|
|
public String loadMarkdownForTextViewFromRaw(@RawRes int rawMdFile, String prepend) {
|
|
|
|
try {
|
|
|
|
return new SimpleMarkdownParser()
|
2017-08-09 17:23:19 +02:00
|
|
|
.parse(_context.getResources().openRawResource(rawMdFile),
|
2017-07-29 04:44:28 +02:00
|
|
|
prepend, SimpleMarkdownParser.FILTER_ANDROID_TEXTVIEW)
|
2018-03-05 23:37:24 +01:00
|
|
|
.replaceColor("#000001", rcolor(getResId(ResType.COLOR, "accent")))
|
2017-05-29 19:05:37 +02:00
|
|
|
.removeMultiNewlines().replaceBulletCharacter("*").getHtml();
|
|
|
|
} catch (IOException e) {
|
|
|
|
e.printStackTrace();
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Load html into a {@link Spanned} object and set the
|
|
|
|
* {@link TextView}'s text using {@link TextView#setText(CharSequence)}
|
|
|
|
*/
|
2017-07-29 04:44:28 +02:00
|
|
|
public void setHtmlToTextView(TextView textView, String html) {
|
|
|
|
textView.setMovementMethod(LinkMovementMethod.getInstance());
|
2017-08-29 14:44:43 +02:00
|
|
|
textView.setText(new SpannableString(htmlToSpanned(html)));
|
2017-07-29 04:44:28 +02:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Estimate this device's screen diagonal size in inches
|
|
|
|
*/
|
2017-05-29 19:05:37 +02:00
|
|
|
public double getEstimatedScreenSizeInches() {
|
2017-08-09 17:23:19 +02:00
|
|
|
DisplayMetrics dm = _context.getResources().getDisplayMetrics();
|
2017-05-29 19:05:37 +02:00
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
double calc = dm.density * 160d;
|
|
|
|
double x = Math.pow(dm.widthPixels / calc, 2);
|
|
|
|
double y = Math.pow(dm.heightPixels / calc, 2);
|
|
|
|
calc = Math.sqrt(x + y) * 1.16; // 1.16 = est. Nav/Statusbar
|
|
|
|
return Math.min(12, Math.max(4, calc));
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Check if the device is currently in portrait orientation
|
|
|
|
*/
|
2017-05-29 19:05:37 +02:00
|
|
|
public boolean isInPortraitMode() {
|
2017-08-09 17:23:19 +02:00
|
|
|
return _context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Get an {@link Locale} out of a android language code
|
|
|
|
* The {@code androidLC} may be in any of the forms: de, en, de-rAt
|
|
|
|
*/
|
|
|
|
public Locale getLocaleByAndroidCode(String androidLC) {
|
|
|
|
if (!TextUtils.isEmpty(androidLC)) {
|
|
|
|
return androidLC.contains("-r")
|
|
|
|
? new Locale(androidLC.substring(0, 2), androidLC.substring(4, 6)) // de-rAt
|
|
|
|
: new Locale(androidLC); // de
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
2017-08-24 13:34:32 +02:00
|
|
|
return Resources.getSystem().getConfiguration().locale;
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Set the apps language
|
|
|
|
* {@code androidLC} may be in any of the forms: en, de, de-rAt
|
|
|
|
* If given an empty string, the default (system) locale gets loaded
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public void setAppLanguage(final String androidLC) {
|
2018-03-30 00:14:54 +02:00
|
|
|
Locale locale = getLocaleByAndroidCode(androidLC);
|
2019-11-20 00:34:10 +01:00
|
|
|
locale = (locale != null && !androidLC.isEmpty()) ? locale : Resources.getSystem().getConfiguration().locale;
|
|
|
|
setLocale(locale);
|
|
|
|
}
|
|
|
|
|
|
|
|
public ContextUtils setLocale(final Locale locale) {
|
2017-08-09 17:23:19 +02:00
|
|
|
Configuration config = _context.getResources().getConfiguration();
|
2019-11-20 00:34:10 +01:00
|
|
|
config.locale = (locale != null ? locale : Resources.getSystem().getConfiguration().locale);
|
2017-08-09 17:23:19 +02:00
|
|
|
_context.getResources().updateConfiguration(config, null);
|
2019-11-20 00:34:10 +01:00
|
|
|
Locale.setDefault(locale);
|
|
|
|
return this;
|
2017-08-09 17:23:19 +02:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Try to guess if the color on top of the given {@code colorOnBottomInt}
|
|
|
|
* should be light or dark. Returns true if top color should be light
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public boolean shouldColorOnTopBeLight(@ColorInt final int colorOnBottomInt) {
|
2017-08-13 23:02:04 +02:00
|
|
|
return 186 > (((0.299 * Color.red(colorOnBottomInt))
|
|
|
|
+ ((0.587 * Color.green(colorOnBottomInt))
|
|
|
|
+ (0.114 * Color.blue(colorOnBottomInt)))));
|
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Convert a html string to an android {@link Spanned} object
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public Spanned htmlToSpanned(final String html) {
|
2017-08-29 14:44:43 +02:00
|
|
|
Spanned result;
|
|
|
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
|
|
|
result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY);
|
|
|
|
} else {
|
|
|
|
result = Html.fromHtml(html);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
2017-08-24 13:34:32 +02:00
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Convert pixel unit do android dp unit
|
|
|
|
*/
|
2018-03-12 00:05:53 +01:00
|
|
|
public float convertPxToDp(final float px) {
|
2017-08-09 17:23:19 +02:00
|
|
|
return px / _context.getResources().getDisplayMetrics().density;
|
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Convert android dp unit to pixel unit
|
|
|
|
*/
|
2018-03-12 00:05:53 +01:00
|
|
|
public float convertDpToPx(final float dp) {
|
2017-08-09 17:23:19 +02:00
|
|
|
return dp * _context.getResources().getDisplayMetrics().density;
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
2017-09-09 17:09:04 +02:00
|
|
|
|
2019-01-11 02:38:30 +01:00
|
|
|
/**
|
|
|
|
* Get the private directory for the current package (usually /data/data/package.name/)
|
|
|
|
*/
|
2019-07-26 03:19:28 +02:00
|
|
|
@SuppressWarnings("StatementWithEmptyBody")
|
|
|
|
public File getAppDataPrivateDir() {
|
|
|
|
File filesDir;
|
2019-01-11 02:38:30 +01:00
|
|
|
try {
|
2019-07-26 03:19:28 +02:00
|
|
|
filesDir = new File(new File(_context.getPackageManager().getPackageInfo(getPackageIdReal(), 0).applicationInfo.dataDir), "files");
|
2019-01-11 02:38:30 +01:00
|
|
|
} catch (PackageManager.NameNotFoundException e) {
|
2019-07-26 03:19:28 +02:00
|
|
|
filesDir = _context.getFilesDir();
|
2019-01-11 02:38:30 +01:00
|
|
|
}
|
2019-07-26 03:19:28 +02:00
|
|
|
if (!filesDir.exists() && filesDir.mkdirs()) ;
|
|
|
|
return filesDir;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get public (accessible) appdata folders
|
|
|
|
*/
|
|
|
|
@SuppressWarnings("StatementWithEmptyBody")
|
|
|
|
public List<Pair<File, String>> getAppDataPublicDirs(boolean internalStorageFolder, boolean sdcardFolders, boolean storageNameWithoutType) {
|
|
|
|
List<Pair<File, String>> dirs = new ArrayList<>();
|
|
|
|
for (File externalFileDir : ContextCompat.getExternalFilesDirs(_context, null)) {
|
|
|
|
if (externalFileDir == null || Environment.getExternalStorageDirectory() == null) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
boolean isInt = externalFileDir.getAbsolutePath().startsWith(Environment.getExternalStorageDirectory().getAbsolutePath());
|
|
|
|
boolean add = (internalStorageFolder && isInt) || (sdcardFolders && !isInt);
|
|
|
|
if (add) {
|
|
|
|
dirs.add(new Pair<>(externalFileDir, getStorageName(externalFileDir, storageNameWithoutType)));
|
|
|
|
if (!externalFileDir.exists() && externalFileDir.mkdirs()) ;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return dirs;
|
|
|
|
}
|
|
|
|
|
2019-11-20 00:34:10 +01:00
|
|
|
public String getStorageName(final File externalFileDir, final boolean storageNameWithoutType) {
|
2019-07-26 03:19:28 +02:00
|
|
|
boolean isInt = externalFileDir.getAbsolutePath().startsWith(Environment.getExternalStorageDirectory().getAbsolutePath());
|
|
|
|
|
|
|
|
String[] split = externalFileDir.getAbsolutePath().split("/");
|
|
|
|
if (split.length > 2) {
|
|
|
|
return isInt ? (storageNameWithoutType ? "Internal Storage" : "") : (storageNameWithoutType ? split[2] : ("SD Card (" + split[2] + ")"));
|
|
|
|
} else {
|
|
|
|
return "Storage";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-20 00:34:10 +01:00
|
|
|
public List<Pair<File, String>> getStorages(final boolean internalStorageFolder, final boolean sdcardFolders) {
|
2019-07-26 03:19:28 +02:00
|
|
|
List<Pair<File, String>> storages = new ArrayList<>();
|
|
|
|
for (Pair<File, String> pair : getAppDataPublicDirs(internalStorageFolder, sdcardFolders, true)) {
|
|
|
|
if (pair.first != null && pair.first.getAbsolutePath().lastIndexOf("/Android/data") > 0) {
|
|
|
|
try {
|
|
|
|
storages.add(new Pair<>(new File(pair.first.getCanonicalPath().replaceFirst("/Android/data.*", "")), pair.second));
|
|
|
|
} catch (IOException ignored) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return storages;
|
|
|
|
}
|
|
|
|
|
2019-11-20 00:34:10 +01:00
|
|
|
public File getStorageRootFolder(final File file) {
|
2019-07-26 03:19:28 +02:00
|
|
|
String filepath;
|
|
|
|
try {
|
|
|
|
filepath = file.getCanonicalPath();
|
|
|
|
} catch (Exception ignored) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
for (Pair<File, String> storage : getStorages(false, true)) {
|
|
|
|
//noinspection ConstantConditions
|
|
|
|
if (filepath.startsWith(storage.first.getAbsolutePath())) {
|
|
|
|
return storage.first;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
2019-01-11 02:38:30 +01:00
|
|
|
}
|
|
|
|
|
2018-04-08 17:52:04 +02:00
|
|
|
/**
|
|
|
|
* Request the givens paths to be scanned by MediaScanner
|
|
|
|
*
|
|
|
|
* @param files Files and folders to scan
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public void mediaScannerScanFile(final File... files) {
|
2018-04-08 17:52:04 +02:00
|
|
|
if (android.os.Build.VERSION.SDK_INT > 19) {
|
|
|
|
String[] paths = new String[files.length];
|
|
|
|
for (int i = 0; i < files.length; i++) {
|
|
|
|
paths[i] = files[i].getAbsolutePath();
|
|
|
|
}
|
|
|
|
MediaScannerConnection.scanFile(_context, paths, null, null);
|
|
|
|
} else {
|
|
|
|
for (File file : files) {
|
|
|
|
_context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Load an image into a {@link ImageView} and apply a color filter
|
|
|
|
*/
|
2017-09-09 17:09:04 +02:00
|
|
|
public static void setDrawableWithColorToImageView(ImageView imageView, @DrawableRes int drawableResId, @ColorRes int colorResId) {
|
|
|
|
imageView.setImageResource(drawableResId);
|
|
|
|
imageView.setColorFilter(ContextCompat.getColor(imageView.getContext(), colorResId));
|
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Get a {@link Bitmap} out of a {@link Drawable}
|
|
|
|
*/
|
2017-09-09 17:09:04 +02:00
|
|
|
public Bitmap drawableToBitmap(Drawable drawable) {
|
|
|
|
Bitmap bitmap = null;
|
2018-04-08 17:52:04 +02:00
|
|
|
if (drawable instanceof VectorDrawableCompat
|
|
|
|
|| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && drawable instanceof VectorDrawable)
|
|
|
|
|| ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && drawable instanceof AdaptiveIconDrawable))) {
|
|
|
|
|
2017-09-09 17:09:04 +02:00
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
|
|
|
drawable = (DrawableCompat.wrap(drawable)).mutate();
|
|
|
|
}
|
|
|
|
|
|
|
|
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
|
|
|
|
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
|
|
|
Canvas canvas = new Canvas(bitmap);
|
|
|
|
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
|
|
|
drawable.draw(canvas);
|
|
|
|
} else if (drawable instanceof BitmapDrawable) {
|
|
|
|
bitmap = ((BitmapDrawable) drawable).getBitmap();
|
|
|
|
}
|
|
|
|
return bitmap;
|
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Get a {@link Bitmap} out of a {@link DrawableRes}
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public Bitmap drawableToBitmap(@DrawableRes final int drawableId) {
|
|
|
|
try {
|
|
|
|
return drawableToBitmap(ContextCompat.getDrawable(_context, drawableId));
|
|
|
|
} catch (Exception e) {
|
|
|
|
return null;
|
|
|
|
}
|
2018-03-30 00:14:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a {@link Bitmap} from a given {@code imagePath} on the filesystem
|
|
|
|
* Specifying a {@code maxDimen} is also possible and a value below 2000
|
|
|
|
* is recommended, otherwise a {@link OutOfMemoryError} may occur
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public Bitmap loadImageFromFilesystem(final File imagePath, final int maxDimen) {
|
2017-09-09 17:09:04 +02:00
|
|
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
|
|
options.inJustDecodeBounds = true;
|
2017-09-23 20:33:28 +02:00
|
|
|
BitmapFactory.decodeFile(imagePath.getAbsolutePath(), options);
|
2017-09-09 17:09:04 +02:00
|
|
|
options.inSampleSize = calculateInSampleSize(options, maxDimen);
|
|
|
|
options.inJustDecodeBounds = false;
|
2017-09-23 20:33:28 +02:00
|
|
|
return BitmapFactory.decodeFile(imagePath.getAbsolutePath(), options);
|
2017-09-09 17:09:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculates the scaling factor so the bitmap is maximal as big as the maxDimen
|
|
|
|
*
|
|
|
|
* @param options Bitmap-options that contain the current dimensions of the bitmap
|
|
|
|
* @param maxDimen Max size of the Bitmap (width or height)
|
|
|
|
* @return the scaling factor that needs to be applied to the bitmap
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public int calculateInSampleSize(final BitmapFactory.Options options, final int maxDimen) {
|
2017-10-28 09:56:05 +02:00
|
|
|
// Raw height and width of image
|
2017-09-09 17:09:04 +02:00
|
|
|
int height = options.outHeight;
|
|
|
|
int width = options.outWidth;
|
|
|
|
int inSampleSize = 1;
|
|
|
|
|
|
|
|
if (Math.max(height, width) > maxDimen) {
|
|
|
|
inSampleSize = Math.round(1f * Math.max(height, width) / maxDimen);
|
|
|
|
}
|
|
|
|
return inSampleSize;
|
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Scale the bitmap so both dimensions are lower or equal to {@code maxDimen}
|
|
|
|
* This keeps the aspect ratio
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public Bitmap scaleBitmap(final Bitmap bitmap, final int maxDimen) {
|
2017-09-09 17:09:04 +02:00
|
|
|
int picSize = Math.min(bitmap.getHeight(), bitmap.getWidth());
|
|
|
|
float scale = 1.f * maxDimen / picSize;
|
|
|
|
Matrix matrix = new Matrix();
|
|
|
|
matrix.postScale(scale, scale);
|
|
|
|
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
|
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Write the given {@link Bitmap} to {@code imageFile}, in {@link CompressFormat#JPEG} format
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public boolean writeImageToFileJpeg(final File imageFile, final Bitmap image) {
|
2017-09-23 20:33:28 +02:00
|
|
|
return writeImageToFile(imageFile, image, Bitmap.CompressFormat.JPEG, 95);
|
2017-09-09 17:09:04 +02:00
|
|
|
}
|
|
|
|
|
2018-03-12 00:05:53 +01:00
|
|
|
/**
|
2018-03-30 00:14:54 +02:00
|
|
|
* Write the given {@link Bitmap} to filesystem
|
2018-03-12 00:05:53 +01:00
|
|
|
*
|
|
|
|
* @param targetFile The file to be written in
|
|
|
|
* @param image The image as android {@link Bitmap}
|
|
|
|
* @param format One format of {@link CompressFormat}, null will determine based on filename
|
|
|
|
* @param quality Quality level, defaults to 95
|
|
|
|
* @return True if writing was successful
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public boolean writeImageToFile(final File targetFile, final Bitmap image, CompressFormat format, Integer quality) {
|
2018-03-12 00:05:53 +01:00
|
|
|
File folder = new File(targetFile.getParent());
|
|
|
|
if (quality == null || quality < 0 || quality > 100) {
|
|
|
|
quality = 95;
|
2017-09-23 20:33:28 +02:00
|
|
|
}
|
2018-03-12 00:05:53 +01:00
|
|
|
if (format == null) {
|
|
|
|
format = CompressFormat.JPEG;
|
|
|
|
String lc = targetFile.getAbsolutePath().toLowerCase(Locale.ROOT);
|
|
|
|
if (lc.endsWith(".png")) {
|
|
|
|
format = CompressFormat.PNG;
|
|
|
|
}
|
|
|
|
if (lc.endsWith(".webp")) {
|
|
|
|
format = CompressFormat.WEBP;
|
|
|
|
}
|
2017-09-23 20:33:28 +02:00
|
|
|
}
|
|
|
|
if (folder.exists() || folder.mkdirs()) {
|
2017-09-09 17:09:04 +02:00
|
|
|
FileOutputStream stream = null;
|
|
|
|
try {
|
2018-03-12 00:05:53 +01:00
|
|
|
stream = new FileOutputStream(targetFile); // overwrites this image every time
|
2017-09-09 17:09:04 +02:00
|
|
|
image.compress(format, quality, stream);
|
2018-03-12 00:05:53 +01:00
|
|
|
return true;
|
2017-09-09 17:09:04 +02:00
|
|
|
} catch (FileNotFoundException ignored) {
|
|
|
|
} finally {
|
|
|
|
try {
|
|
|
|
if (stream != null) {
|
|
|
|
stream.close();
|
|
|
|
}
|
|
|
|
} catch (IOException ignored) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-03-12 00:05:53 +01:00
|
|
|
return false;
|
2017-09-09 17:09:04 +02:00
|
|
|
}
|
2017-10-28 09:56:05 +02:00
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Draw text in the center of the given {@link DrawableRes}
|
|
|
|
* This may be useful for e.g. badge counts
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public Bitmap drawTextOnDrawable(@DrawableRes final int drawableRes, final String text, final int textSize) {
|
2017-10-28 09:56:05 +02:00
|
|
|
Resources resources = _context.getResources();
|
|
|
|
float scale = resources.getDisplayMetrics().density;
|
2018-03-30 00:14:54 +02:00
|
|
|
Bitmap bitmap = drawableToBitmap(drawableRes);
|
2017-10-28 09:56:05 +02:00
|
|
|
|
|
|
|
bitmap = bitmap.copy(bitmap.getConfig(), true);
|
|
|
|
Canvas canvas = new Canvas(bitmap);
|
|
|
|
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
|
|
paint.setColor(Color.rgb(61, 61, 61));
|
|
|
|
paint.setTextSize((int) (textSize * scale));
|
|
|
|
paint.setShadowLayer(1f, 0f, 1f, Color.WHITE);
|
|
|
|
|
|
|
|
Rect bounds = new Rect();
|
|
|
|
paint.getTextBounds(text, 0, text.length(), bounds);
|
|
|
|
int x = (bitmap.getWidth() - bounds.width()) / 2;
|
|
|
|
int y = (bitmap.getHeight() + bounds.height()) / 2;
|
|
|
|
canvas.drawText(text, x, y, paint);
|
|
|
|
|
|
|
|
return bitmap;
|
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Try to tint all {@link Menu}s {@link MenuItem}s with given color
|
|
|
|
*/
|
2018-03-12 00:05:53 +01:00
|
|
|
@SuppressWarnings("ConstantConditions")
|
2019-11-20 00:34:10 +01:00
|
|
|
public void tintMenuItems(final Menu menu, final boolean recurse, @ColorInt final int iconColor) {
|
2017-10-29 14:47:00 +01:00
|
|
|
for (int i = 0; i < menu.size(); i++) {
|
|
|
|
MenuItem item = menu.getItem(i);
|
2019-01-11 02:38:30 +01:00
|
|
|
try {
|
|
|
|
tintDrawable(item.getIcon(), iconColor);
|
|
|
|
if (item.hasSubMenu() && recurse) {
|
|
|
|
tintMenuItems(item.getSubMenu(), recurse, iconColor);
|
|
|
|
}
|
|
|
|
} catch (Exception ignored) {
|
|
|
|
// This should not happen at all, but may in bad menu.xml configuration
|
2017-10-29 14:47:00 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Loads {@link Drawable} by given {@link DrawableRes} and applies a color
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public Drawable tintDrawable(@DrawableRes final int drawableRes, @ColorInt final int color) {
|
2018-03-30 00:14:54 +02:00
|
|
|
return tintDrawable(rdrawable(drawableRes), color);
|
2018-03-02 15:56:14 +01:00
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Tint a {@link Drawable} with given {@code color}
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public Drawable tintDrawable(@Nullable Drawable drawable, @ColorInt final int color) {
|
2018-03-02 15:56:14 +01:00
|
|
|
if (drawable != null) {
|
|
|
|
drawable = DrawableCompat.wrap(drawable);
|
|
|
|
DrawableCompat.setTint(drawable.mutate(), color);
|
|
|
|
}
|
|
|
|
return drawable;
|
|
|
|
}
|
|
|
|
|
2018-03-30 00:14:54 +02:00
|
|
|
/**
|
|
|
|
* Try to make icons in Toolbar/ActionBars SubMenus visible
|
|
|
|
* This may not work on some devices and it maybe won't work on future android updates
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public void setSubMenuIconsVisiblity(final Menu menu, final boolean visible) {
|
|
|
|
if (TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL) {
|
|
|
|
return;
|
|
|
|
}
|
2017-10-29 14:47:00 +01:00
|
|
|
if (menu.getClass().getSimpleName().equals("MenuBuilder")) {
|
|
|
|
try {
|
2018-03-12 00:05:53 +01:00
|
|
|
@SuppressLint("PrivateApi") Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE);
|
2017-10-29 14:47:00 +01:00
|
|
|
m.setAccessible(true);
|
|
|
|
m.invoke(menu, visible);
|
|
|
|
} catch (Exception ignored) {
|
2018-03-12 00:05:53 +01:00
|
|
|
Log.d(getClass().getName(), "Error: 'setSubMenuIconsVisiblity' not supported on this device");
|
2017-10-29 14:47:00 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-01-11 02:38:30 +01:00
|
|
|
|
|
|
|
|
|
|
|
public String getLocalizedDateFormat() {
|
|
|
|
return ((SimpleDateFormat) android.text.format.DateFormat.getDateFormat(_context)).toPattern();
|
|
|
|
}
|
|
|
|
|
|
|
|
public String getLocalizedTimeFormat() {
|
|
|
|
return ((SimpleDateFormat) android.text.format.DateFormat.getTimeFormat(_context)).toPattern();
|
|
|
|
}
|
|
|
|
|
|
|
|
public String getLocalizedDateTimeFormat() {
|
|
|
|
return getLocalizedDateFormat() + " " + getLocalizedTimeFormat();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A {@link InputFilter} for filenames
|
|
|
|
*/
|
|
|
|
@SuppressWarnings("Convert2Lambda")
|
|
|
|
public static final InputFilter INPUTFILTER_FILENAME = new InputFilter() {
|
|
|
|
public CharSequence filter(CharSequence src, int start, int end, Spanned dest, int dstart, int dend) {
|
|
|
|
if (src.length() < 1) return null;
|
|
|
|
char last = src.charAt(src.length() - 1);
|
2020-12-09 00:57:31 +01:00
|
|
|
String illegal = "|\\?*<\":>[]/'";
|
2019-01-11 02:38:30 +01:00
|
|
|
if (illegal.indexOf(last) > -1) return src.subSequence(0, src.length() - 1);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A simple {@link Runnable} which does a touch event on a view.
|
|
|
|
* This pops up e.g. the keyboard on a {@link android.widget.EditText}
|
|
|
|
* <p>
|
|
|
|
* Example: new Handler().postDelayed(new DoTouchView(editView), 200);
|
|
|
|
*/
|
|
|
|
public static class DoTouchView implements Runnable {
|
|
|
|
View _view;
|
|
|
|
|
|
|
|
public DoTouchView(View view) {
|
|
|
|
_view = view;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
_view.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 0, 0, 0));
|
|
|
|
_view.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 0, 0, 0));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-11-20 00:34:10 +01:00
|
|
|
public String getMimeType(final File file) {
|
2019-01-11 02:38:30 +01:00
|
|
|
return getMimeType(Uri.fromFile(file));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Detect MimeType of given file
|
|
|
|
* Android/Java's own MimeType map is very very small and detection barely works at all
|
|
|
|
* Hence use custom map for some file extensions
|
|
|
|
*/
|
2019-11-20 00:34:10 +01:00
|
|
|
public String getMimeType(final Uri uri) {
|
2019-01-11 02:38:30 +01:00
|
|
|
String mimeType = null;
|
|
|
|
if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
|
|
|
|
ContentResolver cr = _context.getContentResolver();
|
|
|
|
mimeType = cr.getType(uri);
|
|
|
|
} else {
|
2020-12-09 00:57:31 +01:00
|
|
|
String filename = uri.toString();
|
|
|
|
if (filename.endsWith(".jenc")) {
|
|
|
|
filename = filename.replace(".jenc", "");
|
|
|
|
}
|
|
|
|
String ext = MimeTypeMap.getFileExtensionFromUrl(filename);
|
2019-01-11 02:38:30 +01:00
|
|
|
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.toLowerCase());
|
|
|
|
|
|
|
|
// Try to guess if the recommended methods fail
|
|
|
|
if (TextUtils.isEmpty(mimeType)) {
|
|
|
|
switch (ext) {
|
|
|
|
case "md":
|
|
|
|
case "markdown":
|
|
|
|
case "mkd":
|
|
|
|
case "mdown":
|
|
|
|
case "mkdn":
|
|
|
|
case "mdwn":
|
|
|
|
case "rmd":
|
|
|
|
mimeType = "text/markdown";
|
|
|
|
break;
|
|
|
|
case "yaml":
|
|
|
|
case "yml":
|
|
|
|
mimeType = "text/yaml";
|
|
|
|
break;
|
|
|
|
case "json":
|
|
|
|
mimeType = "text/json";
|
|
|
|
break;
|
|
|
|
case "txt":
|
|
|
|
mimeType = "text/plain";
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (TextUtils.isEmpty(mimeType)) {
|
|
|
|
mimeType = "*/*";
|
|
|
|
}
|
|
|
|
return mimeType;
|
|
|
|
}
|
2019-07-26 03:19:28 +02:00
|
|
|
|
2019-11-20 00:34:10 +01:00
|
|
|
public Integer parseColor(final String colorstr) {
|
2019-07-26 03:19:28 +02:00
|
|
|
if (colorstr == null || colorstr.trim().isEmpty()) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
return Color.parseColor(colorstr);
|
|
|
|
} catch (IllegalArgumentException ignored) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-07 21:54:08 +02:00
|
|
|
public boolean isDeviceGoodHardware() {
|
|
|
|
try {
|
|
|
|
ActivityManager activityManager = (ActivityManager) _context.getSystemService(Context.ACTIVITY_SERVICE);
|
|
|
|
return !ActivityManagerCompat.isLowRamDevice(activityManager) &&
|
|
|
|
Runtime.getRuntime().availableProcessors() >= 4 &&
|
|
|
|
activityManager.getMemoryClass() >= 128;
|
|
|
|
} catch (Exception ignored) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
2019-11-20 00:34:10 +01:00
|
|
|
|
|
|
|
// Vibrate device one time by given amount of time, defaulting to 50ms
|
|
|
|
// Requires <uses-permission android:name="android.permission.VIBRATE" /> in AndroidManifest to work
|
|
|
|
@SuppressWarnings("UnnecessaryReturnStatement")
|
|
|
|
@SuppressLint("MissingPermission")
|
|
|
|
public void vibrate(final int... ms) {
|
|
|
|
int ms_v = ms != null && ms.length > 0 ? ms[0] : 50;
|
|
|
|
Vibrator vibrator = ((Vibrator) _context.getSystemService(VIBRATOR_SERVICE));
|
|
|
|
if (vibrator == null) {
|
|
|
|
return;
|
|
|
|
} else if (Build.VERSION.SDK_INT >= 26) {
|
|
|
|
vibrator.vibrate(VibrationEffect.createOneShot(ms_v, VibrationEffect.DEFAULT_AMPLITUDE));
|
|
|
|
} else {
|
|
|
|
vibrator.vibrate(ms_v);
|
|
|
|
}
|
|
|
|
}
|
2021-01-12 20:23:10 +01:00
|
|
|
|
|
|
|
/*
|
|
|
|
Check if Wifi is connected. Requires these permissions in AndroidManifest:
|
|
|
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
|
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
|
|
|
*/
|
|
|
|
@SuppressLint("MissingPermission")
|
|
|
|
public boolean isWifiConnected(boolean... enabledOnly) {
|
|
|
|
final boolean doEnabledCheckOnly = enabledOnly != null && enabledOnly.length > 0 && enabledOnly[0];
|
|
|
|
final ConnectivityManager connectivityManager = (ConnectivityManager) _context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
|
|
final NetworkInfo wifiInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
|
|
|
|
return wifiInfo != null && (doEnabledCheckOnly ? wifiInfo.isAvailable() : wifiInfo.isConnected());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns if the device is currently in portrait orientation (landscape=false)
|
|
|
|
public boolean isDeviceOrientationPortrait() {
|
|
|
|
final int rotation = ((WindowManager) _context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getOrientation();
|
|
|
|
return (rotation == Surface.ROTATION_0) || (rotation == Surface.ROTATION_180);
|
|
|
|
}
|
2017-05-29 19:05:37 +02:00
|
|
|
}
|
2019-01-11 02:38:30 +01:00
|
|
|
|
|
|
|
|