1
0
Fork 0
mirror of https://github.com/gsantner/dandelion synced 2024-11-21 20:02:07 +01:00

Update opoc

Update shared helper utilities of my projects to latest state
This commit is contained in:
Gregor Santner 2019-11-20 00:34:10 +01:00
parent 1948c28cff
commit 7361d4bc3f
No known key found for this signature in database
GPG key ID: 7E83A7834AECB009
9 changed files with 286 additions and 112 deletions

View file

@ -16,6 +16,7 @@ import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
@ -143,4 +144,17 @@ public abstract class GsFragmentBase extends Fragment {
public Menu getFragmentMenu() {
return _fragmentMenu;
}
/**
* Get the toolbar from activity
* Requires id to be set to @+id/toolbar
*/
@SuppressWarnings("ConstantConditions")
protected Toolbar getToolbar() {
try {
return (Toolbar) getActivity().findViewById(new ContextUtils(getActivity()).getResId(ContextUtils.ResType.ID, "toolbar"));
} catch (Exception e) {
return null;
}
}
}

View file

@ -547,7 +547,7 @@ public class SharedPreferencesPropertyBackend implements PropertyBackend<String,
*/
public Date getDateOfDaysAgo(int days) {
Calendar cal = new GregorianCalendar();
cal.add(Calendar.DAY_OF_MONTH, -days);
cal.add(Calendar.DATE, -days);
return cal.getTime();
}

View file

@ -11,8 +11,11 @@
package net.gsantner.opoc.ui;
import android.app.Activity;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -23,6 +26,7 @@ import android.support.v7.widget.AppCompatEditText;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
@ -56,9 +60,14 @@ public class SearchOrCustomTextDialog {
public Callback.a1<String> callback;
public List<? extends CharSequence> data = new ArrayList<>();
public List<? extends CharSequence> highlightData = new ArrayList<>();
public List<Integer> iconsForData = new ArrayList<>();
public String messageText = "";
public boolean isSearchEnabled = true;
public boolean isDarkDialog = false;
public int dialogWidthDp = WindowManager.LayoutParams.MATCH_PARENT;
public int dialogHeightDp = WindowManager.LayoutParams.WRAP_CONTENT;
public int gravity = Gravity.NO_GRAVITY;
public int searchInputType = 0;
@ColorInt
public int textColor = 0xFF000000;
@ -89,6 +98,17 @@ public class SearchOrCustomTextDialog {
TextView textView = (TextView) super.getView(pos, convertView, parent);
String text = textView.getText().toString();
int posInOriginalList = dopt.data.indexOf(text);
if (posInOriginalList >= 0 && dopt.iconsForData != null && posInOriginalList < dopt.iconsForData.size() && dopt.iconsForData.get(posInOriginalList) != 0) {
textView.setCompoundDrawablesWithIntrinsicBounds(dopt.iconsForData.get(posInOriginalList), 0, 0, 0);
textView.setCompoundDrawablePadding(32);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
textView.setCompoundDrawableTintList(ColorStateList.valueOf(dopt.isDarkDialog ? Color.WHITE : Color.BLACK));
}
} else {
textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
boolean hl = dopt.highlightData.contains(text);
textView.setTextColor(hl ? dopt.highlightColor : dopt.textColor);
textView.setTypeface(null, hl ? Typeface.BOLD : Typeface.NORMAL);
@ -132,6 +152,7 @@ public class SearchOrCustomTextDialog {
searchEditText.setTextColor(dopt.textColor);
searchEditText.setHintTextColor((dopt.textColor & 0x00FFFFFF) | 0x99000000);
searchEditText.setHint(dopt.searchHintText);
searchEditText.setInputType(dopt.searchInputType == 0 ? searchEditText.getInputType() : dopt.searchInputType);
searchEditText.addTextChangedListener(new TextWatcher() {
@Override
@ -166,9 +187,11 @@ public class SearchOrCustomTextDialog {
dialogBuilder.setMessage(dopt.messageText);
}
dialogBuilder.setView(linearLayout)
.setTitle(dopt.titleText)
.setOnCancelListener(null)
.setNegativeButton(dopt.cancelButtonText, (dialogInterface, i) -> dialogInterface.dismiss());
if (dopt.titleText != 0) {
dialogBuilder.setTitle(dopt.titleText);
}
if (dopt.isSearchEnabled) {
dialogBuilder.setPositiveButton(dopt.okButtonText, (dialogInterface, i) -> {
dialogInterface.dismiss();
@ -204,7 +227,15 @@ public class SearchOrCustomTextDialog {
}
dialog.show();
if ((w = dialog.getWindow()) != null) {
w.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT);
int ds_w = dopt.dialogWidthDp < 100 ? dopt.dialogWidthDp : ((int) (dopt.dialogWidthDp * activity.getResources().getDisplayMetrics().density));
int ds_h = dopt.dialogHeightDp < 100 ? dopt.dialogHeightDp : ((int) (dopt.dialogHeightDp * activity.getResources().getDisplayMetrics().density));
w.setLayout(ds_w, ds_h);
}
if ((w = dialog.getWindow()) != null && dopt.gravity != Gravity.NO_GRAVITY) {
WindowManager.LayoutParams wlp = w.getAttributes();
wlp.gravity = dopt.gravity;
w.setAttributes(wlp);
}
if (dopt.isSearchEnabled) {

View file

@ -12,6 +12,11 @@ package net.gsantner.opoc.util;
@SuppressWarnings("unused")
public class Callback {
public interface a0 {
void callback();
}
public interface a1<A> {
void callback(A arg1);
}

View file

@ -41,6 +41,8 @@ import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.SystemClock;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.support.annotation.ColorInt;
import android.support.annotation.ColorRes;
import android.support.annotation.DrawableRes;
@ -51,7 +53,9 @@ import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.app.ActivityManagerCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.text.TextUtilsCompat;
import android.support.v4.util.Pair;
import android.support.v4.view.ViewCompat;
import android.text.Html;
import android.text.InputFilter;
import android.text.SpannableString;
@ -82,10 +86,11 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import static android.content.Context.VIBRATOR_SERVICE;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.graphics.Bitmap.CompressFormat;
@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "ObsoleteSdkInt", "deprecation", "SpellCheckingInspection"})
@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "ObsoleteSdkInt", "deprecation", "SpellCheckingInspection", "TryFinallyCanBeTryWithResources", "UnusedAssignment"})
public class ContextUtils {
//
// Members, Constructors
@ -117,21 +122,29 @@ public class ContextUtils {
*
* @return A valid id if the id could be found, else 0
*/
public int getResId(ResType resType, final String name) {
return _context.getResources().getIdentifier(name, resType.name().toLowerCase(), _context.getPackageName());
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;
}
}
/**
* Get String by given string ressource id (nuermic)
*/
public String rstr(@StringRes int strResId) {
return _context.getString(strResId);
public String rstr(@StringRes final int strResId) {
try {
return _context.getString(strResId);
} catch (Exception e) {
return null;
}
}
/**
* Get String by given string ressource identifier (textual)
*/
public String rstr(String strResKey) {
public String rstr(final String strResKey) {
try {
return rstr(getResId(ResType.STRING, strResKey));
} catch (Resources.NotFoundException e) {
@ -142,14 +155,22 @@ public class ContextUtils {
/**
* Get drawable from given ressource identifier
*/
public Drawable rdrawable(@DrawableRes int resId) {
return ContextCompat.getDrawable(_context, resId);
public Drawable rdrawable(@DrawableRes final int resId) {
try {
return ContextCompat.getDrawable(_context, resId);
} catch (Exception e) {
return null;
}
}
/**
* Get color by given color ressource id
*/
public int rcolor(@ColorRes int resId) {
public int rcolor(@ColorRes final int resId) {
if (resId == 0) {
Log.e(getClass().getName(), "ContextUtils::rcolor: resId is 0!");
return Color.BLACK;
}
return ContextCompat.getColor(_context, resId);
}
@ -175,12 +196,12 @@ public class ContextUtils {
* @param intColor The color coded in int
* @param withAlpha Optional; Set first bool parameter to true to also include alpha value
*/
public String colorToHexString(int intColor, boolean... withAlpha) {
public static String colorToHexString(final int intColor, final boolean... withAlpha) {
boolean a = withAlpha != null && withAlpha.length >= 1 && withAlpha[0];
return String.format(a ? "#%08X" : "#%06X", (a ? 0xFFFFFFFF : 0xFFFFFF) & intColor);
}
public String getAndroidVersion() {
public static String getAndroidVersion() {
return Build.VERSION.RELEASE + " (" + Build.VERSION.SDK_INT + ")";
}
@ -205,7 +226,7 @@ public class ContextUtils {
src = _context.getPackageManager().getInstallerPackageName(getPackageIdManifest());
} catch (Exception ignored) {
}
if (TextUtils.isEmpty(src)) {
if (src == null || src.trim().isEmpty()) {
return "Sideloaded";
} else if (src.toLowerCase().contains(".amazon.")) {
return "Amazon Appstore";
@ -213,7 +234,7 @@ public class ContextUtils {
switch (src) {
case "com.android.vending":
case "com.google.android.feedback": {
return "Google Play Store";
return "Google Play";
}
case "org.fdroid.fdroid.privileged":
case "org.fdroid.fdroid": {
@ -237,12 +258,12 @@ public class ContextUtils {
* If the parameter is an string a browser will get triggered
*/
public void openWebpageInExternalBrowser(final String url) {
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
try {
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
_context.startActivity(intent);
} catch (ActivityNotFoundException e) {
} catch (Exception e) {
e.printStackTrace();
}
}
@ -252,7 +273,7 @@ public class ContextUtils {
*/
public String getPackageIdManifest() {
String pkg = rstr("manifest_package_id");
return pkg != null ? pkg : _context.getPackageName();
return !TextUtils.isEmpty(pkg) ? pkg : _context.getPackageName();
}
/**
@ -270,7 +291,7 @@ public class ContextUtils {
* of the package set in manifest (root element).
* Falls back to applicationId of the app which may differ from manifest.
*/
public Object getBuildConfigValue(String fieldName) {
public Object getBuildConfigValue(final String fieldName) {
String pkg = getPackageIdManifest() + ".BuildConfig";
try {
Class<?> c = Class.forName(pkg);
@ -284,9 +305,9 @@ public class ContextUtils {
/**
* Get a BuildConfig bool value
*/
public Boolean bcbool(String fieldName, Boolean defaultValue) {
public Boolean bcbool(final String fieldName, final Boolean defaultValue) {
Object field = getBuildConfigValue(fieldName);
if (field != null && field instanceof Boolean) {
if (field instanceof Boolean) {
return (Boolean) field;
}
return defaultValue;
@ -295,9 +316,9 @@ public class ContextUtils {
/**
* Get a BuildConfig string value
*/
public String bcstr(String fieldName, String defaultValue) {
public String bcstr(final String fieldName, final String defaultValue) {
Object field = getBuildConfigValue(fieldName);
if (field != null && field instanceof String) {
if (field instanceof String) {
return (String) field;
}
return defaultValue;
@ -306,9 +327,9 @@ public class ContextUtils {
/**
* Get a BuildConfig string value
*/
public Integer bcint(String fieldName, int defaultValue) {
public Integer bcint(final String fieldName, final int defaultValue) {
Object field = getBuildConfigValue(fieldName);
if (field != null && field instanceof Integer) {
if (field instanceof Integer) {
return (Integer) field;
}
return defaultValue;
@ -396,8 +417,8 @@ public class ContextUtils {
* Check if app with given {@code packageName} is installed
*/
public boolean isAppInstalled(String packageName) {
PackageManager pm = _context.getApplicationContext().getPackageManager();
try {
PackageManager pm = _context.getApplicationContext().getPackageManager();
pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES);
return true;
} catch (PackageManager.NameNotFoundException e) {
@ -409,17 +430,17 @@ public class ContextUtils {
* Restart the current app. Supply the class to start on startup
*/
public void restartApp(Class classToStart) {
Intent inte = new Intent(_context, classToStart);
PendingIntent inteP = PendingIntent.getActivity(_context, 555, inte, PendingIntent.FLAG_CANCEL_CURRENT);
Intent intent = new Intent(_context, classToStart);
PendingIntent pendi = PendingIntent.getActivity(_context, 555, intent, PendingIntent.FLAG_CANCEL_CURRENT);
AlarmManager mgr = (AlarmManager) _context.getSystemService(Context.ALARM_SERVICE);
if (_context instanceof Activity) {
((Activity) _context).finish();
}
if (mgr != null) {
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, inteP);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendi);
} else {
inte.addFlags(FLAG_ACTIVITY_NEW_TASK);
_context.startActivity(inte);
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
_context.startActivity(intent);
}
Runtime.getRuntime().exit(0);
}
@ -488,19 +509,25 @@ public class ContextUtils {
* {@code androidLC} may be in any of the forms: en, de, de-rAt
* If given an empty string, the default (system) locale gets loaded
*/
public void setAppLanguage(String androidLC) {
public void setAppLanguage(final String androidLC) {
Locale locale = getLocaleByAndroidCode(androidLC);
locale = (locale != null && !androidLC.isEmpty()) ? locale : Resources.getSystem().getConfiguration().locale;
setLocale(locale);
}
public ContextUtils setLocale(final Locale locale) {
Configuration config = _context.getResources().getConfiguration();
config.locale = (locale != null && !androidLC.isEmpty())
? locale : Resources.getSystem().getConfiguration().locale;
config.locale = (locale != null ? locale : Resources.getSystem().getConfiguration().locale);
_context.getResources().updateConfiguration(config, null);
Locale.setDefault(locale);
return this;
}
/**
* 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
*/
public boolean shouldColorOnTopBeLight(@ColorInt int colorOnBottomInt) {
public boolean shouldColorOnTopBeLight(@ColorInt final int colorOnBottomInt) {
return 186 > (((0.299 * Color.red(colorOnBottomInt))
+ ((0.587 * Color.green(colorOnBottomInt))
+ (0.114 * Color.blue(colorOnBottomInt)))));
@ -509,7 +536,7 @@ public class ContextUtils {
/**
* Convert a html string to an android {@link Spanned} object
*/
public Spanned htmlToSpanned(String html) {
public Spanned htmlToSpanned(final String html) {
Spanned result;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY);
@ -568,7 +595,7 @@ public class ContextUtils {
return dirs;
}
public String getStorageName(File externalFileDir, boolean storageNameWithoutType) {
public String getStorageName(final File externalFileDir, final boolean storageNameWithoutType) {
boolean isInt = externalFileDir.getAbsolutePath().startsWith(Environment.getExternalStorageDirectory().getAbsolutePath());
String[] split = externalFileDir.getAbsolutePath().split("/");
@ -579,7 +606,7 @@ public class ContextUtils {
}
}
public List<Pair<File, String>> getStorages(boolean internalStorageFolder, boolean sdcardFolders) {
public List<Pair<File, String>> getStorages(final boolean internalStorageFolder, final boolean sdcardFolders) {
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) {
@ -592,7 +619,7 @@ public class ContextUtils {
return storages;
}
public File getStorageRootFolder(File file) {
public File getStorageRootFolder(final File file) {
String filepath;
try {
filepath = file.getCanonicalPath();
@ -613,7 +640,7 @@ public class ContextUtils {
*
* @param files Files and folders to scan
*/
public void mediaScannerScanFile(File... files) {
public void mediaScannerScanFile(final File... files) {
if (android.os.Build.VERSION.SDK_INT > 19) {
String[] paths = new String[files.length];
for (int i = 0; i < files.length; i++) {
@ -662,8 +689,12 @@ public class ContextUtils {
/**
* Get a {@link Bitmap} out of a {@link DrawableRes}
*/
public Bitmap drawableToBitmap(@DrawableRes int drawableId) {
return drawableToBitmap(ContextCompat.getDrawable(_context, drawableId));
public Bitmap drawableToBitmap(@DrawableRes final int drawableId) {
try {
return drawableToBitmap(ContextCompat.getDrawable(_context, drawableId));
} catch (Exception e) {
return null;
}
}
/**
@ -671,7 +702,7 @@ public class ContextUtils {
* Specifying a {@code maxDimen} is also possible and a value below 2000
* is recommended, otherwise a {@link OutOfMemoryError} may occur
*/
public Bitmap loadImageFromFilesystem(File imagePath, int maxDimen) {
public Bitmap loadImageFromFilesystem(final File imagePath, final int maxDimen) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imagePath.getAbsolutePath(), options);
@ -687,7 +718,7 @@ public class ContextUtils {
* @param maxDimen Max size of the Bitmap (width or height)
* @return the scaling factor that needs to be applied to the bitmap
*/
public int calculateInSampleSize(BitmapFactory.Options options, int maxDimen) {
public int calculateInSampleSize(final BitmapFactory.Options options, final int maxDimen) {
// Raw height and width of image
int height = options.outHeight;
int width = options.outWidth;
@ -703,7 +734,7 @@ public class ContextUtils {
* Scale the bitmap so both dimensions are lower or equal to {@code maxDimen}
* This keeps the aspect ratio
*/
public Bitmap scaleBitmap(Bitmap bitmap, int maxDimen) {
public Bitmap scaleBitmap(final Bitmap bitmap, final int maxDimen) {
int picSize = Math.min(bitmap.getHeight(), bitmap.getWidth());
float scale = 1.f * maxDimen / picSize;
Matrix matrix = new Matrix();
@ -714,7 +745,7 @@ public class ContextUtils {
/**
* Write the given {@link Bitmap} to {@code imageFile}, in {@link CompressFormat#JPEG} format
*/
public boolean writeImageToFileJpeg(File imageFile, Bitmap image) {
public boolean writeImageToFileJpeg(final File imageFile, final Bitmap image) {
return writeImageToFile(imageFile, image, Bitmap.CompressFormat.JPEG, 95);
}
@ -727,7 +758,7 @@ public class ContextUtils {
* @param quality Quality level, defaults to 95
* @return True if writing was successful
*/
public boolean writeImageToFile(File targetFile, Bitmap image, CompressFormat format, Integer quality) {
public boolean writeImageToFile(final File targetFile, final Bitmap image, CompressFormat format, Integer quality) {
File folder = new File(targetFile.getParent());
if (quality == null || quality < 0 || quality > 100) {
quality = 95;
@ -765,7 +796,7 @@ public class ContextUtils {
* Draw text in the center of the given {@link DrawableRes}
* This may be useful for e.g. badge counts
*/
public Bitmap drawTextOnDrawable(@DrawableRes int drawableRes, String text, int textSize) {
public Bitmap drawTextOnDrawable(@DrawableRes final int drawableRes, final String text, final int textSize) {
Resources resources = _context.getResources();
float scale = resources.getDisplayMetrics().density;
Bitmap bitmap = drawableToBitmap(drawableRes);
@ -790,7 +821,7 @@ public class ContextUtils {
* Try to tint all {@link Menu}s {@link MenuItem}s with given color
*/
@SuppressWarnings("ConstantConditions")
public void tintMenuItems(Menu menu, boolean recurse, @ColorInt int iconColor) {
public void tintMenuItems(final Menu menu, final boolean recurse, @ColorInt final int iconColor) {
for (int i = 0; i < menu.size(); i++) {
MenuItem item = menu.getItem(i);
try {
@ -807,14 +838,14 @@ public class ContextUtils {
/**
* Loads {@link Drawable} by given {@link DrawableRes} and applies a color
*/
public Drawable tintDrawable(@DrawableRes int drawableRes, @ColorInt int color) {
public Drawable tintDrawable(@DrawableRes final int drawableRes, @ColorInt final int color) {
return tintDrawable(rdrawable(drawableRes), color);
}
/**
* Tint a {@link Drawable} with given {@code color}
*/
public Drawable tintDrawable(@Nullable Drawable drawable, @ColorInt int color) {
public Drawable tintDrawable(@Nullable Drawable drawable, @ColorInt final int color) {
if (drawable != null) {
drawable = DrawableCompat.wrap(drawable);
DrawableCompat.setTint(drawable.mutate(), color);
@ -826,7 +857,10 @@ public class ContextUtils {
* 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
*/
public void setSubMenuIconsVisiblity(Menu menu, boolean visible) {
public void setSubMenuIconsVisiblity(final Menu menu, final boolean visible) {
if (TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL) {
return;
}
if (menu.getClass().getSimpleName().equals("MenuBuilder")) {
try {
@SuppressLint("PrivateApi") Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE);
@ -886,7 +920,7 @@ public class ContextUtils {
}
public String getMimeType(File file) {
public String getMimeType(final File file) {
return getMimeType(Uri.fromFile(file));
}
@ -895,7 +929,7 @@ public class ContextUtils {
* Android/Java's own MimeType map is very very small and detection barely works at all
* Hence use custom map for some file extensions
*/
public String getMimeType(Uri uri) {
public String getMimeType(final Uri uri) {
String mimeType = null;
if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
ContentResolver cr = _context.getContentResolver();
@ -936,7 +970,7 @@ public class ContextUtils {
return mimeType;
}
public Integer parseColor(String colorstr) {
public Integer parseColor(final String colorstr) {
if (colorstr == null || colorstr.trim().isEmpty()) {
return null;
}
@ -957,6 +991,22 @@ public class ContextUtils {
return true;
}
}
// 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);
}
}
}

View file

@ -29,6 +29,7 @@ import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URLConnection;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@ -240,6 +241,30 @@ public class FileUtils {
}
}
public static boolean copyFile(final File src, final FileOutputStream os) {
InputStream is = null;
try {
try {
is = new FileInputStream(src);
byte[] buf = new byte[BUFFER_SIZE];
int len;
while ((len = is.read(buf)) > 0) {
os.write(buf, 0, len);
}
return true;
} finally {
if (is != null) {
is.close();
}
if (os != null) {
os.close();
}
}
} catch (IOException ex) {
return false;
}
}
// Returns -1 if the file did not contain any of the needles, otherwise,
// the index of which needle was found in the contents of the file.
//
@ -452,7 +477,15 @@ public class FileUtils {
}
String[] units = abbreviation ? new String[]{"B", "kB", "MB", "GB", "TB"} : new String[]{"Bytes", "Kilobytes", "Megabytes", "Gigabytes", "Terabytes"};
int unit = (int) (Math.log10(size) / Math.log10(1024));
return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, unit))
+ " " + units[unit];
return new DecimalFormat("#,##0.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH)).format(size / Math.pow(1024, unit)) + " " + units[unit];
}
public static int[] getTimeDiffHMS(long now, long past) {
int[] ret = new int[3];
long diff = Math.abs(now - past);
ret[0] = (int) (diff / (1000 * 60 * 60)); // hours
ret[1] = (int) (diff / (1000 * 60)) % 60; // min
ret[2] = (int) (diff / 1000) % 60; // sec
return ret;
}
}

View file

@ -77,7 +77,7 @@ public class NetworkUtils {
int written = 0;
final float invLength = 1f / connection.getContentLength();
byte data[] = new byte[BUFFER_SIZE];
byte[] data = new byte[BUFFER_SIZE];
while ((count = input.read(data)) != -1) {
output.write(data, 0, count);
if (invLength != -1f && progressCallback != null) {

View file

@ -96,12 +96,12 @@ public class ShareUtil {
protected String _fileProviderAuthority;
protected String _chooserTitle;
public ShareUtil(Context context) {
public ShareUtil(final Context context) {
_context = context;
_chooserTitle = "";
}
public void setContext(Context c) {
public void setContext(final Context c) {
_context = c;
}
@ -116,13 +116,13 @@ public class ShareUtil {
return _fileProviderAuthority;
}
public ShareUtil setFileProviderAuthority(String fileProviderAuthority) {
public ShareUtil setFileProviderAuthority(final String fileProviderAuthority) {
_fileProviderAuthority = fileProviderAuthority;
return this;
}
public ShareUtil setChooserTitle(String title) {
public ShareUtil setChooserTitle(final String title) {
_chooserTitle = title;
return this;
}
@ -133,7 +133,7 @@ public class ShareUtil {
* @param file the file
* @return Uri for this file
*/
public Uri getUriByFileProviderAuthority(File file) {
public Uri getUriByFileProviderAuthority(final File file) {
return FileProvider.getUriForFile(_context, getFileProviderAuthority(), file);
}
@ -143,7 +143,7 @@ public class ShareUtil {
* @param intent Thing to be shared
* @param chooserText The title text for the chooser, or null for default
*/
public void showChooser(Intent intent, String chooserText) {
public void showChooser(final Intent intent, final String chooserText) {
_context.startActivity(Intent.createChooser(intent,
chooserText != null ? chooserText : _chooserTitle));
}
@ -157,7 +157,7 @@ public class ShareUtil {
* @param iconRes Icon resource for the item
* @param title Title of the item
*/
public void createLauncherDesktopShortcut(Intent intent, @DrawableRes int iconRes, String title) {
public void createLauncherDesktopShortcut(final Intent intent, @DrawableRes final int iconRes, final String title) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
if (intent.getAction() == null) {
@ -182,7 +182,7 @@ public class ShareUtil {
* @param iconRes Icon resource for the item
* @param title Title of the item
*/
public void createLauncherDesktopShortcutLegacy(Intent intent, @DrawableRes int iconRes, String title) {
public void createLauncherDesktopShortcutLegacy(final Intent intent, @DrawableRes final int iconRes, final String title) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
if (intent.getAction() == null) {
@ -203,7 +203,7 @@ public class ShareUtil {
* @param text The text to share
* @param mimeType MimeType or null (uses text/plain)
*/
public void shareText(String text, @Nullable String mimeType) {
public void shareText(final String text, @Nullable final String mimeType) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT, text);
intent.setType(mimeType != null ? mimeType : MIME_TEXT_PLAIN);
@ -216,7 +216,7 @@ public class ShareUtil {
* @param file The file to share
* @param mimeType The files mime type
*/
public boolean shareStream(File file, String mimeType) {
public boolean shareStream(final File file, final String mimeType) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(EXTRA_FILEPATH, file.getAbsolutePath());
intent.setType(mimeType);
@ -237,7 +237,7 @@ public class ShareUtil {
* @param files The files to share
* @param mimeType The files mime type. Usally * / * is the best option
*/
public boolean shareStreamMultiple(Collection<File> files, String mimeType) {
public boolean shareStreamMultiple(final Collection<File> files, final String mimeType) {
ArrayList<Uri> uris = new ArrayList<>();
for (File file : files) {
File uri = new File(file.toString());
@ -258,14 +258,13 @@ public class ShareUtil {
/**
* Start calendar application to add new event, with given details prefilled
*/
public boolean createCalendarAppointment(@Nullable String title, @Nullable String description, @Nullable String location, @Nullable Long... startAndEndTime) {
public boolean createCalendarAppointment(@Nullable final String title, @Nullable final String description, @Nullable final String location, @Nullable final Long... startAndEndTime) {
Intent intent = new Intent(Intent.ACTION_INSERT).setData(CalendarContract.Events.CONTENT_URI);
if (title != null) {
intent.putExtra(CalendarContract.Events.TITLE, title);
}
if (description != null) {
description = description.length() > 800 ? description.substring(0, 800) : description;
intent.putExtra(CalendarContract.Events.DESCRIPTION, description);
intent.putExtra(CalendarContract.Events.DESCRIPTION, (description.length() > 800 ? description.substring(0, 800) : description));
}
if (location != null) {
intent.putExtra(CalendarContract.Events.EVENT_LOCATION, location);
@ -292,7 +291,7 @@ public class ShareUtil {
*
* @param file The file to share
*/
public boolean viewFileInOtherApp(File file, @Nullable String type) {
public boolean viewFileInOtherApp(final File file, @Nullable final String type) {
// On some specific devices the first won't work
Uri fileUri = null;
try {
@ -324,7 +323,7 @@ public class ShareUtil {
* @param format A {@link Bitmap.CompressFormat}, supporting JPEG,PNG,WEBP
* @return if success, true
*/
public boolean shareImage(Bitmap bitmap, Bitmap.CompressFormat format) {
public boolean shareImage(final Bitmap bitmap, final Bitmap.CompressFormat format) {
return shareImage(bitmap, format, 95, "SharedImage");
}
@ -337,7 +336,7 @@ public class ShareUtil {
* @param quality Quality of the exported image [0-100]
* @return if success, true
*/
public boolean shareImage(Bitmap bitmap, Bitmap.CompressFormat format, int quality, String imageName) {
public boolean shareImage(final Bitmap bitmap, final Bitmap.CompressFormat format, final int quality, final String imageName) {
try {
String ext = format.name().toLowerCase();
File file = File.createTempFile(imageName, "." + ext.replace("jpeg", "jpg"), _context.getExternalCacheDir());
@ -359,19 +358,23 @@ public class ShareUtil {
* @return {{@link PrintJob}} or null
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@SuppressWarnings("deprecation")
public PrintJob print(WebView webview, String jobName) {
public PrintJob print(final WebView webview, final String jobName, final boolean... landscape) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
PrintDocumentAdapter printAdapter;
PrintManager printManager = (PrintManager) _context.getSystemService(Context.PRINT_SERVICE);
final PrintDocumentAdapter printAdapter;
final PrintManager printManager = (PrintManager) _context.getSystemService(Context.PRINT_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
printAdapter = webview.createPrintDocumentAdapter(jobName);
} else {
printAdapter = webview.createPrintDocumentAdapter();
}
final PrintAttributes.Builder attrib = new PrintAttributes.Builder();
if (landscape != null && landscape.length > 0 && landscape[0]) {
attrib.setMediaSize(new PrintAttributes.MediaSize("ISO_A4", "android", 11690, 8270));
attrib.setMinMargins(new PrintAttributes.Margins(0, 0, 0, 0));
}
if (printManager != null) {
try {
return printManager.print(jobName, printAdapter, new PrintAttributes.Builder().build());
return printManager.print(jobName, printAdapter, attrib.build());
} catch (Exception ignored) {
}
}
@ -386,8 +389,7 @@ public class ShareUtil {
* See {@link #print(WebView, String) print method}
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@SuppressWarnings("deprecation")
public PrintJob createPdf(WebView webview, String jobName) {
public PrintJob createPdf(final WebView webview, final String jobName) {
return print(webview, jobName);
}
@ -399,7 +401,7 @@ public class ShareUtil {
* @return A {@link Bitmap} or null
*/
@Nullable
public static Bitmap getBitmapFromWebView(WebView webView) {
public static Bitmap getBitmapFromWebView(final WebView webView) {
try {
//Measure WebView's content
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
@ -432,7 +434,7 @@ public class ShareUtil {
* Replace (primary) clipboard contents with given {@code text}
* @param text Text to be set
*/
public boolean setClipboard(CharSequence text) {
public boolean setClipboard(final CharSequence text) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
android.text.ClipboardManager cm = ((android.text.ClipboardManager) _context.getSystemService(Context.CLIPBOARD_SERVICE));
if (cm != null) {
@ -485,7 +487,7 @@ public class ShareUtil {
* @param callback Callback after paste try
* @param serverOrNothing Supply one or no hastebin server. If empty, the default gets taken
*/
public void pasteOnHastebin(final String text, final Callback.a2<Boolean, String> callback, String... serverOrNothing) {
public void pasteOnHastebin(final String text, final Callback.a2<Boolean, String> callback, final String... serverOrNothing) {
final Handler handler = new Handler();
final String server = (serverOrNothing != null && serverOrNothing.length > 0 && serverOrNothing[0] != null)
? serverOrNothing[0] : "https://hastebin.com";
@ -507,7 +509,7 @@ public class ShareUtil {
* @param body Body (content) text to be prefilled in the mail
* @param to recipients to be prefilled in the mail
*/
public void draftEmail(String subject, String body, String... to) {
public void draftEmail(final String subject, final String body, final String... to) {
Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:"));
if (subject != null) {
@ -528,7 +530,7 @@ public class ShareUtil {
* @param receivingIntent The intent from {@link Activity#getIntent()}
* @return A file or null if extraction did not succeed
*/
public File extractFileFromIntent(Intent receivingIntent) {
public File extractFileFromIntent(final Intent receivingIntent) {
String action = receivingIntent.getAction();
String type = receivingIntent.getType();
File tmpf;
@ -572,6 +574,14 @@ public class ShareUtil {
}
}
// media/ prefix for External storage
if (fileStr.startsWith((tmps = "media/"))) {
File f = new File(Uri.decode(Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + fileStr.substring(tmps.length())));
if (f.exists()) {
return f;
}
}
// Next/OwnCloud Fileprovider
for (String fp : new String[]{"org.nextcloud.files", "org.nextcloud.beta.files", "org.owncloud.files"}) {
if (fileProvider.equals(fp) && fileStr.startsWith(tmps = "external_files/")) {
@ -587,6 +597,16 @@ public class ShareUtil {
return new File(Uri.decode(Environment.getExternalStorageDirectory().getAbsolutePath() + fileStr.substring(tmps.length())));
}
if (fileStr.startsWith(tmps = "external_files/")) {
for (String prefix : new String[]{Environment.getExternalStorageDirectory().getAbsolutePath(), "/storage", ""}) {
File f = new File(Uri.decode(prefix + "/" + fileStr.substring(tmps.length())));
if (f.exists()) {
return f;
}
}
}
// URI Encoded paths with full path after content://package/
if (fileStr.startsWith("/") || fileStr.startsWith("%2F")) {
tmpf = new File(Uri.decode(fileStr));
@ -624,6 +644,11 @@ public class ShareUtil {
}
}
public String extractFileFromIntentStr(final Intent receivingIntent) {
File f = extractFileFromIntent(receivingIntent);
return f != null ? f.getAbsolutePath() : null;
}
/**
* Request a picture from camera-like apps
* Result ({@link String}) will be available from {@link Activity#onActivityResult(int, int, Intent)}.
@ -634,7 +659,8 @@ public class ShareUtil {
*
* @param target Path to file to write to, if folder the filename gets app_name + millis + random filename. If null DCIM folder is used.
*/
public String requestCameraPicture(File target) {
@SuppressWarnings("RegExpRedundantEscape")
public String requestCameraPicture(final File target) {
if (!(_context instanceof Activity)) {
throw new RuntimeException("Error: ShareUtil.requestCameraPicture needs an Activity Context.");
}
@ -647,7 +673,7 @@ public class ShareUtil {
if (target != null && !target.isDirectory()) {
photoFile = target;
} else {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss", Locale.getDefault());
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss", Locale.ENGLISH);
File storageDir = target != null ? target : new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "Camera");
String imageFileName = ((new ContextUtils(_context).rstr("app_name")).replaceAll("[^a-zA-Z0-9\\.\\-]", "_") + "_").replace("__", "_") + sdf.format(new Date());
photoFile = new File(storageDir, imageFileName + ".jpg");
@ -686,7 +712,7 @@ public class ShareUtil {
* Also may forward results via local broadcast
*/
@SuppressLint("ApplySharedPref")
public Object extractResultFromActivityResult(int requestCode, int resultCode, Intent data, Activity... activityOrNull) {
public Object extractResultFromActivityResult(final int requestCode, final int resultCode, final Intent data, final Activity... activityOrNull) {
Activity activity = greedyGetActivity(activityOrNull);
switch (requestCode) {
case REQUEST_CAMERA_PICTURE: {
@ -717,6 +743,10 @@ public class ShareUtil {
cursor.close();
}
// Try to grab via file extraction method
data.setAction(Intent.ACTION_VIEW);
picturePath = picturePath != null ? picturePath : extractFileFromIntentStr(data);
// Retrieve image from file descriptor / Cloud, e.g.: Google Drive, Picasa
if (picturePath == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
@ -762,7 +792,7 @@ public class ShareUtil {
* Send a local broadcast (to receive within app), with given action and string-extra+value.
* This is a convenience method for quickly sending just one thing.
*/
public void sendLocalBroadcastWithStringExtra(String action, String extra, CharSequence value) {
public void sendLocalBroadcastWithStringExtra(final String action, final String extra, final CharSequence value) {
Intent intent = new Intent(action);
intent.putExtra(extra, value);
LocalBroadcastManager.getInstance(_context).sendBroadcast(intent);
@ -776,7 +806,7 @@ public class ShareUtil {
* @param filterActions All {@link IntentFilter} actions to filter for
* @return The created instance. Has to be unregistered on {@link Activity} lifecycle events.
*/
public BroadcastReceiver receiveResultFromLocalBroadcast(Callback.a2<Intent, BroadcastReceiver> callback, boolean autoUnregister, String... filterActions) {
public BroadcastReceiver receiveResultFromLocalBroadcast(final Callback.a2<Intent, BroadcastReceiver> callback, final boolean autoUnregister, final String... filterActions) {
IntentFilter intentFilter = new IntentFilter();
for (String filterAction : filterActions) {
intentFilter.addAction(filterAction);
@ -804,7 +834,7 @@ public class ShareUtil {
*
* @param file File that should be edited
*/
public void requestPictureEdit(File file) {
public void requestPictureEdit(final File file) {
Uri uri = getUriByFileProviderAuthority(file);
int flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION;
@ -826,9 +856,10 @@ public class ShareUtil {
*
* @param file Target file
* @param mode 1 for picture, 2 for video, anything else for other
* @return
* @return Media URI
*/
public Uri getMediaUri(File file, int mode) {
@SuppressWarnings("TryFinallyCanBeTryWithResources")
public Uri getMediaUri(final File file, final int mode) {
Uri uri = MediaStore.Files.getContentUri("external");
uri = (mode != 0) ? (mode == 1 ? MediaStore.Images.Media.EXTERNAL_CONTENT_URI : MediaStore.Video.Media.EXTERNAL_CONTENT_URI) : uri;
@ -854,7 +885,7 @@ public class ShareUtil {
* which implement the Chrome Custom Tab interface. This method changes
* the customtab intent to use an available compatible browser, if available.
*/
public void enableChromeCustomTabsForOtherBrowsers(Intent customTabIntent) {
public void enableChromeCustomTabsForOtherBrowsers(final Intent customTabIntent) {
String[] checkpkgs = new String[]{
"com.android.chrome", "com.chrome.beta", "com.chrome.dev", "com.google.android.apps.chrome", "org.chromium.chrome",
"org.mozilla.fennec_fdroid", "org.mozilla.firefox", "org.mozilla.firefox_beta", "org.mozilla.fennec_aurora",
@ -905,7 +936,7 @@ public class ShareUtil {
* Request storage access. The user needs to press "Select storage" at the correct storage.
* @param activity The activity which will receive the result from startActivityForResult
*/
public void requestStorageAccessFramework(Activity... activity) {
public void requestStorageAccessFramework(final Activity... activity) {
Activity a = greedyGetActivity(activity);
if (a != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
@ -961,8 +992,12 @@ public class ShareUtil {
* @param file The file object (file/folder)
* @return Wether or not the file is under storage access folder
*/
public boolean isUnderStorageAccessFolder(File file) {
public boolean isUnderStorageAccessFolder(final File file) {
if (file != null) {
// When file writeable as is, it's the fastest way to learn SAF isn't required
if (file.canWrite()) {
return false;
}
ContextUtils cu = new ContextUtils(_context);
for (Pair<File, String> storage : cu.getStorages(false, true)) {
if (file.getAbsolutePath().startsWith(storage.first.getAbsolutePath())) {
@ -978,7 +1013,7 @@ public class ShareUtil {
/**
* Greedy extract Activity from parameter or convert context if it's a activity
*/
private Activity greedyGetActivity(Activity... activity) {
private Activity greedyGetActivity(final Activity... activity) {
if (activity != null && activity.length != 0 && activity[0] != null) {
return activity[0];
}
@ -996,10 +1031,11 @@ public class ShareUtil {
* @param isDir Wether or not the given file parameter is a directory
* @return Wether or not the file can be written
*/
public boolean canWriteFile(File file, boolean isDir) {
public boolean canWriteFile(final File file, final boolean isDir) {
if (file == null) {
return false;
} else if (file.getAbsolutePath().startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) {
} else if (file.getAbsolutePath().startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())
|| file.getAbsolutePath().startsWith(_context.getFilesDir().getAbsolutePath())) {
boolean s1 = isDir && file.getParentFile().canWrite();
return !isDir && file.getParentFile() != null ? file.getParentFile().canWrite() : file.canWrite();
} else {
@ -1017,7 +1053,8 @@ public class ShareUtil {
* @param isDir Wether or not file is a directory. For non-existing (to be created) files this info is not known hence required.
* @return A {@link DocumentFile} object or null if file cannot be converted
*/
public DocumentFile getDocumentFile(File file, boolean isDir) {
@SuppressWarnings("RegExpRedundantEscape")
public DocumentFile getDocumentFile(final File file, final boolean isDir) {
// On older versions use fromFile
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
return DocumentFile.fromFile(file);
@ -1066,7 +1103,7 @@ public class ShareUtil {
return dof;
}
public void showMountSdDialog(@StringRes int title, @StringRes int description, @DrawableRes int mountDescriptionGraphic, Activity... activityOrNull) {
public void showMountSdDialog(@StringRes final int title, @StringRes final int description, @DrawableRes final int mountDescriptionGraphic, final Activity... activityOrNull) {
Activity activity = greedyGetActivity(activityOrNull);
if (activity == null) {
return;
@ -1087,11 +1124,12 @@ public class ShareUtil {
dialogi.show();
}
public void writeFile(File file, boolean isDirectory, Callback.a2<Boolean, FileOutputStream> writeFileCallback) {
@SuppressWarnings({"ResultOfMethodCallIgnored", "StatementWithEmptyBody"})
public void writeFile(final File file, final boolean isDirectory, final Callback.a2<Boolean, FileOutputStream> writeFileCallback) {
try {
FileOutputStream fileOutputStream = null;
ParcelFileDescriptor pfd = null;
if (file.canWrite()) {
if (file.canWrite() || (!file.exists() && file.getParentFile().canWrite())) {
if (isDirectory) {
file.mkdirs();
} else {
@ -1112,7 +1150,10 @@ public class ShareUtil {
writeFileCallback.callback(fileOutputStream != null || (isDirectory && file.exists()), fileOutputStream);
}
if (fileOutputStream != null) {
fileOutputStream.close();
try {
fileOutputStream.close();
} catch (Exception ignored) {
}
}
if (pfd != null) {
pfd.close();
@ -1132,7 +1173,7 @@ public class ShareUtil {
* @param directCall Direct call number if possible
*/
@SuppressWarnings("SimplifiableConditionalExpression")
public void callTelephoneNumber(String telNo, boolean... directCall) {
public void callTelephoneNumber(final String telNo, final boolean... directCall) {
Activity activity = greedyGetActivity();
if (activity == null) {
throw new RuntimeException("Error: ShareUtil::callTelephoneNumber needs to be contstructed with activity context");

View file

@ -13,8 +13,8 @@ import java.text.SimpleDateFormat
buildscript {
ext {
version_gradle_tools = "3.5.1"
version_plugin_kotlin = "1.3.50"
version_gradle_tools = "3.5.2"
version_plugin_kotlin = "1.3.60"
enable_plugin_kotlin = false
version_compileSdk = 28