diff --git a/app/src/main/java/net/gsantner/opoc/activity/GsFragmentBase.java b/app/src/main/java/net/gsantner/opoc/activity/GsFragmentBase.java index 507d25ff..7dcf0317 100644 --- a/app/src/main/java/net/gsantner/opoc/activity/GsFragmentBase.java +++ b/app/src/main/java/net/gsantner/opoc/activity/GsFragmentBase.java @@ -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; + } + } } diff --git a/app/src/main/java/net/gsantner/opoc/preference/SharedPreferencesPropertyBackend.java b/app/src/main/java/net/gsantner/opoc/preference/SharedPreferencesPropertyBackend.java index 343f2a4c..6950e522 100644 --- a/app/src/main/java/net/gsantner/opoc/preference/SharedPreferencesPropertyBackend.java +++ b/app/src/main/java/net/gsantner/opoc/preference/SharedPreferencesPropertyBackend.java @@ -547,7 +547,7 @@ public class SharedPreferencesPropertyBackend implements PropertyBackend callback; public List data = new ArrayList<>(); public List highlightData = new ArrayList<>(); + public List 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) { diff --git a/app/src/main/java/net/gsantner/opoc/util/Callback.java b/app/src/main/java/net/gsantner/opoc/util/Callback.java index c07f883c..7a3186e8 100644 --- a/app/src/main/java/net/gsantner/opoc/util/Callback.java +++ b/app/src/main/java/net/gsantner/opoc/util/Callback.java @@ -12,6 +12,11 @@ package net.gsantner.opoc.util; @SuppressWarnings("unused") public class Callback { + + public interface a0 { + void callback(); + } + public interface a1 { void callback(A arg1); } diff --git a/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java b/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java index f4b730b4..141f1b47 100644 --- a/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java @@ -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> getStorages(boolean internalStorageFolder, boolean sdcardFolders) { + public List> getStorages(final boolean internalStorageFolder, final boolean sdcardFolders) { List> storages = new ArrayList<>(); for (Pair 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 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); + } + } } diff --git a/app/src/main/java/net/gsantner/opoc/util/FileUtils.java b/app/src/main/java/net/gsantner/opoc/util/FileUtils.java index 14c57344..c8a2b6d3 100644 --- a/app/src/main/java/net/gsantner/opoc/util/FileUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/FileUtils.java @@ -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; } } diff --git a/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java b/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java index 50d11664..a8c564c6 100644 --- a/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java @@ -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) { diff --git a/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java b/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java index cc6ae7bb..4ebea3b7 100644 --- a/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java +++ b/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java @@ -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 files, String mimeType) { + public boolean shareStreamMultiple(final Collection files, final String mimeType) { ArrayList 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 callback, String... serverOrNothing) { + public void pasteOnHastebin(final String text, final Callback.a2 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 callback, boolean autoUnregister, String... filterActions) { + public BroadcastReceiver receiveResultFromLocalBroadcast(final Callback.a2 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 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 writeFileCallback) { + @SuppressWarnings({"ResultOfMethodCallIgnored", "StatementWithEmptyBody"}) + public void writeFile(final File file, final boolean isDirectory, final Callback.a2 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"); diff --git a/build.gradle b/build.gradle index 2d3db78c..7f72a6af 100644 --- a/build.gradle +++ b/build.gradle @@ -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