Java tutorial
/* This file is part of My Expenses. * My Expenses is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * My Expenses is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with My Expenses. If not, see <http://www.gnu.org/licenses/>. */ package org.totschnig.myexpenses.util; import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.support.design.widget.FloatingActionButton; import android.support.v4.app.LoaderManager; import android.support.v4.content.ContextCompat; import android.support.v4.content.FileProvider; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v4.provider.DocumentFile; import android.support.v7.widget.AppCompatDrawableManager; import android.telephony.TelephonyManager; import android.text.Html; import android.text.InputFilter; import android.text.Spanned; import android.text.TextUtils; import android.util.Log; import android.util.SparseIntArray; import android.util.Xml; import android.view.ContextThemeWrapper; import android.view.MenuItem; import android.view.SubMenu; import android.view.View; import android.view.inputmethod.EditorInfo; import android.widget.EditText; import android.widget.Spinner; import android.widget.Toast; import org.totschnig.myexpenses.BuildConfig; import org.totschnig.myexpenses.MyApplication; import org.totschnig.myexpenses.R; import org.totschnig.myexpenses.activity.ProtectedFragmentActivity; import org.totschnig.myexpenses.model.Category; import org.totschnig.myexpenses.model.ContribFeature; import org.totschnig.myexpenses.model.CurrencyEnum; import org.totschnig.myexpenses.model.Grouping; import org.totschnig.myexpenses.model.Money; import org.totschnig.myexpenses.model.Payee; import org.totschnig.myexpenses.preference.PrefKey; import org.totschnig.myexpenses.provider.TransactionDatabase; import org.totschnig.myexpenses.provider.filter.WhereFilter; import org.totschnig.myexpenses.task.GrisbiImportTask; import org.totschnig.myexpenses.ui.SimpleCursorAdapter; import org.xml.sax.SAXException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.BigDecimal; import java.net.URI; import java.net.URISyntaxException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.Normalizer; import java.text.NumberFormat; import java.text.ParseException; import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Currency; import java.util.Date; import java.util.EnumSet; import java.util.GregorianCalendar; import java.util.Iterator; import java.util.List; import java.util.Locale; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_AMOUNT; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_LAST_USED; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_SORT_KEY; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_USAGES; /** * Util class with helper methods * * @author Michael Totschnig * */ public class Utils { public static final boolean IS_FLAVOURED = !TextUtils.isEmpty(BuildConfig.FLAVOR); public static final boolean IS_ANDROID = BuildConfig.PLATTFORM.equals("Android"); public static Currency getLocalCurrency() { Currency result = null; TelephonyManager telephonyManager = (TelephonyManager) MyApplication.getInstance() .getSystemService(Context.TELEPHONY_SERVICE); if (telephonyManager != null) { try { String userCountry = telephonyManager.getNetworkCountryIso(); if (TextUtils.isEmpty(userCountry)) { userCountry = telephonyManager.getSimCountryIso(); } if (!TextUtils.isEmpty(userCountry)) { result = getSaveInstance(Currency.getInstance(new Locale("", userCountry))); } } catch (Exception e) { //fall back to currency from locale } } if (result == null) { try { //makeSure we know about the currency result = getSaveInstance(Currency.getInstance(Locale.getDefault())); } catch (IllegalArgumentException e) { result = Currency.getInstance("EUR"); } } return result; } public enum Feature { ; public boolean isEnabled() { return true; } } /* from https://www.google.com/design/spec/style/color.html#color-color-palette maps the 500 color to the 700 color */ static final SparseIntArray colorPrimaryDarkMap = new SparseIntArray() { { append(0xffF44336, 0xffD32F2F); append(0xffE91E63, 0xffC2185B); append(0xff9C27B0, 0xff7B1FA2); append(0xff673AB7, 0xff512DA8); append(0xff3F51B5, 0xff303F9F); append(0xff2196F3, 0xff1976D2); append(0xff03A9F4, 0xff0288D1); append(0xff00BCD4, 0xff0097A7); append(0xff009688, 0xff00796B); append(0xff4CAF50, 0xff388E3C); append(0xff8BC34A, 0xff689F38); append(0xffCDDC39, 0xffAFB42B); append(0xffFFEB3B, 0xffFBC02D); append(0xffFFC107, 0xffFFA000); append(0xffFF9800, 0xffF57C00); append(0xffFF5722, 0xffE64A19); append(0xff795548, 0xff5D4037); append(0xff9E9E9E, 0xff616161); append(0xff607D8B, 0xff455A64); append(0xff757575, 0xff424242); //aggregate theme light 600 800 append(0xffBDBDBD, 0xff757575); //aggregate theme dark 400 600 } }; private Utils() { } public static boolean hasApiLevel(int checkVersion) { return Build.VERSION.SDK_INT >= checkVersion; } private static NumberFormat numberFormat; private static void initNumberFormat() { String prefFormat = PrefKey.CUSTOM_DECIMAL_FORMAT.getString(""); if (!prefFormat.equals("")) { DecimalFormat nf = new DecimalFormat(); try { nf.applyLocalizedPattern(prefFormat); numberFormat = nf; } catch (IllegalArgumentException e) { //fallback to default currency instance numberFormat = NumberFormat.getCurrencyInstance(); } } else { numberFormat = NumberFormat.getCurrencyInstance(); } } private static NumberFormat getNumberFormat() { if (numberFormat == null) { initNumberFormat(); } return numberFormat; } public static void setNumberFormat(NumberFormat in) { numberFormat = in; } public static char getDefaultDecimalSeparator() { char sep = '.'; NumberFormat nfDLocal = NumberFormat.getNumberInstance(); if (nfDLocal instanceof DecimalFormat) { DecimalFormatSymbols symbols = ((DecimalFormat) nfDLocal).getDecimalFormatSymbols(); sep = symbols.getDecimalSeparator(); } return sep; } public static String defaultOrderBy(String textColumn, PrefKey prefKey) { String currentSortOrder = prefKey.getString("USAGES"); String sortOrder = textColumn + " COLLATE LOCALIZED"; switch (currentSortOrder) { case ProtectedFragmentActivity.SORT_ORDER_USAGES: sortOrder = KEY_USAGES + " DESC, " + sortOrder; break; case ProtectedFragmentActivity.SORT_ORDER_LAST_USED: sortOrder = KEY_LAST_USED + " DESC, " + sortOrder; break; case ProtectedFragmentActivity.SORT_ORDER_CUSTOM: sortOrder = KEY_SORT_KEY + " ASC, " + sortOrder; break; case ProtectedFragmentActivity.SORT_ORDER_AMOUNT: sortOrder = "abs(" + KEY_AMOUNT + ") DESC, " + sortOrder; break; case ProtectedFragmentActivity.SORT_ORDER_NEXT_INSTANCE: sortOrder = null; //handled by PlanInfoCursorWrapper //default is textColumn } return sortOrder; } /** * <a href="http://www.ibm.com/developerworks/java/library/j-numberformat/"> * http://www.ibm.com/developerworks/java/library/j-numberformat/</a> * * @param strFloat * parsed as float with the number format defined in the locale * @return the float retrieved from the string or null if parse did not * succeed */ public static BigDecimal validateNumber(DecimalFormat df, String strFloat) { ParsePosition pp; pp = new ParsePosition(0); pp.setIndex(0); df.setParseBigDecimal(true); BigDecimal n = (BigDecimal) df.parse(strFloat, pp); if (strFloat.length() != pp.getIndex() || n == null) { return null; } else { return n; } } public static URI validateUri(String target) { boolean targetParsable; URI uri = null; if (!target.equals("")) { try { uri = new URI(target); String scheme = uri.getScheme(); // strangely for mailto URIs getHost returns null, // so we make sure that mailto URIs handled as valid targetParsable = scheme != null && (scheme.equals("mailto") || uri.getHost() != null); } catch (URISyntaxException e1) { targetParsable = false; } if (!targetParsable) { return null; } return uri; } return null; } /** * formats an amount with a currency * * @param money * @return formated string */ public static String formatCurrency(Money money) { BigDecimal amount = money.getAmountMajor(); Currency currency = money.getCurrency(); return formatCurrency(amount, currency); } public static String formatCurrency(BigDecimal amount, Currency currency) { NumberFormat nf = getNumberFormat(); int fractionDigits = Money.getFractionDigits(currency); nf.setCurrency(currency); if (fractionDigits <= 3) { nf.setMinimumFractionDigits(fractionDigits); nf.setMaximumFractionDigits(fractionDigits); } else { nf.setMaximumFractionDigits(fractionDigits); } return nf.format(amount); } public static Date dateFromSQL(String dateString) { try { return TransactionDatabase.dateFormat.parse(dateString); } catch (ParseException e) { return null; } } /** * @param currency * @param separator * @return a Decimalformat with the number of fraction digits appropriate for * currency, and with the given separator, but without the currency * symbol appropriate for CSV and QIF export */ public static DecimalFormat getDecimalFormat(Currency currency, char separator) { DecimalFormat nf = new DecimalFormat(); DecimalFormatSymbols symbols = new DecimalFormatSymbols(); symbols.setDecimalSeparator(separator); nf.setDecimalFormatSymbols(symbols); int fractionDigits = currency.getDefaultFractionDigits(); if (fractionDigits != -1) { nf.setMinimumFractionDigits(fractionDigits); nf.setMaximumFractionDigits(fractionDigits); } else { nf.setMaximumFractionDigits(Money.DEFAULTFRACTIONDIGITS); } nf.setGroupingUsed(false); return nf; } /** * utility method that calls formatters for date * * @param text * @return formated string */ public static String convDate(String text, DateFormat format) { Date date = dateFromSQL(text); if (date == null) return text; else return format.format(date); } /** * utility method that calls formatters for date * * @param text * unixEpochAsString * @return formated string */ public static String convDateTime(String text, DateFormat format) { if (text == null) { return "???"; } Date date; try { date = new Date(Long.valueOf(text) * 1000L); } catch (NumberFormatException e) { // legacy, the migration from date string to unix timestamp // might have gone wrong for some users try { date = TransactionDatabase.dateTimeFormat.parse(text); } catch (ParseException e1) { date = new Date(); } } return format.format(date); } /** * utility method that calls formatters for amount this method is called from * adapters that give us the amount as String * * @param text * amount as String * @param currency * @return formated string */ public static String convAmount(String text, Currency currency) { Long amount; try { amount = Long.valueOf(text); } catch (NumberFormatException e) { amount = 0L; } return convAmount(amount, currency); } public static Currency getSaveInstance(String strCurrency) { Currency c; try { c = Currency.getInstance(strCurrency); } catch (IllegalArgumentException e) { Log.e("MyExpenses", strCurrency + " is not defined in ISO 4217"); c = Currency.getInstance(Locale.getDefault()); } return getSaveInstance(c); } public static Currency getSaveInstance(Currency currency) { try { CurrencyEnum.valueOf(currency.getCurrencyCode()); return currency; } catch (IllegalArgumentException e) { return Currency.getInstance("EUR"); } } /** * utility method that calls formatters for amount this method can be called * directly with Long values retrieved from db * * @param amount * @param currency * @return formated string */ public static String convAmount(Long amount, Currency currency) { return formatCurrency(new Money(currency, amount)); } /** * @return the directory user has configured in the settings, if not configured yet * returns {@link android.content.ContextWrapper#getExternalFilesDir(String)} with argument null */ public static DocumentFile getAppDir() { String prefString = PrefKey.APP_DIR.getString(null); if (prefString != null) { Uri pref = Uri.parse(prefString); if (pref.getScheme().equals("file")) { File appDir = new File(pref.getPath()); if (appDir.mkdir() || appDir.isDirectory()) { return DocumentFile.fromFile(appDir); } /* else { Utils.reportToAcra(new Exception("Found invalid preference value " + prefString)); }*/ } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //this will return null, if called on a pre-Lolipop device DocumentFile documentFile = DocumentFile.fromTreeUri(MyApplication.getInstance(), pref); if (dirExistsAndIsWritable(documentFile)) { return documentFile; } } } } File externalFilesDir = MyApplication.getInstance().getExternalFilesDir(null); if (externalFilesDir != null) { return DocumentFile.fromFile(externalFilesDir); } else { String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE; AcraHelper.report(new Exception("getExternalFilesDir returned null; " + permission + " : " + ContextCompat.checkSelfPermission(MyApplication.getInstance(), permission))); return null; } } public static File getCacheDir() { File external = MyApplication.getInstance().getExternalCacheDir(); return external != null ? external : MyApplication.getInstance().getCacheDir(); } /** * @param parentDir * @param prefix * @param addExtension * @return creates a file object in parentDir, with a timestamp appended to * prefix as name, if the file already exists it appends a numeric * postfix */ public static DocumentFile timeStampedFile(DocumentFile parentDir, String prefix, String mimeType, boolean addExtension) { String now = new SimpleDateFormat("yyyMMdd-HHmmss", Locale.US).format(new Date()); return newFile(parentDir, prefix + "-" + now, mimeType, addExtension); } public static DocumentFile newFile(DocumentFile parentDir, String base, String mimeType, boolean addExtension) { int postfix = 0; do { String name = base; if (postfix > 0) { name += "_" + postfix; } if (addExtension) { name += "." + mimeType.split("/")[1]; } if (parentDir.findFile(name) == null) { DocumentFile result = null; try { result = parentDir.createFile(mimeType, name); if (result == null) { AcraHelper.report(new Exception( String.format("createFile returned null: mimeType %s; name %s; parent %s", mimeType, name, parentDir.getUri().toString()))); } } catch (SecurityException e) { AcraHelper.report(new Exception( String.format("createFile threw SecurityException: mimeType %s; name %s; parent %s", mimeType, name, parentDir.getUri().toString()))); } return result; } postfix++; } while (true); } public static DocumentFile newDirectory(DocumentFile parentDir, String base) { int postfix = 0; do { String name = base; if (postfix > 0) { name += "_" + postfix; } if (parentDir.findFile(name) == null) { return parentDir.createDirectory(name); } postfix++; } while (true); } /** * Helper Method to Test if external Storage is Available from * http://www.ibm.com/developerworks/xml/library/x-androidstorage/index.html */ public static boolean isExternalStorageAvailable() { boolean state = false; String extStorageState = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(extStorageState)) { state = true; } return state; } public static Result checkAppDir() { if (!Utils.isExternalStorageAvailable()) { return new Result(false, R.string.external_storage_unavailable); } DocumentFile appDir = getAppDir(); if (appDir == null) { return new Result(false, R.string.io_error_appdir_null); } return dirExistsAndIsWritable(appDir) ? new Result(true) : new Result(false, R.string.app_dir_not_accessible, FileUtils.getPath(MyApplication.getInstance(), appDir.getUri())); } @NonNull public static boolean dirExistsAndIsWritable(DocumentFile appdir) { return appdir.exists() && appdir.canWrite(); } public static boolean copy(File src, File dst) { FileInputStream srcStream = null; FileOutputStream dstStream = null; try { srcStream = new FileInputStream(src); dstStream = new FileOutputStream(dst); dstStream.getChannel().transferFrom(srcStream.getChannel(), 0, srcStream.getChannel().size()); return true; } catch (FileNotFoundException e) { Log.e("MyExpenses", e.getLocalizedMessage()); return false; } catch (IOException e) { Log.e("MyExpenses", e.getLocalizedMessage()); return false; } finally { try { srcStream.close(); } catch (Exception e) { } try { dstStream.close(); } catch (Exception e) { } } } /** Create a File for saving an image or video */ // Source // http://developer.android.com/guide/topics/media/camera.html#saving-media /** * create a File object for storage of picture data * * @param temp * if true the returned file is suitable for temporary storage while * the user is editing the transaction if false the file will serve * as permanent storage, * care is taken that the file does not yet exist * @return a file on the external storage */ public static File getOutputMediaFile(String fileName, boolean temp) { // To be safe, you should check that the SDCard is mounted // using Environment.getExternalStorageState() before doing this. File mediaStorageDir = temp ? getCacheDir() : getPictureDir(); if (mediaStorageDir == null) return null; int postfix = 0; File result; do { result = new File(mediaStorageDir, getOutputMediaFileName(fileName, postfix)); postfix++; } while (result.exists()); return result; } public static Uri getOutputMediaUri(boolean temp) { String fileName = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); File outputMediaFile; if (MyApplication.getInstance().isProtected() && !temp) { outputMediaFile = getOutputMediaFile(fileName, false); if (outputMediaFile == null) return null; return FileProvider.getUriForFile(MyApplication.getInstance(), "org.totschnig.myexpenses.fileprovider", outputMediaFile); } else { outputMediaFile = getOutputMediaFile(fileName, temp); if (outputMediaFile == null) return null; return Uri.fromFile(outputMediaFile); } } public static String getPictureUriBase(boolean temp) { Uri sampleUri = getOutputMediaUri(temp); if (sampleUri == null) return null; String uriString = sampleUri.toString(); return uriString.substring(0, uriString.lastIndexOf('/')); } private static String getOutputMediaFileName(String base, int postfix) { if (postfix > 0) { base += "_" + postfix; } return base + ".jpg"; } public static File getPictureDir() { return getPictureDir(MyApplication.getInstance().isProtected()); } public static File getPictureDir(boolean secure) { File result; if (secure) { result = new File(MyApplication.getInstance().getFilesDir(), "images"); } else { result = MyApplication.getInstance().getExternalFilesDir(Environment.DIRECTORY_PICTURES); } if (result == null) return null; result.mkdir(); return result.exists() ? result : null; } /** * copy src uri to dest uri * * @param src * @param dest * @return */ public static void copy(Uri src, Uri dest) throws IOException { InputStream input = null; OutputStream output = null; try { input = MyApplication.getInstance().getContentResolver().openInputStream(src); if (input == null) { throw new IOException("Could not open InputStream " + src.toString()); } output = MyApplication.getInstance().getContentResolver().openOutputStream(dest); if (output == null) { throw new IOException("Could not open OutputStream " + dest.toString()); } final byte[] buffer = new byte[1024]; int read; while ((read = input.read(buffer)) != -1) { output.write(buffer, 0, read); } output.flush(); } finally { try { if (input != null) input.close(); } catch (IOException e) { } try { if (output != null) output.close(); } catch (IOException e) { } } } public static void share(Context ctx, ArrayList<Uri> fileUris, String target, String mimeType) { URI uri = null; Intent intent; String scheme = "mailto"; boolean multiple = fileUris.size() > 1; if (!target.equals("")) { uri = Utils.validateUri(target); if (uri == null) { Toast.makeText(ctx, ctx.getString(R.string.ftp_uri_malformed, target), Toast.LENGTH_LONG).show(); return; } scheme = uri.getScheme(); } // if we get a String that does not include a scheme, // we interpret it as a mail address if (scheme == null) { scheme = "mailto"; } if (scheme.equals("ftp")) { if (multiple) { Toast.makeText(ctx, "sending multiple file through ftp is not supported", Toast.LENGTH_LONG).show(); return; } intent = new Intent(android.content.Intent.ACTION_SENDTO); intent.putExtra(Intent.EXTRA_STREAM, fileUris.get(0)); intent.setDataAndType(android.net.Uri.parse(target), mimeType); if (!isIntentAvailable(ctx, intent)) { Toast.makeText(ctx, R.string.no_app_handling_ftp_available, Toast.LENGTH_LONG).show(); return; } ctx.startActivity(intent); } else if (scheme.equals("mailto")) { if (multiple) { intent = new Intent(android.content.Intent.ACTION_SEND_MULTIPLE); ArrayList<Uri> uris = new ArrayList<>(); for (Uri fileUri : fileUris) { uris.add(fileUri); } intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); } else { intent = new Intent(android.content.Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_STREAM, fileUris.get(0)); } intent.setType(mimeType); if (uri != null) { String address = uri.getSchemeSpecificPart(); intent.putExtra(Intent.EXTRA_EMAIL, new String[] { address }); } intent.putExtra(Intent.EXTRA_SUBJECT, R.string.export_expenses); if (!isIntentAvailable(ctx, intent)) { Toast.makeText(ctx, R.string.no_app_handling_email_available, Toast.LENGTH_LONG).show(); return; } // if we got mail address, we launch the default application // if we are called without target, we launch the chooser // in order to make action more explicit if (uri != null) { ctx.startActivity(intent); } else { ctx.startActivity(Intent.createChooser(intent, ctx.getString(R.string.share_sending))); } } else { Toast.makeText(ctx, ctx.getString(R.string.share_scheme_not_supported, scheme), Toast.LENGTH_LONG) .show(); return; } } public static void setBackgroundFilter(View v, int c) { v.getBackground().setColorFilter(c, PorterDuff.Mode.MULTIPLY); } /** * Indicates whether the specified action can be used as an intent. This * method queries the package manager for installed packages that can respond * to an intent with the specified action. If no suitable package is found, * this method returns false. * * From * http://android-developers.blogspot.fr/2009/01/can-i-use-this-intent.html * * @param context * The application's environment. * @param intent * The Intent action to check for availability. * * @return True if an Intent with the specified action can be sent and * responded to, false otherwise. */ public static boolean isIntentAvailable(Context context, Intent intent) { final PackageManager packageManager = context.getPackageManager(); List<ResolveInfo> list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); return !list.isEmpty(); } public static boolean isIntentReceiverAvailable(Context context, Intent intent) { final PackageManager packageManager = context.getPackageManager(); List<ResolveInfo> list = packageManager.queryBroadcastReceivers(intent, 0); return !list.isEmpty(); } public static boolean isBrightColor(int color) { if (android.R.color.transparent == color) return true; boolean rtnValue = false; int[] rgb = { Color.red(color), Color.green(color), Color.blue(color) }; int brightness = (int) Math.sqrt(rgb[0] * rgb[0] * .241 + rgb[1] * rgb[1] * .691 + rgb[2] * rgb[2] * .068); // color is light if (brightness >= 200) { rtnValue = true; } return rtnValue; } /** * get a value from extras that could be either passed as String or a long extra * we need this method, to pass values from monkeyrunner, which is not able to pass long extras * if extras is null, defaultValue is returned * @param extras * @param key * @param defaultValue * @return */ public static long getFromExtra(Bundle extras, String key, long defaultValue) { if (extras == null) return defaultValue; String stringValue = extras.getString(key); if (TextUtils.isEmpty(stringValue)) { return extras.getLong(key, defaultValue); } else { return Long.parseLong(stringValue); } } public static int get700Tint(int color) { int found = colorPrimaryDarkMap.get(color); return found != 0 ? found : color; } @SuppressLint("DefaultLocale") public static String toLocalizedString(int i) { return String.format("%d", i); } @VisibleForTesting public static CharSequence getContribFeatureLabelsAsFormattedList(Context ctx, ContribFeature other) { return getContribFeatureLabelsAsFormattedList(ctx, other, LicenceHandler.LicenceStatus.CONTRIB); } /** * @param ctx * for retrieving resources * @param other * if not null, all features except the one provided will be returned * @param type if not null, only features of this type will be listed * @return construct a list of all contrib features to be included into a * TextView */ public static CharSequence getContribFeatureLabelsAsFormattedList(Context ctx, ContribFeature other, LicenceHandler.LicenceStatus type) { CharSequence result = "", linefeed = Html.fromHtml("<br>"); Iterator<ContribFeature> iterator = EnumSet.allOf(ContribFeature.class).iterator(); while (iterator.hasNext()) { ContribFeature f = iterator.next(); if (!f.equals(other) && (!f.equals(ContribFeature.AD_FREE) || IS_FLAVOURED)) { if (type != null && ((f.isExtended() && !type.equals(LicenceHandler.LicenceStatus.EXTENDED)) || (!f.isExtended() && type.equals(LicenceHandler.LicenceStatus.EXTENDED)))) { continue; } String resName = "contrib_feature_" + f.toString() + "_label"; int resId = ctx.getResources().getIdentifier(resName, "string", ctx.getPackageName()); if (resId == 0) { AcraHelper.report(new Resources.NotFoundException(resName)); continue; } if (!result.equals("")) { result = TextUtils.concat(result, linefeed); } result = TextUtils.concat(result, "\u25b6 ", ctx.getText(resId)); } } return result; } public static String md5(String s) { try { // Create MD5 Hash MessageDigest digest = java.security.MessageDigest.getInstance("MD5"); digest.update(s.getBytes()); byte messageDigest[] = digest.digest(); // Create Hex String StringBuffer hexString = new StringBuffer(); for (int i = 0; i < messageDigest.length; i++) hexString.append(Integer.toHexString(0xFF & messageDigest[i])); return hexString.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return ""; } public static class StringBuilderWrapper { public StringBuilderWrapper() { this.sb = new StringBuilder(); } private StringBuilder sb; public StringBuilderWrapper append(String s) { sb.append(s); return this; } public StringBuilderWrapper appendQ(String s) { sb.append(s.replace("\"", "\"\"")); return this; } public String toString() { return sb.toString(); } public void clear() { sb = new StringBuilder(); } } public static <E extends Enum<E>> String joinEnum(Class<E> enumClass) { String result = ""; Iterator<E> iterator = EnumSet.allOf(enumClass).iterator(); while (iterator.hasNext()) { result += "'" + iterator.next().name() + "'"; if (iterator.hasNext()) result += ","; } return result; } /** * Credit: * https://groups.google.com/forum/?fromgroups#!topic/actionbarsherlock * /Z8Ic8djq-3o * * @param item * @param enabled */ public static void menuItemSetEnabledAndVisible(MenuItem item, boolean enabled) { item.setEnabled(enabled).setVisible(enabled); } public static boolean doesPackageExist(Context context, String targetPackage) { try { context.getPackageManager().getPackageInfo(targetPackage, PackageManager.GET_META_DATA); } catch (NameNotFoundException e) { return false; } return true; } public static DateFormat localizedYearlessDateFormat() { Locale l = Locale.getDefault(); final String contextPattern = ((SimpleDateFormat) android.text.format.DateFormat .getDateFormat(MyApplication.getInstance())).toPattern(); String yearlessPattern = contextPattern.replaceAll("\\W?[Yy]+\\W?", ""); return new SimpleDateFormat(yearlessPattern, l); } public static Result analyzeGrisbiFileWithSAX(InputStream is) { GrisbiHandler handler = new GrisbiHandler(); try { Xml.parse(is, Xml.Encoding.UTF_8, handler); } catch (IOException e) { return new Result(false, R.string.parse_error_other_exception, e.getMessage()); } catch (GrisbiHandler.FileVersionNotSupportedException e) { return new Result(false, R.string.parse_error_grisbi_version_not_supported, e.getMessage()); } catch (SAXException e) { return new Result(false, R.string.parse_error_parse_exception); } return handler.getResult(); } public static int importParties(ArrayList<String> partiesList, GrisbiImportTask task) { int total = 0; for (int i = 0; i < partiesList.size(); i++) { if (Payee.maybeWrite(partiesList.get(i)) != -1) { total++; } if (task != null && i % 10 == 0) { task.publishProgress(i); } } return total; } public static int importCats(CategoryTree catTree, GrisbiImportTask task) { int count = 0, total = 0; String label; long main_id, sub_id; int size = catTree.children().size(); for (int i = 0; i < size; i++) { CategoryTree mainCat = catTree.children().valueAt(i); label = mainCat.getLabel(); count++; main_id = Category.find(label, null); if (main_id != -1) { Log.i("MyExpenses", "category with label" + label + " already defined"); } else { main_id = Category.write(0L, label, null); if (main_id != -1) { total++; if (task != null && count % 10 == 0) { task.publishProgress(count); } } else { // this should not happen Log.w("MyExpenses", "could neither retrieve nor store main category " + label); continue; } } int subSize = mainCat.children().size(); for (int j = 0; j < subSize; j++) { label = mainCat.children().valueAt(j).getLabel(); count++; sub_id = Category.write(0L, label, main_id); if (sub_id != -1) { total++; } else { Log.i("MyExpenses", "could not store sub category " + label); } if (task != null && count % 10 == 0) { task.publishProgress(count); } } } return total; } public static String concatResStrings(Context ctx, String separator, Integer... resIds) { String result = ""; Iterator<Integer> itemIterator = Arrays.asList(resIds).iterator(); if (itemIterator.hasNext()) { result += ctx.getString(itemIterator.next()); while (itemIterator.hasNext()) { result += separator + ctx.getString(itemIterator.next()); } } return result; } /** * @return false if the configured folder is inside the application folder * that will be deleted upon app uninstall and hence user should be * warned about the situation, unless he already has opted to no * longer see this warning */ @SuppressLint("NewApi") public static boolean checkAppFolderWarning() { // if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) { // return true; // } if (PrefKey.APP_FOLDER_WARNING_SHOWN.getBoolean(false)) { return true; } try { DocumentFile configuredDir = Utils.getAppDir(); if (configuredDir == null) { return true; } File externalFilesDir = MyApplication.getInstance().getExternalFilesDir(null); if (externalFilesDir == null) { return true; } Uri dirUri = configuredDir.getUri(); if (!dirUri.getScheme().equals("file")) { return true; //nothing we can do if we can not compare paths } URI defaultDir = externalFilesDir.getParentFile().getCanonicalFile().toURI(); return defaultDir.relativize(new File(dirUri.getPath()).getCanonicalFile().toURI()).isAbsolute(); } catch (IOException e) { return true; } } // From Financisto public static String[] joinArrays(String[] a1, String[] a2) { if (a1 == null || a1.length == 0) { return a2; } if (a2 == null || a2.length == 0) { return a1; } String[] a = new String[a1.length + a2.length]; System.arraycopy(a1, 0, a, 0, a1.length); System.arraycopy(a2, 0, a, a1.length, a2.length); return a; } public static void configDecimalSeparator(final EditText editText, final char decimalSeparator, final int fractionDigits) { // mAmountText.setInputType( // InputType.TYPE_CLASS_NUMBER|InputType.TYPE_NUMBER_FLAG_DECIMAL); // due to bug in Android platform // http://code.google.com/p/android/issues/detail?id=2626 // the soft keyboard if it occupies full screen in horizontal orientation // does not display the , as comma separator // TODO we should take into account the arab separator as well final char otherSeparator = decimalSeparator == '.' ? ',' : '.'; editText.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); editText.setFilters(new InputFilter[] { new InputFilter() { @Override public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { int separatorPositionInDest = dest.toString().indexOf(decimalSeparator); char[] v = new char[end - start]; TextUtils.getChars(source, start, end, v, 0); String input = new String(v).replace(otherSeparator, decimalSeparator); if (fractionDigits == 0 || separatorPositionInDest != -1 || dest.length() - dend > fractionDigits) { input = input.replace(String.valueOf(decimalSeparator), ""); } else { int separatorPositionInSource = input.lastIndexOf(decimalSeparator); if (separatorPositionInSource != -1) { //we make sure there is only one separator in the input and after the separator we do not use //more minor digits as allowed int existingMinorUnits = dest.length() - dend; int additionalAllowedMinorUnits = fractionDigits - existingMinorUnits; int additionalPossibleMinorUnits = input.length() - separatorPositionInSource - 1; int extractMinorUnits = additionalPossibleMinorUnits >= additionalAllowedMinorUnits ? additionalAllowedMinorUnits : additionalPossibleMinorUnits; input = input.substring(0, separatorPositionInSource) .replace(String.valueOf(decimalSeparator), "") + decimalSeparator + (extractMinorUnits > 0 ? input.substring(separatorPositionInSource + 1, separatorPositionInSource + 1 + extractMinorUnits) : ""); } } if (fractionDigits == 0) { return input; } if (separatorPositionInDest != -1 && dend > separatorPositionInDest && dstart > separatorPositionInDest) { int existingMinorUnits = dest.length() - (separatorPositionInDest + 1); int remainingMinorUnits = fractionDigits - existingMinorUnits; if (remainingMinorUnits < 1) { return ""; } return input.length() > remainingMinorUnits ? input.substring(0, remainingMinorUnits) : input; } else { return input; } } }, new InputFilter.LengthFilter(16) }); } /** * @param str * @return a representation of str converted to lower case, Unicode * normalization applied and markers removed this allows * case-insentive comparison for non-ascii and non-latin strings works * only above Gingerbread, on Froyo only lower case transformation is * performed */ @SuppressLint({ "NewApi", "DefaultLocale" }) public static String normalize(String str) { str = str.toLowerCase(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) { return str; } // Credits: http://stackoverflow.com/a/3322174/1199911 return Normalizer.normalize(str, Normalizer.Form.NFD).replaceAll("\\p{M}", ""); } public static String esacapeSqlLikeExpression(String str) { return str .replace(WhereFilter.LIKE_ESCAPE_CHAR, WhereFilter.LIKE_ESCAPE_CHAR + WhereFilter.LIKE_ESCAPE_CHAR) .replace("%", WhereFilter.LIKE_ESCAPE_CHAR + "%").replace("_", WhereFilter.LIKE_ESCAPE_CHAR + "_"); } public static String printDebug(Object[] objects) { if (objects == null) { return "null"; } String result = ""; for (Object object : objects) { if (!result.equals("")) result += ","; result += object.toString(); } return result; } @SuppressLint("InlinedApi") public static String getContentIntentAction() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ? Intent.ACTION_OPEN_DOCUMENT : Intent.ACTION_GET_CONTENT; } public static Bitmap decodeSampledBitmapFromUri(Uri uri, int reqWidth, int reqHeight) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; if (uri.getScheme().equals("file")) { String filePath = uri.getPath(); BitmapFactory.decodeFile(filePath, options); // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(filePath, options); } else { InputStream is = null; try { is = MyApplication.getInstance().getContentResolver().openInputStream(uri); BitmapFactory.decodeStream(is, null, options); // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; is.close(); is = MyApplication.getInstance().getContentResolver().openInputStream(uri); return BitmapFactory.decodeStream(is, null, options); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } return null; } public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; // Calculate the largest inSampleSize value that is a power of 2 and keeps // both // height and width larger than the requested height and width. while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { inSampleSize *= 2; } } return inSampleSize; } /** * filters out the '/' character and characters of type {@link java.lang.Character#SURROGATE} or * {@link java.lang.Character#OTHER_SYMBOL}, meant primarily to skip emojs * @param in * @return */ public static String escapeForFileName(String in) { return in.replace("/", "").replaceAll("\\p{Cs}", "").replaceAll("\\p{So}", ""); } //http://stackoverflow.com/a/11072627/1199911 public static void selectSpinnerItemByValue(Spinner spnr, long value) { SimpleCursorAdapter adapter = (SimpleCursorAdapter) spnr.getAdapter(); for (int position = 0; position < adapter.getCount(); position++) { if (adapter.getItemId(position) == value) { spnr.setSelection(position); return; } } } @SuppressLint("NewApi") public static void setBackgroundTintListOnFab(FloatingActionButton fab, int color) { fab.setBackgroundTintList(ColorStateList.valueOf(color)); DrawableCompat.setTint(fab.getDrawable(), isBrightColor(color) ? Color.BLACK : Color.WHITE); fab.invalidate(); } public static int getFirstDayOfWeek(Locale locale) { return new GregorianCalendar(locale).getFirstDayOfWeek(); } public static void configureSortMenu(SubMenu sortMenu, String currentSortOrder) { MenuItem activeItem; switch (currentSortOrder) { case ProtectedFragmentActivity.SORT_ORDER_USAGES: activeItem = sortMenu.findItem(R.id.SORT_USAGES_COMMAND); break; case ProtectedFragmentActivity.SORT_ORDER_LAST_USED: activeItem = sortMenu.findItem(R.id.SORT_LAST_USED_COMMAND); break; case ProtectedFragmentActivity.SORT_ORDER_AMOUNT: activeItem = sortMenu.findItem(R.id.SORT_AMOUNT_COMMAND); break; case ProtectedFragmentActivity.SORT_ORDER_CUSTOM: activeItem = sortMenu.findItem(R.id.SORT_CUSTOM_COMMAND); break; case ProtectedFragmentActivity.SORT_ORDER_NEXT_INSTANCE: activeItem = sortMenu.findItem(R.id.SORT_NEXT_INSTANCE_COMMAND); break; default: activeItem = sortMenu.findItem(R.id.SORT_TITLE_COMMAND); } activeItem.setChecked(true); } public static String getSortOrderFromMenuItemId(int id) { switch (id) { case R.id.SORT_USAGES_COMMAND: return ProtectedFragmentActivity.SORT_ORDER_USAGES; case R.id.SORT_LAST_USED_COMMAND: return ProtectedFragmentActivity.SORT_ORDER_LAST_USED; case R.id.SORT_TITLE_COMMAND: return ProtectedFragmentActivity.SORT_ORDER_TITLE; case R.id.SORT_CUSTOM_COMMAND: return ProtectedFragmentActivity.SORT_ORDER_CUSTOM; case R.id.SORT_AMOUNT_COMMAND: return ProtectedFragmentActivity.SORT_ORDER_AMOUNT; case R.id.SORT_NEXT_INSTANCE_COMMAND: return ProtectedFragmentActivity.SORT_ORDER_NEXT_INSTANCE; } return null; } public static void configureGroupingMenu(SubMenu groupingMenu, Grouping currentGrouping) { MenuItem activeItem; switch (currentGrouping) { case DAY: activeItem = groupingMenu.findItem(R.id.GROUPING_DAY_COMMAND); break; case WEEK: activeItem = groupingMenu.findItem(R.id.GROUPING_WEEK_COMMAND); break; case MONTH: activeItem = groupingMenu.findItem(R.id.GROUPING_MONTH_COMMAND); break; case YEAR: activeItem = groupingMenu.findItem(R.id.GROUPING_YEAR_COMMAND); break; default: activeItem = groupingMenu.findItem(R.id.GROUPING_NONE_COMMAND); break; } activeItem.setChecked(true); } public static Grouping getGroupingFromMenuItemId(int id) { switch (id) { case R.id.GROUPING_NONE_COMMAND: return Grouping.NONE; case R.id.GROUPING_DAY_COMMAND: return Grouping.DAY; case R.id.GROUPING_WEEK_COMMAND: return Grouping.WEEK; case R.id.GROUPING_MONTH_COMMAND: return Grouping.MONTH; case R.id.GROUPING_YEAR_COMMAND: return Grouping.YEAR; } return null; } //TODO TintContextWrapper is not public in Support library 23.3.0, //need to find another solution //maybe http://stackoverflow.com/a/37097656/1199911 public static Bitmap getTintedBitmapForTheme(Context context, int drawableResId, int themeResId) { Context wrappedContext = new ContextThemeWrapper(context, themeResId); Drawable d = AppCompatDrawableManager.get().getDrawable(wrappedContext, drawableResId); Bitmap b = Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); Canvas c = new Canvas(b); d.setBounds(0, 0, c.getWidth(), c.getHeight()); d.draw(c); return b; } public static void requireLoader(LoaderManager manager, int loaderId, Bundle args, LoaderManager.LoaderCallbacks callback) { if (manager.getLoader(loaderId) != null && !manager.getLoader(loaderId).isReset()) { manager.restartLoader(loaderId, args, callback); } else { manager.initLoader(loaderId, args, callback); } } public static final boolean shouldUseAndroidPlatformCalendar() { return IS_ANDROID; } //Integer compare is API 19 public static int compare(int lhs, int rhs) { return lhs < rhs ? -1 : (lhs == rhs ? 0 : 1); } // From Guava public static int indexOf(int[] array, int target) { return indexOf(array, target, 0, array.length); } private static int indexOf(int[] array, int target, int start, int end) { for (int i = start; i < end; i++) { if (array[i] == target) { return i; } } return -1; } public static int pow(int b, int k) { switch (b) { case 0: return (k == 0) ? 1 : 0; case 1: return 1; case (-1): return ((k & 1) == 0) ? 1 : -1; case 2: return (k < Integer.SIZE) ? (1 << k) : 0; case (-2): if (k < Integer.SIZE) { return ((k & 1) == 0) ? (1 << k) : -(1 << k); } else { return 0; } default: // continue below to handle the general case } for (int accum = 1;; k >>= 1) { switch (k) { case 0: return accum; case 1: return b * accum; default: accum *= ((k & 1) == 0) ? 1 : b; b *= b; } } } }