/* * Copyright (c) 2013, Psiphon Inc. * All rights reserved. * * This program 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. * * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. * */ package ca.psiphon.ploggy; import java.io.ByteArrayOutputStream; 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.io.OutputStreamWriter; import java.security.SecureRandom; import java.text.DateFormat; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.location.Location; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.FileObserver; import android.os.Handler; import android.util.Base64; import android.view.View; import android.view.inputmethod.InputMethodManager; import de.schildbach.wallet.util.LinuxSecureRandom; /** * Utility functions */ public class Utils { private static final String LOG_TAG = "Utils"; public static class ApplicationError extends Exception { private static final long serialVersionUID = -3656367025650685613L; public ApplicationError(String tag, String message) { if (tag != null) { Log.addEntry(tag, message); } } public ApplicationError(String tag, Exception e) { // TODO: require message param as well? super(e); String message = e.getLocalizedMessage(); if (message == null) { message = "(null)"; } Log.addEntry(tag, String.format("%s: %s", e.getClass().toString(), message)); // TODO: log stack trace? } } public static void writeStringToFile(String data, File file) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream(file); try { OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream); outputStreamWriter.write(data); outputStreamWriter.close(); } finally { fileOutputStream.close(); } } public static String readFileToString(File file) throws IOException { FileInputStream fileInputStream = new FileInputStream(file); try { return readInputStreamToString(fileInputStream); } finally { fileInputStream.close(); } } public static int readFileToInt(File file) throws IOException { try { return Integer.parseInt(Utils.readFileToString(file).trim()); } catch (NumberFormatException e) { throw new IOException(e); } } public static String readInputStreamToString(InputStream inputStream) throws IOException { return new String(readInputStreamToBytes(inputStream), "UTF-8"); } public static byte[] readFileToBytes(File file) throws IOException { FileInputStream fileInputStream = new FileInputStream(file); try { return readInputStreamToBytes(fileInputStream); } finally { fileInputStream.close(); } } public static byte[] readInputStreamToBytes(InputStream inputStream) throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); int readCount; byte[] buffer = new byte[16384]; while ((readCount = inputStream.read(buffer, 0, buffer.length)) != -1) { outputStream.write(buffer, 0, readCount); } outputStream.flush(); return outputStream.toByteArray(); } public static void copyStream(InputStream inputStream, OutputStream outputStream) throws IOException { try { byte[] buffer = new byte[16384]; int length; while ((length = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0 , length); } } finally { inputStream.close(); outputStream.close(); } } public static class NullOutputStream extends OutputStream { @Override public void write(int arg0) throws IOException { } @Override public void write(byte[] b) throws IOException { } @Override public void write(byte[] b, int off, int len) throws IOException { } } public static void discardStream(InputStream inputStream) throws IOException { copyStream(inputStream, new NullOutputStream()); } public static class FileInitializedObserver extends FileObserver { private final CountDownLatch mLatch; private final ArrayList<String> mTargetFilenames; public FileInitializedObserver(File directory, String ... filenames) { // MOVED_TO is required for the Tor case where the Tor process creates <target>.tmp, // writes to that file, then renames to <target>. There's no CLOSE_WRITE event for <target>. super( directory.getAbsolutePath(), FileObserver.MOVED_TO | FileObserver.CLOSE_WRITE); mTargetFilenames = new ArrayList<String>(Arrays.asList(filenames)); mLatch = new CountDownLatch(mTargetFilenames.size()); } @Override public void onEvent(int event, String path) { if (path != null) { for (int i = 0; i < mTargetFilenames.size(); i++) { if (path.equals(mTargetFilenames.get(i))) { mTargetFilenames.remove(i); mLatch.countDown(); if (mTargetFilenames.size() == 0) { stopWatching(); } break; } } } } public boolean await(long timeoutMilliseconds) throws InterruptedException { return mLatch.await(timeoutMilliseconds, TimeUnit.MILLISECONDS); } } public static void initSecureRandom() { new LinuxSecureRandom(); } public static byte[] getRandomBytes(int byteCount) { byte[] buffer = new byte[byteCount]; new SecureRandom().nextBytes(buffer); return buffer; } public static String encodeBase64(byte[] data) { return Base64.encodeToString(data, Base64.NO_WRAP); } public static byte[] decodeBase64(String data) throws Utils.ApplicationError { try { return Base64.decode(data, Base64.DEFAULT); } catch (IllegalArgumentException e) { throw new Utils.ApplicationError(LOG_TAG, e); } } public static String formatFingerprint(byte[] fingerprintBytes) { // Adapted from: http://stackoverflow.com/questions/332079/in-java-how-do-i-convert-a-byte-array-to-a-string-of-hex-digits-while-keeping-l char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'}; char[] chars = new char[fingerprintBytes.length * 3 - 1]; for (int i = 0; i < fingerprintBytes.length; i++) { chars[i*3] = hexArray[(fingerprintBytes[i] & 0xFF)/16]; chars[i*3 + 1] = hexArray[(fingerprintBytes[i] & 0xFF)%16]; if (i < fingerprintBytes.length - 1) { chars[i*3 + 2] = ':'; } } return new String(chars); } public static int calculateLocationDistanceInMeters( double latitudeA, double longitudeA, double latitudeB, double longitudeB) { float[] results = new float[1]; Location.distanceBetween(latitudeA, longitudeA, latitudeB, longitudeB, results); return Math.round(results[0]); } public static String formatDistance(Context context, int distanceInMeters) { if (distanceInMeters < 1000) { return context.getString( R.string.format_distance_meters, NumberFormat.getInstance().format(distanceInMeters)); } else { double distanceInKilometers = distanceInMeters/1000.0; return context.getString( R.string.format_distance_kilometers, NumberFormat.getInstance().format(distanceInKilometers)); } } public static class DateFormatter { private static DateFormat mShortTimeFormat = DateFormat.getTimeInstance(DateFormat.SHORT); private static DateFormat mWeekdayFormat = new SimpleDateFormat("EEE", Locale.getDefault()); private static DateFormat mMonthFormat = new SimpleDateFormat("MMM", Locale.getDefault()); private static DateFormat mMonthDayFormat = new SimpleDateFormat("d", Locale.getDefault()); private static DateFormat mYearFormat = new SimpleDateFormat("yyyy", Locale.getDefault()); private static long MILLIS_IN_SEC = 1000; private static long MILLIS_IN_MINUTE = 60 * MILLIS_IN_SEC; private static long MILLIS_IN_HOUR = 60 * MILLIS_IN_MINUTE; /** * Returns a human-readable form of the given date-time. It will be relative * to now or absolute, depending on how long ago it is. * @param context The `Context` to use for locale and resource access. * @param startDate The date-time to render. * @param ago Add the word "ago" to relative forms. * @return The formatted string representing the date-time. */ public static String formatRelativeDatetime(Context context, Date startDate, boolean ago) { return formatRelativeDatetime(context, startDate, new Date(), ago); } /** * Returns a human-readable form of the given date-time. It will be relative * to `endDate` or absolute, depending on how long ago it is. * @param context The `Context` to use for resource access. * @param startDate The date-time to render. * @param endDate The date-time that `startDate` will be considered relative to. * @param ago Add the word "ago" to relative forms. * @return The formatted string representing the date-time. */ public static String formatRelativeDatetime(Context context, Date startDate, Date endDate, boolean ago) { Resources res = context.getResources(); long diffMS = endDate.getTime() - startDate.getTime(); // Within a minute if (diffMS < MILLIS_IN_MINUTE) { int secs = (int)(diffMS / MILLIS_IN_SEC); return res.getQuantityString( ago ? R.plurals.period_seconds_ago_abbrev : R.plurals.period_seconds_abbrev, secs, secs); } // Within an hour if (diffMS < MILLIS_IN_HOUR) { int mins = (int)(diffMS / MILLIS_IN_MINUTE); return res.getQuantityString( ago ? R.plurals.period_minutes_ago_abbrev : R.plurals.period_minutes_abbrev, mins, mins); } // If we haven't returned yet, we're going to need Calendar objects Calendar startCal = new GregorianCalendar(); startCal.setTimeInMillis(startDate.getTime()); Calendar endCal = new GregorianCalendar(); endCal.setTimeInMillis(endDate.getTime()); // Same day if (startCal.get(Calendar.YEAR) == endCal.get(Calendar.YEAR) && startCal.get(Calendar.DAY_OF_YEAR) == endCal.get(Calendar.DAY_OF_YEAR)) { return mShortTimeFormat.format(startDate); } // Same week if (startCal.get(Calendar.YEAR) == endCal.get(Calendar.YEAR) && startCal.get(Calendar.WEEK_OF_YEAR) == endCal.get(Calendar.WEEK_OF_YEAR)) { return res.getString( R.string.diff_day_same_week_datetime, mWeekdayFormat.format(startDate), mShortTimeFormat.format(startDate)); } // Same year if (startCal.get(Calendar.YEAR) == endCal.get(Calendar.YEAR)) { return res.getString( R.string.same_year_datetime, mMonthFormat.format(startDate), mMonthDayFormat.format(startDate), mShortTimeFormat.format(startDate)); } // Older than the same year return res.getString( R.string.older_datetime, mMonthFormat.format(startDate), mMonthDayFormat.format(startDate), mYearFormat.format(startDate), mShortTimeFormat.format(startDate)); } } public static void shutdownExecutorService(ExecutorService threadPool) { try { threadPool.shutdown(); if (!threadPool.awaitTermination(1000, TimeUnit.MILLISECONDS)) { threadPool.shutdownNow(); threadPool.awaitTermination(100, TimeUnit.MILLISECONDS); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } public static class FixedDelayExecutor { private final Handler mHandler; private Runnable mExecutorTask; private final Runnable mTask; private final int mDelayInMilliseconds; public FixedDelayExecutor(Runnable task, int delayInMilliseconds) { mHandler = new Handler(); mTask = task; mDelayInMilliseconds = delayInMilliseconds; } public void start() { stop(); mExecutorTask = new Runnable() { @Override public void run() { mTask.run(); mHandler.postDelayed(mExecutorTask, mDelayInMilliseconds); } }; mHandler.postDelayed(mExecutorTask, mDelayInMilliseconds); } public void stop() { if (mExecutorTask != null) { mHandler.removeCallbacks(mExecutorTask); mExecutorTask = null; } } } private static Context mApplicationContext; public static void setApplicationContext(Context context) { mApplicationContext = context; } public static Context getApplicationContext() { return mApplicationContext; } public static void hideKeyboard(Activity activity) { if (activity == null) { return; } View currentFocusView = activity.getCurrentFocus(); if (currentFocusView == null) { return; } InputMethodManager inputManager = (InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE); if (inputManager == null) { return; } inputManager.hideSoftInputFromWindow( currentFocusView.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } public static boolean isConnectedNetworkWifi(Context context) { ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); if (connectivityManager == null) { return false; } NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected() && networkInfo.getType() == ConnectivityManager.TYPE_WIFI; } }