/******************************************************************************* * This file is part of RedReader. * * RedReader 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. * * RedReader 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 RedReader. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package org.quantumbadger.redreader.common; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.Typeface; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; import android.os.Looper; import android.os.Message; import android.os.StatFs; import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.Toast; import org.quantumbadger.redreader.R; import org.quantumbadger.redreader.activities.BugReportActivity; import org.quantumbadger.redreader.cache.CacheRequest; import org.quantumbadger.redreader.fragments.ErrorPropertiesDialog; import org.quantumbadger.redreader.reddit.APIResponseHandler; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Locale; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; public final class General { public static int COLOR_INVALID = Color.MAGENTA; private static long lastBackPress = -1; public static boolean onBackPressed() { if(lastBackPress < SystemClock.uptimeMillis() - 300) { lastBackPress = SystemClock.uptimeMillis(); return true; } return false; } private static Typeface monoTypeface; public static Typeface getMonoTypeface(Context context) { if(monoTypeface == null) { monoTypeface = Typeface.createFromAsset(context.getAssets(), "fonts/VeraMono.ttf"); } return monoTypeface; } public static Message handlerMessage(int what, Object obj) { final Message msg = Message.obtain(); msg.what = what; msg.obj = obj; return msg; } public static void moveFile(final File src, final File dst) throws IOException { if(!src.renameTo(dst)) { copyFile(src, dst); if(!src.delete()) { src.deleteOnExit(); } } } public static void copyFile(final File src, final File dst) throws IOException { final FileInputStream fis = new FileInputStream(src); final FileOutputStream fos = new FileOutputStream(dst); copyFile(fis, fos); } public static void copyFile(final InputStream fis, final File dst) throws IOException { final FileOutputStream fos = new FileOutputStream(dst); copyFile(fis, fos); } public static void copyFile(final InputStream fis, final OutputStream fos) throws IOException { final byte[] buf = new byte[32 * 1024]; int bytesRead; while((bytesRead = fis.read(buf)) > 0) { fos.write(buf, 0, bytesRead); } fis.close(); fos.close(); } public static boolean isCacheDiskFull(final Context context) { final long space = getFreeSpaceAvailable(PrefsUtility.pref_cache_location(context, PreferenceManager.getDefaultSharedPreferences(context))); return space < 128 * 1024 * 1024; } /// Get the number of free bytes that are available on the external storage. @SuppressWarnings("deprecation") public static long getFreeSpaceAvailable(String path) { StatFs stat = new StatFs(path); long availableBlocks; long blockSize; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { availableBlocks = stat.getAvailableBlocksLong(); blockSize = stat.getBlockSizeLong(); } else { availableBlocks = stat.getAvailableBlocks(); blockSize = stat.getBlockSize(); } return availableBlocks * blockSize; } /** Takes a size in bytes and converts it into a human-readable * String with units. */ public static String addUnits(final long input) { int i = 0; long result = input; while (i <= 3 && result >= 1024) result = input / (long) Math.pow(1024, ++i); switch (i) { case 1: return result + " KiB"; case 2: return result + " MiB"; case 3: return result + " GiB"; default: return result + " B"; } } public static String bytesToMegabytes(final long input) { final long totalKilobytes = input / 1024; final long totalMegabytes = totalKilobytes / 1024; final long remainder = totalKilobytes % 1024; return String.format(Locale.US, "%d.%02d MB", totalMegabytes, (remainder / 10)); } public static int dpToPixels(final Context context, final float dp) { return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics())); } public static int spToPixels(final Context context, final float sp) { return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, context.getResources().getDisplayMetrics())); } public static void quickToast(final Context context, final int textRes) { quickToast(context, context.getString(textRes)); } public static void quickToast(final Context context, final String text) { AndroidApi.UI_THREAD_HANDLER.post(new Runnable() { @Override public void run() { Toast.makeText(context, text, Toast.LENGTH_LONG).show(); } }); } public static void quickToast(final Context context, final String text, final int duration) { AndroidApi.UI_THREAD_HANDLER.post(new Runnable() { @Override public void run() { Toast.makeText(context, text, duration).show(); } }); } public static boolean isTablet(final Context context, final SharedPreferences sharedPreferences) { final PrefsUtility.AppearanceTwopane pref = PrefsUtility.appearance_twopane(context, sharedPreferences); switch(pref) { case AUTO: return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE; case NEVER: return false; case FORCE: return true; default: BugReportActivity.handleGlobalError(context, "Unknown AppearanceTwopane value " + pref.name()); return false; } } public static boolean isConnectionWifi(final Context context){ final ConnectivityManager cm = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); final NetworkInfo info = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI); return info != null && info.getDetailedState() == NetworkInfo.DetailedState.CONNECTED; } public static boolean isNetworkConnected(final Context context) { final ConnectivityManager cm = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); final NetworkInfo activeNetworkInfo = cm.getActiveNetworkInfo(); return activeNetworkInfo != null && activeNetworkInfo.isConnected(); } public static RRError getGeneralErrorForFailure(Context context, @CacheRequest.RequestFailureType int type, Throwable t, Integer status, String url) { final int title, message; switch (type) { case CacheRequest.REQUEST_FAILURE_CANCELLED: title = R.string.error_cancelled_title; message = R.string.error_cancelled_message; break; case CacheRequest.REQUEST_FAILURE_PARSE: title = R.string.error_parse_title; message = R.string.error_parse_message; break; case CacheRequest.REQUEST_FAILURE_CACHE_MISS: title = R.string.error_unexpected_cache_title; message = R.string.error_unexpected_cache_message; break; case CacheRequest.REQUEST_FAILURE_STORAGE: title = R.string.error_unexpected_storage_title; message = R.string.error_unexpected_storage_message; break; case CacheRequest.REQUEST_FAILURE_CONNECTION: // TODO check network and customise message title = R.string.error_connection_title; message = R.string.error_connection_message; break; case CacheRequest.REQUEST_FAILURE_MALFORMED_URL: title = R.string.error_malformed_url_title; message = R.string.error_malformed_url_message; break; case CacheRequest.REQUEST_FAILURE_DISK_SPACE: title = R.string.error_disk_space_title; message = R.string.error_disk_space_message; break; case CacheRequest.REQUEST_FAILURE_CACHE_DIR_DOES_NOT_EXIST: title = R.string.error_cache_dir_does_not_exist_title; message = R.string.error_cache_dir_does_not_exist_message; break; case CacheRequest.REQUEST_FAILURE_REQUEST: if(status != null) { switch (status) { case 400: case 401: case 403: { final URI uri = General.uriFromString(url); final boolean isRedditRequest = uri != null && uri.getHost() != null && ("reddit.com".equalsIgnoreCase(uri.getHost()) || uri.getHost().endsWith(".reddit.com")); if(isRedditRequest) { title = R.string.error_403_title; message = R.string.error_403_message; } else { title = R.string.error_403_title_nonreddit; message = R.string.error_403_message_nonreddit; } break; } case 404: title = R.string.error_404_title; message = R.string.error_404_message; break; case 502: case 503: case 504: title = R.string.error_redditdown_title; message = R.string.error_redditdown_message; break; default: title = R.string.error_unknown_api_title; message = R.string.error_unknown_api_message; break; } } else { title = R.string.error_unknown_api_title; message = R.string.error_unknown_api_message; } break; case CacheRequest.REQUEST_FAILURE_REDDIT_REDIRECT: title = R.string.error_403_title; message = R.string.error_403_message; break; case CacheRequest.REQUEST_FAILURE_PARSE_IMGUR: title = R.string.error_parse_imgur_title; message = R.string.error_parse_imgur_message; break; case CacheRequest.REQUEST_FAILURE_UPLOAD_FAIL_IMGUR: title = R.string.error_upload_fail_imgur_title; message = R.string.error_upload_fail_imgur_message; break; default: title = R.string.error_unknown_title; message = R.string.error_unknown_message; break; } return new RRError(context.getString(title), context.getString(message), t, status, url); } public static RRError getGeneralErrorForFailure(Context context, final APIResponseHandler.APIFailureType type) { final int title, message; switch(type) { case INVALID_USER: title = R.string.error_403_title; message = R.string.error_403_message; break; case BAD_CAPTCHA: title = R.string.error_bad_captcha_title; message = R.string.error_bad_captcha_message; break; case NOTALLOWED: title = R.string.error_403_title; message = R.string.error_403_message; break; case SUBREDDIT_REQUIRED: title = R.string.error_subreddit_required_title; message = R.string.error_subreddit_required_message; break; case URL_REQUIRED: title = R.string.error_url_required_title; message = R.string.error_url_required_message; break; case TOO_FAST: title = R.string.error_too_fast_title; message = R.string.error_too_fast_message; break; case TOO_LONG: title = R.string.error_too_long_title; message = R.string.error_too_long_message; break; default: title = R.string.error_unknown_api_title; message = R.string.error_unknown_api_message; break; } return new RRError(context.getString(title), context.getString(message)); } // TODO add button to show more detail public static void showResultDialog(final AppCompatActivity context, final RRError error) { AndroidApi.UI_THREAD_HANDLER.post(new Runnable() { @Override public void run() { try { final AlertDialog.Builder alertBuilder = new AlertDialog.Builder(context); alertBuilder.setNeutralButton(R.string.dialog_close, null); alertBuilder.setNegativeButton(R.string.button_moredetail, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ErrorPropertiesDialog.newInstance(error).show(context.getSupportFragmentManager(), "ErrorPropertiesDialog"); } }); alertBuilder.setTitle(error.title); alertBuilder.setMessage(error.message); alertBuilder.create().show(); } catch(final WindowManager.BadTokenException e) { Log.e("General", "Tried to show result dialog after activity closed", e); } } }); } private static final Pattern urlPattern = Pattern.compile("^(https?)://([^/]+)/+([^\\?#]+)((?:\\?[^#]+)?)((?:#.+)?)$"); public static String filenameFromString(String url) { String filename = uriFromString(url).getPath().replace(File.separator, ""); String[] parts = filename.substring(1).split("\\.", 2); if(parts.length < 2) filename += ".jpg"; return filename; } public static URI uriFromString(String url) { try { return new URI(url); } catch(Throwable t1) { try { Log.i("RR DEBUG uri", "Beginning aggressive parse of '" + url + "'"); final Matcher urlMatcher = urlPattern.matcher(url); if(urlMatcher.find()) { final String scheme = urlMatcher.group(1); final String authority = urlMatcher.group(2); final String path = urlMatcher.group(3).length() == 0 ? null : "/" + urlMatcher.group(3); final String query = urlMatcher.group(4).length() == 0 ? null : urlMatcher.group(4); final String fragment = urlMatcher.group(5).length() == 0 ? null : urlMatcher.group(5); try { return new URI(scheme, authority, path, query, fragment); } catch(Throwable t3) { if(path != null && path.contains(" ")) { return new URI(scheme, authority, path.replace(" ", "%20"), query, fragment); } else { return null; } } } else { return null; } } catch(Throwable t2) { return null; } } } public static String sha1(final byte[] plaintext) { final MessageDigest digest; try { digest = MessageDigest.getInstance("SHA-1"); } catch(Exception e) { throw new RuntimeException(e); } digest.update(plaintext, 0, plaintext.length); final byte[] hash = digest.digest(); final StringBuilder result = new StringBuilder(hash.length * 2); for(byte b : hash) result.append(String.format(Locale.US, "%02X", b)); return result.toString(); } // Adapted from Android: // http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.1.1_r1/android/net/Uri.java?av=f public static Set<String> getUriQueryParameterNames(final Uri uri) { if(uri.isOpaque()) { throw new UnsupportedOperationException("This isn't a hierarchical URI."); } final String query = uri.getEncodedQuery(); if(query == null) { return Collections.emptySet(); } final Set<String> names = new LinkedHashSet<>(); int pos = 0; while(pos < query.length()) { int next = query.indexOf('&', pos); int end = (next == -1) ? query.length() : next; int separator = query.indexOf('=', pos); if (separator > end || separator == -1) { separator = end; } String name = query.substring(pos, separator); names.add(Uri.decode(name)); // Move start to end of name. pos = end + 1; } return Collections.unmodifiableSet(names); } public static int divideCeil(int num, int divisor) { return (num + divisor - 1) / divisor; } public static void checkThisIsUIThread() { if(!isThisUIThread()) { throw new RuntimeException("Called from invalid thread"); } } public static boolean isThisUIThread() { return Looper.getMainLooper().getThread() == Thread.currentThread(); } public static <E> ArrayList<E> listOfOne(E obj) { final ArrayList<E> result = new ArrayList<>(1); result.add(obj); return result; } public static String asciiUppercase(final String input) { final char[] chars = input.toCharArray(); for(int i = 0; i < chars.length; i++) { if(chars[i] >= 'a' && chars[i] <= 'z') { chars[i] -= 'a'; chars[i] += 'A'; } } return new String(chars); } @NonNull public static String asciiLowercase(@NonNull final String input) { final char[] chars = input.toCharArray(); for(int i = 0; i < chars.length; i++) { if(chars[i] >= 'A' && chars[i] <= 'Z') { chars[i] -= 'A'; chars[i] += 'a'; } } return new String(chars); } public static void copyStream(final InputStream in, final OutputStream out) throws IOException { int bytesRead; final byte[] buffer = new byte[64 * 1024]; while((bytesRead = in.read(buffer)) > 0) { out.write(buffer, 0, bytesRead); } } public static void setAllMarginsDp(final Context context, final View view, final int marginDp) { final ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)view.getLayoutParams(); final int marginPx = dpToPixels(context, marginDp); layoutParams.leftMargin = marginPx; layoutParams.rightMargin = marginPx; layoutParams.topMargin = marginPx; layoutParams.bottomMargin = marginPx; } public static void setLayoutMatchParent(final View view) { final ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; } public static void recreateActivityNoAnimation(final AppCompatActivity activity) { // http://stackoverflow.com/a/3419987/1526861 final Intent intent = activity.getIntent(); activity.overridePendingTransition(0, 0); intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); activity.finish(); activity.overridePendingTransition(0, 0); activity.startActivity(intent); } public static long hoursToMs(final long hours) { return hours * 60L * 60L * 1000L; } public static void safeDismissDialog(final Dialog dialog) { try { if(dialog.isShowing()) dialog.dismiss(); } catch(final Exception e) { Log.e("safeDismissDialog", "Caught exception while dismissing dialog", e); } } }